<?xml version="1.0" encoding="utf-8"?>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
<rfc version="3" ipr="trust200902" docName="draft-mery-nagy-taistamp-00" submissionType="independent" category="info" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" indexInclude="true">

<front>
<title abbrev="Taistamp">Taistamp: Signed TAI64N Timestamps over HTTP</title><seriesInfo value="draft-mery-nagy-taistamp-00" stream="independent" status="informational" name="Internet-Draft"></seriesInfo>
<author initials="A." surname="Mery" fullname="Alejandro Mery"><organization>Apptly Software Ltd</organization><address><postal><street></street>
</postal><email>amery@apptly.co</email>
<uri>https://apptly.co</uri>
</address></author><author initials="K. G." surname="Nagy" fullname="Károly Gábriel Nagy"><organization>Apptly Software Ltd</organization><address><postal><street></street>
</postal><email>nagy.karoly@apptly.co</email>
<uri>https://apptly.co</uri>
</address></author><date year="2026" month="May" day="19"></date>
<area>Applications</area>
<workgroup></workgroup>
<keyword>TAI64N</keyword>
<keyword>TAI</keyword>
<keyword>time</keyword>
<keyword>Ed25519</keyword>
<keyword>well-known</keyword>
<keyword>DKIM</keyword>

<abstract>
<t>This memo defines Taistamp, an HTTP-accessible service
that returns the current time as a TAI64N label,
optionally signed with Ed25519. The service is reached
at the well-known URI <tt>/.well-known/taistamp</tt> and is
intended for clients that need an authenticated
wall-clock reading without running an NTP stack or
relying on the client's own clock for TLS certificate
validation. The
signing scheme uses a length-prefixed, domain-separated
framing and publishes verification keys as DNS TXT
records in a DKIM-inspired selector layout.</t>
</abstract>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>
<t>Many small clients need an accurate wall-clock reading
to validate certificates, evaluate signature freshness,
or stamp local events. Two common sources fall short:</t>

<ul spacing="compact">
<li>An NTP client is intrusive on constrained or sandboxed
runtimes and exposes the device to a separate trust
domain.</li>
<li>TLS does not provide an explicit authenticated time
value to the client; certificate validity checks
depend on the client's existing clock rather than
establishing a trustworthy time reading.</li>
</ul>
<t>Taistamp addresses these by serving the current time as
a 25-byte TAI64N <xref target="TAI64"></xref> label over HTTP, with an
optional Ed25519 <xref target="RFC8032"></xref> signature bound to a
client-supplied nonce. Verification keys are published
in DNS as TXT records under a DKIM-style <xref target="RFC6376"></xref>
selector, so keys can be rotated without
coordinating with clients.</t>
<t>The protocol is deliberately small. A compliant
implementation fits in a single HTTP request handler, has no
state beyond its signing key, and is independent of any
specific HTTP framework or runtime.</t>
</section>

<section anchor="design-rationale"><name>Design Rationale</name>

<section anchor="why-http"><name>Why HTTP</name>
<t>HTTP is available everywhere a network connection is.
Browsers, serverless runtimes (Cloudflare Workers, AWS
Lambda, Deno Deploy), edge nodes, and CDN origins can
all serve and consume HTTP without additional
infrastructure. Port 443 (or 80) is open where
UDP 123 (NTP), UDP 2002 (Roughtime, port
assignment pending), and UDP 4014
(TAICLOCK <xref target="TAICLOCK"></xref>) are commonly firewalled.
A compliant Taistamp server requires nothing beyond a
single request handler; a compliant client requires
nothing beyond an HTTP library, both of which exist in
every language and runtime in common use today.</t>
<t>Taistamp works over plain HTTP. The Ed25519 signature
provides authentication and integrity at the
application layer, independent of the transport: any
in-transit modification breaks the signature, and a
client-supplied nonce prevents replay. These guarantees
apply only to responses bearing a valid <tt>TAI-Signature</tt>
matching a request <tt>TAI-Nonce</tt>. A network attacker who
cannot forge the signature can at most drop or delay
responses, which is a denial of service rather than
a security breach.</t>
<t>TLS <xref target="RFC8446"></xref> is RECOMMENDED. It adds transport
confidentiality (hiding that a client is querying for
time and from which server) and an additional layer of
integrity. Where used, the two layers are
complementary: TLS protects the channel, the Ed25519
signature protects the claim independently of which
certificate authority issued the server certificate.</t>
<t>The server is stateless beyond its signing key. This
is a deliberate design goal: a stateless handler can
be deployed as a CDN edge function, a serverless
worker, or behind a load balancer with no session
affinity or shared state.</t>
<t>Taistamp is transported over TCP-based HTTP, which
rules out UDP-style spoofed-source amplification.
The protocol-mandated fields of a signed response
contribute at most approximately 530 bytes at maximum
field lengths, excluding transport framing and any
headers added by intermediaries — the response body
is a fixed 25 bytes, and the protocol-defined headers
(<tt>TAI-Nonce</tt> echo, <tt>TAI-Signature</tt>, <tt>TAI-Key-Selector</tt>,
<tt>TAI-Leap-Seconds</tt>) account for the remainder — an
attacker cannot induce a response meaningfully larger
than the request, so Taistamp is not usable as an
amplification vector.</t>
</section>

<section anchor="why-dns-based-key-discovery"><name>Why DNS-Based Key Discovery</name>
<t>Roughtime <xref target="Roughtime"></xref> addresses the same authenticated
time problem but distributes trust differently: clients
ship with a configured list of trusted server public keys.
Key rotation therefore requires a client update or a
coordinated key-distribution mechanism outside the
protocol.</t>
<t>Taistamp operators instead publish verification keys as DNS TXT
records under a DKIM-inspired <xref target="RFC6376"></xref> selector
scheme; they rotate a key by publishing a new TXT record
under a new selector and reconfiguring the server, with
no client coordination required. A client
that has cached a signed response can continue to verify
it as long as the TXT record at the old selector remains
in DNS. DNSSEC <xref target="RFC4033"></xref> protects the key material
end-to-end and is strongly recommended where the
operator controls a signed zone.</t>
<t>This approach deliberately trades the ecosystem-level
misbehaviour detection that Roughtime's multi-server
cross-checking provides for simpler, decentralised
deployability:
any operator with a domain name and an HTTP endpoint can
run a Taistamp server without registering with a central
authority.</t>
</section>

