Tspex.TimeStampedData (tspex v0.1.0)

Copy Markdown View Source

A document bound to a chain of time-stamps: TimeStampedData, RFC 5544.

Experimental. RFC 5544 does not say which octets the second and later time-stamps cover, and there is no tool to test against; this module makes a choice and documents it below. The functions and fields here may change in a minor release until the choice is confirmed by another implementation. For the same reason only decode!/2 has a ! variant for now: the other functions return {:ok, value} or {:error, %Tspex.Error{}}.

A TimeStampedData is a ContentInfo (content type id-ct-timestampedData) that holds the document, or says where to find it (dataUri); some metadata (fileName, mediaType and attributes); and the temporal evidence, which here is a list of time-stamp tokens. Each token covers the one before it, so a long chain shows the document existed at the time of the first token, and that each token existed when the next one was issued.

{:ok, imprint} = Tspex.TimeStampedData.imprint(contract, file_name: "contract.pdf")
{:ok, first} = Tspex.stamp(imprint, url: url, trust: roots)
{:ok, tsd} = Tspex.TimeStampedData.new(contract, first, file_name: "contract.pdf")
File.write!("contract.pdf.tsd", Tspex.TimeStampedData.encode(tsd))

{:ok, result} = Tspex.TimeStampedData.verify(File.read!("contract.pdf.tsd"), trust: roots)
result.time

What each token covers

  • The first token: the value octets of content (or of the document given as :content, since dataUri is never fetched). When the metadata has hashProtected true, the DER encoding of the MetaData, followed by the content (RFC 5544, section 2).
  • Each later token: the previous element of the sequence, as the TimeStampAndCRL that is in the structure, byte for byte. This is the reading of "computed over its preceding element"; it includes the crl of that element if it has one. The elements this module writes are DER when the tokens are.
  • The crl of an element shows that the certificate of the TSA of that element's token was not revoked when the next element was added, so it is put in the element before the next token is asked for: see extension_imprint/2.

What it does not do

  • It never fetches dataUri. Give the document as :content.
  • ersEvidence and otherEvidence are read and written as bytes, and verify/2 answers :unsupported_evidence for them.
  • It does not check the crl of an element. verify/2 returns it, with the stamps, for you to check (the guide on verification shows how, with the recipe for revocation); :revocation is called for each token like in Tspex.verify/3.
  • It does not refuse a chain whose genTime goes back unless you give :max_time_back; :ordered in the result says whether it did.
  • The token of a time-stamp is only as good as its TSA, as always: read Tspex.verify/3 for what the checks of each token mean.

Summary

Types

A TimeStampAndCRL: the token in DER, the CRL in DER or nil, and raw, the whole element as it is in the structure, which the next token covers.

The temporal evidence: a chain of time-stamps (:tst), the evidence of an archive time-stamp (:ers) or another kind, by its OID. Only the first is verified.

The MetaData: other is a list of attributes, %{oid: tuple, values: [der]}.

t()

A TimeStampedData (RFC 5544), experimental.

Functions

Reads a TimeStampedData from a ContentInfo in BER or DER, without verifying it. Takes the limits :max_size, :max_depth, :max_nodes and :max_stamps (64 elements in the chain).

The ContentInfo of tsd in DER. The elements of the chain are written as they are, so what the tokens cover does not change.

Adds a time-stamp to the chain of tsd. stamp is as in new/3, and has to cover the last element of the chain, with the :crl if one is given (see extension_imprint/2).

The imprint to time-stamp next, to extend the chain of tsd: the hash of the last element, with the crl that is to go in it.

The imprint to time-stamp first for content: the hash of the content, or of the MetaData and the content when hash_protected: true. Returns {:ok, {:digest, hash, digest}}, which Tspex.request/2, Tspex.stamp/2 and new/3 take.

Makes a TimeStampedData of content and the time-stamp of its imprint (see imprint/2).

The TSTInfo of each token of the chain, read and not verified, in the order of the chain. Takes the limits of decode/2. A chain of other evidence is :unsupported_evidence.

Verifies a TimeStampedData, given as DER or as the struct: the content and the whole chain.

Types

element()

@type element() :: %{token: binary(), crl: binary() | nil, raw: binary()}

A TimeStampAndCRL: the token in DER, the CRL in DER or nil, and raw, the whole element as it is in the structure, which the next token covers.

evidence()

@type evidence() ::
  {:tst, [element(), ...]} | {:ers, binary()} | {:other, tuple(), binary()}

The temporal evidence: a chain of time-stamps (:tst), the evidence of an archive time-stamp (:ers) or another kind, by its OID. Only the first is verified.

meta_data()

@type meta_data() :: %{
  hash_protected: boolean(),
  file_name: String.t() | nil,
  media_type: String.t() | nil,
  other: [%{oid: tuple(), values: [binary()]}]
}

The MetaData: other is a list of attributes, %{oid: tuple, values: [der]}.

t()

@type t() :: %Tspex.TimeStampedData{
  content: binary() | nil,
  data_uri: String.t() | nil,
  evidence: evidence(),
  meta_data: meta_data() | nil,
  version: 1
}

A TimeStampedData (RFC 5544), experimental.

Functions

decode(der, opts \\ [])

@spec decode(binary(), keyword()) :: {:ok, t()} | {:error, Tspex.Error.t()}

Reads a TimeStampedData from a ContentInfo in BER or DER, without verifying it. Takes the limits :max_size, :max_depth, :max_nodes and :max_stamps (64 elements in the chain).

