Reporting a vulnerability

Please do not open a public issue. Use GitHub's private vulnerability reporting on this repository ("Security" tab, "Report a vulnerability"). You should get an answer within a week. Fixes are released for the latest minor version.

Threat model

The library reads what a TSA sends, which crosses a network, and tokens that come with documents from anyone. An attacker controls every byte of a response or a token, and may sit between you and the TSA. The attacker wants:

  • a forged, changed or replayed time-stamp accepted;
  • a time-stamp of other data, or for another request, accepted;
  • a certificate that is not a TSA's, or a chain that does not lead to your roots, accepted;
  • a weaker algorithm than the TSA used;
  • to stop or slow down the process: CPU, memory, atoms, sockets.

Out of the model: an attacker who holds the private key of a TSA you trust, or who chooses the roots you give to :trust; a TSA whose clock is wrong or that lies; and flaws of the OpenSSL that :crypto uses for the hash and signature operations.

Malformed input

  • Every reader returns {:error, %Tspex.Error{}} for input it cannot read and does not raise on it. The fuzz tests feed changed and random bytes to the readers (decode_request/2, decode_response/2, decode_token/2, verify/3), to the functions that read time-stamps from a CMS message and to Tspex.TimeStampedData.decode/2 and verify/2.
  • Parsing is bounded: :max_size (1 MiB), :max_depth (32) and :max_nodes (100,000), and the limits of cmsex for the message that carries a token. Every length is checked against the input before it is read, and nothing is allocated from a declared length. A TimeStampedData has one more limit, :max_stamps (64), for the length of its chain; each token in it is read under the same limits.
  • Nothing from the input becomes an atom. Unknown algorithms, policies and extensions are reported by their OID, as a tuple.
  • The result keeps the input binary alive: copy with :binary.copy/1 what you keep from a large message.
  • Errors and inspect/1 show sizes and reasons, never the data that was time-stamped. A Tspex.Verified is inspected as its fields and the size of the token.

What verify/3 establishes

  • The token is a SignedData with exactly one signer, and its signature is valid for the TSTInfo.
  • The signer's certificate is the one the signing-certificate attribute names (ESSCertID or ESSCertIDv2, hash included), and it is a certificate for time-stamping: extKeyUsage critical, once, and with id-kp-timeStamping alone (tsa_eku: :strict, the default), and a key usage, if any, that allows signing.
  • The chain leads to one of the :trust roots, and is validated at the genTime and not today, so that a token outlives the certificate that signed it. A trust anchor that has expired, or is not valid at the genTime, is refused. :trust is required; trust: :none is explicit, and the result says so.
  • The imprint is that of the data, with the algorithm the token declares when the token is checked later, and with the request's when the answer is fresh. In fresh mode the nonce of the request is the nonce of the token, and the policy is the one that was asked for.
  • The tsa name of the token is one of the names of the certificate (tsa_name: :strict).
  • SHA-1 is refused (for the imprint and for the signature) unless allow_sha1: true. MD5 is never accepted. RSA keys under :min_rsa_bits (2048) are refused.
  • A critical extension of the TSTInfo that the library does not know is refused, unless critical_extensions: :ignore.

What verify/3 does not establish

  • That the TSA is honest or its clock right. genTime is the word of the TSA; :max_skew compares it with the local clock, plus the accuracy the TSA states, and is off by default because a stored token is old. Use more than one TSA, with verify_signature_time_stamps/3 and stamps: :all, when one clock is not enough for you.
  • That the certificate of the TSA was not revoked. The result says revocation: :not_checked unless you give :revocation, which receives the validated path and the genTime. RFC 3161, section 4: a revocation for keyCompromise, or without a reason, invalidates every token of that key; any other reason only the tokens made after the revocation. A path valid at a date is not a certificate that was good at that date. The guide "What a verified time-stamp means" has a tested recipe that builds this function from CRLs you hold.
  • That the stamp keeps proving anything for ever. Keys and hash algorithms age; a time-stamp is evidence for as long as its algorithms hold. Archive time-stamps (a stamp over a stamp) are outside this library.
  • That the stamped document or signature is valid, or that the signer was authorized to sign. verify_signature_at_time_stamp/2 checks a signature and its certificate path at the time of the stamp, not a signature policy.
  • For a TimeStampedData (experimental): that the choice of what the second and later tokens cover is the one other implementations make. RFC 5544 does not say, and this library reads it as the whole previous element. It does not check the crl of an element (verify/2 returns it, and the guide shows how to check it), and it refuses a chain whose time goes back only if you give :max_time_back (ordered in the result says it either way). The evidence of an archive time-stamp (ersEvidence) and other evidence are read and written but not verified.
  • Unsigned attributes are not protected by the signature. A time-stamp in one is verified against the signature value it claims to cover, and a stamp that is not about that signature is :imprint_mismatch; anyone can still remove a stamp from a message.

The network

  • The built-in client, Tspex.HTTP, is HTTP/1.1 over :gen_tcp or :ssl. It limits the head, the number of headers, the size of the body (also a chunked one, or one that ends when the connection closes) and the time of the whole exchange, and reads no body at all from an answer that is not a
    1. A Content-Length above the limit is refused before any of the body is read.
  • It follows no redirect unless :max_redirects is raised, and then only 307 and 308, never from https to http, and without the headers you gave when the new host is another one.
  • https verifies the peer with the system certificates (:public_key.cacerts_get/0) and the host name of the URL, also when it is an IP address, where the OTP alone would skip the check. The :ssl option replaces the store (a private CA) or adds a client certificate; turning verification off is yours to choose, and the token is still verified.
  • http works, since many TSAs are served that way, and the hash of your data goes in the clear. The answer is verified just the same, but anyone on the path learns that you stamp that hash.
  • A TimeStampedData can carry a dataUri. It is never fetched, by this library or on any path: the document is given as :content, so a structure cannot make the verifier open a connection.
  • Nothing is retried and nothing is cached. Header names and values given in :headers are checked, so that a value cannot write another header.
  • Request nonces are 63 bits from :crypto.strong_rand_bytes/1. An answer to another request, or an old one, has another nonce and is refused. The imprint and the digests are compared with :crypto.hash_equals/2.

Secrets

The library holds no secret: a time-stamp request carries a hash. Do not send the data itself, or the hash of something whose hash is itself secret, to a TSA you do not trust with it.

Writing

  • Hash algorithms for imprints: SHA-224, SHA-256, SHA-384 and SHA-512; SHA-1 only with allow_sha1: true, and only in a request.
  • add_signature_time_stamp/4 adds a token only if its imprint is the hash of the signature value of that signer, so that a stamp of another signature is not attached to this one. A token in DER is verified first, with your roots.