<section anchor="why-tai64n"><name>Why TAI64N</name>
<t>POSIX time and UTC both admit leap seconds, making a
given second ambiguous: a timestamp of 23:59:60 is
legal in UTC but meaningless in most system clocks,
which either smear the leap or repeat the last second.
For applications that check certificate validity windows
or evaluate signature freshness, an ambiguous second is
a correctness hazard.</t>
<t>TAI <xref target="TAI64"></xref> is strictly monotonic. TAI64N encodes TAI
seconds and nanoseconds in a fixed 25-byte ASCII label
with no ambiguity at any leap-second boundary. The
<tt>TAI-Leap-Seconds</tt> header carries the offset as informational
guidance; clients requiring UTC equivalence MUST source the offset
from a trusted table rather than from the server
(see <xref target="leap-second-reporting"></xref>).</t>
</section>

<section anchor="why-the-external-representation"><name>Why the External Representation</name>
<t>TAICLOCK <xref target="TAICLOCK"></xref> encodes timestamps as 16 bytes of
binary TAI64NA (8 bytes of seconds, 4 of nanoseconds,
4 of attoseconds). Taistamp instead uses the TAI64N
external representation <xref target="TAI64"></xref>: the ASCII character
<tt>@</tt> followed by 16 lowercase hexadecimal digits for
the seconds field and 8 for the nanoseconds field,
totalling 25 bytes.</t>
<t>The 9-byte difference over TAICLOCK's binary encoding
arises from three sources: the <tt>@</tt> leader (+1 byte),
hex-expanding the 8-byte seconds field to 16 ASCII
digits (+8 bytes), and hex-expanding the 4-byte
nanoseconds field to 8 ASCII digits (+4 bytes), offset
by dropping the 4-byte attosecond field that TAI64NA
carries and TAI64N omits (−4 bytes).</t>
<t>The choice is driven by the HTTP context. A hex-encoded
ASCII label is trivially inspectable with <tt>curl</tt>, safe
to log as plain text, and requires no additional
encoding in an HTTP response body. The 9-byte overhead
over binary is negligible in an HTTP exchange.
Attoseconds are omitted because no deployed clock
provides sub-nanosecond resolution.</t>
</section>

<section anchor="why-ed25519"><name>Why Ed25519</name>
<t>Ed25519 <xref target="RFC8032"></xref> was chosen for three reasons.
First, verification is fast and keys and signatures are
compact: a public key is 32 bytes and a signature is 64
bytes, keeping the signed response small and the
verification cost negligible on constrained clients.
Second, signing is deterministic — no per-signature
random nonce is required, which removes an entire class
of implementation hazard present in ECDSA. Third,
Ed25519 has no negotiable parameters; there is nothing
to downgrade.</t>
</section>

<section anchor="why-nonce-based-anti-replay"><name>Why Nonce-Based Anti-Replay</name>
<t>An alternative to a client-supplied nonce would be a
server-enforced freshness window: reject any response
older than N seconds. This approach is circular: it
requires the client to have an approximately correct
clock already, which is precisely the precondition
Taistamp is designed to avoid.</t>
<t>A client-supplied nonce breaks the circularity. The
client need have no prior knowledge of the time; it
generates an unpredictable value, sends it in the
request, and the server's signature binds the returned
label to that value. A replayed response from an
earlier exchange will carry a different nonce and the
signature will not verify.</t>
</section>

<section anchor="precision-expectations"><name>Precision Expectations</name>
<t>The TAI64N format encodes nanoseconds, but server
implementations are commonly limited to millisecond
resolution. Runtimes such as Cloudflare Workers
deliberately freeze the clock at the last I/O boundary
as a Spectre mitigation; no sub-millisecond source is
available. Clients SHOULD NOT assume that the
nanosecond field carries sub-millisecond information;
in practice the last six decimal digits of the
nanosecond value are often zero.</t>
</section>

<section anchor="non-goals"><name>Non-Goals</name>
<t>Taistamp does not synchronise or discipline a local
clock; callers MUST treat the returned label as a
single measurement subject to network round-trip
uncertainty. It does not protect against a server
operator who deliberately signs an incorrect time: the
signature attests to what the server claimed, not to
the accuracy of its clock. It does not provide
confidentiality; TLS <xref target="RFC8446"></xref> SHOULD be used when
confidentiality of the query or response is required.
It is not a document timestamping service in the sense
of RFC 3161 <xref target="RFC3161"></xref>: it timestamps moments,
not artefacts.</t>
</section>
</section>

