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, typicallyid-datafor 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 whatto_be_signed/1returns. - As
[0] IMPLICIT Attributes(context-specific tag0xA0) — the form embedded insideSignerInfo. The OTP codec emits this form automatically when you encode aSignerInfo.
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 IDENTIFIERid-messageDigest→ OCTET STRINGid-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
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
@type attribute() :: {:Attribute, :public_key.oid(), [term()]}
Erlang Attribute record-shaped tuple.
@type build_opts() :: [ digest: binary(), content_oid: :public_key.oid(), signing_time: DateTime.t() ]
Required + optional input for build/1.
Functions
@spec build(build_opts()) :: {:ok, [attribute()]} | {:error, term()}
Build the three required signed attributes (contentType,
messageDigest, signingTime) plus any extras.
Required opts
:digest—binary(). 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 toid-data(1.2.840.113549.1.7.1). Use a different content-type OID for non-detached payloads.:signing_time— defaults toDateTime.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.
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.