synta 0.2.3

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# SignedData and SignerInfo


## SignedData

Encapsulates signed content plus signer information (RFC 5652 §5).

```python
class SignedData:
    @staticmethod
    def from_der(data: bytes) -> SignedData: ...
    def to_der(self) -> bytes: ...

    version: int
    encap_content_type: ObjectIdentifier
    encap_content: bytes | None     # OCTET STRING value bytes
    certificates: bytes | None      # value bytes of the [0] IMPLICIT CertificateSet (no outer tag/length)
    crls: bytes | None              # value bytes of the [1] IMPLICIT RevocationInfoChoices (no outer tag/length)
    signer_infos: list[SignerInfo]
```

### Usage

```python
from synta.cms import SignedData
import synta

# Parse a PKCS#7 SignedData blob (the [0] EXPLICIT wrapping from ContentInfo is
# handled internally when using synta.load_der_pkcs7_certificates; strip it
# manually if you have a raw ContentInfo).
data = open("message.p7s", "rb").read()
sd = SignedData.from_der(data)

print(f"version: {sd.version}")
print(f"content type: {sd.encap_content_type}")
print(f"signers: {len(sd.signer_infos)}")

# Access embedded certificates
if sd.certificates:
    # certificates contains only the value bytes of the CertificateSet (no outer tag/length).
    # Individual Certificate SEQUENCES are concatenated; decode them directly:
    import synta
    dec = synta.Decoder(sd.certificates, synta.Encoding.DER)
    while not dec.is_empty():
        cert_der = dec.decode_raw_tlv()   # one DER Certificate TLV at a time

# Iterate signer infos
for si in sd.signer_infos:
    print(f"  digest alg: {si.digest_algorithm_oid}")
    print(f"  signature alg: {si.signature_algorithm_oid}")
```

---

## SignerInfo

Per-signer structure within `SignedData` (RFC 5652 §5.3).

```python
class SignerInfo:
    @staticmethod
    def from_der(data: bytes) -> SignerInfo: ...
    def to_der(self) -> bytes: ...

    version: int
    sid: bytes
    # Raw TLV bytes of the SignerIdentifier CHOICE:
    # - SEQUENCE tag (0x30) → issuerAndSerialNumber
    # - tag 0x80           → subjectKeyIdentifier

    digest_algorithm_oid: ObjectIdentifier
    digest_algorithm_params: bytes | None
    signature_algorithm_oid: ObjectIdentifier
    signature_algorithm_params: bytes | None
    signature: bytes

    signed_attrs: bytes | None
    # Value bytes of the [0] IMPLICIT SignedAttributes (no outer tag/length).
    # To hash for signature verification, prepend b'\x31' + DER-encoded length.

    unsigned_attrs: bytes | None
    # Value bytes of the [1] IMPLICIT UnsignedAttributes (no outer tag/length).
```

### Usage

```python
from synta.cms import SignedData
import synta

sd = SignedData.from_der(data)
for si in sd.signer_infos:
    print(f"digest:    {si.digest_algorithm_oid}")
    print(f"signature: {si.signature_algorithm_oid}")
    print(f"sig bytes: {si.signature.hex()}")

    if si.signed_attrs:
        # signed_attrs contains only the value bytes of the SignedAttributes SET
        # (no outer tag or length).  To produce a hashable SET TLV for CMS
        # signature verification, prepend b'\x31' and a properly DER-encoded
        # length header before hashing.  The individual Attribute SEQUENCES can
        # be iterated directly from the value bytes:
        import synta
        dec = synta.Decoder(si.signed_attrs, synta.Encoding.DER)
        while not dec.is_empty():
            attr_tlv = dec.decode_raw_tlv()
            # each attr_tlv is a complete Attribute SEQUENCE
```

---

## SignedDataBuilder

Builder for CMS `SignedData` (RFC 5652 §5). Assembles signed content, one or more
signers, and optional extra certificates into a DER-encoded `ContentInfo` wrapping a
`SignedData`. Each method returns the same builder so calls can be chained.

The builder is exported from `synta.cms` alongside `SignedData`.