<section anchor="relation-to-other-protocols"><name>Relation to Other Protocols</name>
<t><strong>TAICLOCK <xref target="TAICLOCK"></xref></strong> is D. J. Bernstein's UDP-based
TAI64N time service (IANA port 4014). A client sends a
packet between 20 and 256 bytes whose first four bytes
are the ASCII string <tt>ctai</tt>; the server echoes the
packet with the first byte replaced by <tt>s</tt> and bytes
4–19 replaced with the current time in TAI64NA format.
TAICLOCK is the direct conceptual predecessor of
Taistamp: both serve TAI64N timestamps on demand.
Taistamp differs in using HTTP (deployable over existing
HTTPS infrastructure, no dedicated daemon), adding
optional Ed25519 signatures,
and publishing keys via DNS rather than relying on the
transport for integrity.</t>
<t><strong>RFC 5905 (NTP) <xref target="RFC5905"></xref></strong> synchronises a local
clock continuously over UDP, adjusting for network
delay across multiple exchanges. Taistamp makes no
attempt to discipline a local clock; it provides a
single authenticated reading that the caller treats as
a measurement subject to one round-trip time.</t>
<t><strong>RFC 8915 (NTS) <xref target="RFC8915"></xref></strong> adds cryptographic
authentication to NTP using TLS for key exchange and
AEAD for packet authentication. NTS inherits NTP's
requirement for a dedicated daemon and UDP port 123.
Taistamp targets runtimes where neither is available.</t>
<t><strong>RFC 3161 (TSP) <xref target="RFC3161"></xref></strong> is a document
timestamping protocol: a client submits a hash of a
document and receives a signed token attesting that the
document existed at a given time. TSP timestamps
artefacts; Taistamp timestamps moments. The two
protocols are complementary rather than overlapping.</t>
<t><strong>Roughtime <xref target="Roughtime"></xref></strong> is an authenticated time
protocol that also uses Ed25519 signatures and is
designed for constrained clients. It operates over UDP,
distributes trust via a configured list of server
public keys, and detects misbehaving servers by
comparing timestamps and accuracy radii across
multiple providers. Taistamp is simpler and more narrowly
scoped: a single HTTP endpoint, DNS-based key
discovery, and no ecosystem coordination requirement.</t>
</section>

<section anchor="conventions-and-definitions"><name>Conventions and Definitions</name>
<t>The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;,
&quot;SHALL NOT&quot;, &quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;,
&quot;NOT RECOMMENDED&quot;, &quot;MAY&quot;, and &quot;OPTIONAL&quot; in this
document are to be interpreted as described in BCP 14
<xref target="RFC2119"></xref> <xref target="RFC8174"></xref> when, and only when, they
appear in all capitals, as shown here.</t>
<t>The terms &quot;TAI&quot; and &quot;TAI64N&quot; are used as defined in
<xref target="TAI64"></xref>.</t>
<t>The term &quot;selector&quot; is used in the DKIM
sense <xref target="RFC6376"></xref>: an opaque, host-scoped label that
selects one verification key from possibly many
published under the same host.</t>
<t>The term &quot;client&quot; refers to any HTTP agent that fetches
the Taistamp resource at <tt>/.well-known/taistamp</tt>.</t>
<t>The term &quot;verifier&quot; refers to a client that performs
signature verification on a Taistamp response.</t>
<t>HTTP field values defined in this document use
Structured Field Values <xref target="RFC9651"></xref>. Type annotations
(Integer, Binary, Token) refer to the item types
defined in that specification.</t>
</section>

<section anchor="resource"><name>Resource</name>
<t>A Taistamp service is reached at the well-known
URI <xref target="RFC8615"></xref>:</t>

<artwork><![CDATA[/.well-known/taistamp
]]></artwork>
<t>A request URI MUST be the exact path above. Query
strings, if any, are ignored by this version of the
protocol and MUST NOT change the response.</t>

<section anchor="methods"><name>Methods</name>
<t>A server MUST accept <tt>GET</tt> and <tt>HEAD</tt>. A server that
supports CORS (see <xref target="cross-origin-resource-sharing"></xref>)
MUST also accept <tt>OPTIONS</tt>; a server without CORS
support MAY accept <tt>OPTIONS</tt>. A server MUST respond
<tt>405 Method Not Allowed</tt> to any method it does not
accept. The <tt>Allow</tt> header on a 405 response, and
on a successful <tt>OPTIONS</tt> response, MUST enumerate
the methods the server accepts (<xref target="RFC9110"></xref>,
Section 10.2.1): a server that accepts <tt>OPTIONS</tt>
emits <tt>Allow: GET, HEAD, OPTIONS</tt>; one that does
not emits <tt>Allow: GET, HEAD</tt>.</t>
<t>A <tt>HEAD</tt> response MUST NOT include a body and MUST
carry the same headers as the corresponding <tt>GET</tt>
response, with the exception that <tt>TAI-Nonce</tt>,
<tt>TAI-Key-Selector</tt>, and <tt>TAI-Signature</tt> MUST NOT
be present. Because the signed payload covers the
response body, a signature cannot be verified
without it; <tt>HEAD</tt> is therefore useful only as a
liveness or configuration probe. Note that this
makes <tt>HEAD</tt> responses non-equivalent to the
corresponding <tt>GET</tt> response in the sense of
<xref target="RFC9110"></xref> Section 9.3.2; carrying an unverifiable
signature on a bodyless response would be misleading.</t>
<t><tt>OPTIONS</tt> responses MUST NOT carry any <tt>TAI-*</tt> response
header and MUST NOT be signed.</t>
</section>

<section anchor="cross-origin-resource-sharing"><name>Cross-Origin Resource Sharing</name>
<t>A Taistamp server intended for access from browser pages
SHOULD respond to <tt>OPTIONS</tt> requests with status <tt>200</tt>
and the following headers:</t>

<ul spacing="compact">
<li><tt>Access-Control-Allow-Origin: *</tt> (or a site-specific
origin policy)</li>
<li><tt>Access-Control-Allow-Methods: GET, HEAD</tt></li>
<li><tt>Access-Control-Allow-Headers: TAI-Nonce</tt></li>
<li><tt>Access-Control-Expose-Headers: TAI-Leap-Seconds,
TAI-Nonce, TAI-Key-Selector, TAI-Signature</tt></li>
<li><tt>Access-Control-Max-Age: 600</tt> (or a higher operator-
chosen value)</li>
</ul>
<t>A browser making a cross-origin request that includes the
<tt>TAI-Nonce</tt> header will send an <tt>OPTIONS</tt> preflight before
the actual <tt>GET</tt>; without the appropriate
<tt>Access-Control-*</tt> response headers the browser will block
the request. Non-browser clients are unaffected.</t>
<t>Servers SHOULD emit <tt>Access-Control-Max-Age</tt> with a
value of at least 600 seconds. Pre-flight responses
carry no per-request data and are safe to cache for
the lifetime of the deployed CORS configuration.
In the absence of this header, browser pre-flight
cache lifetimes diverge significantly across
implementations, which may cause a pre-flight
request to precede every cross-origin GET in
high-traffic deployments.</t>
</section>

