Cryptographic Message Syntax (CMS, RFC 5652) in pure Elixir: the signed,
encrypted and other messages of S/MIME, of .p7s, .p7m and .p7b
files, and of the many systems built on them.
Messages
| Type | Written by | Read by |
|---|---|---|
| SignedData | sign/3, certs_only/2; changed by add_signer/3, countersign/4 and add_unsigned_attribute/4 | verify/2, verify_each/2, verify_countersignature/3, certificates/2 |
| EnvelopedData, AuthEnvelopedData | encrypt/3 | decrypt/2 |
| EncryptedData | encrypt_with_key/3 | decrypt/2 |
| DigestedData | digest/2 | verify_digest/2 |
| AuthenticatedData | mac/3 | verify_mac/2 |
decode/2 reads any of them into its struct, without verifying or
decrypting it.
A signer is a Cmsex.Signer, a certificate with its key, and a recipient
a Cmsex.Recipient: a certificate, a shared key or a password. The
README has examples of each, with the keys and certificates of RFC 9216.
Input and output
The functions that read take a message in DER, BER or PEM, and the
functions that write return DER; Cmsex.PEM.encode/2 turns it into PEM.
Content is a binary, and for signing, verifying, digests and MACs it may
also be {:stream, enumerable}, read once, for content that does not
fit in memory.
Reading is bounded by limits on what a message may hold, listed in
decode/2, which every function that reads takes.
Errors
Every function returns {:ok, result}, or {:error, %Cmsex.Error{}} for
a message it cannot read or that does not check; the functions ending in
! return the result alone and raise the error. An argument of the
wrong kind, an unknown option or an option given twice raises
ArgumentError: it is a mistake of the calling code, not of the
message.
Examples
A digest:
iex> {:ok, cms} = Cmsex.digest("hello")
iex> {:ok, message} = Cmsex.verify_digest(cms)
iex> message.content
"hello"Encryption with a key the parties share, here with AES-GCM:
iex> key = :crypto.strong_rand_bytes(32)
iex> recipient = Cmsex.Recipient.kek(key, "key 1")
iex> {:ok, cms} = Cmsex.encrypt("secret", recipient, cipher: :aes256_gcm)
iex> {:ok, decrypted} = Cmsex.decrypt(cms, kek: {"key 1", key})
iex> decrypted.content
"secret"
iex> {:error, error} = Cmsex.decrypt(cms, kek: {"key 1", :crypto.strong_rand_bytes(32)})
iex> error.reason
:decryption_failedA MAC, with the same key:
iex> key = :crypto.strong_rand_bytes(32)
iex> {:ok, cms} = Cmsex.mac("content", Cmsex.Recipient.kek(key, "key 1"))
iex> {:ok, authenticated} = Cmsex.verify_mac(cms, kek: {"key 1", key})
iex> authenticated.content
"content"
Summary
Types
The position of a signer: an integer for a signer of the message, in
the order of decode/2, or a list for a countersignature, where [0, 1]
is the second countersignature of the first signer, counted across all
its countersignature attributes. Cmsex.Error gives positions the same
way.
Functions
Adds signers to a SignedData message, over the content the message signs. The message may be in DER, BER or PEM, and comes back in DER as far as it was: what does not change keeps its bytes and its place, the other SignerInfos, the signed part of each, the certificates and attributes already there, and what is added goes after them. So every signature of the message still verifies, and the positions of the signers and countersignatures stay the same from one change to the next. The SETs are not sorted again; an encapsulated content in BER is written in DER, which changes no signature.
Like add_signer/3, but raises Cmsex.Error.
Adds unsigned attributes, one Cmsex.Attribute or a list, to the signer
or countersignature at position. The message may be in DER, BER or PEM, and comes back in DER as far as
it was: what does not change keeps its bytes and its place, the other
SignerInfos, the signed part of each, the certificates and attributes
already there, and what is added goes after them. So every signature of
the message still verifies, and the positions of the signers and
countersignatures stay the same from one change to the next. The SETs
are not sorted again; an encapsulated content in BER is written in DER,
which changes no signature.
Like add_unsigned_attribute/4, but raises Cmsex.Error.
The X.509 certificates of a SignedData message, in DER, in the order of
the message: the certificates of a .p7b bundle, or those a signer
included. Takes the options of decode/2.
Like certificates/2, but raises Cmsex.Error.
A SignedData message with certificates and revocation lists and no
signers (RFC 5652, 5.2), as in a .p7b file: version 1, no digest
algorithms, id-data without content. Takes the option :crls, a list of
DER revocation lists.
Like certs_only/2, but raises Cmsex.Error.
Countersigns a signer of a message, or a countersignature: a new
SignerInfo over the signature value of the one at position, added to
it as a countersignature attribute (RFC 5652, 11.4). The message may be in DER, BER or PEM, and comes back in DER as far as
it was: what does not change keeps its bytes and its place, the other
SignerInfos, the signed part of each, the certificates and attributes
already there, and what is added goes after them. So every signature of
the message still verifies, and the positions of the signers and
countersignatures stay the same from one change to the next. The SETs
are not sorted again; an encapsulated content in BER is written in DER,
which changes no signature.
Like countersign/4, but raises Cmsex.Error.
Reads a CMS message without verifying or decrypting it, for inspection and for the libraries built on this one. The message may be in DER, BER or PEM. Returns the struct of its type
Like decode/2, but raises Cmsex.Error.
Decrypts an EnvelopedData, AuthEnvelopedData or EncryptedData message, in DER, BER or PEM.
Like decrypt/2, but raises Cmsex.Error.
Returns a DigestedData message in DER (RFC 5652, 7): content and its
digest.
Like digest/2, but returns the message alone.
Encrypts content, a binary, for recipients, and returns an
EnvelopedData message in DER, or an AuthEnvelopedData (RFC 5083) when the
cipher is an authenticated one, such as AES-GCM.
Like encrypt/3, but raises Cmsex.Error.
Encrypts content with key, a content-encryption key the parties
manage by other means, and returns an EncryptedData message in DER
(RFC 5652, 8). There are no recipients, and nothing in the message says
which key it is: decrypt/2 takes it in :content_key.
Like encrypt_with_key/3, but returns the message alone.
Returns an AuthenticatedData message in DER (RFC 5652, 9): content, in
the clear, with an HMAC over it, and the MAC key, random and new for
each message, wrapped for each of recipients as in encrypt/3.
Like mac/3, but raises Cmsex.Error.
Signs content and returns a SignedData message in DER (use
Cmsex.PEM.encode/2 for PEM).
Like sign/3, but raises Cmsex.Error.
Verifies the signers of a SignedData message, in DER, BER or PEM.
Like verify/2, but raises Cmsex.Error.
Verifies one countersignature (RFC 5652, 11.4) on its own: the one at
position, such as [0, 0] for the first countersignature of the first
signer (see position/0), against the signature value of the
SignerInfo it countersigns, which is not verified here.
Checks the digest of a DigestedData message, in DER, BER or PEM, and
returns the message as decode/2 reads it, whose :content is the
content, or nil for a detached message.
Like verify_digest/2, but raises Cmsex.Error.
The result of each signer, rather than the first failure. Takes the
options of verify/2, except :signers.
Checks the MAC of an AuthenticatedData message, in DER, BER or PEM.
Like verify_mac/2, but raises Cmsex.Error.
Types
@type position() :: non_neg_integer() | [non_neg_integer(), ...]
The position of a signer: an integer for a signer of the message, in
the order of decode/2, or a list for a countersignature, where [0, 1]
is the second countersignature of the first signer, counted across all
its countersignature attributes. Cmsex.Error gives positions the same
way.
Functions
@spec add_signer( binary(), Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()], keyword() ) :: {:ok, binary()} | {:error, Cmsex.Error.t()}
Adds signers to a SignedData message, over the content the message signs. The message may be in DER, BER or PEM, and comes back in DER as far as it was: what does not change keeps its bytes and its place, the other SignerInfos, the signed part of each, the certificates and attributes already there, and what is added goes after them. So every signature of the message still verifies, and the positions of the signers and countersignatures stay the same from one change to the next. The SETs are not sorted again; an encapsulated content in BER is written in DER, which changes no signature.
signers is one signer or a list, as in sign/3. The new SignerInfos
go after the others; their digest algorithms join digestAlgorithms, and
the version of the message goes up to 3 when RFC 5652, 5.1 asks for it.
A detached message needs its content in :content, a binary,
{:stream, enumerable} or {:digest, hash, digest}. It must be the
content the signers there signed: their message digests are checked
before anything is signed, and a difference is :digest_mismatch with
the position of that signer. A message with its content refuses
:content (:unexpected_content).
Options: :content, the limits of decode/2, and the options of
sign/3 that are about the signers: :certificates (those of the new
signers are added to the message by default; :none; or a list of DER
certificates), :crls, :no_attributes, :min_rsa_bits, :digest,
:rsa_padding, :identifier, :signing_time and
:algorithm_protection. The content type is that of the message.
@spec add_signer!( binary(), Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()], keyword() ) :: binary()
Like add_signer/3, but raises Cmsex.Error.
@spec add_unsigned_attribute( binary(), position(), Cmsex.Attribute.t() | [Cmsex.Attribute.t()], keyword() ) :: {:ok, binary()} | {:error, Cmsex.Error.t()}
Adds unsigned attributes, one Cmsex.Attribute or a list, to the signer
or countersignature at position. The message may be in DER, BER or PEM, and comes back in DER as far as
it was: what does not change keeps its bytes and its place, the other
SignerInfos, the signed part of each, the certificates and attributes
already there, and what is added goes after them. So every signature of
the message still verifies, and the positions of the signers and
countersignatures stay the same from one change to the next. The SETs
are not sorted again; an encapsulated content in BER is written in DER,
which changes no signature.
The attributes go after the unsigned attributes the signer has. This is
how a signature time-stamp enters a message (RFC 3161, appendix A): the
token is over Cmsex.SignerInfo.signature/1, and goes in a
:time_stamp_token attribute.
{:ok, %{signer_infos: [signer_info]}} = Cmsex.decode(cms)
token = time_stamp(Cmsex.SignerInfo.signature(signer_info))
attribute = Cmsex.Attribute.new(:time_stamp_token, [token])
{:ok, cms} = Cmsex.add_unsigned_attribute(cms, 0, attribute)Options: the limits of decode/2. A position the message does not have
raises ArgumentError.
@spec add_unsigned_attribute!( binary(), position(), Cmsex.Attribute.t() | [Cmsex.Attribute.t()], keyword() ) :: binary()
Like add_unsigned_attribute/4, but raises Cmsex.Error.
@spec certificates(binary(), keyword()) :: {:ok, [binary()]} | {:error, Cmsex.Error.t()}
The X.509 certificates of a SignedData message, in DER, in the order of
the message: the certificates of a .p7b bundle, or those a signer
included. Takes the options of decode/2.
Like certificates/2, but raises Cmsex.Error.
@spec certs_only([binary()], keyword()) :: {:ok, binary()} | {:error, Cmsex.Error.t()}
A SignedData message with certificates and revocation lists and no
signers (RFC 5652, 5.2), as in a .p7b file: version 1, no digest
algorithms, id-data without content. Takes the option :crls, a list of
DER revocation lists.
Other tools read a .p7b in PEM mostly with the PKCS7 label:
Cmsex.PEM.encode(p7b, :pkcs7). certificates/2 reads it back.
Like certs_only/2, but raises Cmsex.Error.
@spec countersign( binary(), position(), Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()], keyword() ) :: {:ok, binary()} | {:error, Cmsex.Error.t()}
Countersigns a signer of a message, or a countersignature: a new
SignerInfo over the signature value of the one at position, added to
it as a countersignature attribute (RFC 5652, 11.4). The message may be in DER, BER or PEM, and comes back in DER as far as
it was: what does not change keeps its bytes and its place, the other
SignerInfos, the signed part of each, the certificates and attributes
already there, and what is added goes after them. So every signature of
the message still verifies, and the positions of the signers and
countersignatures stay the same from one change to the next. The SETs
are not sorted again; an encapsulated content in BER is written in DER,
which changes no signature.
The countersignature goes at the end of the last countersignature
attribute of that signer, or in a new attribute at the end. Its signed
attributes are the message digest of the signature value, the signing
time and CMSAlgorithmProtection, unless turned off, and those of the
countersigner; never content-type. With no_attributes: true it signs
the signature value itself. verify/2 checks it with
countersignatures: true.
countersigners is one signer or a list, as in sign/3. Options: the
limits of decode/2, and :certificates, :crls, :no_attributes,
:min_rsa_bits, :digest, :rsa_padding, :identifier,
:signing_time and :algorithm_protection, as in add_signer/3.
A position the message does not have raises ArgumentError.
@spec countersign!( binary(), position(), Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()], keyword() ) :: binary()
Like countersign/4, but raises Cmsex.Error.
@spec decode(binary(), keyword()) :: {:ok, Cmsex.SignedData.t() | Cmsex.EnvelopedData.t() | Cmsex.EncryptedData.t() | Cmsex.DigestedData.t() | Cmsex.AuthenticatedData.t()} | {:error, Cmsex.Error.t()}
Reads a CMS message without verifying or decrypting it, for inspection and for the libraries built on this one. The message may be in DER, BER or PEM. Returns the struct of its type:
Cmsex.SignedData- signed content, or a certificate bundle;Cmsex.EnvelopedData- encrypted content, for EnvelopedData and AuthEnvelopedData;Cmsex.EncryptedData- content encrypted with a key managed by other means;Cmsex.DigestedData- content with its digest;Cmsex.AuthenticatedData- content with a MAC.
Options, the limits on what the message may hold:
:max_depth- nesting of the encoding (32).:max_elements- encoded elements at all levels (100,000).:max_signers- signers, and countersignatures of one signer (64).:max_certificates- certificates, and revocation lists (256).:max_attributes- attributes in one set (64).:max_recipients- recipients, and encrypted keys of one key agreement recipient (64).
A ContentInfo of another type, such as a bare :data, gives
:unsupported_content_type.
@spec decode!(binary(), keyword()) :: Cmsex.SignedData.t() | Cmsex.EnvelopedData.t() | Cmsex.EncryptedData.t() | Cmsex.DigestedData.t() | Cmsex.AuthenticatedData.t()
Like decode/2, but raises Cmsex.Error.
@spec decrypt(binary(), keyword()) :: {:ok, Cmsex.Decrypted.t()} | {:error, Cmsex.Error.t()}
Decrypts an EnvelopedData, AuthEnvelopedData or EncryptedData message, in DER, BER or PEM.
{:ok, %Cmsex.Decrypted{content: content}} =
Cmsex.decrypt(cms, key: key_der, certificate: cert_der)
{:ok, decrypted} = Cmsex.decrypt(cms, password: "correct horse battery staple")The credentials, one or more of:
:keyand:certificate- a private key and its certificate, in DER (PKCS #8, PKCS #1 or SEC 1 for the key, which may also be a key record of:public_key). The certificate finds the recipient: a KeyTransRecipientInfo for RSA, a KeyAgreeRecipientInfo for EC, X25519 and X448, by issuer and serial number or subject key identifier. The key may also be a function of one argument, for a key kept in an HSM, which gets a map and returns{:ok, bytes}or{:error, term}:%{operation: :decrypt, padding: :oaep, ciphertext:, hash:, mgf_hash:, label:}- RSAES-OAEP decryption;%{operation: :decrypt, padding: :pkcs1, ciphertext:}- RSA PKCS #1 v1.5 decryption, which should itself follow RFC 3218;%{operation: :agree, curve:, public_key:}- the ECDH, X25519 or X448 shared secret with the public key of the sender.
:kek-{key_identifier, key}, for a KEKRecipientInfo with that identifier.:password- for a PasswordRecipientInfo, taken as bytes.:content_key- the content-encryption key itself: the key of an EncryptedData, or of any message, which skips the recipients.
The recipients that the credentials name are tried in the order of the message, and the first that gives a key decrypts the content. Errors:
:invalid_key,:invalid_certificateand:key_certificate_mismatch- for the credentials.:recipient_not_found- no recipient for the credentials.:unsupported_algorithm,:not_available,:malformed, or:limit_exceededwith detail:max_iterations- what can be told without the secrets.:decryption_failed- anything else: a wrong key, a key wrap, a padding or a tag that does not check, a message that was changed. Nothing tells these apart, on purpose. For RSA PKCS #1 v1.5, which the library reads but never writes, a padding that does not check gives a random key, so that the failure comes from the content, as RFC 3218 asks.:content_requiredand:unexpected_content- as inverify/2.- for AuthEnvelopedData,
:missing_attribute,:invalid_attributeand:content_type_mismatch, about the content-type attribute, once the tag has checked.
EnvelopedData and EncryptedData use CBC, which has no integrity check: a changed ciphertext, or rarely a wrong key, can decrypt to garbage rather than fail. AuthEnvelopedData detects both; sign the content, or use an authenticated cipher, when that matters.
Other options:
:content- the encrypted content of a detached message, a binary.:max_iterations- the most PBKDF2 iterations accepted for a password (2,000,000), since the sender chooses them.- the limits of
decode/2.
Without any credentials, ArgumentError.
@spec decrypt!(binary(), keyword()) :: Cmsex.Decrypted.t()
Like decrypt/2, but raises Cmsex.Error.
@spec digest( binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()}, keyword() ) :: {:ok, binary()}
Returns a DigestedData message in DER (RFC 5652, 7): content and its
digest.
{:ok, cms} = Cmsex.digest(content)
{:ok, %Cmsex.DigestedData{content: ^content}} = Cmsex.verify_digest(cms)A digest shows that the content was not changed by accident, and
nothing more: whoever changes the content can write a new digest. To
show who wrote it, sign it, or use mac/3.
The content is a binary, {:stream, enumerable} read once, or
{:digest, hash, digest} already computed; a stream or a digest makes
a detached message.
Options:
:digest-:sha256(the default),:sha224,:sha384or:sha512. A ready digest gives its own hash.:content_type- the type of the content (:data), a name ofCmsex.OIDor an OID tuple.:detached- leave the content out of the message (falsefor a binary).
The version is 0 for :data and 2 for another type (RFC 5652, 7).
@spec digest!( binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()}, keyword() ) :: binary()
Like digest/2, but returns the message alone.
@spec encrypt( binary(), Cmsex.Recipient.t() | binary() | [Cmsex.Recipient.t() | binary()], keyword() ) :: {:ok, binary() | {binary(), binary()}} | {:error, Cmsex.Error.t()}
Encrypts content, a binary, for recipients, and returns an
EnvelopedData message in DER, or an AuthEnvelopedData (RFC 5083) when the
cipher is an authenticated one, such as AES-GCM.
recipients is one recipient or a list of them, each a
Cmsex.Recipient or a certificate in DER, which is taken as
Cmsex.Recipient.certificate/2 with the defaults: RSA-OAEP for an RSA
key, ephemeral-static ECDH for an EC key, X25519 or X448.
{:ok, cms} = Cmsex.encrypt(content, alice_cert_der)
{:ok, cms} =
Cmsex.encrypt(content, [
alice_cert_der,
Cmsex.Recipient.kek(shared_key, "backup key 1"),
Cmsex.Recipient.password("correct horse battery staple")
], cipher: :aes256_gcm)The content-encryption key is random, new for each message, and wrapped
for each recipient. Every recipient is checked before anything is
encrypted; an error that belongs to one carries its position in
:recipient. A certificate whose keyUsage does not allow
keyEncipherment (RSA) or keyAgreement (the others) is
:invalid_recipient with detail :key_usage; a key that cannot encrypt,
such as Ed25519 or RSASSA-PSS, is :invalid_recipient with :key_type.
Options:
:cipher-:aes256_cbc(the default),:aes128_cbcor:aes192_cbcfor EnvelopedData (RFC 3565);:aes128_gcm,:aes192_gcm,:aes256_gcm, the same with_ccm(RFC 5084), or:chacha20_poly1305(RFC 8103) for AuthEnvelopedData.:tag_size- the size of the tag of an authenticated cipher, in bytes (16): 12 to 16 for GCM, an even number from 4 to 16 for CCM, 16 for ChaCha20-Poly1305.:content_type- the type of the content (:data), a name ofCmsex.OIDor an OID tuple.:detached- leave the encrypted content out of the message (false); the result is then{:ok, {cms, encrypted_content}}, anddecrypt/2takes the encrypted content in:content.:unprotected_attributes- a list ofCmsex.Attributethat nothing protects: the unprotectedAttrs of EnvelopedData, the unauthAttrs of AuthEnvelopedData ([]).:authenticated_attributes- for an authenticated cipher, a list ofCmsex.Attributethat the tag covers ([]). The content-type attribute is written by the library, with them or for a content type other than:data; the message-digest attribute is not written (RFC 5083, 2.1).:certificatesand:crls- certificates and revocation lists in DER for the OriginatorInfo ([]).:min_rsa_bits- the shortest RSA key accepted (2048).
The versions are those of RFC 5652, 6.1 and of the RecipientInfos.
@spec encrypt!( binary(), Cmsex.Recipient.t() | binary() | [Cmsex.Recipient.t() | binary()], keyword() ) :: binary() | {binary(), binary()}
Like encrypt/3, but raises Cmsex.Error.
Encrypts content with key, a content-encryption key the parties
manage by other means, and returns an EncryptedData message in DER
(RFC 5652, 8). There are no recipients, and nothing in the message says
which key it is: decrypt/2 takes it in :content_key.
Options: :cipher, a CBC one, as in encrypt/3 (:aes256_cbc), since
EncryptedData has no place for a tag; :content_type; :detached; and
:unprotected_attributes. A key that is not of the size of the cipher
raises ArgumentError.
To share a key for many messages, Cmsex.Recipient.kek/3 with
encrypt/3 is often the better choice: a new key for each message,
with an authenticated cipher if wanted, and the identifier of the key in
the message.
Like encrypt_with_key/3, but returns the message alone.
@spec mac( binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()}, Cmsex.Recipient.t() | binary() | [Cmsex.Recipient.t() | binary()], keyword() ) :: {:ok, binary()} | {:error, Cmsex.Error.t()}
Returns an AuthenticatedData message in DER (RFC 5652, 9): content, in
the clear, with an HMAC over it, and the MAC key, random and new for
each message, wrapped for each of recipients as in encrypt/3.
{:ok, cms} = Cmsex.mac(content, Cmsex.Recipient.kek(shared_key, "key 1"))
{:ok, %Cmsex.Authenticated{content: ^content}} =
Cmsex.verify_mac(cms, kek: {"key 1", shared_key})A MAC tells a recipient that the content comes from someone who had the MAC key, and that nobody changed it since. How much that says depends on the recipients: with a KEK or a password, the sender is someone who holds it; with a certificate, RSA or ephemeral ECDH, anyone who has the certificate can write such a message, and it shows nothing about who did (RFC 5652, 14). To show who wrote the content, sign it.
The content is a binary, {:stream, enumerable} read once, or
{:digest, hash, digest} already computed; a stream or a digest makes
a detached message.
Options:
:mac-:hmac_sha256(the default),:hmac_sha384,:hmac_sha512or:hmac_sha224(RFC 4231). The key is as long as the hash, and 32 bytes for SHA-224.:content_type- the type of the content (:data), a name ofCmsex.OIDor an OID tuple.:detached- leave the content out of the message (falsefor a binary).:authenticated_attributes- a list ofCmsex.Attributethat the MAC covers ([]). With them, for a content type other than:data, and for a ready digest, the MAC is over the attributes, among which the library writes the content-type and the message-digest attributes (RFC 5652, 9.2).:digest- the hash of the message-digest attribute: that of the HMAC unless given,:sha224,:sha256,:sha384or:sha512; for a ready digest, its hash.:unauthenticated_attributes- a list ofCmsex.Attributethat nothing covers ([]).:certificatesand:crls- certificates and revocation lists in DER for the OriginatorInfo ([]).:min_rsa_bits- the shortest RSA key accepted (2048).
Recipients are checked as in encrypt/3, and an error that belongs to
one carries its position in :recipient.
@spec mac!( binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()}, Cmsex.Recipient.t() | binary() | [Cmsex.Recipient.t() | binary()], keyword() ) :: binary()
Like mac/3, but raises Cmsex.Error.
@spec sign( binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()}, Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()], keyword() ) :: {:ok, binary()} | {:error, Cmsex.Error.t()}
Signs content and returns a SignedData message in DER (use
Cmsex.PEM.encode/2 for PEM).
signers is one signer or a list of them, each a Cmsex.Signer, or a
map or keyword list with its fields: at least the certificate and the
private key.
signer = %{certificate: cert_der, key: key_der, chain: [ca_der]}
{:ok, cms} = Cmsex.sign(content, signer)
{:ok, p7s} = Cmsex.sign(content, signer, detached: true)The content is a binary, {:stream, enumerable} read once to compute the
digests, or {:digest, hash, digest} already computed, which every
signer must use. A stream or a digest makes a detached message with
signed attributes.
Each signer gets the content-type and message-digest attributes, the
signing time and the CMSAlgorithmProtection attribute (RFC 6211) unless
turned off, and its own attributes; see Cmsex.Signer. The algorithms
are checked with the policy of verify/2, and SHA-1 is never used.
Options:
:detached- leave the content out of the message (falsefor a binary; a stream or a digest is always detached).:content_type- the type of the content (:data), a name ofCmsex.OIDor an OID tuple. Another type needs signed attributes.:no_attributes- sign without signed attributes (false): only for id-data given as a binary, and only for a verifier that cannot read them, since nothing then binds the algorithms or the time.:certificates-:signers(the certificate and:chainof each signer, the default),:none, or a list of DER certificates.:crls- revocation lists in DER ([]).:min_rsa_bits- the shortest RSA key accepted (2048).:digest,:rsa_padding,:identifier,:signing_timeand:algorithm_protection- defaults for the signers that leave them out; seeCmsex.Signer.
The versions are those of RFC 5652, 5.1 and 5.3. Errors that belong to
one signer carry its position in :signer.
@spec sign!( binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()}, Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()], keyword() ) :: binary()
Like sign/3, but raises Cmsex.Error.
@spec verify(binary(), keyword()) :: {:ok, Cmsex.Verified.t()} | {:error, Cmsex.Error.t()}
Verifies the signers of a SignedData message, in DER, BER or PEM.
A valid signature is not trust. Without :trust, {:ok, verified} only
says that the key of a certificate signed the content, and that
certificate may have come in the message itself, from anyone. Give the
roots you trust in :trust to know who signed.
For each signer, the checks are those of RFC 5652, 5.6: a certificate
that matches its identifier, from :certificates or from the message;
algorithms that are allowed and agree with each other and with the key;
the content-type, message-digest and CMSAlgorithmProtection attributes;
the signing-certificate attributes, when present; the signature; and,
with :trust, the certificate path. When several certificates match the
identifier, each is tried, and the one that verified is returned.
Options:
:content- the content of a detached message: a binary,{:stream, enumerable}, or{:digest, hash, digest}, already hashed. A stream is read once for all the digests, and once more only for an EdDSA signer without signed attributes, which signs the whole content and so needs it in memory; a digest is not enough for that signer (:content_required). Content given for a message that carries its own is:unexpected_content.:certificates- certificates in DER to find the signers in, tried before those of the message ([]).:signers-:all(the default) fails at the first signer that does not verify;:anyneeds one, and returns only those that verified.:trust- root certificates in DER. The path of each signer is built from the certificates at hand and validated by:public_key.pkix_path_validation/3, at the current time; the signer's key usage, if any, must allow digitalSignature or nonRepudiation. A failure is:untrusted, with the reason indetail.:path_validation- options for:public_key.pkix_path_validation/3, such as a:verify_funthat checks revocation ([]).:countersignatures- also verify the countersignatures of each signer (false).:allow_sha1- accept SHA-1 (false). MD5 is never accepted.:min_rsa_bits- the shortest RSA key accepted (2048).:ed25519_digest-:lenient(the default) accepts an Ed25519 signer whose digest algorithm is a SHA-2 other than SHA-512, and says so in its:deviations;:strictrefuses it.- the limits of
decode/2.
The signing-time attribute is returned as the signer's claim; it is never the time at which the path is validated.
@spec verify!(binary(), keyword()) :: Cmsex.Verified.t()
Like verify/2, but raises Cmsex.Error.
@spec verify_countersignature(binary(), [non_neg_integer(), ...], keyword()) :: {:ok, Cmsex.Verified.signer()} | {:error, Cmsex.Error.t()}
Verifies one countersignature (RFC 5652, 11.4) on its own: the one at
position, such as [0, 0] for the first countersignature of the first
signer (see position/0), against the signature value of the
SignerInfo it countersigns, which is not verified here.
This is for a countersignature whose signer cannot be verified, such as
one in an algorithm the library leaves out, or when only the
countersignature matters. verify/2 with countersignatures: true
checks the signers and their countersignatures together.
Returns the countersigner as in Cmsex.Verified.signer/0. Takes the
options of verify/2, except :content and :signers; with
countersignatures: true, the countersignatures below this one are
verified too. A position the message does not have raises
ArgumentError.
@spec verify_digest(binary(), keyword()) :: {:ok, Cmsex.DigestedData.t()} | {:error, Cmsex.Error.t()}
Checks the digest of a DigestedData message, in DER, BER or PEM, and
returns the message as decode/2 reads it, whose :content is the
content, or nil for a detached message.
Options:
:content- the content of a detached message: a binary,{:stream, enumerable}or{:digest, hash, digest}.:allow_sha1- take a SHA-1 digest (false). MD5 is never taken.- the limits of
decode/2.
Errors: :digest_mismatch, :weak_algorithm, :unsupported_algorithm,
:content_required and :unexpected_content, and those of reading.
A message of another type is :unsupported_content_type.
@spec verify_digest!(binary(), keyword()) :: Cmsex.DigestedData.t()
Like verify_digest/2, but raises Cmsex.Error.
@spec verify_each(binary(), keyword()) :: {:ok, Cmsex.SignedData.t(), [ {Cmsex.SignerInfo.t(), {:ok, Cmsex.Verified.signer()} | {:error, Cmsex.Error.t()}} ]} | {:error, Cmsex.Error.t()}
The result of each signer, rather than the first failure. Takes the
options of verify/2, except :signers.
{:ok, signed_data, [{signer_info, {:ok, signer}}, {signer_info, {:error, error}}]}An error that is not about one signer, such as a malformed message, a
missing content or a message without signers, is returned as
{:error, error}.
@spec verify_mac(binary(), keyword()) :: {:ok, Cmsex.Authenticated.t()} | {:error, Cmsex.Error.t()}
Checks the MAC of an AuthenticatedData message, in DER, BER or PEM.
The credentials are those of decrypt/2, with :mac_key, the MAC key
itself, in place of :content_key. The recipients are tried as in
decrypt/2; a key shorter than 16 bytes is not taken, since whoever
sees the MAC could find it.
With authenticated attributes, the MAC is over them, and once it has checked, so are the content-type attribute, against the content, and the message-digest attribute, against the digest of the content.
Errors:
:invalid_mac- the MAC does not check, or the key of the recipient does not unwrap; nothing tells these apart.:digest_mismatch,:content_type_mismatch,:missing_attributeand:invalid_attribute- about the authenticated attributes, once the MAC has checked. Content other than:dataneeds them.:weak_algorithm- a digest of SHA-1 withoutallow_sha1: true, or MD5. HMAC-SHA1, the MAC of RFC 3370, is taken.- those of
decrypt/2for the credentials and the recipients, and:content_requiredand:unexpected_content.
Other options:
:content- the content of a detached message: a binary,{:stream, enumerable}, or{:digest, hash, digest}when the message has authenticated attributes with that hash.:allow_sha1(false),:max_iterations(2,000,000), and the limits ofdecode/2.
@spec verify_mac!(binary(), keyword()) :: Cmsex.Authenticated.t()
Like verify_mac/2, but raises Cmsex.Error.