Tspex.verify/3 answers one question: did a TSA that you trust sign a
statement that this data existed at this time? This guide says what is
checked to answer it, what is left to you, and what to do with each error.
Two ways to ask
Fresh. You just asked a TSA, and you hold the Tspex.Request:
Tspex.verify(response_der, request, trust: roots)The nonce and the policy of the request are compared with the token, and the imprint with the hash in the request. This is the only mode where a stale or replayed answer is caught: the nonce is what ties the answer to this request.
Stored. You kept a token and the data, and you check them later:
Tspex.verify(token_der, {:data, data}, trust: roots)The data is hashed with the algorithm the token declares (if you allow it). There is no nonce to compare; a nonce in the token is only information.
The chain is judged at the genTime
A TSA certificate lives a few years and a time-stamp has to outlast it, so the chain is validated as of the time in the token, not today. A token made in 2024 by a certificate that expired in 2026 is still good, if the certificate was valid in 2024 and the chain (the root included) was too.
This is also what makes the date a claim: the TSA can sign any genTime, and
nothing in the token proves it was made then. What stops a TSA from lying
about the past is its reputation, its audits, its key being kept in a
module, and the revocation of the key if it is ever compromised. Which is
why two things below are yours.
What you decide
- The roots.
:trustis required. It is your list of TSAs you accept.trust: :noneis allowed and explicit: the result then saystrust: :noneand proves only that some key signed this imprint. - Revocation. The library does not fetch a CRL or an OCSP answer.
Without
:revocation,stamp.revocationis:not_checked. If the key of the TSA was revoked forkeyCompromise(or with no reason), every token of that key is void; for another reason, only those made after the revocation (RFC 3161, section 4). Give a function that gets the validated path and thegenTime, and returns:okor{:error, reason}; it is called after everything else passed. - The policy.
:policieslists the TSA policies you accept. Without it any policy passes, and the policy of a request, if it had one, must still be the one of the token. - The clock.
:max_skewcompares thegenTimewith the local clock, allowing for the accuracy the TSA states. It is off by default because the time of a stored token is old. Use it for a fresh answer. - How long it lasts. A token is evidence while its hash and signature algorithms hold. For decades, stamp again (an archive time-stamp, outside this library) before they weaken.
A revocation check from CRLs you hold
The library does not go to the network, but :revocation is where you plug
what you have. This module turns CRLs you already fetched (DER, the CRL of
each issuer in the path) into that function. It follows RFC 3161, section 4:
a certificate revoked for keyCompromise or without a reason voids every
token of its key, and one revoked for another reason voids only the tokens
made at or after the revocation date.
defmodule MyApp.TsaRevocation do
@crl_reason {2, 5, 29, 21}
@voids_every_token [:unspecified, :keyCompromise, :cACompromise]
@doc "The `:revocation` function of `Tspex.verify/3`, from CRLs in DER."
def check(crls, now \\ DateTime.utc_now()) do
fn path, gen_time -> path_ok(path, gen_time, crls, now) end
end
# The path is root first. The root is a trust anchor and has no CRL here.
defp path_ok(path, gen_time, crls, now) do
path
|> Enum.chunk_every(2, 1, :discard)
|> Enum.find_value(:ok, fn [issuer, certificate] ->
case certificate_ok(issuer, certificate, gen_time, crls, now) do
:ok -> nil
{:error, _} = error -> error
end
end)
end
defp certificate_ok(issuer, certificate, gen_time, crls, now) do
{:OTPCertificate, tbs, _, _} = :public_key.pkix_decode_cert(certificate, :otp)
serial = elem(tbs, 2)
# a CRL counts only if the issuer signed it
case Enum.find(crls, &:public_key.pkix_crl_verify(&1, issuer)) do
nil -> {:error, :no_crl}
crl -> in_crl(:public_key.der_decode(:CertificateList, crl), serial, gen_time, now)
end
end
defp in_crl({:CertificateList, tbs, _, _}, serial, gen_time, now) do
{:TBSCertList, _, _, _, this_update, next_update, revoked, _} = tbs
cond do
DateTime.compare(to_datetime(this_update), now) == :gt ->
{:error, :crl_not_yet_valid}
next_update != :asn1_NOVALUE and DateTime.compare(to_datetime(next_update), now) != :gt ->
{:error, :crl_expired}
true ->
revoked_at(revoked, serial, gen_time)
end
end
defp revoked_at(:asn1_NOVALUE, _serial, _gen_time), do: :ok
defp revoked_at(revoked, serial, gen_time) do
case List.keyfind(revoked, serial, 1) do
nil ->
:ok
{_, _, date, extensions} ->
if reason(extensions) in @voids_every_token or
DateTime.compare(to_datetime(date), gen_time) != :gt,
do: {:error, :revoked},
else: :ok
end
end
defp reason(:asn1_NOVALUE), do: :unspecified
defp reason(extensions) do
case List.keyfind(extensions, @crl_reason, 1) do
{:Extension, _, _, value} -> :public_key.der_decode(:CRLReason, value)
nil -> :unspecified
end
end
defp to_datetime({:utcTime, [y1, y2 | rest]}) do
year = List.to_integer([y1, y2])
to_datetime(
{:generalTime, Integer.to_charlist(if(year >= 50, do: 1900, else: 2000) + year) ++ rest}
)
end
defp to_datetime({:generalTime, chars}) do
<<y::binary-4, mo::binary-2, d::binary-2, h::binary-2, mi::binary-2, s::binary-2, "Z">> =
List.to_string(chars)
DateTime.new!(
Date.new!(String.to_integer(y), String.to_integer(mo), String.to_integer(d)),
Time.new!(String.to_integer(h), String.to_integer(mi), String.to_integer(s)),
"Etc/UTC"
)
end
endUse it with the CRLs you hold:
Tspex.verify(token, {:data, document},
trust: roots,
revocation: MyApp.TsaRevocation.check([intermediate_crl, root_crl])
)What it does not do: it does not fetch anything, it reads no delta CRL, no
indirect CRL and no CRL split by distribution point, and it asks for a CRL
that is current now (now), not at the genTime, because what you want to
know is whether the key has been revoked since. A path with no CRL for one of
its issuers is {:error, :no_crl} and the token is refused, so give a CRL for
every issuer in the path. For OCSP, or for CRLs by distribution point, the
OTP has :public_key.pkix_crls_validate/3 and :public_key.pkix_ocsp_validate/5,
and the function you give can call them.
The CRLs inside a TimeStampedData
In a chain of Tspex.TimeStampedData, the element before each new token may
carry a CRL that vouches for the TSA of that element, made when the next token
was added. Tspex.TimeStampedData.verify/2 returns them in crls and does not
judge them. The recipe does, as of the time the next token was issued, which
is what now is for:
{:ok, result} = Tspex.TimeStampedData.verify(der, trust: roots)
issued = result.stamps |> tl() |> Enum.map(& &1.info.gen_time)
for {stamp, crl, issued_at} <- Enum.zip([result.stamps, result.crls, issued]), crl do
MyApp.TsaRevocation.check([crl], issued_at).(stamp.signer.path, stamp.info.gen_time)
endEach result is :ok or {:error, reason}. The CRL has to cover every issuer of
the path of that token, so with an intermediate CA the element needs more than
one, and the recipe says :no_crl for the one it does not find.
Checks, in order
Size and form; status and failInfo; the token; the SignedData and the
type of its content; one signer; the version of the TSTInfo; the signature;
the signing-certificate attribute; the rules for the certificate of a TSA; the
chain at the genTime; the imprint; the nonce; the policy; the tsa name;
critical extensions; the clock; :revocation. The error you get is the first
one that fails.
The certificate rules come before the chain so that a certificate that is not for time-stamping is reported as that, and not as an obscure error of the OTP's path validation.
Errors
reason | What happened | What to do |
|---|---|---|
:malformed, :limit_exceeded | Not a response or token, or beyond a limit | Do not retry the same bytes |
:rejection | The TSA refused; detail.fail_info says why (:bad_alg, :bad_request, ...) | Fix the request, or the TSA you ask |
:waiting, :revocation_warning, :revocation_notification | A status that is not a time-stamp | Treat as a failure |
:token_invalid | The signature or the algorithm of the token is refused | Not a valid token |
:bad_tsa_certificate | The certificate is not one a TSA may use (detail says which rule) | The TSA or its certificate is wrong |
:untrusted | The chain does not lead to your roots at the genTime | Wrong root, or an expired chain at that time |
:imprint_mismatch | The data is not what was stamped | The data was changed, or it is another file |
:nonce_mismatch, :nonce_missing | The answer is not for this request | Do not use it |
:policy_mismatch, :tsa_name_mismatch | Not the policy you want, or the tsa name is not the certificate's | |
:weak_algorithm | MD5, or SHA-1 without allow_sha1: true | Some TSAs still sign with SHA-1; opt in knowingly |
:gen_time_skew | The time differs from your clock by more than :max_skew | |
:transport | The network failed (stamp/2); detail says how | Retry is up to you; nothing is retried here |
The full list is in Tspex.Error.
Where the time-stamp goes
For a signature in a CMS message, see Tspex.add_signature_time_stamp/4 (the
stamp over the signature value, an unsigned attribute) and
Tspex.verify_signature_at_time_stamp/2 (the signature and its certificate,
judged as of the stamp). For the content, see Tspex.content_time_stamp_attribute/1.
For a document kept with a chain of time-stamps in one file, see
Tspex.TimeStampedData: verify/2 verifies every token as Tspex.verify/3
does, each against the certificate of its own TSA at its own genTime, and the
first against the document, the others against the element before them. The
time it returns is that of the first token, and a chain says that each token
existed when the next was issued. It is experimental: RFC 5544 leaves open what
the later tokens cover.
verify_signature_at_time_stamp/2 does not check a signature policy; whether
this signer was allowed to sign this is for the application.