<section anchor="successful-response"><name>Successful Response</name>
<t>A successful <tt>GET</tt> response has status <tt>200 OK</tt> and a
body consisting of exactly 25 ASCII bytes: the TAI64N
external representation of the server's current time,
formatted per <xref target="TAI64"></xref>:</t>

<artwork><![CDATA['@' hex16(seconds-since-TAI-epoch) hex8(nanoseconds)
]]></artwork>
<t>where <tt>hex16</tt> and <tt>hex8</tt> denote 16 and 8 lowercase
hexadecimal digits respectively. The leading <tt>@</tt> is
literal.</t>
<t>The following headers MUST be present:</t>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>Content-Type</tt></td>
<td><tt>application/tai64n</tt></td>
</tr>

<tr>
<td><tt>Content-Length</tt></td>
<td><tt>25</tt></td>
</tr>

<tr>
<td><tt>Cache-Control</tt></td>
<td><tt>no-store</tt></td>
</tr>

<tr>
<td><tt>TAI-Leap-Seconds</tt></td>
<td>sf-integer, e.g. <tt>37</tt></td>
</tr>
</tbody>
</table><t><tt>TAI-Leap-Seconds</tt> carries the offset, in seconds,
between TAI and UTC at the moment of the response,
as a Structured Field Integer <xref target="RFC9651"></xref>. leap-seconds
emitted MUST be a non-negative integer in [0, 2^32−1];
sf-integer supports values up to ±10^15 but values outside
[0, 2^32−1] cannot be encoded in <tt>leap-u32be</tt>. A verifier
that receives a <tt>TAI-Leap-Seconds</tt> value outside this range
MUST treat the response as unsigned.
Its value is included in the signed payload as <tt>leap-u32be</tt>
(see <xref target="framed-payload"></xref>). A server MUST emit it on
every successful response. It is a singleton field
(Section 5.5 of <xref target="RFC9110"></xref>) and MUST NOT appear
more than once in a response. A recipient that
encounters more than one instance MUST treat the field
as absent per Section 4.2 of <xref target="RFC9651"></xref>;
the recipient then has no server-supplied TAI-to-UTC
offset for this response and MUST treat the response
as unsigned regardless of whether <tt>TAI-Signature</tt> is present.</t>
</section>

<section anchor="nonce-echo"><name>Nonce Echo</name>
<t>A client MAY send a request header <tt>TAI-Nonce</tt> carrying
an opaque byte string encoded as a Structured Field
Binary <xref target="RFC9651"></xref>. If present on a <tt>GET</tt> request, the server
MUST include a <tt>TAI-Nonce</tt> response field whose
<tt>sf-binary</tt> value encodes the same octet sequence
as the request field. <tt>TAI-Nonce</tt> is a
singleton field (Section 5.5 of <xref target="RFC9110"></xref>) and MUST
NOT appear more than once in a request or response. A
recipient that encounters more than one instance MUST
treat the field as absent per Section 4.2 of
<xref target="RFC9651"></xref>. A server that treats a request <tt>TAI-Nonce</tt>
as absent for this reason MUST NOT sign the response.
A server MAY instead respond with <tt>400 Bad Request</tt> when
a request contains duplicate <tt>TAI-Nonce</tt> fields.
A client that treats a response <tt>TAI-Nonce</tt> as absent
MUST treat the response as unsigned regardless of
whether <tt>TAI-Signature</tt> is present.</t>
<t>A server MUST NOT interpret the nonce. If a server
receives a <tt>TAI-Nonce</tt> that is a malformed sf-binary
it MUST treat the field as absent and MUST NOT sign
the response. The decoded <tt>sf-binary</tt> value MUST contain at least
7 octets and MUST NOT exceed 129 octets. When
serialized as <tt>sf-binary</tt>, these correspond exactly
to field values of 14 octets (: + 12 base64 +
padding + :) and 174 octets (: + 172 base64 + :)
respectively; implementations MAY reject on wire
length before decoding as an optimisation, but the
normative check is on decoded length.</t>
<t>The lower bound of 7 decoded octets provides
sufficient client-supplied entropy for the replay
defence in this section. The upper bound of 129
decoded octets caps the nonce's contribution to
overall response size; combined with the 25-byte body and
the other protocol-defined response headers, the
protocol-mandated fields contribute at most
approximately 530 bytes in total, excluding transport
framing and intermediary-added headers. Implementations
that require
larger nonces for compatibility with a higher-entropy
convention may negotiate a future extension; this
version of the protocol holds the bounds fixed for
interoperability.</t>
<t>A server that receives a <tt>TAI-Nonce</tt>
outside this range MUST NOT sign the response.
A zero-length or syntactically empty field value is below
the minimum and MUST be treated as absent.
Clients SHOULD choose nonces that are unpredictable to a
network observer when freshness matters; 16 random
bytes encoded as an sf-binary value are sufficient for
typical use.</t>
<t>When a client included a <tt>TAI-Nonce</tt> in the request,
the trust level of the response is determined as follows.
If the response carries no <tt>TAI-Nonce</tt> field, the response
is Plain (level 0) regardless of whether a signature is
present; a conformant server only signs responses that echo
a nonce (see <xref target="signing"></xref>), so any signature in such a
response MUST be ignored. If the response carries a
<tt>TAI-Nonce</tt> whose decoded octets do not match the request
nonce, the response is Inconsistent (level -1) and MUST be
rejected regardless of signature state. If the response
carries a <tt>TAI-Nonce</tt> whose decoded octets match the request
nonce, the trust level depends on the key/signature state as
defined in <xref target="trust-levels"></xref>. The key/signature state is
Unresolvable when <tt>TAI-Key-Selector</tt> is malformed or
present but does not resolve in DNS.
These cases are summarised in the following table.</t>
<figure><name>Client trust level by response nonce and key/signature state.
</name>
<artwork><![CDATA[+----------------+---------+--------------+--------------+-------+
| Response nonce | Matches | Key/Sig      | Outcome      | Level |
+----------------+---------+--------------+--------------+-------+
| Absent         | N/A     | Any          | Plain        |  0    |
| Present        | No      | Any          | Inconsistent | -1    |
| Present        | Yes     | Absent       | Unique       |  1    |
| Present        | Yes     | Malformed    | Unique       |  1    |
| Present        | Yes     | Unresolvable | Unique       |  1    |
| Present        | Yes     | Invalid      | Inconsistent | -1    |
| Present        | Yes     | Valid        | Signed       |  2    |
+----------------+---------+--------------+--------------+-------+
]]></artwork>
</figure>
<t>A network attacker who strips the <tt>TAI-Nonce</tt> from the
outgoing request causes the server to respond at level 0.
A client that requires level 1 or above treats this as a
denial of service; a client that accepts level 0 receives
no freshness guarantee.</t>
</section>
</section>

