Cmsex (cmsex v0.1.0)

Copy Markdown View Source

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

TypeWritten byRead by
SignedDatasign/3, certs_only/2; changed by add_signer/3, countersign/4 and add_unsigned_attribute/4verify/2, verify_each/2, verify_countersignature/3, certificates/2
EnvelopedData, AuthEnvelopedDataencrypt/3decrypt/2
EncryptedDataencrypt_with_key/3decrypt/2
DigestedDatadigest/2verify_digest/2
AuthenticatedDatamac/3verify_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_failed

A 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.

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 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.

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.

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.

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

Decrypts an EnvelopedData, AuthEnvelopedData or EncryptedData message, in DER, BER or PEM.

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.

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.

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.

Signs content and returns a SignedData message in DER (use Cmsex.PEM.encode/2 for PEM).

Verifies the signers of a SignedData message, in DER, BER or PEM.

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.

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.

Types

position()

@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

add_signer(cms, signers, opts \\ [])

@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.

add_signer!(cms, signers, opts \\ [])

@spec add_signer!(
  binary(),
  Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()],
  keyword()
) :: binary()

Like add_signer/3, but raises Cmsex.Error.

add_unsigned_attribute(cms, position, attributes, opts \\ [])

@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.

add_unsigned_attribute!(cms, position, attributes, opts \\ [])

@spec add_unsigned_attribute!(
  binary(),
  position(),
  Cmsex.Attribute.t() | [Cmsex.Attribute.t()],
  keyword()
) :: binary()

Like add_unsigned_attribute/4, but raises Cmsex.Error.

certificates(cms, opts \\ [])

@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.

certificates!(cms, opts \\ [])

@spec certificates!(binary(), keyword()) :: [binary()]

Like certificates/2, but raises Cmsex.Error.

certs_only(certificates, opts \\ [])

@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.

certs_only!(certificates, opts \\ [])

@spec certs_only!([binary()], keyword()) :: binary()

Like certs_only/2, but raises Cmsex.Error.

countersign(cms, position, countersigners, opts \\ [])

@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.

countersign!(cms, position, countersigners, opts \\ [])

@spec countersign!(
  binary(),
  position(),
  Cmsex.Signer.t() | map() | keyword() | [Cmsex.Signer.t() | map() | keyword()],
  keyword()
) :: binary()

Like countersign/4, but raises Cmsex.Error.

decode(cms, opts \\ [])

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:

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.

decode!(cms, opts \\ [])

Like decode/2, but raises Cmsex.Error.

decrypt(cms, opts \\ [])

@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:

  • :key and :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_certificate and :key_certificate_mismatch - for the credentials.
  • :recipient_not_found - no recipient for the credentials.
  • :unsupported_algorithm, :not_available, :malformed, or :limit_exceeded with 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_required and :unexpected_content - as in verify/2.
  • for AuthEnvelopedData, :missing_attribute, :invalid_attribute and :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.

decrypt!(cms, opts \\ [])

@spec decrypt!(binary(), keyword()) :: Cmsex.Decrypted.t()

Like decrypt/2, but raises Cmsex.Error.

digest(content, opts \\ [])

@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, :sha384 or :sha512. A ready digest gives its own hash.
  • :content_type - the type of the content (:data), a name of Cmsex.OID or an OID tuple.
  • :detached - leave the content out of the message (false for a binary).

The version is 0 for :data and 2 for another type (RFC 5652, 7).

digest!(content, opts \\ [])

@spec digest!(
  binary() | {:stream, Enumerable.t()} | {:digest, atom(), binary()},
  keyword()
) :: binary()

Like digest/2, but returns the message alone.

encrypt(content, recipients, opts \\ [])

@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_cbc or :aes192_cbc for 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 of Cmsex.OID or an OID tuple.
  • :detached - leave the encrypted content out of the message (false); the result is then {:ok, {cms, encrypted_content}}, and decrypt/2 takes the encrypted content in :content.
  • :unprotected_attributes - a list of Cmsex.Attribute that nothing protects: the unprotectedAttrs of EnvelopedData, the unauthAttrs of AuthEnvelopedData ([]).
  • :authenticated_attributes - for an authenticated cipher, a list of Cmsex.Attribute that 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).
  • :certificates and :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.

encrypt!(content, recipients, opts \\ [])

@spec encrypt!(
  binary(),
  Cmsex.Recipient.t() | binary() | [Cmsex.Recipient.t() | binary()],
  keyword()
) ::
  binary() | {binary(), binary()}

Like encrypt/3, but raises Cmsex.Error.

encrypt_with_key(content, key, opts \\ [])

@spec encrypt_with_key(binary(), binary(), keyword()) ::
  {:ok, binary() | {binary(), binary()}}

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.

encrypt_with_key!(content, key, opts \\ [])

@spec encrypt_with_key!(binary(), binary(), keyword()) ::
  binary() | {binary(), binary()}

Like encrypt_with_key/3, but returns the message alone.

mac(content, recipients, opts \\ [])

@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_sha512 or :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 of Cmsex.OID or an OID tuple.
  • :detached - leave the content out of the message (false for a binary).
  • :authenticated_attributes - a list of Cmsex.Attribute that 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, :sha384 or :sha512; for a ready digest, its hash.
  • :unauthenticated_attributes - a list of Cmsex.Attribute that nothing covers ([]).
  • :certificates and :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.

mac!(content, recipients, opts \\ [])

@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.

sign(content, signers, opts \\ [])

@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 (false for a binary; a stream or a digest is always detached).
  • :content_type - the type of the content (:data), a name of Cmsex.OID or 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 :chain of 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_time and :algorithm_protection - defaults for the signers that leave them out; see Cmsex.Signer.

The versions are those of RFC 5652, 5.1 and 5.3. Errors that belong to one signer carry its position in :signer.

sign!(content, signers, opts \\ [])

@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.

verify(cms, opts \\ [])

@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; :any needs 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 in detail.
  • :path_validation - options for :public_key.pkix_path_validation/3, such as a :verify_fun that 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; :strict refuses 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.

verify!(cms, opts \\ [])

@spec verify!(binary(), keyword()) :: Cmsex.Verified.t()

Like verify/2, but raises Cmsex.Error.

verify_countersignature(cms, position, opts \\ [])

@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.

verify_digest(cms, opts \\ [])

@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.

verify_digest!(cms, opts \\ [])

@spec verify_digest!(binary(), keyword()) :: Cmsex.DigestedData.t()

Like verify_digest/2, but raises Cmsex.Error.

verify_each(cms, opts \\ [])

@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}.

verify_mac(cms, opts \\ [])

@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_attribute and :invalid_attribute - about the authenticated attributes, once the MAC has checked. Content other than :data needs them.
  • :weak_algorithm - a digest of SHA-1 without allow_sha1: true, or MD5. HMAC-SHA1, the MAC of RFC 3370, is taken.
  • those of decrypt/2 for the credentials and the recipients, and :content_required and :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 of decode/2.

verify_mac!(cms, opts \\ [])

@spec verify_mac!(binary(), keyword()) :: Cmsex.Authenticated.t()

Like verify_mac/2, but raises Cmsex.Error.