SignCore.CMS.SignedAttributes (sign_core v0.1.0)

Copy Markdown View Source

Build and encode the signedAttrs SET-OF Attribute that goes into a CMS SignerInfo (RFC 5652 §5.3) — and produce the to-be-signed bytes per RFC 5652 §5.4 ("the message digest calculation process").

What's in here

Three required PKCS#9 attributes per RFC 5652 §11:

  • contentType (1.2.840.113549.1.9.3) — the OID of the encapsulated content type, typically id-data for detached PAdES / CAdES.
  • messageDigest (1.2.840.113549.1.9.4) — the digest of the encapsulated content (or the to-be-signed bytes for detached).
  • signingTime (1.2.840.113549.1.9.5) — the time the signature was produced, as recorded by the signer (not authoritative — that's what RFC 3161 timestamping is for).

The §5.4 re-tag (no, OTP handles it)

CMS distinguishes two encodings of the same SET-OF Attribute:

  • As SET OF Attribute (universal SET tag 0x31) — the input to the signature digest. This is what to_be_signed/1 returns.
  • As [0] IMPLICIT Attributes (context-specific tag 0xA0) — the form embedded inside SignerInfo. The OTP codec emits this form automatically when you encode a SignerInfo.

Callers should compute the signature over to_be_signed/1's output and let the codec handle the IMPLICIT-tagged embed during the final SignerInfo assembly. We never re-tag bytes by hand.

OPEN-TYPE Attribute encoding

An ASN.1 Attribute is {type OID, values SET OF ANY}. The OTP CMS module ships an information-object-class table that maps known PKCS#9 attribute OIDs to typed value definitions:

  • id-contentType → OBJECT IDENTIFIER
  • id-messageDigest → OCTET STRING
  • id-signingTime → Time CHOICE (UTCTime or GeneralizedTime)

So when building the inner Attribute tuple, we pass the typed Erlang/Elixir value directly (an OID tuple, a binary, a tagged Time choice) rather than wrapping bytes as {:asn1_OPENTYPE, der}. OTP encodes them properly. This is the "OPEN-TYPE Attribute encoding gotcha" that the Phase 4 plan §8 walks through; in practice the OTP codec absorbs it.

Time choice cutover

Per RFC 5280 §4.1.2.5 (which CMS adopts via §11.3): UTCTime for years 1950..2049 (inclusive), GeneralizedTime otherwise. Phase 4 expects to ship in the UTCTime window for the foreseeable future, but the selection is automatic.

Summary

Types

Erlang Attribute record-shaped tuple.

Required + optional input for build/1.

Functions

Build the three required signed attributes (contentType, messageDigest, signingTime) plus any extras.

Encode attrs as the universal SET OF Attribute (RFC 5652 §5.4 "to be signed" form). The returned bytes are the digest input — the signer calls Pkcs11ex.sign_bytes/2 over them, and the resulting raw signature is glued back into the SignerInfo.

Types

attribute()

@type attribute() :: {:Attribute, :public_key.oid(), [term()]}

Erlang Attribute record-shaped tuple.

build_opts()

@type build_opts() :: [
  digest: binary(),
  content_oid: :public_key.oid(),
  signing_time: DateTime.t()
]

Required + optional input for build/1.

Functions

build(opts)

@spec build(build_opts()) :: {:ok, [attribute()]} | {:error, term()}

Build the three required signed attributes (contentType, messageDigest, signingTime) plus any extras.

Required opts

  • :digestbinary(). The SHA-256 (or matching algorithm) digest over the encapsulated content. For detached signatures this is computed over the document bytes the signer commits to — for PAdES, the bytes covered by /ByteRange.

Optional opts

  • :content_oid — defaults to id-data (1.2.840.113549.1.7.1). Use a different content-type OID for non-detached payloads.
  • :signing_time — defaults to DateTime.utc_now/0. Truncated to seconds; sub-second precision is not encoded (UTCTime granularity).

Returns a list of Attribute tuples — sorted by the OTP codec into DER canonical SET order at encode time.

to_be_signed(attrs)

@spec to_be_signed([attribute()]) :: {:ok, binary()} | {:error, term()}

Encode attrs as the universal SET OF Attribute (RFC 5652 §5.4 "to be signed" form). The returned bytes are the digest input — the signer calls Pkcs11ex.sign_bytes/2 over them, and the resulting raw signature is glued back into the SignerInfo.

Equivalent to Codec.encode(:SignedAttributes, attrs); this name documents intent at the call site.