<section anchor="signing"><name>Signing</name>
<t>A Taistamp service MAY be configured with an Ed25519
<xref target="RFC8032"></xref> signing key and a selector. When both are
configured, the server signs every response that
carries a <tt>TAI-Nonce</tt>.</t>
<t>A signed successful response carries two additional headers:</t>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>TAI-Key-Selector</tt></td>
<td>the selector, as an sf-token</td>
</tr>

<tr>
<td><tt>TAI-Signature</tt></td>
<td>the Ed25519 signature, as an sf-binary</td>
</tr>
</tbody>
</table><t>The selector MUST match the following ABNF production
<xref target="RFC5234"></xref>. The grammar is constrained to the subset
of DNS LDH labels (<xref target="RFC1035"></xref> Section 2.3.1) that are
also valid sf-tokens (<xref target="RFC9651"></xref>); the leading-ALPHA
requirement follows from sf-token, not from DNS alone,
because sf-token requires its first character to be
ALPHA or <tt>*</tt> (<xref target="RFC9651"></xref> Section 4.1.7), excluding
digit-leading labels that DNS would otherwise permit:</t>

<artwork><![CDATA[selector = ALPHA [ *61( ALPHA / DIGIT / "-" )
                   ( ALPHA / DIGIT ) ]
]]></artwork>
<t><tt>TAI-Key-Selector</tt> is a singleton field
(Section 5.5 of <xref target="RFC9110"></xref>) and MUST NOT appear more
than once in a response. A recipient that encounters
more than one instance MUST treat the field as absent
per Section 4.2 of <xref target="RFC9651"></xref>; the response MUST
then be treated as unsigned.</t>
<t><tt>TAI-Signature</tt> is a singleton field (Section 5.5 of
<xref target="RFC9110"></xref>) and MUST NOT appear more than once in a
response. A recipient that encounters more than one
instance MUST treat the field as absent per Section
4.2 of <xref target="RFC9651"></xref>; the response MUST then be treated
as unsigned.</t>
<t>A response that carries <tt>TAI-Signature</tt> without
<tt>TAI-Key-Selector</tt> MUST be treated as unsigned; the
absent selector yields key/signature state Absent, as
if no signature were present.</t>

<section anchor="framed-payload"><name>Framed Payload</name>
<t>The signed payload is the concatenation, in order, of
the following fields:</t>

<artwork><![CDATA[domain-tag    = "taistamp-v1" %x00
label-bytes   = 25 octets ; the response body
leap-u32be    = 4 octets  ; TAI-Leap-Seconds,
              ;   big-endian unsigned
selector-len  = 1 octet   ; length of selector-bytes, 1..63
selector-bytes = 1..63 octets ; the selector, ASCII
nonce-bytes   = octets    ; decoded sf-binary octets
              ;   of the TAI-Nonce field value

payload = domain-tag label-bytes leap-u32be
          selector-len selector-bytes nonce-bytes
]]></artwork>
<t>nonce-bytes is the octet sequence obtained by
parsing the <tt>TAI-Nonce</tt> field value as an
<tt>sf-binary</tt> item per <xref target="RFC9651"></xref> and decoding
the base64 value. The textual representation
of the field is not part of the signed input;
only the decoded bytes are signed.</t>
<t>The trailing NUL on <tt>domain-tag</tt> is significant: it
prevents extension of the tag into adjacent bytes by
an attacker who controls the protocol version string.</t>
<t>The selector is length-prefixed by a single byte. A
downgrade attacker who rewrites <tt>TAI-Key-Selector</tt> on
the wire cannot match an existing signature without
also matching its length, as the signature is bound
to the exact payload including the length byte;
forging a valid signature for a different selector
length is computationally infeasible under the
existential unforgeability of Ed25519.</t>
<t>The nonce occupies the tail of the payload. Its length
is therefore self-delimiting: a verifier computes
<tt>nonce-bytes</tt> from the decoded <tt>TAI-Nonce</tt> sf-binary
value and appends it as the final payload component,
with no explicit length field required.</t>
</section>

<section anchor="when-to-sign"><name>When to Sign</name>
<t>A server MUST sign a response if and only if all of
the following hold:</t>

<ul spacing="compact">
<li>The server is configured with a key and selector.</li>
<li>The request carried a <tt>TAI-Nonce</tt> header.</li>
<li>The response is a 200 to <tt>GET</tt>.</li>
</ul>
<t>A request without a nonce MAY receive an unsigned
response even when the server is configured to sign.
This allows lightweight unsigned probes to coexist with
authenticated reads, though the response itself MUST
remain <tt>Cache-Control: no-store</tt> because the label is
time-sensitive.</t>
</section>