Errors: :malformed, :limit_exceeded, :unsupported_version.

decode!(der, opts \\ [])

@spec decode!(binary(), keyword()) :: t()

Like decode/2, but raises Tspex.Error.

encode(tsd)

@spec encode(t()) :: binary()

The ContentInfo of tsd in DER. The elements of the chain are written as they are, so what the tokens cover does not change.

extend(tsd, stamp, opts \\ [])

@spec extend(t(), Tspex.Verified.t() | binary(), keyword()) ::
  {:ok, t()} | {:error, Tspex.Error.t()}

Adds a time-stamp to the chain of tsd. stamp is as in new/3, and has to cover the last element of the chain, with the :crl if one is given (see extension_imprint/2).

Takes :crl and, for a token in DER, the options of Tspex.verify/3. Only a chain of tstEvidence can be extended.

extension_imprint(tsd, opts \\ [])

@spec extension_imprint(t(), keyword()) ::
  {:ok, {:digest, Tspex.hash(), binary()}} | {:error, Tspex.Error.t()}

The imprint to time-stamp next, to extend the chain of tsd: the hash of the last element, with the crl that is to go in it.

Options: :crl (a CertificateList in DER, which replaces the one in the last element, if any), :hash (:sha256) and :allow_sha1. Ask the TSA for a token over this imprint, then give extend/3 the same :crl.

imprint(content, opts \\ [])

@spec imprint(Tspex.data(), keyword()) :: {:ok, {:digest, Tspex.hash(), binary()}}

The imprint to time-stamp first for content: the hash of the content, or of the MetaData and the content when hash_protected: true. Returns {:ok, {:digest, hash, digest}}, which Tspex.request/2, Tspex.stamp/2 and new/3 take.

content is a binary or {:stream, enumerable} (read once).

Options:

  • :hash - :sha256 (the default), :sha224, :sha384, :sha512; :sha1 only with allow_sha1: true.
  • :file_name, :media_type, :other_meta_data - the MetaData. The first is a UTF-8 string, the second ASCII, the third a list of attributes %{oid: tuple, values: [der]}. At least one is needed for there to be a MetaData.
  • :hash_protected - true puts the MetaData under the first token (false, the default). It needs a MetaData.
  • :allow_sha1 - false.

With hash_protected: true the imprint covers the file name too, so it is not the hash of the content alone:

iex> alias Tspex.TimeStampedData
iex> {:ok, {:digest, :sha256, plain}} = TimeStampedData.imprint("hello")
iex> Base.encode16(plain, case: :lower)
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
iex> {:ok, bound} = TimeStampedData.imprint("hello", file_name: "a.txt", hash_protected: true)
iex> bound == {:digest, :sha256, plain}
false

new(content, stamp, opts \\ [])

@spec new(Tspex.data(), Tspex.Verified.t() | binary(), keyword()) ::
  {:ok, t()} | {:error, Tspex.Error.t()}

Makes a TimeStampedData of content and the time-stamp of its imprint (see imprint/2).

stamp is a Tspex.Verified.t/0, taken to have been verified already, or a token in DER, which is verified first with the options of Tspex.verify/3 (:trust is required). Either way its imprint has to be the one of content and the MetaData (:imprint_mismatch otherwise).

Takes the options of imprint/2 except :hash and :allow_sha1 (the hash is the token's) and:

  • :data_uri - where the document can be found, a URI as ASCII text.
  • :detached - true leaves the content out of the structure, which then needs a :data_uri. A stream is always detached.

time_stamps(tsd_or_der, opts \\ [])

@spec time_stamps(t() | binary(), keyword()) ::
  {:ok, [Tspex.TSTInfo.t()]} | {:error, Tspex.Error.t()}

The TSTInfo of each token of the chain, read and not verified, in the order of the chain. Takes the limits of decode/2. A chain of other evidence is :unsupported_evidence.

verify(tsd_or_der, opts)

@spec verify(t() | binary(), keyword()) :: {:ok, map()} | {:error, Tspex.Error.t()}

Verifies a TimeStampedData, given as DER or as the struct: the content and the whole chain.

Each token is verified like Tspex.verify/3 verifies a stored token, with the same options, against what it has to cover (see the module doc): the first against the content (and the MetaData if it is hash protected), the others against the element before them. The certificate of each TSA is judged at the genTime of its own token.

Options: those of Tspex.verify/3 (:trust is required), the limits of decode/2, and:

  • :content - the document, as a binary or {:stream, enumerable}, for a structure that does not carry it. A structure that carries its content and is given :content is :unexpected_content, and one with neither is :content_missing. The dataUri is not used.
  • :max_time_back - how many seconds the genTime of a token may be earlier than the one of the token before it. A token covers the element that holds the one before, so it was issued later; if its genTime is earlier, one of the two clocks is wrong, or lies. With a number, a token earlier by more than that is :time_order; 0 asks for a strictly non-decreasing chain. nil (the default) does not refuse: the result says it in :ordered. TSAs that are not the same have clocks that differ by seconds, so choose the number knowing which.

Returns {:ok, result} with:

  • :time - the genTime of the first token, when the document existed.
  • :stamps - the Tspex.Verified.t/0 of each token, in order.
  • :ordered - true when no token has a genTime earlier than the one before it (see :max_time_back).
  • :crls - the crl of each element in DER, or nil. Not checked here; the guide on verification shows how to check them with the recipe for revocation.
  • :meta_data - the MetaData, or nil.

Evidence that is not tstEvidence is :unsupported_evidence.