What a verified time-stamp means

Copy Markdown View Source

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

  1. The roots. :trust is required. It is your list of TSAs you accept. trust: :none is allowed and explicit: the result then says trust: :none and proves only that some key signed this imprint.
  2. Revocation. The library does not fetch a CRL or an OCSP answer. Without :revocation, stamp.revocation is :not_checked. If the key of the TSA was revoked for keyCompromise (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 the genTime, and returns :ok or {:error, reason}; it is called after everything else passed.
  3. The policy. :policies lists 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.
  4. The clock. :max_skew compares the genTime with 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.
  5. 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
end

Use 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)
end

Each 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

reasonWhat happenedWhat to do
:malformed, :limit_exceededNot a response or token, or beyond a limitDo not retry the same bytes
:rejectionThe TSA refused; detail.fail_info says why (:bad_alg, :bad_request, ...)Fix the request, or the TSA you ask
:waiting, :revocation_warning, :revocation_notificationA status that is not a time-stampTreat as a failure
:token_invalidThe signature or the algorithm of the token is refusedNot a valid token
:bad_tsa_certificateThe certificate is not one a TSA may use (detail says which rule)The TSA or its certificate is wrong
:untrustedThe chain does not lead to your roots at the genTimeWrong root, or an expired chain at that time
:imprint_mismatchThe data is not what was stampedThe data was changed, or it is another file
:nonce_mismatch, :nonce_missingThe answer is not for this requestDo not use it
:policy_mismatch, :tsa_name_mismatchNot the policy you want, or the tsa name is not the certificate's
:weak_algorithmMD5, or SHA-1 without allow_sha1: trueSome TSAs still sign with SHA-1; opt in knowingly
:gen_time_skewThe time differs from your clock by more than :max_skew
:transportThe network failed (stamp/2); detail says howRetry 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.