<section anchor="intermediary-handling"><name>Intermediary Handling</name>
<t>An intermediary (proxy or gateway) that forwards a
Taistamp request or response MUST forward the
<tt>TAI-Nonce</tt>, <tt>TAI-Leap-Seconds</tt>, <tt>TAI-Key-Selector</tt>,
and <tt>TAI-Signature</tt> fields intact and MUST NOT modify
or remove them. Stripping or rewriting <tt>TAI-Nonce</tt> on
a request prevents the server from signing the response.
Stripping or rewriting any of the response fields
prevents the client from verifying the signature.</t>
<t>These requirements apply to Taistamp-aware intermediaries.
General-purpose proxies that do not recognise <tt>TAI-*</tt> fields
may strip or modify them without violating their own
specifications. Operators who require signed responses SHOULD
avoid placing non-transparent intermediaries between client and
server, or verify that any intermediaries are configured to
forward these fields intact.</t>
</section>
</section>

<section anchor="key-publication"><name>Key Publication</name>
<t>This section applies only to servers configured
with a signing key and selector.</t>
<t>A server publishes its verification key as a DNS
TXT record. The owner name is</t>

<artwork><![CDATA[<selector> "._taistamp." <host>
]]></artwork>
<t>where <tt>&lt;host&gt;</tt> is the authority component of the URL at
which <tt>/.well-known/taistamp</tt> is served, and
<tt>&lt;selector&gt;</tt> is the value the server emits in
<tt>TAI-Key-Selector</tt>.</t>
<t>The TXT record value is a single string, of length at
most 255 octets, formatted as a tag-value list in the
DKIM/DMARC style:</t>

<artwork><![CDATA[v=tai1; k=ed25519; p=<base64-of-32-raw-pubkey-bytes>
]]></artwork>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Value</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>v</tt></td>
<td>Protocol version. <tt>tai1</tt> for the framing in this document.</td>
</tr>

<tr>
<td><tt>k</tt></td>
<td>Key algorithm. <tt>ed25519</tt> is the only algorithm currently defined.</td>
</tr>

<tr>
<td><tt>p</tt></td>
<td>Public key, standard base64 of the 32 raw Ed25519 public key bytes.</td>
</tr>
</tbody>
</table><t>Parsing is done as specified in Section 3.2 of <xref target="RFC6376"></xref>.
A verifier MUST treat unknown tags as informational.
A verifier MUST treat an unknown <tt>v</tt> or <tt>k</tt> as an
unrecoverable failure for that selector and MUST NOT
fall back silently.</t>

<section anchor="key-rotation"><name>Key Rotation</name>
<t>Key rotation follows a publish-then-switch-then-retire
sequence.</t>
<t>Operators SHOULD choose a DNS TTL for the TXT
record that balances query load against key rotation
speed. A TTL between 3600 seconds (1 hour) and
86400 seconds (24 hours) is RECOMMENDED for stable
keys. When an operator plans to rotate a key, they
SHOULD lower the TTL of the existing TXT record
well in advance of the rotation to ensure that
verifier caches clear quickly once the new key
is deployed.</t>
<t><strong>Publish.</strong> The operator publishes a new TXT record
under a new selector and waits at least one DNS TTL
interval for propagation before reconfiguring the
server.</t>
<t><strong>Switch.</strong> The server is reconfigured to sign
responses with the new key and selector.</t>
<t><strong>Retire.</strong> The old TXT record SHOULD remain in DNS
for at least one DNS TTL interval after the switch,
to allow verifiers that cached the old record to
complete any in-flight verification. The old TXT
record MAY then be removed.</t>

<section anchor="verifier-key-caching"><name>Verifier Key Caching</name>
<t>A verifier MAY cache the parsed public key for a
selector. Verifiers MUST respect the DNS TTL
returned by the resolver and MUST NOT use a cached
entry beyond that TTL. Verifiers MUST NOT apply a
hardcoded minimum or maximum cache lifetime that
overrides the TTL set by the authoritative operator.
If verification of a cached key fails, the verifier
SHOULD re-fetch the TXT record once before treating
the signature as invalid.</t>
</section>

<section anchor="key-revocation"><name>Key Revocation</name>
<t>If a signing key is compromised, the operator MUST
remove its TXT record immediately, without a
retirement period. Removing the TXT record is the
only revocation mechanism defined by this protocol;
no out-of-band revocation channel is specified.</t>
<t>Verifiers that have cached the public key will
continue to accept signatures made with it until
their cached entry's DNS TTL expires. The exposure
window is therefore bounded by the TTL remaining on
the cached entry at the moment of revocation.
Operators who require rapid revocation SHOULD
configure a short TTL on key records, accepting the
trade-off of increased DNS query load.</t>
</section>
</section>
</section>

<section anchor="trust-levels"><name>Trust Levels</name>
<t>A Taistamp response is assigned one of four trust levels,
combining the nonce echo result and the key/signature state.</t>
<t><strong>Level 2 — Signed.</strong> The response <tt>TAI-Nonce</tt> matches the
request nonce and the <tt>TAI-Signature</tt> verifies against the
public key resolved from <tt>TAI-Key-Selector</tt>. The time value
is both fresh and authenticated.</t>
<t><strong>Level 1 — Unique.</strong> The response <tt>TAI-Nonce</tt> matches the
request nonce, but the signature is absent, the
<tt>TAI-Key-Selector</tt> is malformed, or the <tt>TAI-Key-Selector</tt>
does not resolve in DNS. The time value is fresh but
unauthenticated. A malformed or unresolvable selector
indicates a configuration error or that the signing key has
not yet propagated in DNS; this is a soft degradation, not
a failure.</t>
<t><strong>Level 0 — Plain.</strong> The response carries no echoed
<tt>TAI-Nonce</tt>. The time value carries no freshness or
authentication guarantee from this protocol.</t>
<t><strong>Level -1 — Inconsistent.</strong> Either the response
<tt>TAI-Nonce</tt> is present but does not match the request
nonce, or the <tt>TAI-Key-Selector</tt> resolves but the
<tt>TAI-Signature</tt> does not verify. The response MUST be
rejected.</t>
<t>A client MUST NOT use a response at trust level -1.
Whether a client accepts level 0 or level 1 when a higher
level is expected is an application policy decision outside
the scope of this document.</t>
</section>

