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.timeWhat each token covers
- The first token: the value octets of
content(or of the document given as:content, sincedataUriis never fetched). When the metadata hashashProtectedtrue, the DER encoding of theMetaData, followed by the content (RFC 5544, section 2). - Each later token: the previous element of the sequence, as the
TimeStampAndCRLthat is in the structure, byte for byte. This is the reading of "computed over its preceding element"; it includes thecrlof that element if it has one. The elements this module writes are DER when the tokens are. - The
crlof 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: seeextension_imprint/2.
What it does not do
- It never fetches
dataUri. Give the document as:content. ersEvidenceandotherEvidenceare read and written as bytes, andverify/2answers:unsupported_evidencefor them.- It does not check the
crlof an element.verify/2returns it, with the stamps, for you to check (the guide on verification shows how, with the recipe for revocation);:revocationis called for each token like inTspex.verify/3. - It does not refuse a chain whose
genTimegoes back unless you give:max_time_back;:orderedin the result says whether it did. - The token of a time-stamp is only as good as its TSA, as always: read
Tspex.verify/3for 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]}.
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).
Like decode/2, but raises Tspex.Error.
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
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.
@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]}.
@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
@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.
Like decode/2, but raises Tspex.Error.
The ContentInfo of tsd in DER. The elements of the chain are written
as they are, so what the tokens cover does not change.
@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.
@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.
@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;:sha1only withallow_sha1: true.:file_name,:media_type,:other_meta_data- theMetaData. 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 aMetaData.:hash_protected-trueputs theMetaDataunder the first token (false, the default). It needs aMetaData.: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
@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-trueleaves the content out of the structure, which then needs a:data_uri. A stream is always detached.
@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.
@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:contentis:unexpected_content, and one with neither is:content_missing. ThedataUriis not used.:max_time_back- how many seconds thegenTimeof 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 itsgenTimeis earlier, one of the two clocks is wrong, or lies. With a number, a token earlier by more than that is:time_order;0asks 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- thegenTimeof the first token, when the document existed.:stamps- theTspex.Verified.t/0of each token, in order.:ordered-truewhen no token has agenTimeearlier than the one before it (see:max_time_back).:crls- thecrlof each element in DER, ornil. Not checked here; the guide on verification shows how to check them with the recipe for revocation.:meta_data- theMetaData, ornil.
Evidence that is not tstEvidence is :unsupported_evidence.