```python
class SignedDataBuilder:
    def __init__(
        self,
        content: bytes,
        *,
        content_type: str | None = None,
        detached: bool = False,
    ) -> None: ...
    # content       — the data to be signed (the eContent bytes).
    # content_type  — dotted-decimal OID string for eContentType; defaults to
    #                 id-data (1.2.840.113549.1.7.1).
    # detached      — when True the eContent field is omitted from the output
    #                 (detached signature); when False (default) the content is
    #                 embedded in the SignedData.
    # Raises ValueError if content_type is not a valid OID string.

    def add_signer(
        self,
        key: PrivateKey,
        cert_der: bytes,
        hash_algorithm: str = "sha256",
    ) -> SignedDataBuilder: ...
    # Add a signer.
    # key            — the private key used for signing.
    # cert_der       — DER-encoded signing certificate; issuer name and serial
    #                  number are extracted from it to build IssuerAndSerialNumber,
    #                  and the certificate is included in the certificates field.
    # hash_algorithm — digest algorithm; one of "sha1", "sha256" (default),
    #                  "sha384", or "sha512".
    # Returns the same builder to allow chaining.
    # Raises ValueError for unsupported hash algorithms.

    def add_cert(self, cert_der: bytes) -> SignedDataBuilder: ...
    # Add an extra certificate to the certificates [0] field.
    # Returns the same builder to allow chaining.

    def build(self) -> bytes: ...
    # Assemble and return the DER-encoded ContentInfo wrapping a SignedData.
    # At least one signer must have been added before calling build().
    # Raises ValueError on certificate parse errors, unsupported algorithms,
    # or crypto backend failures, or if no signer was added.
```

### Signing steps

For each signer `build()`:

1. Computes the message digest of the content with the requested hash algorithm.
2. Builds and DER-encodes the `signedAttrs` (`contentType` + `messageDigest`).
3. Signs the `signedAttrs` bytes with the private key.
4. Assembles the `SignerInfo` SEQUENCE.

Then constructs `SignedData` (version 1) and wraps it in a `ContentInfo` with
`id-signedData` (1.2.840.113549.1.7.2).

### Examples

```python,ignore
import synta
import synta.cms

# ── Attached signature (content embedded) ─────────────────────────────────────
content = b"Hello, World!"
cert_der = open("signing_cert.der", "rb").read()
key = synta.PrivateKey.from_pem(open("signing_key.pem", "rb").read())

sd_der = (
    synta.cms.SignedDataBuilder(content, content_type="1.2.840.113549.1.7.1")
    .add_signer(key, cert_der, hash_algorithm="sha256")
    .build()
)
open("signed.p7s", "wb").write(sd_der)

# ── Detached signature (content excluded from SignedData) ─────────────────────
sd_detached = (
    synta.cms.SignedDataBuilder(content, detached=True)
    .add_signer(key, cert_der, hash_algorithm="sha384")
    .build()
)

# ── Multiple signers and extra certificates ───────────────────────────────────
key2 = synta.PrivateKey.from_pem(open("key2.pem", "rb").read())
cert2_der = open("cert2.der", "rb").read()
intermediate_der = open("intermediate.der", "rb").read()

sd_multi = (
    synta.cms.SignedDataBuilder(content)
    .add_signer(key, cert_der, hash_algorithm="sha256")
    .add_signer(key2, cert2_der, hash_algorithm="sha512")
    .add_cert(intermediate_der)
    .build()
)

# ── Round-trip: parse back the SignedData ─────────────────────────────────────
sd = synta.cms.SignedData.from_der(sd_der)
print(f"version: {sd.version}")
print(f"signers: {len(sd.signer_infos)}")
for si in sd.signer_infos:
    print(f"  digest: {si.digest_algorithm_oid}")
    print(f"  sig:    {si.signature.hex()[:32]}...")
```

See also [CMS Overview](overview.md) and [PKCS Loaders](../pki/pkcs-loaders.md) for
`load_der_pkcs7_certificates` which extracts certificates from a `SignedData` automatically.