<section anchor="verification"><name>Verification</name>
<t>This section applies when the response carries both
<tt>TAI-Key-Selector</tt> and <tt>TAI-Signature</tt>. A verifier
reads the response and reconstructs the framed
payload as defined in <xref target="signing"></xref>. The verifier MUST:</t>

<ol spacing="compact">
<li>Confirm that the decoded <tt>sf-binary</tt> octets of
the response <tt>TAI-Nonce</tt> match those of the
request <tt>TAI-Nonce</tt>.</li>
<li>Confirm that the <tt>TAI-Key-Selector</tt> value
is a well-formed selector matching the ABNF
in <xref target="signing"></xref>.</li>
<li>Resolve the TXT record at
<tt>&lt;selector&gt;._taistamp.&lt;host&gt;</tt> and parse the
<tt>v</tt>, <tt>k</tt>, and <tt>p</tt> tags.</li>
<li>Verify the Ed25519 signature using the public key
from <tt>p</tt> over the framed payload.</li>
</ol>
<t>Each step produces a trust level outcome. If step 1 fails
(nonce mismatch), the response is Inconsistent (level -1)
and MUST be rejected. If step 2 fails (malformed selector)
or step 3 yields no DNS record (unresolvable selector),
signature verification stops and the response is Unique
(level 1). If step 4 fails (signature does not verify),
the response is Inconsistent (level -1) and MUST be
rejected. If all four steps succeed, the response is
Signed (level 2). Trust levels are defined in
<xref target="trust-levels"></xref>.</t>
<t>Verifiers MUST use the strict verification procedure
described in Section 5.1.7 of <xref target="RFC8032"></xref>.</t>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>

<section anchor="threat-model"><name>Threat Model</name>
<t>Taistamp authenticates a single time reading at the
moment a client asks. It does not establish a long-
running synchronised clock and does not bound network
delay; a client is responsible for treating the
returned label as a measurement subject to the
round-trip time of its request. The trust level
framework (see <xref target="trust-levels"></xref>) expresses the combined
freshness and authentication guarantees that a response
carries.</t>
</section>

<section anchor="replay"><name>Replay</name>
<t>A response is bound to its request only via the
<tt>TAI-Nonce</tt> echo and its inclusion in the signed
payload. A client that omits the nonce receives a
Plain (level 0) response with no freshness guarantee.
A client that reuses a nonce across requests gains no
replay protection even at level 1 or 2, since the nonce
no longer uniquely identifies the exchange. Clients
SHOULD generate a fresh, unpredictable nonce for each
request.</t>
<t>Replay protection is scoped to the client's own nonce
uniqueness enforcement. The protocol does not prevent
a recorded request/response pair from being presented
to a second application that does not track nonce
lifecycle. Clients SHOULD treat each Unique (level 1)
or Signed (level 2) response as valid for a single use
and SHOULD NOT accept a response whose nonce was issued
by a previous request.</t>
</section>

<section anchor="domain-separation"><name>Domain Separation</name>
<t>The <tt>taistamp-v1%x00</tt> prefix prevents an Ed25519 key
configured for Taistamp from being induced to sign a
message valid under another protocol that happens to
embed the same suffix. Operators MUST NOT reuse a
Taistamp signing key for any other purpose.</t>
</section>

<section anchor="dns-trust"><name>DNS Trust</name>
<t>A Signed (level 2) response reduces to trusting the
DNS lookup of the TXT record at
<tt>&lt;selector&gt;._taistamp.&lt;host&gt;</tt>. A verifier that does not
validate DNS responses (for instance via DNSSEC or a
trusted resolver path) is vulnerable to a DNS attacker
who can substitute a public key and then sign responses
with the corresponding private key, producing a forged
time value that verifies successfully. Unlike DKIM,
where key substitution causes verification failures,
here it enables complete spoofing. Without authenticated
DNS, a response that appears Signed (level 2) provides
no stronger guarantee than Unique (level 1). A Unique
(level 1) response does not involve DNS key resolution
and is therefore not subject to this attack, though it
provides no key authentication.</t>
<t>Operators SHOULD publish under a DNSSEC-signed zone.
Verifiers SHOULD validate DNSSEC signatures or use a
resolver path that provides equivalent guarantees.</t>
</section>

<section anchor="clock-manipulation"><name>Clock Manipulation</name>
<t>A signature attests that the server emitted a given
label; it does not attest that the server's clock is
correct. An operator whose clock is drifting or
deliberately set wrong will sign whatever label its
clock produces. Clients that depend on Taistamp for
high-stakes decisions SHOULD compare readings from
multiple independently operated services.</t>
</section>

<section anchor="leap-second-reporting"><name>Leap-Second Reporting</name>
<t>The <tt>TAI-Leap-Seconds</tt> header is included in the
signed payload as <tt>leap-u32be</tt>; the signature attests
that the server emitted this specific value, but not
that it is correct. Signing it allows the verifier to
reconstruct the server's own UTC interpretation of the
timestamp, even though the verifier need not trust that
value for UTC conversion purposes. An operator whose
leap-second table is wrong, or who deliberately sets it
wrong, will sign whatever count they emit. Clients who
require UTC equivalence MUST source the TAI-to-UTC
offset from a trusted table rather than from the
server.</t>
</section>

<section anchor="key-revocation-window"><name>Key Revocation Window</name>
<t>When a TXT record is deleted to revoke a compromised
key, verifiers that have cached the corresponding public
key will continue to assign Signed (level 2) to
responses made with that key until their cached entry's
DNS TTL expires. An attacker in possession of the
compromised private key can forge responses during this
window; the forged signatures will verify against the
cached key, assigning Signed (level 2) when the correct
assignment — given the revoked key — would be Unique
(level 1) at best.</t>
<t>The exposure window is bounded by the DNS TTL
remaining on the cached entry at the moment of
revocation. Verifiers that honour the TTL limit required
by <xref target="verifier-key-caching"></xref> will naturally limit this
window to at most one TTL interval. Operators who
require a short revocation window SHOULD configure a
low DNS TTL on key records.</t>
</section>

<section anchor="selector-based-dns-amplification"><name>Selector-Based DNS Amplification</name>
<t>A verifier that performs one DNS lookup per incoming response
carrying a selector that is malformed or does not resolve
(Unresolvable key/signature state, yielding Unique (level 1)
per <xref target="trust-levels"></xref>) amplifies
query load onto the authoritative server publishing the
<tt>_taistamp.&lt;host&gt;</tt> TXT records. Two cases arise: a cached key
that fails verification triggers a re-fetch per
<xref target="verifier-key-caching"></xref>; a previously unseen selector triggers
a first-time lookup, which the per-name re-fetch limit does not
bound. Verifiers MUST apply rate-limiting or exponential back-off
on all DNS lookups for names under <tt>_taistamp.&lt;host&gt;</tt>,
regardless of whether the lookup is a first fetch or a
re-fetch.</t>
</section>

<section anchor="caching-intermediaries"><name>Caching Intermediaries</name>
<t>Note: this section is non-normative.</t>
<t>Successful <tt>GET</tt> responses carry <tt>Cache-Control: no-store</tt>
(see <xref target="successful-response"></xref>), which instructs intermediaries
not to retain or serve the response from cache. However,
CDN and proxy configurations sometimes override this
directive, for instance via path-based cache rules that
match <tt>/.well-known/</tt> resources. A client that included a
<tt>TAI-Nonce</tt> is protected: a cached response carries a
different nonce, producing Inconsistent (level -1), which
MUST be rejected. A client that did not include a <tt>TAI-Nonce</tt>
receives a Plain (level 0) response with no indication that
the time value is stale.</t>
<t>Operators deploying Taistamp behind a CDN or reverse
proxy SHOULD explicitly configure the intermediary to
pass <tt>GET</tt> responses to <tt>/.well-known/taistamp</tt> through
without caching, and SHOULD verify this behaviour
before relying on Signed (level 2) responses for
time-sensitive decisions.</t>
</section>
</section>

<section anchor="iana-considerations"><name>IANA Considerations</name>

<section anchor="well-known-uri"><name>Well-Known URI</name>
<t>IANA is requested to register the following entry in
the &quot;Well-Known URIs&quot; registry <xref target="RFC8615"></xref>:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
</tr>
</thead>

<tbody>
<tr>
<td>URI Suffix</td>
<td><tt>taistamp</tt></td>
</tr>

<tr>
<td>Change Controller</td>
<td>Apptly Software Ltd</td>
</tr>

<tr>
<td>Reference</td>
<td>This document</td>
</tr>

<tr>
<td>Status</td>
<td>permanent</td>
</tr>

<tr>
<td>Related Information</td>
<td>none</td>
</tr>
</tbody>
</table></section>

<section anchor="media-type"><name>Media Type</name>
<t>IANA is requested to register the media type
<tt>application/tai64n</tt> in the &quot;Media Types&quot; registry
per <xref target="RFC6838"></xref>:</t>

<ul spacing="compact">
<li>Type name: <tt>application</tt></li>
<li>Subtype name: <tt>tai64n</tt></li>
<li>Required parameters: none</li>
<li>Optional parameters: none</li>
<li>Encoding considerations: 7-bit (ASCII)</li>
<li>Security considerations: see <xref target="security-considerations"></xref></li>
<li>Interoperability considerations: none</li>
<li>Published specification: this document</li>
<li>Applications that use this media type: time-
authentication clients</li>
<li>Fragment identifier considerations: none</li>
<li>Restrictions on usage: none</li>
<li>Provisional registration: no</li>
<li>Author/Change controller: Apptly Software Ltd</li>
</ul>
</section>

<section anchor="header-fields"><name>Header Fields</name>
<t>IANA is requested to register the following entries in
the &quot;Hypertext Transfer Protocol (HTTP) Field Name
Registry&quot; <xref target="RFC9110"></xref>:</t>
<table>
<thead>
<tr>
<th>Field Name</th>
<th>Status</th>
<th>Structured Type</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>TAI-Leap-Seconds</tt></td>
<td>permanent</td>
<td>Integer</td>
<td>this document</td>
</tr>

<tr>
<td><tt>TAI-Nonce</tt></td>
<td>permanent</td>
<td>Binary</td>
<td>this document</td>
</tr>

<tr>
<td><tt>TAI-Key-Selector</tt></td>
<td>permanent</td>
<td>Token</td>
<td>this document</td>
</tr>

<tr>
<td><tt>TAI-Signature</tt></td>
<td>permanent</td>
<td>Binary</td>
<td>this document</td>
</tr>
</tbody>
</table></section>
</section>

</middle>

<back>
<references><name>References</name>
<references><name>Normative References</name>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.1035.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.2119.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.5234.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.6376.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8032.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8174.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8615.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9651.xml"/>
<reference anchor="TAI64" target="https://cr.yp.to/libtai/tai64.html">
  <front>
    <title>TAI64, TAI64N, and TAI64NA</title>
    <author fullname="Daniel J. Bernstein" initials="D. J." surname="Bernstein"></author>
    <date year="1997"></date>
  </front>
</reference>
</references>
<references><name>Informative References</name>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.3161.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.4033.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.5905.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.6838.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8446.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8915.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9110.xml"/>
<reference anchor="Roughtime" target="https://datatracker.ietf.org/doc/draft-ietf-ntp-roughtime/">
  <front>
    <title>Roughtime</title>
    <author fullname="Watson Ladd" initials="W." surname="Ladd"></author>
    <author fullname="Marcus Dansarie" initials="M." surname="Dansarie"></author>
    <date year="2026"></date>
  </front>
  <annotation>Aanchal Malhotra and Adam Langley authored early drafts of this document.</annotation>
</reference>
<reference anchor="TAICLOCK" target="https://cr.yp.to/proto/taiclock.txt">
  <front>
    <title>TAICLOCK</title>
    <author fullname="Daniel J. Bernstein" initials="D. J." surname="Bernstein"></author>
    <date year="1997"></date>
  </front>
</reference>
</references>
</references>

</back>

</rfc>
