Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Synta Python Bindings

Synta is a high-performance Rust ASN.1 library. This book documents its Python bindings — a native extension module built with PyO3 and maturin.

What the bindings provide

The synta Python package exposes Rust types directly to Python, providing near-native performance while preserving Python’s ease of use:

  • Core ASN.1 codecDecoder, Encoder, all primitive types (Integer, OctetString, ObjectIdentifier, BitString, Boolean, Real, Null, string types, time types, TaggedElement, RawElement).

  • X.509 PKI — parse Certificate, CertificationRequest, CertificateList, and OCSPResponse; extract bundles from PKCS#7 and PKCS#12 archives; build CRLs and OCSP responses; verify certificate chains; encode X.509 extension values.

  • CMS cryptography — full RFC 5652 and RFC 9629 type coverage: ContentInfo, SignedData/SignerInfo, EnvelopedData, EncryptedData, DigestedData, AuthenticatedData, and CMS-KEM types.

  • Protocol schemas — Kerberos V5 and PKINIT (synta.krb5), SPNEGO (synta.spnego), RFC 3279 algorithm parameters (synta.pkixalgs), Attribute Certificates (synta.ac), CRMF (synta.crmf), CMP (synta.cmp), PKCS#8 (synta.pkcs8), Microsoft PKI extensions (synta.ms_pki), and Merkle Tree Certificates (synta.mtc).

  • OID constants — 70+ well-known OIDs and helper functions in synta.oids and synta.oids.attr.

Implementation notes

The extension module is implemented in the synta-python workspace crate, which compiles to a cdylib named _synta and is installed as synta._synta by maturin. Three Rust crates contribute to the module surface:

Rust crateContributes
syntaEncoding, Decoder, Encoder, all primitive types
synta-certificateObjectIdentifier, Certificate, CertificationRequest, CertificateList, OCSPResponse, PublicKey, PrivateKey, PKCS#7/12 loaders, synta.oids, and all protocol schema submodules
synta-krb5synta.krb5 submodule: Krb5PrincipalName + PKINIT classes
synta-x509-verificationsynta.x509 submodule: TrustStore, CrlStore, VerificationPolicy, chain verification functions

How to navigate this book

  • Installation and Building — how to build the wheel from source and install it.
  • Quick Start — minimal decode and encode examples to get started immediately.
  • Module Layout — the full synta module tree.
  • Core ASN.1 Codec — the Decoder, Encoder, and all primitive types.
  • X.509 PKI — certificate parsing, PKCS#7/12, extension builders, chain verification, and PyCA interoperability.
  • CMS Cryptography — all CMS content types and their builders.
  • Protocol Schemas — Kerberos, SPNEGO, and other protocol-specific schemas.
  • OID Reference — tables of well-known OID constants.
  • Performance and Development — benchmark results and how to build and test the library.
  • Example Programs — 28 runnable example programs covering every binding.

For the Rust API, code-generation tutorial, and ASN.1 schema authoring see docs/tutorial.md.

Installation and Building

Prerequisites

  • Rust toolchain (1.70 or later)
  • Python 3.8 or later (abi3 stable ABI; compatible with CPython 3.8–3.14+)
  • maturin 1.x

Build Instructions

# Install maturin
pip install maturin

# Build the wheel (release mode) — maturin reads pyproject.toml automatically
maturin build --release

# Or build in development mode (with a virtualenv active)
maturin develop

The wheel will be created in target/wheels/.

Maturin is configured in pyproject.toml to build the synta-python crate:

[tool.maturin]
manifest-path = "synta-python/Cargo.toml"
module-name = "synta._synta"
bindings = "pyo3"
python-source = "python"

To enable OpenSSL-backed features (PKCS#12 encryption, CMS EnvelopedData construction, X.509 chain verification):

maturin develop --features openssl
maturin build --release --features openssl

To additionally enable legacy PKCS#12 decryption algorithms (3DES, RC2):

maturin develop --features openssl,deprecated-pkcs12-algorithms

Installation

# Install the built wheel
pip install target/wheels/synta-0.1.0-cp38-abi3-manylinux_2_34_x86_64.whl

Running tests

# Build in development mode first
python -m venv venv
source venv/bin/activate
maturin develop

# Run the Python test suite
python -m pytest tests/python/

Note on PYTHONPATH: When running scripts directly (not via pytest), use PYTHONPATH=python python3 script.py so Python can find the synta package in python/synta/.

Quick Start

After installing the package, import synta and start decoding or encoding ASN.1 data.

Decode a single element

import synta

# Decode an INTEGER
data = b'\x02\x01\x2A'  # DER-encoded INTEGER 42
decoder = synta.Decoder(data, synta.Encoding.DER)
integer = decoder.decode_integer()
print(integer.to_int())  # Output: 42

# Decode an OBJECT IDENTIFIER
oid_data = b'\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
decoder = synta.Decoder(oid_data, synta.Encoding.DER)
oid = decoder.decode_oid()
print(str(oid))  # Output: 1.2.840.113549.1.1.1

# Decode an OCTET STRING
octet_data = b'\x04\x05hello'
decoder = synta.Decoder(octet_data, synta.Encoding.DER)
octet_string = decoder.decode_octet_string()
print(octet_string.to_bytes())  # Output: b'hello'

Encode a single element

import synta

# Encode an INTEGER
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_integer(42)
output = encoder.finish()
print(output.hex())  # Output: 02012a

# Encode an OCTET STRING
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_octet_string(b'hello')
output = encoder.finish()
print(output.hex())  # Output: 040568656c6c6f

Encode a SEQUENCE

Encode inner elements into a separate Encoder, then wrap the result in a SEQUENCE using an outer Encoder.

import synta

# Build SEQUENCE { INTEGER 42, BOOLEAN TRUE }
inner = synta.Encoder(synta.Encoding.DER)
inner.encode_integer(42)
inner.encode_boolean(True)

outer = synta.Encoder(synta.Encoding.DER)
outer.encode_sequence(inner.finish())
result = outer.finish()
# result == b'\x30\x06\x02\x01\x2a\x01\x01\xff'

Decode a SEQUENCE

Use decode_sequence() to enter a SEQUENCE and get a child Decoder positioned over the content bytes.

import synta

# Encoded SEQUENCE { INTEGER 42, BOOLEAN TRUE }
data = b'\x30\x06\x02\x01\x2a\x01\x01\xff'

decoder = synta.Decoder(data, synta.Encoding.DER)
child = decoder.decode_sequence()    # advances past the outer SEQUENCE TLV
assert decoder.is_empty()

while not child.is_empty():
    obj = child.decode_any()         # INTEGER, then BOOLEAN

Parse an X.509 certificate

import synta

with open('cert.der', 'rb') as f:
    cert = synta.Certificate.from_der(f.read())

print(cert.subject)            # RFC 4514-style DN string
print(cert.issuer)
print(cert.not_before, "–", cert.not_after)
print(cert.signature_algorithm)

See Certificate for the full property list.

Module Layout

Crate-to-module mapping

The synta Python package is implemented as a PyO3 extension module (_synta.abi3.so) built from three Rust crates:

Rust crateContributes
syntaEncoding, Decoder, Encoder, all primitive types
synta-certificateObjectIdentifier, Certificate, CertificationRequest, CertificateList, OCSPResponse, PublicKey, PrivateKey, CertificateListBuilder, OCSPResponseBuilder, synta.oids, PKCS#7/12 loaders, synta.pkixalgs, synta.ac, synta.crmf, synta.cmp, synta.pkcs8, synta.pkcs9, synta.kem; OpensslSignatureVerifier
synta-krb5 (via synta-python)synta.krb5 submodule: Krb5PrincipalName + PKINIT classes
synta-x509-verification (via synta-python)synta.x509 submodule: TrustStore, CrlStore, VerificationPolicy, verify_server_certificate, verify_client_certificate

The package surface is defined in python/synta/__init__.py, which re-exports everything from _synta and makes synta.krb5 and synta.oids available without an explicit sub-import.

Full module tree

synta                          # top-level package
├── Encoding                   # enum: DER | BER | CER
├── Decoder                    # streaming ASN.1 decoder
├── Encoder                    # ASN.1 encoder
├── SyntaError                 # exception class
├── __version__                # str, e.g. "0.1.0"
│
├── Integer                    # primitive types
├── OctetString
├── ObjectIdentifier
├── BitString
├── Boolean
├── Real
├── Null
├── UtcTime
├── GeneralizedTime
├── Utf8String
├── PrintableString
├── IA5String
├── NumericString
├── TeletexString
├── VisibleString
├── GeneralString
├── UniversalString
├── BmpString
├── TaggedElement
├── RawElement
│
├── Certificate                # PKI types
├── CertificationRequest
├── CertificateList
├── OCSPResponse
├── PublicKey                  # backend-agnostic key types (RSA, EC, EdDSA, ML-DSA, ML-KEM)
├── PrivateKey
├── CertificateListBuilder     # RFC 5280 CRL TBS builder
├── OCSPResponseBuilder        # RFC 6960 OCSP TBS builder
│
├── pem_to_der()               # PEM helpers
├── der_to_pem()
│
├── load_der_pkcs7_certificates()   # PKCS#7 / PKCS#12 loaders
├── load_pem_pkcs7_certificates()
├── load_pkcs12_certificates()
├── load_pkcs12_keys()
├── load_pkcs12()
├── read_pki_blocks()
│
├── general_name               # synta.general_name submodule
│   ├── OTHER_NAME, RFC822_NAME, DNS_NAME, X400_ADDRESS
│   ├── DIRECTORY_NAME, EDI_PARTY_NAME, URI
│   ├── IP_ADDRESS, REGISTERED_ID   (integer tag constants)
│   ├── OtherName, RFC822Name, DNSName, X400Address
│   ├── DirectoryName, EDIPartyName
│   ├── UniformResourceIdentifier, IPAddress, RegisteredID
│   └── AnyGeneralName  (union type alias for all typed classes)
│
├── oids                       # synta.oids submodule
│   ├── RSA_ENCRYPTION, SHA256_WITH_RSA, ...
│   ├── EC_PUBLIC_KEY, EC_CURVE_P256, ...
│   ├── ED25519, ED448, ML_DSA_44, ...
│   ├── SUBJECT_ALT_NAME, BASIC_CONSTRAINTS, ...
│   ├── KP_SERVER_AUTH, KP_CLIENT_AUTH, ...
│   ├── ID_PKINIT_SAN, ID_PKINIT_KPCLIENT_AUTH, ...
│   ├── ID_MS_SAN_UPN, ID_MS_KP_SMARTCARD_LOGON, ...
│   └── attr                   # synta.oids.attr submodule
│       ├── COMMON_NAME, ORGANIZATION, COUNTRY, ...
│       └── ...
│
├── krb5                       # synta.krb5 submodule
│   ├── KRB5_PRINCIPAL_NAME_OID
│   ├── NT_UNKNOWN, NT_PRINCIPAL, NT_SRV_INST, ...
│   ├── Krb5PrincipalName
│   ├── EncryptionKey
│   ├── Checksum
│   ├── KDFAlgorithmId
│   ├── IssuerAndSerialNumber
│   ├── ExternalPrincipalIdentifier
│   ├── PKAuthenticator
│   ├── AuthPack
│   ├── PaPkAsReq
│   ├── DHRepInfo
│   ├── KDCDHKeyInfo
│   ├── ReplyKeyPack
│   └── PaPkAsRep
│
├── pkixalgs                   # synta.pkixalgs submodule (RFC 3279)
│   ├── DssParms, DssSigValue, EcdsaSigValue, ECParameters
│   └── ID_DSA, ID_EC_PUBLIC_KEY, ECDSA_WITH_SHA256, PRIME256V1, ...
│
├── ac                         # synta.ac submodule (RFC 5755)
│   ├── AttributeCertificate   # parse / PEM / verify_issued_by
│   ├── AttributeCertificateBuilder
│   └── ID_AT_ROLE, ID_AT_CLEARANCE, ID_PE_AC_AUDIT_IDENTITY, ...
│
├── crmf                       # synta.crmf submodule (RFC 4211)
│   ├── CertReqMessages, CertReqMsg
│   ├── CertReqMsgBuilder, CertReqMessagesBuilder
│   ├── PUB_METHOD_DONT_CARE, PUB_METHOD_X500, PUB_METHOD_WEB, PUB_METHOD_LDAP
│   └── ID_REG_CTRL_REG_TOKEN, ID_REG_CTRL_AUTHENTICATOR, ...
│
├── cmp                        # synta.cmp submodule (RFC 9810)
│   ├── CMPMessage
│   ├── CMPMessageBuilder
│   └── ID_PASSWORD_BASED_MAC, ID_DHBASED_MAC, ID_KP_CM_KGA, ...
│
├── pkcs8                      # synta.pkcs8 submodule (RFC 5958 / PKCS#8)
│   ├── OneAsymmetricKey       # parse DER-encoded private-key envelope
│   └── PrivateKeyInfo         # alias for OneAsymmetricKey
│
├── pkcs9                      # synta.pkcs9 submodule (RFC 2985 / PKCS#9)
│   ├── ID_PKCS_9, ID_EMAIL_ADDRESS, ID_CONTENT_TYPE, ID_MESSAGE_DIGEST
│   ├── ID_SIGNING_TIME, ID_COUNTERSIGNATURE, ID_CHALLENGE_PASSWORD
│   └── ID_EXTENSION_REQUEST, ID_FRIENDLY_NAME, ID_LOCAL_KEY_ID, ...
│
├── kem                        # synta.kem submodule (RFC 9629 / FIPS 203)
│   ├── KEMRecipientInfo       # KEM recipient structure in CMS EnvelopedData
│   ├── CMSORIforKEMOtherInfo  # KDF input structure for KEMRecipientInfo
│   ├── ID_ML_KEM_512, ID_ML_KEM_768, ID_ML_KEM_1024
│   └── ID_ORI, ID_ORI_KEM
│
├── spnego                     # synta.spnego submodule (RFC 4178)
│   ├── NegTokenInit           # initiator proposal (mech_types, mech_token, …)
│   ├── NegTokenResp           # acceptor response (neg_state, supported_mech, …)
│   ├── NegotiationToken       # CHOICE wrapper; from_der handles GSSAPI 0x60 form
│   ├── NEG_STATE_ACCEPT_COMPLETED, NEG_STATE_ACCEPT_INCOMPLETE
│   ├── NEG_STATE_REJECT, NEG_STATE_REQUEST_MIC
│   └── SPNEGO_OID             # "1.3.6.1.5.5.2"
│
├── ms_pki                     # synta.ms_pki submodule (Microsoft AD CS)
│   ├── MSCSTemplateV2         # id-ms-certificate-template (OID 1.3.6.1.4.1.311.21.7)
│   ├── RequestClientInfo      # id-ms-request-Client-Info (OID 1.3.6.1.4.1.311.21.20)
│   └── ID_MS_CERTSRV_CA_VERSION, ID_MS_KP_CA_EXCHANGE, ID_MS_KP_EFS_CRYPTO, …
│
├── mtc                        # synta.mtc submodule (draft-ietf-plants-merkle-tree-certs)
│   ├── ProofNode, Subtree, SubtreeProof, InclusionProof
│   ├── LogID, CosignerID, Checkpoint, SubtreeSignature
│   ├── TbsCertificateLogEntry, MerkleTreeCertEntry
│   ├── LandmarkID, StandaloneCertificate, LandmarkCertificate
│   └── (all classes parsed via from_der; names are raw DER, pass to parse_name_attrs)
│
└── x509                       # synta.x509 submodule (RFC 5280 / CABF path validation)
    ├── TrustStore             # trusted root CA store (DER bytes)
    ├── CrlStore               # CRL revocation checking store (DER bytes)
    ├── VerificationPolicy     # server_names, name_match, validation_time, max_chain_depth, profile
    ├── X509VerificationError  # raised on any chain or policy failure
    ├── verify_server_certificate(leaf, intermediates, store, policy, crls=None) → list[bytes]
    └── verify_client_certificate(leaf, intermediates, store, policy, crls=None) → list[bytes]

Encoding Rules: DER, BER, CER

Constants and enumerations

NameTypeDescription
Encoding.DEREncodingDistinguished Encoding Rules
Encoding.BEREncodingBasic Encoding Rules
Encoding.CEREncodingCanonical Encoding Rules
SyntaErrorexception classRaised on ASN.1 parse or encode failures
__version__strPackage version string (from Cargo.toml)

Encoding

Pass an Encoding value when constructing a Decoder or Encoder:

import synta

# DER — deterministic, used for X.509 certificates and PKCS structures
decoder = synta.Decoder(data, synta.Encoding.DER)

# BER — accepts indefinite-length forms (PKCS#7, LDAP, SNMP)
decoder = synta.Decoder(data, synta.Encoding.BER)

# CER — canonical streaming subset of BER
decoder = synta.Decoder(data, synta.Encoding.CER)

The Decoder and Encoder classes accept any Encoding value. Most PKI standards mandate DER. Use BER when reading PKCS#7 files from older tools that emit indefinite-length encodings.

API types

import synta

# DER encoder example
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_integer(42)
out = encoder.finish()   # b'\x02\x01\x2a'

See Decoder and Encoder for full API reference.

Decoder

Constructor

Decoder(data: bytes, encoding: Encoding)

Creates a streaming decoder over data. The internal position starts at 0. Each decode_* call advances the position past the decoded element.

Primitive decode methods

MethodReturnsASN.1 typeTag
decode_integer()IntegerINTEGER0x02
decode_octet_string()OctetStringOCTET STRING0x04
decode_oid()ObjectIdentifierOBJECT IDENTIFIER0x06
decode_bit_string()BitStringBIT STRING0x03
decode_boolean()BooleanBOOLEAN0x01
decode_utc_time()UtcTimeUTCTime0x17
decode_generalized_time()GeneralizedTimeGeneralizedTime0x18
decode_null()NullNULL0x05
decode_real()RealREAL0x09
decode_utf8_string()Utf8StringUTF8String0x0c
decode_printable_string()PrintableStringPrintableString0x13
decode_ia5_string()IA5StringIA5String0x16
decode_numeric_string()NumericStringNumericString0x12
decode_teletex_string()TeletexStringTeletexString / T61String0x14
decode_visible_string()VisibleStringVisibleString0x1a
decode_general_string()GeneralStringGeneralString0x1b
decode_universal_string()UniversalStringUniversalString0x1c
decode_bmp_string()BmpStringBMPString0x1e
decode_any()any Python objectany element
decode_any_str()strany string type

decode_any() dispatch table

decode_any() dispatches on the tag at the current position:

ASN.1 TypePython value
BOOLEANBoolean
INTEGERInteger
BIT STRINGBitString
OCTET STRINGOctetString
NULLNull
OBJECT IDENTIFIERObjectIdentifier
UTF8StringUtf8String
PrintableStringPrintableString
IA5StringIA5String
NumericStringNumericString
TeletexStringTeletexString
VisibleStringVisibleString
GeneralStringGeneralString
UniversalStringUniversalString
BmpStringBmpString
UTCTimeUtcTime
GeneralizedTimeGeneralizedTime
SEQUENCE / SETlist of the above
TaggedTaggedElement
Unknown universalRawElement

decode_any_str() encoding table

decode_any_str() reads one TLV and decodes it as a native Python str, applying the correct encoding for each of the nine ASN.1 string types:

TagTypeDecoding
12UTF8StringUTF-8 (lossy)
18NumericStringUTF-8
19PrintableStringUTF-8
20TeletexString / T61StringLatin-1 (each byte → U+0000–U+00FF)
22IA5StringUTF-8
26VisibleStringUTF-8
27GeneralStringUTF-8
28UniversalStringUCS-4 big-endian
30BMPStringUCS-2 big-endian

Raises ValueError for any other tag; raises EOFError if the decoder is empty. This is the single-call replacement for the duck-typing probe on decode_any():

# Before — three-way probe:
val = decoder.decode_any()
if hasattr(val, 'as_str'):
    s = val.as_str()
elif hasattr(val, 'to_bytes'):
    s = val.to_bytes().decode('latin-1')
else:
    raise ValueError(f"not a string: {type(val)}")

# After — one call, correct encoding for all nine types:
s = decoder.decode_any_str()

Structured / container decode methods

MethodSignatureReturnsDescription
decode_sequence()DecoderConsume a SEQUENCE TLV; return child decoder over its contents.
decode_set()DecoderConsume a SET TLV; return child decoder over its contents.
decode_explicit_tag(tag_num: int)DecoderStrip an explicit context-specific tag [tag_num]; return child decoder over the content.
decode_implicit_tag(tag_num: int, tag_class: str)DecoderStrip an implicit tag; return child decoder over the value bytes only (no tag/length). tag_class is "Context", "Application", "Private", or "Universal".
decode_raw_tlv()bytesRead the next complete TLV (tag + length + value) as raw bytes and advance past it.

Introspection helpers

MethodReturnsDescription
peek_tag()tuple[int, str, bool](tag_number, tag_class, is_constructed) — does not advance the position. Raises EOFError if no data remains.
remaining_bytes()bytesAll bytes from the current position to the end. Useful after decode_implicit_tag to retrieve bare primitive value bytes.
is_empty()boolTrue when the current position equals the data length.
position()intCurrent byte offset.
remaining()intNumber of bytes left.

Full class stub

class Decoder:
    def __init__(self, data: bytes, encoding: Encoding) -> None: ...

    # Primitive types
    def decode_integer(self) -> Integer: ...
    def decode_octet_string(self) -> OctetString: ...
    def decode_oid(self) -> ObjectIdentifier: ...
    def decode_bit_string(self) -> BitString: ...
    def decode_boolean(self) -> Boolean: ...
    def decode_real(self) -> Real: ...
    def decode_null(self) -> Null: ...
    def decode_utc_time(self) -> UtcTime: ...
    def decode_generalized_time(self) -> GeneralizedTime: ...

    # String types
    def decode_utf8_string(self) -> Utf8String: ...
    def decode_printable_string(self) -> PrintableString: ...
    def decode_ia5_string(self) -> IA5String: ...
    def decode_numeric_string(self) -> NumericString: ...       # tag 18
    def decode_teletex_string(self) -> TeletexString: ...      # tag 20
    def decode_visible_string(self) -> VisibleString: ...      # tag 26
    def decode_general_string(self) -> GeneralString: ...      # tag 27
    def decode_universal_string(self) -> UniversalString: ...  # tag 28
    def decode_bmp_string(self) -> BmpString: ...              # tag 30

    # Constructed / tagged
    def decode_sequence(self) -> Decoder: ...
    # Reads a SEQUENCE TLV, advances past it, and returns a new Decoder over
    # the content bytes.  Raises ValueError if the next element is not a SEQUENCE.

    def decode_explicit_tag(self, tag_num: int) -> Decoder: ...
    # Reads an explicit context-specific tag [tag_num], advances past it, and
    # returns a new Decoder over the tagged content.
    # Raises ValueError if the tag number does not match.

    def decode_set(self) -> Decoder: ...
    # Reads a SET TLV (tag 0x31), advances past it, and returns a new Decoder
    # over the content bytes.  Raises ValueError if the next element is not a SET.

    def decode_implicit_tag(self, tag_num: int, tag_class: str) -> Decoder: ...
    # Strips an implicit tag of the given number and class and returns a new
    # Decoder over the raw value bytes.  tag_class must be "Universal",
    # "Context", "Application", or "Private".  Raises ValueError on mismatch.
    # The caller must know the original type and call the appropriate decode_*
    # method on the returned Decoder.
    #
    # Example:
    #   raw_decoder = decoder.decode_implicit_tag(0, "Context")
    #   value = raw_decoder.decode_integer()

    def peek_tag(self) -> tuple[int, str, bool]: ...
    # Returns (tag_number, tag_class, is_constructed) of the next element without
    # consuming any bytes.  Raises EOFError if the decoder is empty.
    # Use for CHOICE dispatch or optional-field detection:
    #   tag_num, tag_class, _ = decoder.peek_tag()
    #   if tag_class == "Context" and tag_num == 0:
    #       version = decoder.decode_explicit_tag(0)

    def decode_raw_tlv(self) -> bytes: ...
    # Reads the complete next TLV (tag + length + value bytes) as a bytes object
    # and advances past it.  Useful when the element type is unknown or when
    # decoding should be deferred:
    #   tlv = decoder.decode_raw_tlv()
    #   inner = synta.Decoder(tlv, synta.Encoding.DER)

    def remaining_bytes(self) -> bytes: ...
    # Returns all remaining bytes from the current position without advancing.
    # Primarily useful after decode_implicit_tag() for **primitive** implicit
    # types whose raw value bytes cannot be decoded with the decode_* methods
    # (those expect a full TLV, but implicit stripping leaves only the value):
    #
    #   # Decode dNSName [2] IMPLICIT IA5String
    #   child = decoder.decode_implicit_tag(2, "Context")
    #   dns_name = child.remaining_bytes().decode("ascii")
    #
    #   # Decode iPAddress [7] IMPLICIT OCTET STRING
    #   child = decoder.decode_implicit_tag(7, "Context")
    #   ip_bytes = child.remaining_bytes()   # 4 or 16 raw bytes

    # Dynamic decoding
    def decode_any(self) -> object: ...
    # Returns a typed Python object.  Sequence/Set → list.
    # Tagged elements → TaggedElement.
    # Unknown universal tags → RawElement.

    def decode_any_str(self) -> str: ...
    # Decode any ASN.1 string type as a Python str (correct encoding per type).
    # Raises ValueError for non-string tags; EOFError if empty.

    # State
    def is_empty(self) -> bool: ...
    def position(self) -> int: ...
    def remaining(self) -> int: ...

Usage examples

Decoding ASN.1 data

import synta

# Decode an INTEGER
data = b'\x02\x01\x2A'  # DER-encoded INTEGER 42
decoder = synta.Decoder(data, synta.Encoding.DER)
integer = decoder.decode_integer()
print(integer.to_int())  # Output: 42

# Decode an OBJECT IDENTIFIER
oid_data = b'\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
decoder = synta.Decoder(oid_data, synta.Encoding.DER)
oid = decoder.decode_oid()
print(str(oid))  # Output: 1.2.840.113549.1.1.1

# Decode an OCTET STRING
octet_data = b'\x04\x05hello'
decoder = synta.Decoder(octet_data, synta.Encoding.DER)
octet_string = decoder.decode_octet_string()
print(octet_string.to_bytes())  # Output: b'hello'

# Decode a NULL
null_data = b'\x05\x00'
decoder = synta.Decoder(null_data, synta.Encoding.DER)
null = decoder.decode_null()

# Decode a REAL (IEEE 754 double)
real_data = b'\x09\x01\x40'  # PLUS-INFINITY
decoder = synta.Decoder(real_data, synta.Encoding.DER)
r = decoder.decode_real()
import math
assert math.isinf(float(r))

# Decode any element dynamically
data = b'\x02\x01\x2A'
decoder = synta.Decoder(data, synta.Encoding.DER)
obj = decoder.decode_any()  # Returns Integer, OctetString, list (Sequence/Set), etc.

Decoding SEQUENCE structures

Use decode_sequence() to enter a SEQUENCE and get a child Decoder positioned over the content bytes. Iterate with typed decode_* methods and is_empty().

import synta

# Encoded SEQUENCE { INTEGER 42, BOOLEAN TRUE }
data = b'\x30\x06\x02\x01\x2a\x01\x01\xff'

decoder = synta.Decoder(data, synta.Encoding.DER)
child = decoder.decode_sequence()    # advances past the outer SEQUENCE TLV
assert decoder.is_empty()

while not child.is_empty():
    obj = child.decode_any()         # INTEGER, then BOOLEAN

# Decode an explicit context tag [1] wrapping an INTEGER
tagged_data = b'\xa1\x05\x02\x03\x00\x00\x63'  # [1] EXPLICIT INTEGER 99
decoder = synta.Decoder(tagged_data, synta.Encoding.DER)
child = decoder.decode_explicit_tag(1)   # raises ValueError if tag != [1]
integer = child.decode_integer()
assert integer.to_int() == 99

Encoder

Constructor

Encoder(encoding: Encoding)

Creates a new in-memory encoder. Elements are appended in call order. Call finish() to retrieve the accumulated bytes.

Primitive encode methods — raw values

MethodSignatureASN.1 type
encode_integer(value: int)INTEGER — accepts any Python int (arbitrary magnitude via bigint path)
encode_octet_string(data: bytes)OCTET STRING
encode_oid(value: ObjectIdentifier)OBJECT IDENTIFIER
encode_bit_string(value: BitString)BIT STRING
encode_boolean(value: bool)BOOLEAN
encode_utc_time(value: UtcTime)UTCTime
encode_generalized_time(value: GeneralizedTime)GeneralizedTime
encode_real(value: float)REAL
encode_null()NULL
encode_utf8_string(value: str)UTF8String
encode_printable_string(value: str)PrintableString — raises ValueError for invalid chars
encode_ia5_string(value: str)IA5String — raises ValueError for non-ASCII
encode_numeric_string(value: str)NumericString — raises ValueError for non-digit/non-space
encode_teletex_string(data: bytes)TeletexString / T61String
encode_visible_string(value: str)VisibleString — raises ValueError for invalid chars
encode_general_string(data: bytes)GeneralString
encode_universal_string(value: str)UniversalString (UCS-4 BE)
encode_bmp_string(value: str)BMPString (UCS-2 BE) — raises ValueError for non-BMP code points

Primitive encode methods — typed objects

Each primitive type has an _object variant that accepts the corresponding Python wrapper instead of a raw value.

MethodAccepts
encode_integer_objectInteger
encode_octet_string_objectOctetString
encode_oid_objectObjectIdentifier (alias for encode_oid)
encode_bit_string_objectBitString (alias for encode_bit_string)
encode_boolean_objectBoolean
encode_utc_time_objectUtcTime (alias for encode_utc_time)
encode_generalized_time_objectGeneralizedTime (alias for encode_generalized_time)
encode_real_objectReal
encode_null_objectNull
encode_utf8_string_objectUtf8String
encode_printable_string_objectPrintableString
encode_ia5_string_objectIA5String
encode_numeric_string_objectNumericString
encode_teletex_string_objectTeletexString
encode_visible_string_objectVisibleString
encode_general_string_objectGeneralString
encode_universal_string_objectUniversalString
encode_bmp_string_objectBmpString

Container / tagging encode methods

MethodSignatureDescription
encode_sequence(inner_bytes: bytes)Wrap inner_bytes with SEQUENCE TLV (tag 0x30).
encode_set(inner_bytes: bytes)Wrap inner_bytes with SET TLV (tag 0x31).
encode_explicit_tag(tag_num: int, tag_class: str, inner_bytes: bytes)Wrap inner_bytes in an explicit constructed tag. tag_class: "Context", "Application", "Private".
encode_implicit_tag(tag_num: int, tag_class: str, is_constructed: bool, value_bytes: bytes)Emit an implicit tag over raw value bytes. Set is_constructed=True for SEQUENCE/SET underlying types.

Finalisation

MethodReturnsDescription
finish()bytesConsume the encoder and return the accumulated DER/BER bytes. The encoder is reset and can be reused (rarely needed).

Full class stub

class Encoder:
    def __init__(self, encoding: Encoding) -> None: ...

    # Primitive types
    def encode_integer(self, value: int) -> None: ...
    # Accepts any Python int, including values beyond i64/i128 range (e.g.
    # 20-byte X.509 serial numbers up to 160 bits).
    def encode_integer_object(self, value: Integer) -> None: ...
    def encode_octet_string(self, data: bytes) -> None: ...
    def encode_octet_string_object(self, value: OctetString) -> None: ...
    def encode_oid(self, value: ObjectIdentifier) -> None: ...
    def encode_bit_string(self, value: BitString) -> None: ...
    def encode_boolean(self, value: bool) -> None: ...
    def encode_real(self, value: float) -> None: ...
    def encode_real_object(self, value: Real) -> None: ...
    def encode_null(self) -> None: ...
    def encode_null_object(self, value: Null) -> None: ...
    def encode_utc_time(self, value: UtcTime) -> None: ...
    def encode_generalized_time(self, value: GeneralizedTime) -> None: ...

    # String types
    def encode_utf8_string(self, value: str) -> None: ...
    def encode_utf8_string_object(self, value: Utf8String) -> None: ...
    def encode_printable_string(self, value: str) -> None: ...       # Raises ValueError for invalid chars
    def encode_printable_string_object(self, value: PrintableString) -> None: ...
    def encode_ia5_string(self, value: str) -> None: ...             # Raises ValueError for non-ASCII
    def encode_ia5_string_object(self, value: IA5String) -> None: ...
    def encode_numeric_string(self, value: str) -> None: ...         # Raises ValueError for non-digit/space
    def encode_numeric_string_object(self, value: NumericString) -> None: ...
    def encode_teletex_string(self, data: bytes) -> None: ...        # Raw bytes, tag 20
    def encode_teletex_string_object(self, value: TeletexString) -> None: ...
    def encode_visible_string(self, value: str) -> None: ...         # Raises ValueError for control chars
    def encode_visible_string_object(self, value: VisibleString) -> None: ...
    def encode_general_string(self, data: bytes) -> None: ...        # Raw bytes, tag 27
    def encode_general_string_object(self, value: GeneralString) -> None: ...
    def encode_universal_string(self, value: str) -> None: ...       # UCS-4 BE, tag 28
    def encode_universal_string_object(self, value: UniversalString) -> None: ...
    def encode_bmp_string(self, value: str) -> None: ...             # UCS-2 BE, tag 30; Raises ValueError if > U+FFFF
    def encode_bmp_string_object(self, value: BmpString) -> None: ...

    # Constructed / tagged
    def encode_sequence(self, inner_bytes: bytes) -> None: ...
    # Wraps pre-encoded bytes in a SEQUENCE TLV (tag 0x30).
    # Encode inner elements into a separate Encoder, call finish(), then pass
    # the result here.

    def encode_set(self, inner_bytes: bytes) -> None: ...
    # Wraps pre-encoded bytes in a SET TLV (tag 0x31).

    def encode_explicit_tag(self, tag_num: int, tag_class: str, inner_bytes: bytes) -> None: ...
    # Wraps pre-encoded bytes in an explicit tag TLV (always constructed).
    # tag_class must be "Context", "Application", or "Private".
    # Raises ValueError for any other value.

    def encode_implicit_tag(self, tag_num: int, tag_class: str,
                            is_constructed: bool, value_bytes: bytes) -> None: ...
    # Writes an implicit tag with the given value bytes (not a full TLV).
    # For implicit tagging the original type tag is replaced, so pass the raw
    # value content (not the original TLV).  Set is_constructed=True when the
    # underlying type is SEQUENCE, SET, or another constructed form.
    # tag_class must be "Context", "Application", or "Private".
    #
    # Example — [1] IMPLICIT INTEGER with raw value bytes b'\x2a':
    #   enc.encode_implicit_tag(1, "Context", False, b'\x2a')

    def finish(self) -> bytes: ...

Usage examples

Encoding ASN.1 data

import synta

# Encode an INTEGER
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_integer(42)
output = encoder.finish()
print(output.hex())  # Output: 02012a

# Encode an OCTET STRING
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_octet_string(b'hello')
output = encoder.finish()
print(output.hex())  # Output: 040568656c6c6f

# Encode a NULL
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_null()
output = encoder.finish()
print(output.hex())  # Output: 0500

# Encode a REAL
import math
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_real(0.0)
output = encoder.finish()
print(output.hex())  # Output: 0900

encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_real(math.inf)
output = encoder.finish()
print(output.hex())  # Output: 090140

# Encode a UTF8String
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_utf8_string("Hello, world!")
output = encoder.finish()

# Encode a GeneralString (Kerberos realm, FreeIPA use-case)
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_general_string(b"EXAMPLE.COM")
output = encoder.finish()

# Encode a BMPString (Microsoft AD-CS template name)
encoder = synta.Encoder(synta.Encoding.DER)
encoder.encode_bmp_string("User")
output = encoder.finish()

Building SEQUENCE structures

Encode inner elements into a separate Encoder, then wrap the output in a SEQUENCE or SET using the outer Encoder. Use encode_explicit_tag to add explicit context tags.

import synta

# Build SEQUENCE { INTEGER 42, BOOLEAN TRUE }
inner = synta.Encoder(synta.Encoding.DER)
inner.encode_integer(42)
inner.encode_boolean(True)

outer = synta.Encoder(synta.Encoding.DER)
outer.encode_sequence(inner.finish())
result = outer.finish()
# result == b'\x30\x06\x02\x01\x2a\x01\x01\xff'

# Build [1] EXPLICIT INTEGER 99 (explicit context tag)
inner = synta.Encoder(synta.Encoding.DER)
inner.encode_integer(99)

outer = synta.Encoder(synta.Encoding.DER)
outer.encode_explicit_tag(1, "Context", inner.finish())
result = outer.finish()
# First byte 0xa1 = context-specific constructed tag [1]

Primitive Types

The following primitive ASN.1 types are available as Python classes in the synta package. Each section below links to the detailed API reference.

Numeric types

ClassASN.1 typeTag
IntegerINTEGER0x02
BooleanBOOLEAN0x01
RealREAL0x09
NullNULL0x05

See Numeric Types for full API reference.

String types

ClassASN.1 typeTagCharacter set
Utf8StringUTF8String0x0cUnicode
PrintableStringPrintableString0x13Restricted ASCII
IA5StringIA5String0x16ASCII (0x00–0x7f)
NumericStringNumericString0x12Digits and space
TeletexStringTeletexString / T61String0x14Arbitrary bytes (Latin-1 decode)
VisibleStringVisibleString0x1aPrintable ASCII
GeneralStringGeneralString0x1bArbitrary bytes (Latin-1 decode)
UniversalStringUniversalString0x1cUCS-4 big-endian
BmpStringBMPString0x1eUCS-2 big-endian (BMP only)

See String Types for full API reference.

Time types

ClassASN.1 typeTag
UtcTimeUTCTime0x17
GeneralizedTimeGeneralizedTime0x18

See Time Types for full API reference.

Bit and octet strings

ClassASN.1 typeTag
OctetStringOCTET STRING0x04
BitStringBIT STRING0x03

See Bit and Octet Strings for full API reference.

Tagged and raw elements

ClassDescription
TaggedElementReturned by decode_any() for explicitly-tagged or application/private-tagged values.
RawElementReturned by decode_any() for unknown universal tags.

See Tagged and Raw Elements for full API reference.

Numeric Types: Integer, Real, Boolean, Null

Integer

ASN.1 tag 0x02.

Constructors

Integer(value: int)            # constructor — i64 range only
Integer.from_bytes(data: bytes) -> Integer   # big-endian two's complement
Integer.from_u64(value: int) -> Integer      # unsigned 64-bit

Methods

MethodReturnsDescription
to_int()intConvert to Python int via i64; raises OverflowError if too large.
to_i128()intConvert via i128; raises OverflowError if too large.
to_bytes()bytesRaw big-endian two’s complement bytes.

Special methods: __repr__, __str__, __eq__, __hash__.

Full class stub

class Integer:
    def __init__(self, value: int) -> None: ...
    def to_int(self) -> int: ...          # Raises OverflowError if > i64
    def to_i128(self) -> int: ...         # Raises OverflowError if > i128
    def to_bytes(self) -> bytes: ...      # Big-endian two's complement
    @staticmethod
    def from_bytes(data: bytes) -> Integer: ...
    @staticmethod
    def from_u64(value: int) -> Integer: ...
    def __eq__(self, other: object) -> bool: ...    # Compares canonical DER encoding
    def __hash__(self) -> int: ...                  # Hash over canonical DER bytes

Boolean

ASN.1 tag 0x01.

Constructors

Boolean(value: bool)

Methods

MethodReturns
value()bool
__bool__()bool

Full class stub

class Boolean:
    def __init__(self, value: bool) -> None: ...
    def value(self) -> bool: ...
    def __bool__(self) -> bool: ...
    def __eq__(self, other: object) -> bool: ...
    def __hash__(self) -> int: ...   # hash(True) == 1, hash(False) == 0

Real

ASN.1 tag 0x09.

Constructors

Real(value: float)

Methods

MethodReturnsDescription
value()float
is_infinite()boolTrue for ±∞
is_nan()boolTrue for NaN
is_finite()boolTrue for finite, non-NaN
__float__()floatEnables float(r)
__eq__, __hash__Hash consistent with IEEE 754 (NaN != NaN, -0.0 == 0.0)

Special values (X.690 encoding)

ValueDER bytes
0.009 00
math.inf09 01 40
-math.inf09 01 41
math.nan09 01 42
finite f6409 09 80 <8 bytes>

Full class stub

class Real:
    def __init__(self, value: float) -> None: ...
    def value(self) -> float: ...
    def is_infinite(self) -> bool: ...   # True for +∞ and −∞
    def is_nan(self) -> bool: ...
    def is_finite(self) -> bool: ...
    def __float__(self) -> float: ...
    def __eq__(self, other: Real) -> bool: ...  # NaN != NaN (IEEE 754)
    def __hash__(self) -> int: ...              # -0.0 normalised to 0.0 before hashing

Null

ASN.1 tag 0x05.

Null()

__repr__ returns "Null()". All Null() instances compare equal.

Full class stub

class Null:
    def __init__(self) -> None: ...
    def __eq__(self, other: object) -> bool: ...  # Always True for Null instances
    def __hash__(self) -> int: ...                 # Always 0

String Types

All string types expose .as_str() -> str and support __str__, __eq__, __len__. Types whose character sets are subsets of ASCII raise ValueError on construction if invalid characters are supplied.

Overview table

ClassConstructorValid charsetNotes
Utf8String(value: str)Unicode
PrintableString(value: str)A–Z a–z 0–9 ' ( ) + , - . / : = ?
IA5String(value: str)ASCII (0x00–0x7f)
NumericString(value: str)Digits and space
TeletexString(data: bytes)Arbitrary bytesAlso: TeletexString.from_latin1(str), from_str(str). .to_bytes() returns raw bytes.
VisibleString(value: str)Printable ASCII (0x21–0x7e)
GeneralString(data: bytes)Arbitrary bytesAlso: GeneralString.from_ascii(str) (ASCII only, raises ValueError for > U+007F), from_str(str). .to_bytes() returns raw bytes.
UniversalString(value: str)Unicode (UCS-4 BE)Also: UniversalString.from_bytes(data: bytes).
BmpString(value: str)BMP only (U+0000–U+FFFF)Also: BmpString.from_bytes(data: bytes). Raises ValueError for non-BMP code points.

Utf8String

ASN.1 tag 0x0c.

class Utf8String:
    def __init__(self, value: str) -> None: ...
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: Utf8String) -> bool: ...
    def __len__(self) -> int: ...

PrintableString

ASN.1 tag 0x13. Valid characters: A-Z a-z 0-9 ' ( ) + , - . / : = ?

class PrintableString:
    def __init__(self, value: str) -> None: ...  # Raises ValueError for invalid chars
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: PrintableString) -> bool: ...
    def __len__(self) -> int: ...

IA5String

ASN.1 tag 0x16. ASCII subset (0x00–0x7f).

class IA5String:
    def __init__(self, value: str) -> None: ...  # Raises ValueError for non-ASCII
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: IA5String) -> bool: ...
    def __len__(self) -> int: ...

NumericString

ASN.1 tag 0x12. Restricted to the digits 09 and the space character.

Valid characters: 0 1 2 3 4 5 6 7 8 9 (digits and space)

class NumericString:
    def __init__(self, value: str) -> None: ...  # Raises ValueError for invalid chars
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: NumericString) -> bool: ...
    def __len__(self) -> int: ...

TeletexString

ASN.1 tag 0x14, also known as T61String. Stores arbitrary 8-bit bytes without enforcing a character encoding. as_str() interprets the bytes as Latin-1 (ISO 8859-1).

class TeletexString:
    def __init__(self, data: bytes) -> None: ...
    @staticmethod
    def from_str(value: str) -> TeletexString: ...  # Encodes as Latin-1 (lossy for non-Latin-1 code points)
    def to_bytes(self) -> bytes: ...
    def as_str(self) -> str: ...    # Latin-1 decode
    def __len__(self) -> int: ...   # byte length
    def __eq__(self, other: TeletexString) -> bool: ...

VisibleString

ASN.1 tag 0x1a. Printable ASCII subset: bytes 0x200x7e (space through tilde). Control characters (below 0x20) and DEL (0x7f) are rejected.

class VisibleString:
    def __init__(self, value: str) -> None: ...  # Raises ValueError for control chars or non-ASCII
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: VisibleString) -> bool: ...
    def __len__(self) -> int: ...

GeneralString

ASN.1 tag 0x1b. Stores arbitrary 8-bit bytes without enforcing a character encoding. as_str() interprets the bytes as Latin-1 (ISO 8859-1). Commonly used for Kerberos realm names and principal name components (RFC 4120).

class GeneralString:
    def __init__(self, data: bytes) -> None: ...
    @staticmethod
    def from_ascii(value: str) -> GeneralString: ...  # Raises ValueError for non-ASCII (> U+007F)
    @staticmethod
    def from_str(value: str) -> GeneralString: ...    # Alias for from_ascii
    def to_bytes(self) -> bytes: ...
    def as_str(self) -> str: ...    # Latin-1 decode
    def __len__(self) -> int: ...   # byte length
    def __eq__(self, other: GeneralString) -> bool: ...

UniversalString

ASN.1 tag 0x1c. Encodes Unicode text as UCS-4 big-endian (4 bytes per code point, superset of BMP). Accepts any valid Unicode string.

class UniversalString:
    def __init__(self, value: str) -> None: ...
    @staticmethod
    def from_bytes(data: bytes) -> UniversalString: ...
    # Decodes UCS-4 BE bytes.  Raises ValueError if length is not a multiple of 4
    # or if any four-byte sequence is not a valid Unicode scalar value.
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: UniversalString) -> bool: ...
    def __len__(self) -> int: ...

BmpString

ASN.1 tag 0x1e. Encodes Unicode text as UCS-2 big-endian (2 bytes per code point, Basic Multilingual Plane only, U+0000–U+FFFF). Code points above U+FFFF are rejected.

class BmpString:
    def __init__(self, value: str) -> None: ...
    # Raises ValueError if the string contains code points outside the BMP (> U+FFFF).
    @staticmethod
    def from_bytes(data: bytes) -> BmpString: ...
    # Decodes UCS-2 BE bytes.  Raises ValueError if length is odd or if any
    # two-byte sequence is a surrogate half.
    def as_str(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: BmpString) -> bool: ...
    def __len__(self) -> int: ...

Time Types: UtcTime and GeneralizedTime

UtcTime

ASN.1 tag 0x17. Represents a time in the range 1950–2049.

Constructor

UtcTime(year: int, month: int, day: int, hour: int, minute: int, second: int)

Year must be in the range 1950–2049. Raises ValueError on invalid input.

Properties

Properties: year, month, day, hour, minute, second (all int). __str__ returns the canonical YYMMDDHHMMSSZ form.

Full class stub

class UtcTime:
    def __init__(self, year: int, month: int, day: int,
                 hour: int, minute: int, second: int) -> None: ...
    year: int
    month: int
    day: int
    hour: int
    minute: int
    second: int
    def __str__(self) -> str: ...   # e.g. "230101120000Z"

Year must be in the range 1950–2049.


GeneralizedTime

ASN.1 tag 0x18. Represents a time with optional millisecond precision.

Constructor

GeneralizedTime(year: int, month: int, day: int, hour: int, minute: int, second: int,
                milliseconds: int | None = None)

Properties

Properties: year, month, day, hour, minute, second (int), milliseconds (int | None). __str__ returns the canonical YYYYMMDDHHMMSS[.fff]Z form.

Full class stub

class GeneralizedTime:
    def __init__(self, year: int, month: int, day: int,
                 hour: int, minute: int, second: int,
                 milliseconds: int | None = None) -> None: ...
    year: int
    month: int
    day: int
    hour: int
    minute: int
    second: int
    milliseconds: int | None
    def __str__(self) -> str: ...   # e.g. "20230101120000Z"

Bit and Octet Strings

OctetString

ASN.1 tag 0x04.

Constructor

OctetString(data: bytes)

Methods

MethodReturns
to_bytes()bytes
__len__()int
__eq__(other)bool

Full class stub

class OctetString:
    def __init__(self, data: bytes) -> None: ...
    def to_bytes(self) -> bytes: ...
    def __len__(self) -> int: ...
    def __eq__(self, other: OctetString) -> bool: ...

BitString

ASN.1 tag 0x03.

Constructor

BitString(data: bytes, unused_bits: int)   # unused_bits in 0–7

Methods

MethodReturnsDescription
to_bytes()bytesRaw content bytes (excluding unused-bits octet).
unused_bits()intNumber of padding bits in the last byte.
bit_len()intTotal significant bits (len(data) * 8 - unused_bits).
__len__()intSame as bit_len().
__eq__(other)bool

Full class stub

class BitString:
    def __init__(self, data: bytes, unused_bits: int) -> None: ...
    def to_bytes(self) -> bytes: ...
    def unused_bits(self) -> int: ...
    def bit_len(self) -> int: ...
    def __len__(self) -> int: ...         # byte length (same as len(to_bytes()))
    def __eq__(self, other: BitString) -> bool: ...

Tagged and Raw Elements

TaggedElement

Returned by Decoder.decode_any() for explicitly-tagged or application/private-tagged values.

AttributeTypeDescription
tag_numberintThe tag number
tag_classstr"Universal", "Context", "Application", or "Private"
is_constructedboolTrue for constructed (e.g. wrapped SEQUENCE)
valueanyThe inner decoded element

RawElement

Returned by Decoder.decode_any() for unknown universal tags (enables forward-compatible parsing).

Attribute / MethodTypeDescription
tag_numberint
tag_classstr
is_constructedbool
databytesRaw content bytes (no tag or length)

Full class stub

class RawElement:
    tag_number: int    # Universal tag number
    tag_class: str     # "Universal", "Application", "Context", or "Private"
    is_constructed: bool  # True if the element is constructed
    data: bytes        # Raw content bytes (tag and length stripped)

data is a #[getter] property — access it as elem.data (not elem.data()).

ObjectIdentifier

ObjectIdentifier is a frozen (immutable, thread-safe) wrapper around an ASN.1 OBJECT IDENTIFIER value.

Constructors

ObjectIdentifier(oid_str: str)                     # from dotted-decimal, e.g. "2.5.4.3"
ObjectIdentifier.from_components(comps: list[int]) # from arc list
ObjectIdentifier.from_der_value(data: bytes)       # from implicit-tag content bytes (tag+length stripped)

Methods and dunders

Method / DunderReturnsDescription
components()tuple[int, ...]OID arc components
__str__()strDotted-decimal notation (cached)
__repr__()strObjectIdentifier('2.5.4.3')
__eq__(other)boolCompares against another ObjectIdentifier or a dotted str
__hash__()intConsistent with hash(str(oid)) so oid in {"2.5.4.3"} works

Full class stub

class ObjectIdentifier:
    def __init__(self, oid_str: str) -> None: ...
    @staticmethod
    def from_components(components: list[int]) -> ObjectIdentifier: ...
    def components(self) -> list[int]: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: ObjectIdentifier) -> bool: ...
    def __hash__(self) -> int: ...

Working with Object Identifiers

import synta

# Create OID from dotted string
oid = synta.ObjectIdentifier("1.2.840.10045.2.1")
print(str(oid))  # Output: 1.2.840.10045.2.1

# Get components
components = oid.components()
print(components)  # Output: [1, 2, 840, 10045, 2, 1]

# Create OID from components
oid = synta.ObjectIdentifier.from_components([1, 2, 840, 10045, 4, 3, 2])
print(str(oid))  # Output: 1.2.840.10045.4.3.2

# Equality comparison against a string
import synta.oids as oids
assert oids.EC_PUBLIC_KEY == "1.2.840.10045.2.1"

# Use as a dict key (hashable)
lookup = {oids.SHA256: "SHA-256", oids.SHA384: "SHA-384"}
name = lookup.get(cert.signature_algorithm_oid, "unknown")

See also Well-known OIDs for the full OID constant catalog.

Error Handling

Exception table

ExceptionWhen raised
synta.SyntaErrorASN.1 parse or encode failure (wraps the Rust synta::Error)
synta.x509.X509VerificationErrorCertificate chain verification failure (see synta.x509)
ValueErrorInvalid arguments (e.g. invalid OID string, bad charset for PrintableString, wrong password format)
OverflowErrorInteger.to_int() / to_i128() when the value does not fit
EOFErrorDecoder.peek_tag() / decode_* when no data remains
ImportErrorCertificate.to_pyca() / from_pyca() when cryptography is not installed

Example

import synta

try:
    # Invalid OID (first component must be 0, 1, or 2)
    oid = synta.ObjectIdentifier("5.2.840")
except ValueError as e:
    print(f"Error: {e}")

try:
    # Truncated data
    decoder = synta.Decoder(b'\x02\x05', synta.Encoding.DER)
    integer = decoder.decode_integer()
except EOFError as e:
    print(f"Unexpected end of data: {e}")

try:
    # Invalid DER
    decoder = synta.Decoder(b'\xff\xff', synta.Encoding.DER)
    obj = decoder.decode_any()
except synta.SyntaError as e:
    print(f"Parse error: {e}")

Error mapping

SyntaErr (the internal Rust error type) is mapped to Python exceptions as follows:

  • EOF / truncated data → EOFError
  • Integer overflow → OverflowError
  • Any other ASN.1 parse error → ValueError or synta.SyntaError

The SyntaError exception class is a subclass of Exception.

PEM and DER Utilities

Two top-level functions handle conversion between PEM and DER byte representations.

Free functions

SignatureReturnsDescription
pem_to_der(data: bytes)list[bytes]Decode one or more PEM blocks to DER. Always returns a list (one entry per block); no block present → ValueError.
der_to_pem(der: bytes, label: str)bytesWrap DER bytes in a -----BEGIN {label}----- / -----END {label}----- block.

Usage

import synta

# Decode a PEM file with one or more certificates
pem_data = open("bundle.pem", "rb").read()
der_list = synta.pem_to_der(pem_data)   # list[bytes] — one entry per PEM block
for der in der_list:
    cert = synta.Certificate.from_der(der)
    print(cert.subject)

# Single-block PEM (typical for one certificate)
der_list = synta.pem_to_der(open("cert.pem", "rb").read())
cert = synta.Certificate.from_der(der_list[0])

# Wrap DER bytes back into PEM
der = cert.to_der()
pem = synta.der_to_pem(der, "CERTIFICATE")
open("cert.pem", "wb").write(pem)

# Private key PEM
key_der = open("key.der", "rb").read()
pem = synta.der_to_pem(key_der, "PRIVATE KEY")

# Empty PEM raises ValueError
try:
    synta.pem_to_der(b"not pem")
except ValueError as e:
    print(f"No PEM block found: {e}")

Notes

  • pem_to_der always returns a list, even when only one block is present. A single-block result is accessed as result[0].
  • pem_to_der raises ValueError when no PEM block is found. It does not silently return an empty list.
  • Malformed base64 inside a PEM block is also rejected with ValueError.
  • der_to_pem produces the standard 64-character line wrap required by RFC 7468.
  • The label argument to der_to_pem appears verbatim between the BEGIN / END markers (e.g. "CERTIFICATE", "PRIVATE KEY", "CERTIFICATE REQUEST").

See also PKCS Loaders for format-agnostic reading of PEM bundles, PKCS#7 files, and PKCS#12 archives.

Certificate

Certificate represents an RFC 5280 X.509 v3 certificate. Parsing is lazy: from_der performs only a 4-operation shallow scan (outer SEQUENCE TLV + TBSCertificate SEQUENCE TLV); the full RFC 5280 decode is deferred to the first field access, after which every property is cached in an OnceLock and returned in O(1) on subsequent calls.

Construction

Certificate.from_der(data: bytes) -> Certificate

Parse a DER-encoded X.509 certificate. Performs a shallow 4-operation structural scan on construction; the full decode is deferred to the first field access.

Certificate.full_from_der(data: bytes) -> Certificate

Parse and perform a complete RFC 5280 decode immediately. All field getters are on the warm path from the first call. Use when benchmarking full parse cost or when latency on the first field access must be avoided.

Certificate.from_pem(data: bytes) -> Certificate | list[Certificate]

Parse a PEM-encoded certificate. Returns a single Certificate for a single PEM block, or a list[Certificate] when multiple blocks are present.

Certificate.to_pem(cert_or_list) -> bytes

Encode one or more Certificate objects back to PEM.

Certificate.from_pyca(pyca_cert: object) -> Certificate

Convert a cryptography.x509.Certificate to a synta.Certificate by serialising to DER and calling from_der. Raises ImportError if the cryptography package is not installed.

cert.to_pyca() -> cryptography.x509.Certificate

Return a cryptography.x509.Certificate backed by the same DER bytes (no re-encoding). Use for cryptographic operations not directly supported by synta. Raises ImportError if the cryptography package is not installed.

Properties

All properties are cached after first access. to_der skips the lock entirely (direct reference to the stored bytes, no copy).

Identity

PropertyTypeDescription
serial_numberintArbitrary-precision Python int (RFC 5280: up to 20 bytes)
versionint | NoneVersion field: None = v1, 1 = v2, 2 = v3
issuerstrRFC 4514 DN string
issuer_raw_derbytesRaw DER of issuer Name SEQUENCE
subjectstrRFC 4514 DN string
subject_raw_derbytesRaw DER of subject Name SEQUENCE

Validity

PropertyTypeDescription
not_beforestr"YYMMDDHHMMSSZ" (UTCTime) or "YYYYMMDDHHMMSSZ" (GeneralizedTime)
not_afterstrSame format as not_before

Signature

PropertyTypeDescription
signature_algorithmstrHuman-readable algorithm name (e.g. "RSA", "ECDSA", "ML-DSA-65") or dotted OID for unrecognised algorithms
signature_algorithm_oidObjectIdentifierAlways machine-readable dotted OID
signature_algorithm_paramsbytes | NoneDER-encoded parameters, or None when absent (e.g. Ed25519)
signature_valuebytesRaw signature bytes (BIT STRING value, unused-bit prefix stripped)

Subject Public Key

PropertyTypeDescription
public_key_algorithmstrHuman-readable name or dotted OID
public_key_algorithm_oidObjectIdentifierDotted OID (e.g. "1.2.840.10045.2.1" for id-ecPublicKey)
public_key_algorithm_paramsbytes | NoneDER parameters (curve OID for EC; None for RSA/EdDSA)
public_keybytesRaw SubjectPublicKeyInfo BIT STRING content (unused-bit byte stripped)
subject_public_key_info_raw_derbytesFull SubjectPublicKeyInfo SEQUENCE DER (for SKI/AKI computation)

Raw DER Spans

Property / MethodTypeDescription
to_der()bytesComplete original certificate DER (zero-copy)
tbs_bytesbytesTBSCertificate DER — the bytes that were signed
issuer_raw_derbytesIssuer Name SEQUENCE DER
subject_raw_derbytesSubject Name SEQUENCE DER
extensions_derbytes | NoneSEQUENCE OF Extension DER, or None for v1/v2

Methods

MethodSignatureReturnsDescription
to_der()()bytesOriginal DER bytes (zero-copy)
get_extension_value_der(oid: str | ObjectIdentifier)bytes | NoneReturn the extnValue content bytes for the named extension OID, or None if absent. Scans in O(n) over the already-parsed Extension list. Raises ValueError for invalid OID strings.
subject_alt_names()()list[tuple[int, bytes]]SAN entries as (tag_number, content) pairs. Returns [] when no SAN extension is present. Use synta.general_name constants to dispatch on tag_number.
general_names(oid: str | ObjectIdentifier)list[tuple[int, bytes]]Decode any extension whose value is SEQUENCE OF GeneralName.
certificate_policies()()list[PolicyInformation]Decode the CertificatePolicies extension (OID 2.5.29.32). Returns [] when absent.
verify_issued_by(issuer: Certificate)NoneVerify this certificate was signed by issuer. Raises ValueError on failure.
fingerprint(algorithm: str)bytesCertificate fingerprint using the named hash (e.g. "sha256"). Raises ValueError for unknown algorithms.

Full class stub

class Certificate:
    @staticmethod
    def from_der(data: bytes) -> Certificate: ...
    @staticmethod
    def full_from_der(data: bytes) -> Certificate: ...
    @staticmethod
    def from_pyca(pyca_cert: object) -> Certificate: ...
    def to_pyca(self) -> object: ...
    def __repr__(self) -> str: ...

    # Identity
    serial_number: int
    version: int | None
    issuer: str
    issuer_raw_der: bytes
    subject: str
    subject_raw_der: bytes

    # Validity
    not_before: str
    not_after: str

    # Signature
    signature_algorithm: str
    signature_algorithm_oid: ObjectIdentifier
    signature_algorithm_params: bytes | None
    signature_value: bytes

    # Subject public key
    public_key_algorithm: str
    public_key_algorithm_oid: ObjectIdentifier
    public_key_algorithm_params: bytes | None
    public_key: bytes
    subject_public_key_info_raw_der: bytes

    # Raw DER spans
    def to_der(self) -> bytes: ...
    tbs_bytes: bytes
    extensions_der: bytes | None

    # Methods
    def get_extension_value_der(self, oid: str) -> bytes | None: ...
    def subject_alt_names(self) -> list[tuple[int, bytes]]: ...
    def verify_issued_by(self, issuer: Certificate) -> None: ...
    def fingerprint(self, algorithm: str) -> bytes: ...

Usage

import synta

# Parse a DER-encoded certificate
with open('cert.der', 'rb') as f:
    cert = synta.Certificate.from_der(f.read())

# Identity fields
print(cert.subject)            # RFC 4514-style DN string
print(cert.issuer)             # RFC 4514-style DN string
print(cert.serial_number)      # Python int (arbitrary precision)
print(cert.version)            # None (v1), 1 (v2), or 2 (v3)

# Validity
print(cert.not_before)         # String, e.g. "230101000000Z"
print(cert.not_after)          # String, e.g. "320101000000Z"

# Signature algorithm
print(cert.signature_algorithm)      # Human-readable name or dotted OID
print(cert.signature_algorithm_oid)  # Dotted OID string (always machine-readable)
print(cert.signature_algorithm_params)  # DER bytes or None (e.g. None for Ed25519)
print(cert.signature_value)          # Raw signature bytes

# Subject public key
print(cert.public_key_algorithm)       # Human-readable name or dotted OID
print(cert.public_key_algorithm_oid)   # Dotted OID string
print(cert.public_key_algorithm_params)  # DER bytes or None (EC: curve OID)
print(cert.public_key)                 # Raw SubjectPublicKeyInfo bit-string bytes

# Raw DER spans
print(cert.to_der())        # Complete original certificate DER bytes
print(cert.tbs_bytes)       # TBSCertificate DER (the bytes that were signed)
print(cert.issuer_raw_der)  # Issuer Name SEQUENCE DER
print(cert.subject_raw_der) # Subject Name SEQUENCE DER
print(cert.extensions_der)  # Extensions SEQUENCE OF DER, or None for v1/v2

# Look up a single extension by OID — returns the extnValue OCTET STRING
# content (the DER-encoded extension-specific structure), or None if absent.
san_der = cert.get_extension_value_der("2.5.29.17")   # SubjectAltName
bc_der  = cert.get_extension_value_der("2.5.29.19")   # BasicConstraints
eku_der = cert.get_extension_value_der("2.5.29.37")   # ExtendedKeyUsage

SAN access

import ipaddress
import synta.general_name as gn

# High-level: subject_alt_names() combines extension lookup and GeneralName parsing
for tag_num, content in cert.subject_alt_names():
    if tag_num == gn.DNS_NAME:
        print("dns:",   content.decode("ascii"))
    elif tag_num == gn.IP_ADDRESS:
        print("ip:",    ipaddress.ip_address(content))
    elif tag_num == gn.RFC822_NAME:
        print("email:", content.decode("ascii"))
    elif tag_num == gn.URI:
        print("uri:",   content.decode("ascii"))
    elif tag_num == gn.DIRECTORY_NAME:
        attrs = synta.parse_name_attrs(content)   # [(oid_str, value_str), …]
        print("dirname:", attrs)

Iterating extensions manually

# Iterate all extensions when you need every extension
ext_der = cert.extensions_der
if ext_der:
    dec = synta.Decoder(ext_der, synta.Encoding.DER)
    exts = dec.decode_sequence()
    while not exts.is_empty():
        ext_tlv = exts.decode_raw_tlv()  # bytes covering one Extension TLV

Signature verification

# Verify a certificate was signed by a CA certificate
ca_cert = synta.Certificate.from_der(open("ca.der", "rb").read())
end_cert = synta.Certificate.from_der(open("end.der", "rb").read())
try:
    end_cert.verify_issued_by(ca_cert)
    print("Signature valid")
except ValueError as e:
    print(f"Invalid: {e}")

See also:

CertificationRequest (CSR)

CertificationRequest represents an RFC 2986 PKCS#10 Certificate Signing Request.

Construction

CertificationRequest.from_der(data: bytes) -> CertificationRequest
CertificationRequest.from_pem(data: bytes) -> CertificationRequest | list[CertificationRequest]
CertificationRequest.to_pem(csr_or_list) -> bytes

Properties

PropertyTypeDescription
versionintCSR version (always 0 = v1 per RFC 2986)
subjectstrRFC 4514 DN string
subject_raw_derbytesRaw DER of subject Name SEQUENCE
signature_algorithmstrAlgorithm name or dotted OID
signature_algorithm_oidObjectIdentifier
signaturebytesRaw signature bytes
public_key_algorithmstrAlgorithm name or dotted OID
public_key_algorithm_oidObjectIdentifier
public_keybytesRaw subject public key bytes

Methods

MethodSignatureReturnsDescription
to_der()()bytesOriginal DER bytes
get_extension_value_der(oid: str | ObjectIdentifier)bytes | NoneReturn the extnValue bytes of the named extension from the CSR’s extensionRequest attribute, or None if absent.
verify_self_signature()()NoneVerify the PKCS#10 self-signature. Raises ValueError if the signature is invalid or the algorithm is unsupported.

Full class stub

class CertificationRequest:
    @staticmethod
    def from_der(data: bytes) -> CertificationRequest: ...
    @staticmethod
    def from_pem(data: bytes) -> CertificationRequest | list[CertificationRequest]: ...
    @staticmethod
    def to_pem(csr_or_list) -> bytes: ...

    version: int
    subject: str
    subject_raw_der: bytes
    signature_algorithm: str
    signature_algorithm_oid: ObjectIdentifier
    signature: bytes
    public_key_algorithm: str
    public_key_algorithm_oid: ObjectIdentifier
    public_key: bytes

    def to_der(self) -> bytes: ...
    def get_extension_value_der(self, oid: str) -> bytes | None: ...
    def verify_self_signature(self) -> None: ...

Usage

import synta

# Parse from DER
with open("request.csr", "rb") as f:
    csr = synta.CertificationRequest.from_der(f.read())

# Parse from PEM
pem_data = open("request.pem", "rb").read()
csr = synta.CertificationRequest.from_pem(pem_data)

# Access fields
print(csr.subject)
print(csr.public_key_algorithm)
print(csr.public_key_algorithm_oid)

# Verify the self-signature
try:
    csr.verify_self_signature()
    print("CSR self-signature is valid")
except ValueError as e:
    print(f"Invalid CSR: {e}")

# Retrieve the extensionRequest attribute (if present)
san_der = csr.get_extension_value_der("2.5.29.17")   # SubjectAltName
if san_der:
    # san_der is the extnValue content bytes — pass to a Decoder
    dec = synta.Decoder(san_der, synta.Encoding.DER)
    san_seq = dec.decode_sequence()
    while not san_seq.is_empty():
        tag_num, tag_class, _ = san_seq.peek_tag()
        child = san_seq.decode_implicit_tag(tag_num, "Context")
        print(f"SAN tag {tag_num}: {child.remaining_bytes()}")

See also Certificate for the issued certificate type and CRL for certificate revocation lists.

CertificateList (CRL)

CertificateList represents an RFC 5280 Certificate Revocation List.

Construction

CertificateList.from_der(data: bytes) -> CertificateList
CertificateList.from_pem(data: bytes) -> CertificateList | list[CertificateList]
CertificateList.to_pem(crl_or_list) -> bytes

Properties

PropertyTypeDescription
versionint | NoneCRL version (1 = v2); None implies v1 (RFC 5280 §5.1.2.1)
issuerstrRFC 4514 DN string
issuer_raw_derbytesRaw DER of issuer Name SEQUENCE
this_updatestrthisUpdate time as string
next_updatestr | NonenextUpdate time as string, or None if absent
signature_algorithmstrAlgorithm name or dotted OID
signature_algorithm_oidObjectIdentifier
signature_valuebytesRaw signature bytes
crl_numberint | NoneCRL sequence number from cRLNumber extension (OID 2.5.29.20), or None
revoked_countintNumber of revoked certificate entries

Methods

MethodSignatureReturnsDescription
to_der()()bytesOriginal DER bytes
get_extension_value_der(oid: str | ObjectIdentifier)bytes | NoneReturn the extnValue bytes of the named CRL extension, or None if absent.
verify_issued_by(issuer: Certificate)NoneVerify that this CRL was signed by issuer. Checks issuer Name match then signature. Raises ValueError on mismatch or invalid signature.

Full class stub

class CertificateList:
    @staticmethod
    def from_der(data: bytes) -> CertificateList: ...
    @staticmethod
    def from_pem(data: bytes) -> CertificateList | list[CertificateList]: ...
    @staticmethod
    def to_pem(crl_or_list) -> bytes: ...

    version: int | None
    issuer: str
    issuer_raw_der: bytes
    this_update: str
    next_update: str | None
    signature_algorithm: str
    signature_algorithm_oid: ObjectIdentifier
    signature_value: bytes
    crl_number: int | None
    revoked_count: int

    def to_der(self) -> bytes: ...
    def get_extension_value_der(self, oid: str) -> bytes | None: ...
    def verify_issued_by(self, issuer: Certificate) -> None: ...

Usage

import synta

# Parse a DER-encoded CRL
with open("ca.crl", "rb") as f:
    crl = synta.CertificateList.from_der(f.read())

# Access fields
print(crl.issuer)
print(crl.this_update)
print(crl.next_update)
print(f"Revoked entries: {crl.revoked_count}")
print(f"CRL number: {crl.crl_number}")

# Verify the CRL signature
ca_cert = synta.Certificate.from_der(open("ca.der", "rb").read())
try:
    crl.verify_issued_by(ca_cert)
    print("CRL signature valid")
except ValueError as e:
    print(f"Invalid CRL: {e}")

# Read the CRL number extension
crl_num_der = crl.get_extension_value_der("2.5.29.20")

See also Certificate and OCSP.

OCSPResponse

OCSPResponse represents an RFC 6960 OCSP Response (outer envelope only).

Construction

OCSPResponse.from_der(data: bytes) -> OCSPResponse
OCSPResponse.from_pem(data: bytes) -> OCSPResponse | list[OCSPResponse]
OCSPResponse.to_pem(resp_or_list) -> bytes

Properties

PropertyTypeDescription
statusstrResponse status: "successful", "malformedRequest", "internalError", "tryLater", "sigRequired", "unauthorized"
response_type_oidObjectIdentifier | NoneOID of the responseBytes contentType, or None for non-successful responses
response_bytesbytes | NoneRaw content of the responseBytes OCTET STRING, or None

Methods

MethodSignatureReturnsDescription
to_der()()bytesOriginal DER bytes
verify_signature(responder: Certificate)NoneVerify the BasicOCSPResponse signature using the responder’s public key. Raises ValueError if no responseBytes is present, the response type is not id-pkix-ocsp-basic, or the signature is invalid.

Full class stub

class OCSPResponse:
    @staticmethod
    def from_der(data: bytes) -> OCSPResponse: ...
    @staticmethod
    def from_pem(data: bytes) -> OCSPResponse | list[OCSPResponse]: ...
    @staticmethod
    def to_pem(resp_or_list) -> bytes: ...

    status: str
    response_type_oid: ObjectIdentifier | None
    response_bytes: bytes | None

    def to_der(self) -> bytes: ...
    def verify_signature(self, responder: Certificate) -> None: ...

Usage

import synta

# Parse a DER-encoded OCSP response
with open("ocsp.der", "rb") as f:
    resp = synta.OCSPResponse.from_der(f.read())

# Check the outer status
print(resp.status)              # e.g. "successful"
print(resp.response_type_oid)   # OID, typically id-pkix-ocsp-basic

# Access the raw inner bytes for further decoding
if resp.status == "successful" and resp.response_bytes:
    # resp.response_bytes is the content of the responseBytes OCTET STRING
    # Decode it as a BasicOCSPResponse using the Decoder
    dec = synta.Decoder(resp.response_bytes, synta.Encoding.DER)
    inner = dec.decode_sequence()
    # ... (decode tbsResponseData, signatureAlgorithm, signature, etc.)

# Verify the response signature
responder_cert = synta.Certificate.from_der(open("responder.der", "rb").read())
try:
    resp.verify_signature(responder_cert)
    print("OCSP signature valid")
except ValueError as e:
    print(f"OCSP verification failed: {e}")

See also Certificate and CRL.

PublicKey and PrivateKey

PublicKey and PrivateKey are backend-agnostic key types that wrap OpenSSL keys. They support RSA, EC (NIST curves), EdDSA (Ed25519 / Ed448), ML-DSA (FIPS 204), and ML-KEM (FIPS 203). Both types are exported from the top-level synta package.

PublicKey

Construction

PublicKey.from_pem(data: bytes) -> PublicKey

Load from PEM-encoded SubjectPublicKeyInfo.

PublicKey.from_der(data: bytes) -> PublicKey

Load from DER-encoded SubjectPublicKeyInfo.

PublicKey.from_rsa_components(n: bytes, e: bytes) -> PublicKey

Construct an RSA public key from big-endian modulus n and public exponent e bytes. Raises ValueError if the inputs are not a valid RSA key.

PublicKey.from_ec_components(x: bytes, y: bytes, curve: str) -> PublicKey

Construct an EC public key from affine coordinates x, y and NIST curve name. curve must be "P-256", "P-384", or "P-521". Raises ValueError for unknown curve names or invalid coordinates.

Serialisation

pub_key.to_pem() -> bytes
pub_key.to_der() -> bytes

Properties

PropertyTypeDescription
key_typestrLowercase key algorithm: "rsa", "ec", "ed25519", "ed448", "dsa", "ml-dsa-44", "ml-dsa-65", "ml-dsa-87", "ml-kem-512", "ml-kem-768", "ml-kem-1024", or "unknown"
key_sizeint | NoneKey size in bits, or None for EdDSA and post-quantum keys
modulusbytes | NoneRSA modulus n as big-endian bytes, or None for non-RSA keys
public_exponentbytes | NoneRSA public exponent e as big-endian bytes, or None for non-RSA keys
curve_namestr | NoneNIST curve name for EC keys (e.g. "P-256"), or None
xbytes | NoneEC affine X coordinate as big-endian bytes, or None for non-EC keys
ybytes | NoneEC affine Y coordinate as big-endian bytes, or None for non-EC keys

Methods

pub_key.rsa_oaep_encrypt(plaintext: bytes, hash_algorithm: str = "sha256") -> bytes
pub_key.rsa_pkcs1v15_encrypt(plaintext: bytes) -> bytes
pub_key.kem_encapsulate() -> tuple[bytes, bytes]   # ML-KEM: (ciphertext, shared_secret)
pub_key.verify_signature(
    signature: bytes,
    data: bytes,
    algorithm: str | None = None,
    context: bytes | None = None,
) -> None

Verify a signature over data. Raises ValueError on failure.

  • algorithm: hash name (e.g. "sha256") for RSA and ECDSA. Pass None for Ed25519, Ed448, and ML-DSA keys.
  • context: ML-DSA domain-separation string (FIPS 204); None = empty. Ignored for non-ML-DSA keys.

Full class stub

class PublicKey:
    @staticmethod
    def from_pem(data: bytes) -> PublicKey: ...
    @staticmethod
    def from_der(data: bytes) -> PublicKey: ...
    @staticmethod
    def from_rsa_components(n: bytes, e: bytes) -> PublicKey: ...
    @staticmethod
    def from_ec_components(x: bytes, y: bytes, curve: str) -> PublicKey: ...
    def to_pem(self) -> bytes: ...
    def to_der(self) -> bytes: ...
    key_type: str
    key_size: int | None
    modulus: bytes | None
    public_exponent: bytes | None
    curve_name: str | None
    x: bytes | None
    y: bytes | None
    def rsa_oaep_encrypt(self, plaintext: bytes, hash_algorithm: str = "sha256") -> bytes: ...
    def rsa_pkcs1v15_encrypt(self, plaintext: bytes) -> bytes: ...
    def kem_encapsulate(self) -> tuple[bytes, bytes]: ...
    def verify_signature(
        self,
        signature: bytes,
        data: bytes,
        algorithm: str | None = None,
        context: bytes | None = None,
    ) -> None: ...

PrivateKey

Construction

PrivateKey.from_pem(data: bytes, password: bytes | None = None) -> PrivateKey

Load from PEM-encoded data (optionally password-protected).

PrivateKey.from_der(data: bytes) -> PrivateKey

Load from unencrypted PKCS#8 DER bytes.

PrivateKey.from_pkcs8_encrypted(data: bytes, password: bytes) -> PrivateKey

Decrypt and load from PKCS#8 EncryptedPrivateKeyInfo DER.

Serialisation

key.to_pem() -> bytes
key.to_der() -> bytes   # unencrypted PKCS#8 DER

Properties

PropertyTypeDescription
key_typestrSame values as PublicKey.key_type
key_sizeint | NoneKey size in bits, or None

Methods

key.public_key() -> PublicKey              # extract the matching public key
key.sign(data: bytes, algorithm: str | None = None, context: bytes | None = None) -> bytes
key.rsa_oaep_decrypt(ciphertext: bytes, hash_algorithm: str = "sha256") -> bytes
key.rsa_pkcs1v15_decrypt(ciphertext: bytes) -> bytes
key.kem_decapsulate(ciphertext: bytes) -> bytes   # ML-KEM: returns shared_secret

Full class stub

class PrivateKey:
    @staticmethod
    def from_pem(data: bytes, password: bytes | None = None) -> PrivateKey: ...
    @staticmethod
    def from_der(data: bytes) -> PrivateKey: ...
    @staticmethod
    def from_pkcs8_encrypted(data: bytes, password: bytes) -> PrivateKey: ...
    def to_pem(self) -> bytes: ...
    def to_der(self) -> bytes: ...
    key_type: str
    key_size: int | None
    def public_key(self) -> PublicKey: ...
    def sign(
        self,
        data: bytes,
        algorithm: str | None = None,
        context: bytes | None = None,
    ) -> bytes: ...
    def rsa_oaep_decrypt(self, ciphertext: bytes, hash_algorithm: str = "sha256") -> bytes: ...
    def rsa_pkcs1v15_decrypt(self, ciphertext: bytes) -> bytes: ...
    def kem_decapsulate(self, ciphertext: bytes) -> bytes: ...

Usage

import synta

# Load a certificate and verify a signature using the embedded public key
cert = synta.Certificate.from_der(open("cert.der", "rb").read())
pub_key = synta.PublicKey.from_der(cert.public_key)
# For ECDSA with SHA-256:
try:
    pub_key.verify_signature(signature, message, algorithm="sha256")
    print("Valid")
except ValueError:
    print("Invalid")

# Load a private key and sign
priv_key = synta.PrivateKey.from_pem(open("key.pem", "rb").read())
sig = priv_key.sign(message, algorithm="sha256")

# ML-KEM key exchange
kem_pub = synta.PublicKey.from_der(kem_pub_der)
ciphertext, shared_secret = kem_pub.kem_encapsulate()

priv_kem = synta.PrivateKey.from_der(kem_priv_der)
recovered = priv_kem.kem_decapsulate(ciphertext)
assert shared_secret == recovered

See also PKCS#8 for parsing PKCS#8 private-key envelopes and CMS-KEM for RFC 9629 quantum-safe KEM recipient structures.

PKCS Loaders

Five top-level functions extract X.509 certificates and/or PKCS#8 private keys from container formats (PKCS#7, PKCS#12). All functions accept DER, BER, or CER input; the encoding is detected automatically.

Functions

load_der_pkcs7_certificates

load_der_pkcs7_certificates(data: bytes) -> list[Certificate]

Parse a DER or BER PKCS#7 SignedData blob and return its embedded certificates. BER indefinite-length encodings (common in PKCS#7 files from older tools) are handled transparently. Non-certificate entries in the CertificateSet are silently skipped.

Raises ValueError if the outer ContentInfo contentType is not id-signedData, or on any ASN.1 structural error.

load_pem_pkcs7_certificates

load_pem_pkcs7_certificates(data: bytes) -> list[Certificate]

Decode PEM block(s) in data (any label accepted: PKCS7, CMS, CERTIFICATE, etc.), then extract certificates from each PKCS#7 payload. All certificates across all blocks are returned as a single flat list.

Raises ValueError if no PEM block is found, or if any block fails to parse as PKCS#7 SignedData.

load_pkcs12_certificates

load_pkcs12_certificates(data: bytes, password: bytes = None) -> list[Certificate]

Extract certificates from a PKCS#12 / PFX archive. Non-certificate bag types (keyBag, pkcs8ShroudedKeyBag, crlBag, secretBag) are silently skipped.

password is the archive password as raw bytes (UTF-8, no NUL terminator). Pass b"" or omit for no-password archives.

Encrypted bags are supported when built with the openssl Cargo feature (PBES2/PBKDF2/AES-256-CBC) and optionally deprecated-pkcs12-algorithms (legacy 3DES). Without the openssl feature a ValueError is raised when any encrypted bag is encountered.

load_pkcs12_keys

load_pkcs12_keys(data: bytes, password: bytes = None) -> list[bytes]

Extract PKCS#8 private keys from a PKCS#12 archive. Returns a list of raw DER-encoded OneAsymmetricKey blobs, one per keyBag (unencrypted) and pkcs8ShroudedKeyBag (decrypted with the openssl feature) entry. certBag, crlBag, and secretBag entries are silently skipped.

load_pkcs12

load_pkcs12(data: bytes, password: bytes = None) -> tuple[list[Certificate], list[bytes]]

Extract both certificates and private keys from a PKCS#12 archive in a single call. Returns (certs, keys) where certs is a list[Certificate] and keys is a list[bytes] of DER-encoded PKCS#8 structures.

create_pkcs12

create_pkcs12(
    certificates: list[Certificate | bytes],
    private_key: PrivateKey | bytes | None = None,
    password: bytes | None = None,
) -> bytes

Build a DER-encoded PKCS#12 PFX archive. Each element of certificates may be a Certificate object or raw DER bytes; both types may appear in the same list. private_key may be a PrivateKey object or DER-encoded PKCS#8 bytes.

When password is None or omitted, the archive is written without encryption. When password is provided and the library is built with the openssl Cargo feature, bags are encrypted with PBES2/PBKDF2-SHA256/AES-256-CBC using 600,000 iterations. Without the openssl feature, supplying a password raises ValueError.

Raises TypeError for unrecognised element types in certificates or for an unrecognised private_key type.

Usage

import synta

# ── PKCS#7 SignedData (DER or BER) ───────────────────────────────────────────
# amazon-roots.p7b uses BER indefinite-length encoding (0x30 0x80…); both are
# handled transparently — no caller-visible difference.
data = open("bundle.p7b", "rb").read()
certs = synta.load_der_pkcs7_certificates(data)
for cert in certs:
    print(cert.subject)

# ── PKCS#7 SignedData (PEM-wrapped) ──────────────────────────────────────────
pem = open("bundle.pem", "rb").read()
certs = synta.load_pem_pkcs7_certificates(pem)

# ── PKCS#12 — certificates only ──────────────────────────────────────────────
data = open("archive.p12", "rb").read()
certs = synta.load_pkcs12_certificates(data)             # unencrypted
certs = synta.load_pkcs12_certificates(data, b"s3cr3t")  # password-protected

print(f"found {len(certs)} certificate(s)")
for cert in certs:
    print(cert.subject, cert.not_before, "–", cert.not_after)

# ── PKCS#12 — private keys only ──────────────────────────────────────────────
# Returns a list[bytes] of DER-encoded OneAsymmetricKey (PKCS#8) structures.
keys = synta.load_pkcs12_keys(data, b"s3cr3t")
for key_der in keys:
    from cryptography.hazmat.primitives.serialization import load_der_private_key
    key = load_der_private_key(key_der, None)

# ── PKCS#12 — both certificates and keys in one call ─────────────────────────
certs, keys = synta.load_pkcs12(data, b"s3cr3t")

Creating PKCS#12 Archives

import synta

# ── Certificate objects directly ─────────────────────────────────────────────
certs = synta.load_pkcs12_certificates(open("src.p12", "rb").read())
pfx = synta.create_pkcs12(certs)                        # no password
open("bundle.p12", "wb").write(pfx)

# ── With a password (openssl feature required) ────────────────────────────────
try:
    pfx = synta.create_pkcs12(certs, password=b"s3cr3t")
except ValueError:
    # Raised when password is provided without the openssl Cargo feature
    pass

# ── With a cert and a PrivateKey object ──────────────────────────────────────
certs, keys = synta.load_pkcs12(open("src.p12", "rb").read())
key = synta.PrivateKey.from_der(keys[0])
pfx = synta.create_pkcs12(certs, private_key=key, password=b"s3cr3t")

# ── Raw DER bytes also accepted ───────────────────────────────────────────────
pfx = synta.create_pkcs12(
    [cert.to_der() for cert in certs],
    private_key=keys[0],           # bytes from load_pkcs12_keys
)

# ── Roundtrip verification ────────────────────────────────────────────────────
parsed_back = synta.load_pkcs12_certificates(pfx, None)
assert len(parsed_back) == len(certs)

Encryption support: when password is provided and the library is built with --features openssl, bags are encrypted with PBES2/PBKDF2-SHA256/AES-256-CBC using 600,000 iterations. Without the openssl feature, supplying a non-None password raises ValueError.

See also PKI Blocks for format-agnostic reading, and PEM/DER for PEM encode/decode helpers.

Format-Agnostic PKI Reader

read_pki_blocks is the single entry point for reading any supported PKI file format — PEM, PKCS#7/CMS, PKCS#12, or raw DER — without knowing the format in advance.

Function

read_pki_blocks(data: bytes, password: bytes = None) -> list[tuple[str, bytes]]

Auto-detect the encoding of data and return every PKI object as a (label, der_bytes) tuple, in document order.

Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER), PKCS#12 PFX (DER or BER), and raw DER.

Label semantics

Input formatLabel per entry
PEM — PKCS#7 blocks"CERTIFICATE" per embedded cert (block expanded)
PEM — all other block typesBlock type as-is ("CERTIFICATE", "PRIVATE KEY", …)
PKCS#7 / CMS SignedData (binary)"CERTIFICATE" per cert
PKCS#12 — certBag"CERTIFICATE" per cert
PKCS#12 — keyBag / pkcs8ShroudedKeyBag"PRIVATE KEY" per key
Raw DER"CERTIFICATE"

Malformed PEM blocks (bad base64, truncated header) are silently skipped.

PKCS#12 encryption

When password is supplied and the library is built with --features openssl, encrypted bags are decrypted. Without that feature (or when no password is given) encrypted bags are silently skipped; unencrypted certificates in the same archive are still returned. This differs from load_pkcs12_certificates, which raises ValueError on encrypted bags when the openssl feature is absent.

Errors

Raises ValueError on structural failures:

  • A PKCS#7 block has malformed DER, or its contentType OID is not id-signedData.
  • The PKCS#12 input has a structural ASN.1 error.
  • password was supplied with the openssl feature and decryption failed (e.g. wrong password, unsupported cipher).

Raises TypeError if data is not bytes, or password is not bytes or None.

Usage

import synta

# Any format — let synta detect it automatically
data = open("bundle.p7b", "rb").read()   # or .pem, .p12, .der, …
blocks = synta.read_pki_blocks(data)
for label, der in blocks:
    print(f"{label}: {len(der)} bytes")

# Parse the DER bytes directly
certs = [synta.Certificate.from_der(der)
         for label, der in blocks if label == "CERTIFICATE"]

# PKCS#12 with a password (openssl feature required for encrypted bags)
data = open("archive.p12", "rb").read()
blocks = synta.read_pki_blocks(data, b"s3cr3t")

# Split certificates and private keys
certs = [synta.Certificate.from_der(der) for label, der in blocks if label == "CERTIFICATE"]
key_ders = [der for label, der in blocks if label == "PRIVATE KEY"]

See also PKCS Loaders for format-specific functions with precise error semantics, and PEM/DER for simple PEM encode/decode.

X.509 Extension Value Builders

synta.ext provides DER-encoding helpers for the most common X.509 v3 extension values. Each function returns the raw extnValue bytes (the content inside the OCTET STRING wrapper in the Extension SEQUENCE). Import with import synta.ext as ext.

subject_key_identifier and authority_key_identifier require the openssl Cargo feature. basic_constraints, key_usage, and the three builder classes do not.

Key identifier method constants

Pass one of these to subject_key_identifier or authority_key_identifier:

ConstantValueHashInputOutput lengthSpecification
KEYID_RFC52800SHA-1BIT STRING value of subjectPublicKey20 bytesRFC 5280 §4.2.1.2
KEYID_RFC7093M11SHA-256BIT STRING value of subjectPublicKey20 bytes (truncated)RFC 7093 §2 m1
KEYID_RFC7093M22SHA-384BIT STRING value of subjectPublicKey20 bytes (truncated)RFC 7093 §2 m2
KEYID_RFC7093M33SHA-512BIT STRING value of subjectPublicKey20 bytes (truncated)RFC 7093 §2 m3
KEYID_RFC7093M44SHA-256full SubjectPublicKeyInfo DER32 bytesRFC 7093 §2 m4

Key usage bitmask constants

OR these together and pass the result to key_usage():

ConstantMaskNamed bit (RFC 5280 §4.2.1.3)
KU_DIGITAL_SIGNATURE0x001digitalSignature
KU_NON_REPUDIATION0x002contentCommitment
KU_KEY_ENCIPHERMENT0x004keyEncipherment
KU_DATA_ENCIPHERMENT0x008dataEncipherment
KU_KEY_AGREEMENT0x010keyAgreement
KU_KEY_CERT_SIGN0x020keyCertSign
KU_CRL_SIGN0x040cRLSign
KU_ENCIPHER_ONLY0x080encipherOnly
KU_DECIPHER_ONLY0x100decipherOnly

Functions

def basic_constraints(ca: bool = False, path_length: int | None = None) -> bytes: ...

Encode a BasicConstraints extension value (OID 2.5.29.19). Returns the DER SEQUENCE bytes. When ca is False the cA field is omitted (DER DEFAULT-FALSE rule). path_length is ignored when ca is False.

def key_usage(bits: int) -> bytes: ...

Encode a KeyUsage extension value (OID 2.5.29.15). bits is an integer formed by OR-ing KU_* constants. Returns the DER BIT STRING bytes.

def subject_key_identifier(spki_der: bytes, method: int = KEYID_RFC5280) -> bytes: ...

Encode a SubjectKeyIdentifier extension value (OID 2.5.29.14). spki_der must be a complete DER-encoded SubjectPublicKeyInfo. method selects the key-identifier algorithm. Returns the DER OCTET STRING value. Raises ValueError if spki_der cannot be decoded.

def authority_key_identifier(issuer_spki_der: bytes, method: int = KEYID_RFC5280) -> bytes: ...

Encode an AuthorityKeyIdentifier extension value (OID 2.5.29.35). Sets only the keyIdentifier [0] field. Returns the DER AuthorityKeyIdentifier SEQUENCE bytes.

Fluent builder classes

Three builder classes accumulate entries and produce the complete DER extnValue on .build(). No openssl Cargo feature is required.

SubjectAlternativeNameBuilder (alias: SAN)

class SubjectAlternativeNameBuilder:
    def __init__(self) -> None: ...
    def dns_name(self, name: str) -> SubjectAlternativeNameBuilder: ...
    def rfc822_name(self, email: str) -> SubjectAlternativeNameBuilder: ...
    def uri(self, uri: str) -> SubjectAlternativeNameBuilder: ...
    def ip_address(self, addr: bytes) -> SubjectAlternativeNameBuilder: ...
    # Pass 4 bytes for IPv4 or 16 bytes for IPv6.
    def directory_name(self, name_der: bytes) -> SubjectAlternativeNameBuilder: ...
    # name_der is the DER TLV of a Name SEQUENCE.
    def registered_id(self, oid_comps: list[int]) -> SubjectAlternativeNameBuilder: ...
    def build(self) -> bytes: ...
    # Return the DER GeneralNames SEQUENCE.  Raises ValueError on encoding error.

SAN = SubjectAlternativeNameBuilder  # short alias

AuthorityInformationAccessBuilder (alias: AIA)

class AuthorityInformationAccessBuilder:
    def __init__(self) -> None: ...
    def ocsp(self, uri: str) -> AuthorityInformationAccessBuilder: ...
    def ca_issuers(self, uri: str) -> AuthorityInformationAccessBuilder: ...
    def build(self) -> bytes: ...

AIA = AuthorityInformationAccessBuilder  # short alias

ExtendedKeyUsageBuilder (alias: EKU)

class ExtendedKeyUsageBuilder:
    def __init__(self) -> None: ...
    def server_auth(self) -> ExtendedKeyUsageBuilder: ...    # id-kp-serverAuth
    def client_auth(self) -> ExtendedKeyUsageBuilder: ...    # id-kp-clientAuth
    def code_signing(self) -> ExtendedKeyUsageBuilder: ...   # id-kp-codeSigning
    def email_protection(self) -> ExtendedKeyUsageBuilder: ... # id-kp-emailProtection
    def time_stamping(self) -> ExtendedKeyUsageBuilder: ...  # id-kp-timeStamping
    def ocsp_signing(self) -> ExtendedKeyUsageBuilder: ...   # id-kp-OCSPSigning
    def add_oid(self, oid: str | ObjectIdentifier) -> ExtendedKeyUsageBuilder: ...
    def build(self) -> bytes: ...

EKU = ExtendedKeyUsageBuilder  # short alias

Usage

import synta
import synta.ext as ext

# Parse a CA certificate to extract its SubjectPublicKeyInfo DER
ca_der = open("ca.der", "rb").read()
ca_cert = synta.Certificate.from_der(ca_der)
spki_der = ca_cert.subject_public_key_info_raw_der

# ── BasicConstraints (OID 2.5.29.19) ──────────────────────────────────────────

# End-entity certificate: empty SEQUENCE → 30 00
ee_bc = ext.basic_constraints()

# CA with no path-length limit: cA = TRUE → 30 03 01 01 ff
ca_bc = ext.basic_constraints(ca=True)

# CA with pathLen = 0 → 30 06 01 01 ff 02 01 00
ca_pl0 = ext.basic_constraints(ca=True, path_length=0)

# ── KeyUsage (OID 2.5.29.15) ──────────────────────────────────────────────────

# Typical CA key usage: keyCertSign | cRLSign | digitalSignature
ku = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN | ext.KU_DIGITAL_SIGNATURE)

# TLS server key usage: digitalSignature | keyEncipherment
ku_server = ext.key_usage(ext.KU_DIGITAL_SIGNATURE | ext.KU_KEY_ENCIPHERMENT)

# ── SubjectKeyIdentifier (OID 2.5.29.14) ──────────────────────────────────────

# RFC 5280 default: SHA-1 of the BIT STRING value of subjectPublicKey
ski = ext.subject_key_identifier(spki_der)

# RFC 7093 method 1: SHA-256 of BIT STRING value, truncated to 160 bits
ski_m1 = ext.subject_key_identifier(spki_der, method=ext.KEYID_RFC7093M1)

# RFC 7093 method 4: SHA-256 of the full SubjectPublicKeyInfo DER
ski_m4 = ext.subject_key_identifier(spki_der, method=ext.KEYID_RFC7093M4)

# ── AuthorityKeyIdentifier (OID 2.5.29.35) ────────────────────────────────────

aki = ext.authority_key_identifier(spki_der)
aki_m1 = ext.authority_key_identifier(spki_der, method=ext.KEYID_RFC7093M1)

# ── Fluent builders ───────────────────────────────────────────────────────────

san = ext.SAN().dns_name("www.example.com").dns_name("example.com").build()
aia = ext.AIA().ocsp("http://ocsp.example.com").ca_issuers("http://ca.example.com/ca.crt").build()
eku = ext.EKU().server_auth().client_auth().build()

See also CRL and OCSP Builders for building complete CRL and OCSP response structures.

CRL and OCSP Response Builders

CertificateListBuilder and OCSPResponseBuilder are top-level synta exports for constructing DER-encoded X.509 CRL and OCSP response structures. Both follow the same pattern: build the unsigned TBS blob, sign it externally, then call assemble to wrap the signature into the final structure.

CertificateListBuilder

Fluent builder for RFC 5280 §5 TBSCertList.

class CertificateListBuilder:
    def __init__(self) -> None: ...

    def issuer(self, name_der: bytes) -> CertificateListBuilder: ...
    # Set the issuer Name from pre-encoded DER bytes.

    def this_update(self, time: str) -> CertificateListBuilder: ...
    # Set thisUpdate ("YYYYMMDDHHmmssZ" or "YYMMDDHHmmssZ").

    def next_update(self, time: str) -> CertificateListBuilder: ...
    # Set optional nextUpdate (same format as this_update).

    def revoke(
        self,
        serial: bytes,
        revocation_date: str,
        reason: int | None = None,
    ) -> CertificateListBuilder: ...
    # Add a revoked certificate entry.
    # serial is the big-endian DER INTEGER value bytes.
    # reason is an optional CRL reason code (0–10).

    def signature_algorithm(self, alg_der: bytes) -> CertificateListBuilder: ...
    # Set the AlgorithmIdentifier DER for TBSCertList.signature.

    def build(self) -> bytes: ...
    # Build the DER-encoded TBSCertList SEQUENCE.
    # Raises ValueError if any required field is absent or encoding fails.

    @staticmethod
    def assemble(tbs_der: bytes, sig_alg_der: bytes, signature: bytes) -> bytes: ...
    # Assemble a complete DER-encoded CertificateList.
    # tbs_der:    TBSCertList bytes from build().
    # sig_alg_der: outer AlgorithmIdentifier SEQUENCE TLV.
    # signature:  raw signature bytes (BIT STRING value).
    # Raises ValueError if DER encoding fails.

Example

import synta

name_der = synta.NameBuilder().common_name("Test CA").build()
alg_der  = bytes.fromhex("300d06092a864886f70d01010b0500")  # sha256WithRSAEncryption

tbs = (
    synta.CertificateListBuilder()
    .issuer(name_der)
    .signature_algorithm(alg_der)
    .this_update("20240101120000Z")
    .revoke(bytes([1]), "20231201000000Z", 1)   # reason 1 = keyCompromise
    .build()
)
# Sign tbs externally, then assemble:
# crl_der = synta.CertificateListBuilder.assemble(tbs, alg_der, sig_bytes)

OCSPResponseBuilder

Fluent builder for RFC 6960 §4.2.1 ResponseData. Set exactly one of responder_name or responder_key_hash before calling build_tbs.

class OCSPResponseBuilder:
    def __init__(self) -> None: ...

    def responder_name(self, name_der: bytes) -> OCSPResponseBuilder: ...
    # Set responderID byName from a pre-encoded DER Name SEQUENCE TLV.

    def responder_key_hash(self, key_hash: bytes) -> OCSPResponseBuilder: ...
    # Set responderID byKey from raw key-hash bytes (OCTET STRING value).

    def produced_at(self, time: str) -> OCSPResponseBuilder: ...
    # Set producedAt time ("YYYYMMDDHHmmssZ").

    def add_response(
        self,
        hash_algorithm_der: bytes,
        issuer_name_hash: bytes,
        issuer_key_hash: bytes,
        serial: bytes,
        status: int,
        this_update: str,
        next_update: str | None = None,
    ) -> OCSPResponseBuilder: ...
    # Add a SingleResponse entry.
    # hash_algorithm_der: pre-encoded AlgorithmIdentifier DER TLV.
    # issuer_name_hash / issuer_key_hash: raw hash bytes.
    # serial: big-endian DER INTEGER value bytes.
    # status: 0 = good, 1 = revoked, 2 = unknown.
    # this_update / next_update: "YYYYMMDDHHmmssZ" format.

    def build_tbs(self) -> bytes: ...
    # Build the DER-encoded ResponseData SEQUENCE.
    # Raises ValueError if any required field is absent or encoding fails.

    @staticmethod
    def assemble(tbs_der: bytes, sig_alg_der: bytes, signature: bytes) -> bytes: ...
    # Assemble a complete DER-encoded OCSPResponse.
    # tbs_der:    ResponseData bytes from build_tbs().
    # sig_alg_der: outer AlgorithmIdentifier SEQUENCE TLV.
    # signature:  raw signature bytes (BIT STRING value).
    # Raises ValueError if DER encoding fails.

See also X.509 Extension Value Builders and OCSP.

GeneralName Constants

synta.general_name provides integer constants for the GeneralName CHOICE type (RFC 5280 §4.2.1.6). These are the tag_number values returned by Certificate.subject_alt_names() and synta.parse_general_names().

import synta.general_name as gn

Constants

ConstantValueGeneralName alternativeContent
OTHER_NAME0otherNameFull OtherNameValue TLV (constructed)
RFC822_NAME1rfc822NameRaw IA5String bytes (e-mail address)
DNS_NAME2dNSNameRaw IA5String bytes (DNS host name)
X400_ADDRESS3x400Address(rarely used)
DIRECTORY_NAME4directoryNameComplete Name SEQUENCE TLV — pass to parse_name_attrs()
EDI_PARTY_NAME5ediPartyName(rarely used)
URI6uniformResourceIdentifierRaw IA5String bytes (URI)
IP_ADDRESS7iPAddress4 bytes (IPv4) or 16 bytes (IPv6)
REGISTERED_ID8registeredIDRaw OID value bytes

parse_general_names

synta.parse_general_names(san_der: bytes) -> list[tuple[int, bytes]]

Parse a DER SEQUENCE OF GeneralName into (tag_number, content) pairs. Use the synta.general_name constants to dispatch on tag_number. This function is the low-level alternative to Certificate.subject_alt_names() — use it when you have raw extension value bytes from cert.get_extension_value_der(...).

parse_name_attrs

synta.parse_name_attrs(name_der: bytes) -> list[tuple[str, str]]

Walk a DER Name SEQUENCE and return (dotted_oid, value_str) pairs in traversal order. Pass DIRECTORY_NAME content bytes (a complete Name SEQUENCE TLV) to this function.

Typical dispatch pattern

import ipaddress
import synta
import synta.general_name as gn

for tag_num, content in cert.subject_alt_names():
    if tag_num == gn.DNS_NAME:
        print("DNS:", content.decode("ascii"))
    elif tag_num == gn.IP_ADDRESS:
        print("IP:", ipaddress.ip_address(content))
    elif tag_num == gn.RFC822_NAME:
        print("email:", content.decode("ascii"))
    elif tag_num == gn.URI:
        print("URI:", content.decode("ascii"))
    elif tag_num == gn.DIRECTORY_NAME:
        attrs = synta.parse_name_attrs(content)   # [(oid_str, value_str), …]
        print("DirName:", attrs)

The same constants apply when calling synta.parse_general_names(san_der) directly on raw extnValue bytes:

san_der = cert.get_extension_value_der(synta.oids.SUBJECT_ALT_NAME)
if san_der:
    for tag_num, content in synta.parse_general_names(san_der):
        if tag_num == gn.DNS_NAME:
            print("DNS:", content.decode("ascii"))

See also Certificate for subject_alt_names() and the Well-known OIDs page for synta.oids.SUBJECT_ALT_NAME and related constants.

PyCA Interoperability

synta.Certificate and cryptography.x509.Certificate can be converted back and forth. The two libraries are complementary and can coexist in the same process; the conversion is designed for environments where both are already in use and you need to hand certificate objects between them without re-parsing.

synta provides its own full cryptographic operation support — signature verification, key generation and signing, asymmetric encryption, and KEM operations — through its PublicKey and PrivateKey API. PyCA interop is not needed to do cryptography; it is for transparent data handover when both libraries are present.

Requires the cryptography package (pip install cryptography). Both methods raise ImportError with an actionable message if the package is absent.

When to use each library

TaskRecommended
Parsing certificates at scalesynta (4–10× faster, lazy decode)
Accessing certificate fieldssynta (cached, zero-copy getters)
Signature verificationsynta Certificate.verify_issued_by() or PublicKey.verify_signature()
Key generation and signingsynta PrivateKey.generate_*() / PrivateKey.sign()
Asymmetric encryption / decryptionsynta PublicKey.rsa_oaep_encrypt() / PrivateKey.rsa_oaep_decrypt()
KEM encapsulate / decapsulatesynta PublicKey.kem_encapsulate() / PrivateKey.kem_decapsulate()
Integrating with an existing PyCA code pathcert.to_pyca() / Certificate.from_pyca()
Receiving a cryptography.x509.Certificate from a third-party libraryCertificate.from_pyca()

Conversion methods

cert.to_pyca() -> cryptography.x509.Certificate

Return a cryptography.x509.Certificate backed by the same DER bytes. No re-encoding occurs; the original DER buffer is passed directly to load_der_x509_certificate. Use this to hand a synta-parsed certificate to a function or library that expects a PyCA object.

Certificate.from_pyca(pyca_cert: object) -> Certificate

Convert a cryptography.x509.Certificate to a synta.Certificate. Checks obj._synta_der_bytes first (the fast path when the object was originally created by synta’s to_pyca()); falls back to public_bytes(Encoding.DER) otherwise. Use this when you receive a PyCA certificate from a third-party library and want synta’s fast field access or bulk-processing capabilities.

Usage

Handing a synta certificate to a PyCA-based API

import synta

synta_cert = synta.Certificate.from_der(open("cert.der", "rb").read())

# Fast field access via synta (cached after first call):
print(synta_cert.subject)
print(synta_cert.not_before, "–", synta_cert.not_after)

# Hand off to a third-party function that requires a PyCA certificate:
pyca_cert = synta_cert.to_pyca()
some_pyca_library.register_certificate(pyca_cert)

Verifying a certificate chain using synta’s own verifier

import synta
import synta.x509 as x509

# synta has full chain verification — no PyCA needed.
cert_der  = open("cert.der",   "rb").read()
ca_der    = open("ca.der",     "rb").read()

store  = x509.TrustStore.from_pem(open("ca.pem", "rb").read())
policy = x509.VerificationPolicy(server_name="example.com")
x509.verify_server_certificate(
    synta.Certificate.from_der(cert_der), store, policy
)
print("chain valid")

Receiving a PyCA certificate from a third-party library

import synta
from cryptography.x509 import load_pem_x509_certificate

# Some external library hands you a PyCA certificate.
pyca_cert = load_pem_x509_certificate(open("cert.pem", "rb").read())

# Convert to synta for fast bulk field access.
synta_cert = synta.Certificate.from_pyca(pyca_cert)
print(synta_cert.serial_number.hex())
print(synta_cert.subject_alt_names())

Signing a CSR with synta’s own key API

import synta
from synta import CsrBuilder

# Generate key and sign a CSR entirely within synta — no PyCA required.
key = synta.PrivateKey.generate_ec("P-256")
csr_der = (
    CsrBuilder()
    .subject_common_name("example.com")
    .add_san_dns("example.com")
    .build(key)
)
csr = synta.CertificationRequest.from_der(csr_der)
csr.verify_self_signature()  # verify the CSR's proof-of-possession

Performance notes

  • to_pyca() is zero-copy: the already-held DER bytes are passed by reference to cryptography’s load_der_x509_certificate. No re-encoding or copying occurs.
  • from_pyca() has a fast path: when the PyCA object was originally created by synta’s to_pyca(), the _synta_der_bytes attribute is present and read back directly, avoiding the public_bytes(Encoding.DER) call entirely.
  • synta is typically 4–10× faster than cryptography for parsing and field access.

See also PublicKey and PrivateKey for synta’s native crypto API, X.509 Path Validation for chain verification, and Performance for benchmark results.

X.509 Path Validation

synta.x509 verifies RFC 5280 / CABF TLS server and client certificate chains.

import synta.x509 as x509

Classes

TrustStore

A set of trusted root CA certificates used as trust anchors for chain verification.

x509.TrustStore(ders: list[bytes])

Construct a TrustStore from a list of DER-encoded CA certificates.

CrlStore

A set of Certificate Revocation Lists for revocation checking.

x509.CrlStore(ders: list[bytes])

Construct a CrlStore from a list of DER-encoded CRLs.

VerificationPolicy

Policy parameters for chain verification.

x509.VerificationPolicy(
    server_names: list[str] | None = None,
    name_match: str = "any",
    validation_time: float | None = None,
    max_chain_depth: int | None = None,
    profile: str | None = None,
)
ParameterTypeDescription
server_nameslist[str] | NoneDNS names the certificate must cover
name_matchstr"any" (default) or "all" — whether the cert must match any or all supplied names
validation_timefloat | NoneUnix timestamp for validity window check; None uses current time
max_chain_depthint | NoneMaximum allowed chain length
profilestr | NoneValidation profile identifier

X509VerificationError

Raised on any chain or policy failure. Subclass of Exception.

try:
    x509.verify_server_certificate(...)
except x509.X509VerificationError as exc:
    print(f"verification failed: {exc}")

Functions

x509.verify_server_certificate(
    leaf: bytes | Certificate,
    intermediates: list[bytes | Certificate],
    store: TrustStore,
    policy: VerificationPolicy,
    crls: CrlStore | None = None,
) -> list[bytes]

Verify a TLS server certificate chain. Returns the verified chain as a list of DER-encoded certificates in order from trust anchor to leaf.

x509.verify_client_certificate(
    leaf: bytes | Certificate,
    intermediates: list[bytes | Certificate],
    store: TrustStore,
    policy: VerificationPolicy,
    crls: CrlStore | None = None,
) -> list[bytes]

Verify a TLS client certificate chain. Same return type as verify_server_certificate.

Usage

import synta
import synta.x509 as x509

# Load trust anchors from a PEM bundle
with open("roots.pem", "rb") as f:
    ders = synta.pem_to_der(f.read())
store = x509.TrustStore(ders)

# Verify a server certificate (single name)
with open("leaf.der", "rb") as f:
    leaf_der = f.read()
with open("intermediate.der", "rb") as f:
    intermediate_der = f.read()

policy = x509.VerificationPolicy(server_names=["example.com"])
chain  = x509.verify_server_certificate(leaf_der, [intermediate_der], store, policy)

for i, cert_der in enumerate(chain):
    cert = synta.Certificate.from_der(cert_der)
    role = "trust anchor" if i == 0 else ("leaf" if i == len(chain) - 1 else "CA")
    print(f"chain[{i}] ({role}): {cert.subject}")

# Multi-name any-match (cert must cover at least one name)
policy = x509.VerificationPolicy(
    server_names=["example.com", "www.example.com"],
    name_match="any",
)

# Multi-name all-match (cert must cover every name)
policy = x509.VerificationPolicy(
    server_names=["example.com", "api.example.com"],
    name_match="all",
)

# Error handling
try:
    chain = x509.verify_server_certificate(leaf_der, [], store, policy)
except x509.X509VerificationError as exc:
    print(f"verification failed: {exc}")

See also Certificate for verify_issued_by (single-issuer signature check without a full chain build) and Error Handling for the exception hierarchy.

CMS Cryptography

The synta.cms submodule provides Python bindings for the Cryptographic Message Syntax (RFC 5652) and CMS-KEM (RFC 9629). It exposes parsers and builders for all major CMS content types.

Content types

ClassRFCDescription
ContentInfoRFC 5652 §3Outer CMS envelope; entry point for all content types
SignedDataRFC 5652 §5Encapsulates signed content plus signer information
SignerInfoRFC 5652 §5.3Per-signer structure within SignedData
EnvelopedDataRFC 5652 §6Encrypted content with key transport per recipient
EnvelopedDataBuilderRFC 5652 §6Fluent builder for EnvelopedData
EncryptedDataRFC 5652 §8Symmetric encryption (shared key, no recipient info)
DigestedDataRFC 5652 §7Hash-protected content
AuthenticatedDataRFC 5652 §9MAC-authenticated content
IssuerAndSerialNumberRFC 5652 §10.2.4Certificate identifier
KEMRecipientInfoRFC 9629 §5Quantum-safe KEM recipient structure
CMSORIforKEMOtherInfoRFC 9629 §5.3KDF input structure for KEM-based key derivation

Import

from synta.cms import (
    ContentInfo,
    SignedData, SignerInfo,
    EnvelopedData, EnvelopedDataBuilder,
    EncryptedData,
    DigestedData,
    AuthenticatedData,
    IssuerAndSerialNumber,
    KEMRecipientInfo, CMSORIforKEMOtherInfo,
    # OID constants:
    ID_DATA, ID_SIGNED_DATA, ID_ENVELOPED_DATA,
    ID_DIGESTED_DATA, ID_ENCRYPTED_DATA, ID_CT_AUTH_DATA,
    ID_AES128_CBC, ID_AES192_CBC, ID_AES256_CBC,
    ID_RSAES_OAEP, ID_RSA_ENCRYPTION,
    ID_ORI, ID_ORI_KEM,
)

OID constants

Content-type OIDs (RFC 5652 §14)

ConstantOIDName
ID_DATA1.2.840.113549.1.7.1id-data
ID_SIGNED_DATA1.2.840.113549.1.7.2id-signedData
ID_ENVELOPED_DATA1.2.840.113549.1.7.3id-envelopedData
ID_DIGESTED_DATA1.2.840.113549.1.7.5id-digestedData
ID_ENCRYPTED_DATA1.2.840.113549.1.7.6id-encryptedData
ID_CT_AUTH_DATA1.2.840.113549.1.9.16.1.2id-ct-authData

Content-encryption algorithm OIDs (RFC 3565)

ConstantOIDKey length
ID_AES128_CBC2.16.840.1.101.3.4.1.216 bytes
ID_AES192_CBC2.16.840.1.101.3.4.1.2224 bytes
ID_AES256_CBC2.16.840.1.101.3.4.1.4232 bytes

Key-transport algorithm OIDs (RFC 8017)

ConstantOIDNotes
ID_RSAES_OAEP1.2.840.113549.1.1.7RSA-OAEP with SHA-256 (recommended)
ID_RSA_ENCRYPTION1.2.840.113549.1.1.1RSA PKCS#1 v1.5 (legacy)

CMS-KEM OtherRecipientInfo OIDs (RFC 9629 §6.2)

ConstantOIDDescription
ID_ORI1.2.840.113549.1.9.16.13Root arc for OtherRecipientInfo alternatives
ID_ORI_KEM1.2.840.113549.1.9.16.13.3Identifies a KEMRecipientInfo

Sections

ContentInfo

ContentInfo is the outer CMS envelope — every CMS structure is wrapped in a ContentInfo SEQUENCE. It is the standard entry point for parsing any CMS payload.

Class

class ContentInfo:
    @staticmethod
    def from_der(data: bytes) -> ContentInfo: ...
    # Parse a DER- or BER-encoded ContentInfo SEQUENCE.

    def to_der(self) -> bytes: ...

    content_type_oid: ObjectIdentifier
    # e.g. ID_SIGNED_DATA, ID_ENVELOPED_DATA, ID_ENCRYPTED_DATA, …

    content: bytes
    # Raw bytes of the content field (including the [0] EXPLICIT tag for most types).

Usage

from synta.cms import ContentInfo, ID_SIGNED_DATA, ID_ENVELOPED_DATA, ID_ENCRYPTED_DATA

data = open("message.p7m", "rb").read()
ci = ContentInfo.from_der(data)

print(ci.content_type_oid)  # e.g. ObjectIdentifier("1.2.840.113549.1.7.2")

if ci.content_type_oid == ID_SIGNED_DATA:
    from synta.cms import SignedData
    sd = SignedData.from_der(ci.content)
    print(f"Signers: {len(sd.signer_infos)}")

elif ci.content_type_oid == ID_ENVELOPED_DATA:
    from synta.cms import EnvelopedData
    ed = EnvelopedData.from_der(ci.content)
    # decrypt with ed.decrypt(private_key)

elif ci.content_type_oid == ID_ENCRYPTED_DATA:
    from synta.cms import EncryptedData
    enc = EncryptedData.from_der(ci.content)
    # decrypt with enc.decrypt(key)

See also the CMS Overview for a summary of all content types.

SignedData and SignerInfo

SignedData

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

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      # raw [0] IMPLICIT value bytes
    crls: bytes | None              # raw [1] IMPLICIT value bytes
    signer_infos: list[SignerInfo]

Usage

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 is the raw [0] IMPLICIT bytes; wrap with SEQUENCE tag for Decoder
    certs_der = bytes([0x30]) + sd.certificates[1:]  # swap IMPLICIT [0] tag with SEQUENCE
    # ... decode with synta.Decoder

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

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
    # Raw [0] IMPLICIT bytes.  Replace the tag byte with 0x31 before hashing.

    unsigned_attrs: bytes | None

Usage

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 carries an IMPLICIT [0] tag; replace the first byte with 0x31
        # to get a valid SET TLV for hashing.
        attrs_set = b'\x31' + si.signed_attrs[1:]
        dec = synta.Decoder(attrs_set, synta.Encoding.DER)
        attrs_seq = dec.decode_set()
        while not attrs_seq.is_empty():
            attr_tlv = attrs_seq.decode_raw_tlv()
            # each attr_tlv is a complete Attribute SEQUENCE

See also CMS Overview and PKCS Loaders for load_der_pkcs7_certificates which extracts certificates from a SignedData automatically.

EnvelopedData

EnvelopedData implements RFC 5652 §6 — encrypted content with per-recipient key transport. The content-encryption key (CEK) is encrypted under each recipient’s public key and carried in RecipientInfo structures. The content itself is encrypted using a symmetric cipher.

Decryption and encryption require the openssl Cargo feature.

EnvelopedData

class EnvelopedData:
    @staticmethod
    def from_der(data: bytes) -> EnvelopedData: ...
    def to_der(self) -> bytes: ...

    def decrypt(self, private_key: PrivateKey) -> bytes: ...
    # Decrypt using the recipient's private key.
    # Raises ValueError if no matching RecipientInfo is found or decryption fails.
    # Requires the openssl Cargo feature.

    version: int
    originator_info: bytes | None
    recipient_infos: bytes           # raw RecipientInfos SET bytes
    content_type: ObjectIdentifier
    content_encryption_algorithm_oid: ObjectIdentifier
    content_encryption_algorithm_params: bytes | None
    encrypted_content: bytes | None
    unprotected_attrs: bytes | None

EnvelopedDataBuilder

Fluent builder for EnvelopedData (RFC 5652 §6). Requires the openssl Cargo feature.

class EnvelopedDataBuilder:
    def __init__(
        self,
        plaintext: bytes,
        recipients: list[tuple[Certificate | bytes, ObjectIdentifier]],
        *,
        content_enc_alg: ObjectIdentifier | None = None,
    ) -> None: ...
    # recipients: list of (cert, key_wrap_oid) tuples.
    # cert may be a Certificate object or DER bytes.
    # key_wrap_oid: ID_RSAES_OAEP (recommended) or ID_RSA_ENCRYPTION (legacy).
    # content_enc_alg defaults to id-aes256-CBC.

    def add_originator_cert(self, cert: Certificate | bytes) -> EnvelopedDataBuilder: ...
    def add_originator_crl(self, crl: CertificateList | bytes) -> EnvelopedDataBuilder: ...
    def set_unprotected_attrs(self, attrs: bytes) -> EnvelopedDataBuilder: ...
    def build(self) -> EnvelopedData: ...

Usage

import synta
from synta.cms import EnvelopedData, EnvelopedDataBuilder, ID_RSAES_OAEP, ID_AES256_CBC

# Encrypt for one recipient
recipient_cert = synta.Certificate.from_der(open("recipient.der", "rb").read())
plaintext = b"Confidential message."

ed = EnvelopedDataBuilder(
    plaintext,
    [(recipient_cert, ID_RSAES_OAEP)],
    content_enc_alg=ID_AES256_CBC,
).build()
ciphertext_der = ed.to_der()

# Decrypt
priv_key = synta.PrivateKey.from_pem(open("recipient.pem", "rb").read())
ed2 = EnvelopedData.from_der(ciphertext_der)
recovered = ed2.decrypt(priv_key)
assert recovered == plaintext

See also EncryptedData for shared-key symmetric encryption and CMS-KEM for quantum-safe KEM recipient structures.

EncryptedData

EncryptedData implements RFC 5652 §8 — symmetric content encryption using a shared key. Unlike EnvelopedData, there is no per-recipient key transport; the caller provides the symmetric key directly.

Encryption and decryption require the openssl Cargo feature (maturin develop --features openssl).

Class

class EncryptedData:
    @staticmethod
    def from_der(data: bytes) -> EncryptedData: ...
    # Parse a DER- or BER-encoded EncryptedData SEQUENCE.
    # Raises ValueError if the outer SEQUENCE envelope is malformed.

    @staticmethod
    def create(
        plaintext: bytes,
        key: bytes,
        algorithm_oid: ObjectIdentifier | str,
        content_type_oid: ObjectIdentifier | str | None = None,
    ) -> EncryptedData: ...
    # Encrypt plaintext under key using the cipher identified by algorithm_oid.
    # A fresh random IV is generated for every call.
    # content_type_oid defaults to ID_DATA (1.2.840.113549.1.7.1).
    # Raises NotImplementedError if built without the openssl Cargo feature.

    def decrypt(self, key: bytes) -> bytes: ...
    # Recover plaintext.  Raises ValueError on key-length mismatch.
    # Raises NotImplementedError if built without the openssl Cargo feature.

    def to_der(self) -> bytes: ...

    version: int                             # always 0
    content_type: ObjectIdentifier           # e.g. ID_DATA
    content_encryption_algorithm_oid: ObjectIdentifier
    content_encryption_algorithm_params: bytes | None
    # For CBC ciphers: DER OCTET STRING (04 <len> <iv>) carrying the 16-byte IV.
    encrypted_content: bytes | None
    unprotected_attrs: bytes | None

Usage

from synta.cms import EncryptedData, ID_AES128_CBC, ID_AES256_CBC

KEY_128 = bytes.fromhex("00112233445566778899aabbccddeeff")
KEY_256 = bytes(range(32))

# ── Encrypt ───────────────────────────────────────────────────────────────────
plaintext = b"Confidential payload."
ed = EncryptedData.create(plaintext, KEY_128, ID_AES128_CBC)

# Inspect fields
print(ed.version)                            # 0
print(ed.content_type)                       # 1.2.840.113549.1.7.1  (id-data)
print(ed.content_encryption_algorithm_oid)   # 2.16.840.1.101.3.4.1.2
iv = ed.content_encryption_algorithm_params[2:]   # strip 04 <len> header
print(iv.hex())                              # 16-byte random IV

# ── Round-trip ────────────────────────────────────────────────────────────────
assert EncryptedData.from_der(ed.to_der()).decrypt(KEY_128) == plaintext

# ── Re-encrypt with replaced content ─────────────────────────────────────────
decrypted = ed.decrypt(KEY_128)
modified  = decrypted.replace(b"PENDING", b"APPROVED")
ed2 = EncryptedData.create(modified, KEY_256, ID_AES256_CBC)
assert ed2.decrypt(KEY_256) == modified

Each create call generates a fresh random IV; two calls on the same plaintext produce different ciphertext. See examples/example_cms_encrypted_data.py for a full example including openssl enc interop in both directions.

See also EnvelopedData for per-recipient key transport and CMS Overview for all content types.

DigestedData and AuthenticatedData

DigestedData

DigestedData implements RFC 5652 §7 — hash-protected content.

class DigestedData:
    @staticmethod
    def from_der(data: bytes) -> DigestedData: ...
    def to_der(self) -> bytes: ...

    version: int
    digest_algorithm_oid: ObjectIdentifier
    digest_algorithm_params: bytes | None
    encap_content_type: ObjectIdentifier
    encap_content: bytes | None
    digest: bytes

Usage

from synta.cms import DigestedData

dd = DigestedData.from_der(data)
print(f"digest algorithm: {dd.digest_algorithm_oid}")
print(f"digest: {dd.digest.hex()}")
if dd.encap_content:
    content = dd.encap_content  # raw content bytes

AuthenticatedData

AuthenticatedData implements RFC 5652 §9 — MAC-authenticated content.

class AuthenticatedData:
    @staticmethod
    def from_der(data: bytes) -> AuthenticatedData: ...
    def to_der(self) -> bytes: ...

    version: int
    originator_info: bytes | None
    recipient_infos: bytes                    # raw RecipientInfos SET bytes
    mac_algorithm_oid: ObjectIdentifier
    mac_algorithm_params: bytes | None
    digest_algorithm_oid: ObjectIdentifier | None
    digest_algorithm_params: bytes | None
    encap_content_type: ObjectIdentifier
    encap_content: bytes | None
    mac: bytes

    auth_attrs: bytes | None
    # Raw [2] IMPLICIT bytes.  Replace the leading tag byte with 0x31 before MAC
    # verification (same substitution as SignerInfo.signed_attrs).

    unauth_attrs: bytes | None

Usage

from synta.cms import AuthenticatedData

ad = AuthenticatedData.from_der(data)
print(f"mac algorithm: {ad.mac_algorithm_oid}")
print(f"mac: {ad.mac.hex()}")

if ad.auth_attrs:
    # Replace IMPLICIT [2] tag with SET tag 0x31 for MAC computation
    import synta
    attrs_set = b'\x31' + ad.auth_attrs[1:]
    dec = synta.Decoder(attrs_set, synta.Encoding.DER)
    # decode attributes ...

See also CMS Overview for the complete list of content types.

CMS-KEM Types

synta.kem provides KEMRecipientInfo and CMSORIforKEMOtherInfo for RFC 9629 quantum-safe KEM recipient structures, along with ML-KEM (FIPS 203) OID constants.

KEMRecipientInfo is carried as an OtherRecipientInfo alternative identified by id-ori-kem (1.2.840.113549.1.9.16.13.3).

import synta.kem as kem

These types are also re-exported from synta.cms.

KEMRecipientInfo

RFC 9629 §5 recipient structure for KEM-based key transport.

class KEMRecipientInfo:
    @staticmethod
    def from_der(data: bytes) -> KEMRecipientInfo: ...
    def to_der(self) -> bytes: ...

    version: int                             # always 0
    recipient_id: bytes                      # raw RecipientIdentifier CHOICE TLV
    kem_algorithm_oid: ObjectIdentifier      # KEM algorithm (e.g. ML-KEM-768)
    kem_algorithm_params: bytes | None
    kem_ciphertext: bytes                    # KEM ciphertext (kemct field)
    kdf_algorithm_oid: ObjectIdentifier      # key-derivation algorithm
    kdf_algorithm_params: bytes | None
    kek_length: int                          # KEK length in bytes (1–65535)
    ukm: bytes | None                        # user keying material
    key_encryption_algorithm_oid: ObjectIdentifier  # key-wrap algorithm
    key_encryption_algorithm_params: bytes | None
    encrypted_key: bytes                     # encrypted content-encryption key

CMSORIforKEMOtherInfo

RFC 9629 §5.3 — otherInfo input to the KDF when deriving a KEK from a KEM shared secret.

class CMSORIforKEMOtherInfo:
    @staticmethod
    def from_der(data: bytes) -> CMSORIforKEMOtherInfo: ...
    def to_der(self) -> bytes: ...

    key_encryption_algorithm_oid: ObjectIdentifier
    key_encryption_algorithm_params: bytes | None
    kek_length: int
    ukm: bytes | None

OID constants

ML-KEM OIDs (FIPS 203)

ConstantOIDDescription
ID_ML_KEM_5122.16.840.1.101.3.4.4.1ML-KEM-512 key encapsulation
ID_ML_KEM_7682.16.840.1.101.3.4.4.2ML-KEM-768 key encapsulation
ID_ML_KEM_10242.16.840.1.101.3.4.4.3ML-KEM-1024 key encapsulation

CMS OtherRecipientInfo OIDs (RFC 9629 §6.2)

ConstantOIDDescription
ID_ORI1.2.840.113549.1.9.16.13Root arc for OtherRecipientInfo alternatives
ID_ORI_KEM1.2.840.113549.1.9.16.13.3Identifies a KEMRecipientInfo

Usage

import synta
import synta.kem as kem

# Parse a KEMRecipientInfo from an OtherRecipientInfo body
kri = kem.KEMRecipientInfo.from_der(ori_body_der)
print(f"KEM algorithm: {kri.kem_algorithm_oid}")
print(f"KDF algorithm: {kri.kdf_algorithm_oid}")
print(f"KEK length:    {kri.kek_length}")
print(f"ciphertext:    {kri.kem_ciphertext.hex()}")

# Check if this is an ML-KEM-768 recipient
if kri.kem_algorithm_oid == kem.ID_ML_KEM_768:
    # Decapsulate with a PrivateKey
    priv_key = synta.PrivateKey.from_der(kem_priv_der)
    shared_secret = priv_key.kem_decapsulate(kri.kem_ciphertext)
    # Then apply KDF (kri.kdf_algorithm_oid) over shared_secret + kri.ukm to derive KEK

See also Keys for PublicKey.kem_encapsulate() and PrivateKey.kem_decapsulate(), and CMS Overview for the full module import.

CMS OID Constants

All OID constants in synta.cms are ObjectIdentifier instances (frozen, hashable).

Content-type OIDs (RFC 5652 §14)

ConstantOIDName
ID_DATA1.2.840.113549.1.7.1id-data
ID_SIGNED_DATA1.2.840.113549.1.7.2id-signedData
ID_ENVELOPED_DATA1.2.840.113549.1.7.3id-envelopedData
ID_DIGESTED_DATA1.2.840.113549.1.7.5id-digestedData
ID_ENCRYPTED_DATA1.2.840.113549.1.7.6id-encryptedData
ID_CT_AUTH_DATA1.2.840.113549.1.9.16.1.2id-ct-authData

Content-encryption algorithm OIDs (RFC 3565)

Passed as algorithm_oid to EncryptedData.create; also returned by the content_encryption_algorithm_oid property.

ConstantOIDKey length
ID_AES128_CBC2.16.840.1.101.3.4.1.216 bytes
ID_AES192_CBC2.16.840.1.101.3.4.1.2224 bytes
ID_AES256_CBC2.16.840.1.101.3.4.1.4232 bytes

Key-transport algorithm OIDs (RFC 8017)

Passed as key_wrap_oid to EnvelopedDataBuilder.

ConstantOIDNotes
ID_RSAES_OAEP1.2.840.113549.1.1.7RSA-OAEP with SHA-256 (recommended)
ID_RSA_ENCRYPTION1.2.840.113549.1.1.1RSA PKCS#1 v1.5 (legacy)

CMS-KEM OtherRecipientInfo OIDs (RFC 9629 §6.2)

ConstantOIDDescription
ID_ORI1.2.840.113549.1.9.16.13Root arc for OtherRecipientInfo alternatives
ID_ORI_KEM1.2.840.113549.1.9.16.13.3Identifies a KEMRecipientInfo

Import

from synta.cms import (
    ID_DATA, ID_SIGNED_DATA, ID_ENVELOPED_DATA,
    ID_DIGESTED_DATA, ID_ENCRYPTED_DATA, ID_CT_AUTH_DATA,
    ID_AES128_CBC, ID_AES192_CBC, ID_AES256_CBC,
    ID_RSAES_OAEP, ID_RSA_ENCRYPTION,
    ID_ORI, ID_ORI_KEM,
)

ML-KEM OID constants (ID_ML_KEM_512, ID_ML_KEM_768, ID_ML_KEM_1024) are in synta.kem; see CMS-KEM.

See also Well-known OIDs for the full synta.oids catalog which includes the CMS content-type OIDs as CMS_DATA, CMS_SIGNED_DATA, etc.

Kerberos V5 Types

synta.krb5 provides Kerberos V5 principal name types, constants, and PKINIT protocol structures (RFC 4556, RFC 6112, RFC 8636).

import synta.krb5

Principal-name type constants

Integer constants from RFC 4120 §6.2:

ConstantValueDescription
NT_UNKNOWN0Unknown
NT_PRINCIPAL1User/host principal
NT_SRV_INST2Service + instance (e.g. krbtgt)
NT_SRV_HST3Service + hostname
NT_SRV_XHST4Service + host (remaining components)
NT_UID5Unique ID
NT_X500_PRINCIPAL6Encoded X.500 DN
NT_SMTP_NAME7SMTP email address
NT_ENTERPRISE10Enterprise (UPN-style), RFC 6806
NT_WELLKNOWN11Well-known (anonymous), RFC 8062
NT_SRV_HST_DOMAIN12Host-based service, Windows MS-SFU

Encryption type constants

IANA Kerberos Encryption Type Numbers:

ConstantValueRFC / sourceNotes
ETYPE_DES_CBC_CRC1RFC 6649Deprecated
ETYPE_DES_CBC_MD42RFC 6649Deprecated
ETYPE_DES_CBC_MD53RFC 6649Deprecated
ETYPE_DES3_CBC_MD55Deprecated
ETYPE_DES3_CBC_SHA17Deprecated
ETYPE_DES_HMAC_SHA18Deprecated
ETYPE_DES3_CBC_SHA1_KD16RFC 3961 §6.3Deprecated
ETYPE_AES128_CTS_HMAC_SHA1_9617RFC 3962
ETYPE_AES256_CTS_HMAC_SHA1_9618RFC 3962
ETYPE_AES128_CTS_HMAC_SHA256_12819RFC 8009Recommended
ETYPE_AES256_CTS_HMAC_SHA384_19220RFC 8009Recommended
ETYPE_RC4_HMAC23MS-KILEDeprecated
ETYPE_RC4_HMAC_EXP24MS-KILEDeprecated
ETYPE_CAMELLIA128_CTS_CMAC25RFC 6803
ETYPE_CAMELLIA256_CTS_CMAC26RFC 6803

OID constant

KRB5_PRINCIPAL_NAME_OID  # ObjectIdentifier("1.3.6.1.5.2.2") — id-pkinit-san

This is the OtherName type-id for KRB5PrincipalName entries in certificate SAN extensions.

See also:

Krb5PrincipalName

Krb5PrincipalName encodes and decodes the KRB5PrincipalName structure (RFC 4556 §3.2.2) used as an OtherName Subject Alternative Name value in PKINIT certificates.

Class

class Krb5PrincipalName:
    def __init__(self, realm: str, name_type: int, components: list[str]) -> None: ...
    # realm must be ASCII.  name_type is one of the NT_* constants.
    # All string arguments must be ASCII; raises ValueError otherwise.

    @staticmethod
    def from_der(data: bytes) -> Krb5PrincipalName: ...

    def to_der(self) -> bytes: ...

    realm: str             # Kerberos realm (e.g. "EXAMPLE.COM")
    name_type: int         # One of the NT_* constants
    components: list[str]  # Name-string components

    def __repr__(self) -> str: ...
    def __eq__(self, other) -> bool: ...

Usage

import synta.krb5 as krb5

# Construct a host principal: host/server.example.com@EXAMPLE.COM
principal = krb5.Krb5PrincipalName(
    realm="EXAMPLE.COM",
    name_type=krb5.NT_SRV_HST,
    components=["host", "server.example.com"],
)
der_bytes = principal.to_der()

# Parse from DER
parsed = krb5.Krb5PrincipalName.from_der(der_bytes)
print(parsed.realm)          # "EXAMPLE.COM"
print(parsed.name_type)      # 3  (NT_SRV_HST)
print(parsed.components)     # ["host", "server.example.com"]

# Read from a PKINIT certificate SAN (otherName [0] tag)
import synta
import synta.general_name as gn

for tag_num, content in cert.subject_alt_names():
    if tag_num == gn.OTHER_NAME:
        # content is the full OtherNameValue TLV; decode type-id and value
        dec = synta.Decoder(content, synta.Encoding.DER)
        seq = dec.decode_sequence()
        type_id = seq.decode_oid()       # should be krb5.KRB5_PRINCIPAL_NAME_OID
        explicit = seq.decode_explicit_tag(0)
        kpn = krb5.Krb5PrincipalName.from_der(explicit.remaining_bytes())
        print(f"realm={kpn.realm}, name_type={kpn.name_type}, components={kpn.components}")

Encoding a principal name for certificate generation

import synta
import synta.krb5 as krb5

# Build an otherName SAN entry for a PKINIT client certificate
kpn = krb5.Krb5PrincipalName(
    realm="EXAMPLE.COM",
    name_type=krb5.NT_PRINCIPAL,
    components=["alice"],
)
kpn_der = kpn.to_der()

# Wrap in OtherName: SEQUENCE { type-id OID, value [0] EXPLICIT <kpn_der> }
inner = synta.Encoder(synta.Encoding.DER)
inner.encode_oid(krb5.KRB5_PRINCIPAL_NAME_OID)
inner.encode_explicit_tag(0, "Context", kpn_der)

outer = synta.Encoder(synta.Encoding.DER)
outer.encode_sequence(inner.finish())
other_name_der = outer.finish()

# Use with SubjectAlternativeNameBuilder
import synta.ext as ext
san = ext.SAN().directory_name(other_name_der).build()

See also Kerberos V5 Types for principal-name type and encryption type constants, and PKINIT Types for the full PKINIT protocol structure set.

PKINIT Protocol Types

All PKINIT classes in synta.krb5 are frozen (immutable after construction). Each provides a from_der(data: bytes) static method for parsing. Fields that are OPTIONAL in the ASN.1 schema are exposed as ... | None.

import synta.krb5 as krb5

EncryptionKey

RFC 3961 §2 encryption key structure.

class EncryptionKey:
    @staticmethod
    def from_der(data: bytes) -> EncryptionKey: ...
    keytype: int        # Kerberos etype number
    keyvalue: bytes     # raw key material

Checksum

RFC 3961 §4 checksum structure.

class Checksum:
    @staticmethod
    def from_der(data: bytes) -> Checksum: ...
    cksumtype: int      # checksum type
    checksum: bytes     # raw checksum bytes

KDFAlgorithmId

RFC 8636 §3.1 KDF algorithm identifier.

class KDFAlgorithmId:
    @staticmethod
    def from_der(data: bytes) -> KDFAlgorithmId: ...
    kdf_id: ObjectIdentifier    # KDF algorithm OID

IssuerAndSerialNumber

RFC 4556 §3.2.2 — identifies a certificate by issuer name and serial number.

class IssuerAndSerialNumber:
    @staticmethod
    def from_der(data: bytes) -> IssuerAndSerialNumber: ...
    issuer: bytes           # DER-encoded Name SEQUENCE
    serial_number: int      # certificate serial number

ExternalPrincipalIdentifier

RFC 4556 §3.2.2 — identifies a client certificate by one of three optional methods.

class ExternalPrincipalIdentifier:
    @staticmethod
    def from_der(data: bytes) -> ExternalPrincipalIdentifier: ...
    subject_name: bytes | None                               # DER of subject Name
    issuer_and_serial_number: IssuerAndSerialNumber | None
    subject_key_identifier: bytes | None                     # raw SKI bytes

PKAuthenticator

RFC 4556 §3.2.1 — client proof of liveness in AS-REQ.

class PKAuthenticator:
    @staticmethod
    def from_der(data: bytes) -> PKAuthenticator: ...
    cusec: int              # microseconds component (0–999999)
    ctime: str              # client time as "YYYYMMDDHHMMSSz"
    nonce: int
    pa_checksum: bytes | None      # SHA-1 checksum of AS-REQ
    freshness_token: bytes | None  # RFC 8070 freshness token

AuthPack

RFC 4556 §3.2.1 — content signed by the client.

class AuthPack:
    @staticmethod
    def from_der(data: bytes) -> AuthPack: ...
    pk_authenticator: PKAuthenticator
    client_public_value: bytes | None       # DER SubjectPublicKeyInfo
    supported_cmstypes: bytes | None        # DER AlgorithmIdentifiers
    client_dhnonce: bytes | None
    supported_kdfs: list[KDFAlgorithmId] | None   # RFC 8636 KDF list

PaPkAsReq

RFC 4556 §3.2.2 — PKINIT pre-authentication request.

class PaPkAsReq:
    @staticmethod
    def from_der(data: bytes) -> PaPkAsReq: ...
    signed_auth_pack: bytes                          # CMS SignedData wrapping AuthPack
    trusted_certifiers: list[ExternalPrincipalIdentifier] | None
    kdc_pk_id: bytes | None                          # raw SKI bytes for KDC certificate

DHRepInfo

RFC 4556 §3.2.4 — KDC Diffie-Hellman reply data.

class DHRepInfo:
    @staticmethod
    def from_der(data: bytes) -> DHRepInfo: ...
    dh_signed_data: bytes               # CMS SignedData wrapping KDCDHKeyInfo
    server_dhnonce: bytes | None

KDCDHKeyInfo

RFC 4556 §3.2.4 — KDC DH public key and nonce.

class KDCDHKeyInfo:
    @staticmethod
    def from_der(data: bytes) -> KDCDHKeyInfo: ...
    subject_public_key: bytes       # BIT STRING payload bytes (KDC DH public key)
    nonce: int
    dh_key_expiration: str | None   # "YYYYMMDDHHMMSSz"

ReplyKeyPack

RFC 4556 §3.2.3 — session key and checksum from KDC (Diffie-Hellman-less path).

class ReplyKeyPack:
    @staticmethod
    def from_der(data: bytes) -> ReplyKeyPack: ...
    reply_key: EncryptionKey    # the session key
    as_checksum: Checksum       # checksum over the AS-REQ

PaPkAsRep

RFC 4556 §3.2.4 — PKINIT pre-authentication reply (CHOICE type).

class PaPkAsRep:
    @staticmethod
    def from_der(data: bytes) -> PaPkAsRep: ...
    variant: str                    # "DhInfo" or "EncKeyPack"
    dh_info: DHRepInfo | None
    enc_key_pack: bytes | None      # CMS EnvelopedData bytes

See also Kerberos V5 Types for constants and Krb5PrincipalName for principal name encoding.

RFC 3279 Algorithm Parameters

synta.pkixalgs provides types for decoding DSA/DH domain parameters and DSA/ECDSA signature values, as defined in RFC 3279.

import synta.pkixalgs as pa

DssParms

DSA domain parameters (RFC 3279 §2.3.2). Decoded from the parameters field of an AlgorithmIdentifier whose OID is id-dsa.

class DssParms:
    @staticmethod
    def from_der(data: bytes) -> DssParms: ...
    def to_der(self) -> bytes: ...
    p: bytes    # prime modulus (big-endian two's-complement)
    q: bytes    # prime divisor
    g: bytes    # generator

DssSigValue

DSA signature value (RFC 3279 §2.2.2). Contains the (r, s) integer pair.

class DssSigValue:
    @staticmethod
    def from_der(data: bytes) -> DssSigValue: ...
    def to_der(self) -> bytes: ...
    r: bytes    # signature integer r (big-endian two's-complement)
    s: bytes    # signature integer s

EcdsaSigValue

ECDSA signature value (RFC 3279 §2.2.3, X9.62). Contains the (r, s) integer pair.

class EcdsaSigValue:
    @staticmethod
    def from_der(data: bytes) -> EcdsaSigValue: ...
    def to_der(self) -> bytes: ...
    r: bytes
    s: bytes

ECParameters

EC domain parameters CHOICE (RFC 3279 §2.3.5, X9.62). Represents the three-arm CHOICE: namedCurve (OID), ecParameters (explicit domain params), or implicitlyCA (NULL).

class ECParameters:
    @staticmethod
    def from_der(data: bytes) -> ECParameters: ...
    def to_der(self) -> bytes: ...
    arm: str                              # "namedCurve", "ecParameters", or "implicitlyCA"
    named_curve_oid: ObjectIdentifier | None  # None if arm is not "namedCurve"

OID constants

ConstantOIDDescription
ID_DSA1.2.840.10040.4.1DSA public key
ID_DSA_WITH_SHA11.2.840.10040.4.3DSA with SHA-1 signature
DHPUBLICNUMBER1.2.840.10046.2.1Diffie-Hellman public key
ID_EC_PUBLIC_KEY1.2.840.10045.2.1EC public key
ECDSA_WITH_SHA11.2.840.10045.4.1ECDSA with SHA-1
ECDSA_WITH_SHA2561.2.840.10045.4.3.2ECDSA with SHA-256
ECDSA_WITH_SHA3841.2.840.10045.4.3.3ECDSA with SHA-384
ECDSA_WITH_SHA5121.2.840.10045.4.3.4ECDSA with SHA-512
PRIME192V11.2.840.10045.3.1.1NIST P-192 / secp192r1
PRIME256V11.2.840.10045.3.1.7NIST P-256 / secp256r1
SECP224R11.3.132.0.33NIST P-224
SECP384R11.3.132.0.34NIST P-384
SECP521R11.3.132.0.35NIST P-521

Usage

import synta
import synta.pkixalgs as pa

# Decode ECDSA signature value from cert.signature_value
sig = pa.EcdsaSigValue.from_der(cert.signature_value)
print(f"r: {sig.r.hex()}")
print(f"s: {sig.s.hex()}")

# Decode EC curve OID from public key algorithm parameters
if cert.public_key_algorithm_params:
    ec_params = pa.ECParameters.from_der(cert.public_key_algorithm_params)
    if ec_params.arm == "namedCurve":
        print(f"curve OID: {ec_params.named_curve_oid}")

Attribute Certificates

synta.ac provides RFC 5755 Attribute Certificate v2 types. Attribute Certificates (ACs) bind a set of attributes (roles, clearances, service-authentication information) to a holder identified by reference to their Public Key Certificate (PKC), without requiring re-issuance of the PKC.

import synta.ac as ac

AttributeCertificate

class AttributeCertificate:
    @staticmethod
    def from_der(data: bytes) -> AttributeCertificate: ...
    # Raises ValueError if the bytes cannot be decoded.

    @staticmethod
    def from_pem(data: bytes) -> AttributeCertificate: ...
    # Parse the first ATTRIBUTE CERTIFICATE PEM block.
    # Raises ValueError if no valid PEM block is found or the DER is invalid.

    def to_der(self) -> bytes: ...
    def to_pem(self) -> str: ...
    # PEM encoding with the "ATTRIBUTE CERTIFICATE" label (RFC 5755).

    version: int                              # always 1 for v2 (RFC 5755)
    serial_number: bytes                      # certificate serial number as big-endian bytes
    not_before: str                           # "YYYYMMDDHHmmssZ"
    not_after: str                            # "YYYYMMDDHHmmssZ"
    signature_algorithm_oid: ObjectIdentifier
    signature: bytes                          # raw signature bytes (zero-byte padding stripped)
    holder_der: bytes                         # DER bytes of the Holder SEQUENCE
    issuer_der: bytes                         # DER bytes of the AttCertIssuer CHOICE
    attributes_der: bytes                     # DER bytes of SEQUENCE OF Attribute

    def verify_issued_by(self, issuer: Certificate) -> None: ...
    # Verify the cryptographic signature against issuer's public key.
    # No AC issuer-to-subject name comparison is performed.
    # Raises ValueError if the signature is invalid or encoding fails.

AttributeCertificateBuilder

Fluent builder for RFC 5755 AttributeCertificateInfo TBS encoding.

class AttributeCertificateBuilder:
    def __init__(self) -> None: ...

    def serial_number(self, n: int) -> AttributeCertificateBuilder: ...
    def not_before(self, s: str) -> AttributeCertificateBuilder: ...   # "YYYYMMDDHHmmssZ"
    def not_after(self, s: str) -> AttributeCertificateBuilder: ...    # "YYYYMMDDHHmmssZ"
    def issuer_rfc822(self, email: str) -> AttributeCertificateBuilder: ...
    def issuer_dns(self, name: str) -> AttributeCertificateBuilder: ...
    def holder_entity_name_rfc822(self, email: str) -> AttributeCertificateBuilder: ...
    def build(self) -> bytes: ...
    # Encode the AttributeCertificateInfo SEQUENCE to DER bytes.
    # Raises ValueError if any required field is missing or encoding fails.

OID constants

ConstantDescription
ID_PE_AC_AUDIT_IDENTITYAudit identity extension (RFC 5755 §4.4.1)
ID_PE_AA_CONTROLSAA controls extension (RFC 5755 §4.4.2)
ID_PE_AC_PROXYINGAC proxying extension (RFC 5755 §4.4.3)
ID_CE_TARGET_INFORMATIONTarget information extension (RFC 5755 §4.3.2)
ID_ACA_AUTHENTICATION_INFOAuthentication information attribute
ID_ACA_ACCESS_IDENTITYAccess identity attribute
ID_ACA_CHARGING_IDENTITYCharging identity attribute
ID_ACA_GROUPGroup attribute
ID_ACA_ENC_ATTRSEncrypted attributes
ID_AT_ROLERole attribute type
ID_AT_CLEARANCEClearance attribute type

Usage

import synta
import synta.ac as ac

# Parse an attribute certificate
with open("holder.ac", "rb") as f:
    attr_cert = ac.AttributeCertificate.from_der(f.read())

print(f"version: {attr_cert.version}")
print(f"not_before: {attr_cert.not_before}")
print(f"not_after: {attr_cert.not_after}")
print(f"sig alg: {attr_cert.signature_algorithm_oid}")

# Verify the AC signature
issuer_cert = synta.Certificate.from_der(open("issuer.der", "rb").read())
try:
    attr_cert.verify_issued_by(issuer_cert)
    print("AC signature valid")
except ValueError as e:
    print(f"Invalid: {e}")

# Decode attributes (decode holder_der/attributes_der with synta.Decoder)
import synta
dec = synta.Decoder(attr_cert.attributes_der, synta.Encoding.DER)
attrs = dec.decode_sequence()
while not attrs.is_empty():
    attr_tlv = attrs.decode_raw_tlv()
    # attr_tlv is one complete Attribute SEQUENCE

CRMF Messages

synta.crmf provides RFC 4211 Certificate Request Message Format types used in CMP certificate management.

import synta.crmf as crmf

CertReqMessages

A batch of Certificate Request Messages (SEQUENCE OF CertReqMsg, RFC 4211 §3).

class CertReqMessages:
    @staticmethod
    def from_der(data: bytes) -> CertReqMessages: ...
    def to_der(self) -> bytes: ...
    requests: list[CertReqMsg]
    def __len__(self) -> int: ...
    def __iter__(self): ...
    def __getitem__(self, index: int) -> CertReqMsg: ...

CertReqMsg

A single certificate request message (RFC 4211 §4).

class CertReqMsg:
    cert_req_id: int               # certReqId integer
    cert_template_der: bytes       # DER bytes of the CertTemplate SEQUENCE
    popo_type: str | None          # Active ProofOfPossession arm name, or None
    # One of: "raVerified", "signature", "keyEncipherment", "keyAgreement"
    popo_der: bytes | None         # DER bytes of the ProofOfPossession CHOICE
    subject_der: bytes | None      # DER bytes of subject Name from CertTemplate
    issuer_der: bytes | None       # DER bytes of issuer Name from CertTemplate

CertReqMsgBuilder

Fluent builder for a single CertReqMsg.

class CertReqMsgBuilder:
    def __init__(self) -> None: ...
    def cert_req_id(self, id: int) -> CertReqMsgBuilder: ...
    def subject_name(self, name_der: bytes) -> CertReqMsgBuilder: ...
    def issuer_name(self, name_der: bytes) -> CertReqMsgBuilder: ...
    def public_key_der(self, spki_der: bytes) -> CertReqMsgBuilder: ...
    def popo_ra_verified(self) -> CertReqMsgBuilder: ...
    def publication_action(self, action: int) -> CertReqMsgBuilder: ...
    def add_pub_info(self, pub_method: int) -> CertReqMsgBuilder: ...
    def pub_location_uri(self, pub_method: int, uri: str) -> CertReqMsgBuilder: ...
    def pub_location_rfc822(self, pub_method: int, email: str) -> CertReqMsgBuilder: ...
    def pub_location_dns(self, pub_method: int, host: str) -> CertReqMsgBuilder: ...
    def pub_location_directory_name(self, pub_method: int, name_der: bytes) -> CertReqMsgBuilder: ...
    def build(self) -> CertReqMsg: ...
    # Raises ValueError if stored Name/SPKI bytes are invalid.

CertReqMessagesBuilder

Fluent builder for a CRMF batch.

class CertReqMessagesBuilder:
    def __init__(self) -> None: ...
    def add_request(self, req: CertReqMsg) -> CertReqMessagesBuilder: ...
    def build(self) -> CertReqMessages: ...

Publication method constants

ConstantValueMeaning
PUB_METHOD_DONT_CARE0No publication preference
PUB_METHOD_X5001Publish in X.500 directory
PUB_METHOD_WEB2Publish via HTTP/HTTPS URI
PUB_METHOD_LDAP3Publish in LDAP directory

OID constants

ConstantDescription
ID_REG_CTRL_REG_TOKENRegistration token control (RFC 4211 §6.1)
ID_REG_CTRL_AUTHENTICATORAuthenticator control (RFC 4211 §6.2)
ID_REG_CTRL_PKI_PUBLICATION_INFOPKI publication info control
ID_REG_CTRL_PKI_ARCHIVE_OPTIONSPKI archive options control
ID_REG_CTRL_OLD_CERT_IDOld certificate ID control
ID_REG_CTRL_PROTOCOL_ENCR_KEYProtocol encryption key control
ID_REG_INFO_UTF8_PAIRSUTF-8 pairs registration info
ID_REG_INFO_CERT_REQCertificate request registration info

Usage

import synta.crmf as crmf

# Build a CRMF request
req = (
    crmf.CertReqMsgBuilder()
    .cert_req_id(1)
    .subject_name(subject_name_der)
    .public_key_der(spki_der)
    .popo_ra_verified()
    .build()
)

batch = crmf.CertReqMessagesBuilder().add_request(req).build()
batch_der = batch.to_der()

# Parse incoming CRMF
msgs = crmf.CertReqMessages.from_der(data)
for msg in msgs:
    print(f"id={msg.cert_req_id}, popo={msg.popo_type}")
    if msg.subject_der:
        import synta
        attrs = synta.parse_name_attrs(msg.subject_der)
        print(f"  subject: {attrs}")

See also CMP Messages for the CMP envelope that carries CRMF requests.

CMP Messages

synta.cmp provides RFC 9810 Certificate Management Protocol v3 types.

import synta.cmp as cmp

CMPMessage

class CMPMessage:
    @staticmethod
    def from_der(data: bytes) -> CMPMessage: ...
    def to_der(self) -> bytes: ...

    pvno: int              # CMP protocol version (2 = cmp2000, 3 = cmp2021)
    body_type: str         # Active PKIBody arm in lowercase: "ir", "ip", "cr", "cp",
                           # "rr", "rp", "pkiconf", "error", "genm", "genp", etc.
    body_der: bytes | None # Raw DER bytes of the PKIBody content, or None for "pkiconf"
    sender_der: bytes      # DER bytes of the sender GeneralName field
    recipient_der: bytes   # DER bytes of the recipient GeneralName field
    transaction_id: bytes | None
    sender_nonce: bytes | None
    recip_nonce: bytes | None
    protection_alg_oid: ObjectIdentifier | None  # OID of protectionAlg, or None
    message_time: str | None                     # GeneralizedTime string, or None

Body type dispatch

if msg.body_type == "ir" or msg.body_type == "cr":
    # body_der is a CertReqMessages SEQUENCE OF
    import synta.crmf as crmf
    cert_reqs = crmf.CertReqMessages.from_der(msg.body_der)
    for req in cert_reqs:
        print(f"req id={req.cert_req_id}")

CMPMessageBuilder

Fluent builder for a CMP PKIMessage (RFC 9810). Set exactly one sender and one recipient GeneralName before calling build().

class CMPMessageBuilder:
    def __init__(self) -> None: ...

    def pvno(self, pvno: int) -> CMPMessageBuilder: ...
    # 2 = cmp2000, 3 = cmp2021

    # Sender (exactly one must be set)
    def sender_rfc822(self, email: str) -> CMPMessageBuilder: ...
    def sender_dns(self, host: str) -> CMPMessageBuilder: ...
    def sender_uri(self, uri: str) -> CMPMessageBuilder: ...
    def sender_directory_name(self, name_der: bytes) -> CMPMessageBuilder: ...

    # Recipient (exactly one must be set)
    def recipient_rfc822(self, email: str) -> CMPMessageBuilder: ...
    def recipient_dns(self, host: str) -> CMPMessageBuilder: ...
    def recipient_uri(self, uri: str) -> CMPMessageBuilder: ...
    def recipient_directory_name(self, name_der: bytes) -> CMPMessageBuilder: ...

    def transaction_id(self, data: bytes) -> CMPMessageBuilder: ...
    def sender_nonce(self, data: bytes) -> CMPMessageBuilder: ...
    def recip_nonce(self, data: bytes) -> CMPMessageBuilder: ...

    # Body (set exactly one)
    def body_pkiconf(self) -> CMPMessageBuilder: ...        # [19] pkiConf (NULL)
    def body_ir(self, cert_req_messages_der: bytes) -> CMPMessageBuilder: ...   # [0]
    def body_cr(self, cert_req_messages_der: bytes) -> CMPMessageBuilder: ...   # [2]
    def body_kur(self, cert_req_messages_der: bytes) -> CMPMessageBuilder: ...  # [7]
    def body_p10cr(self, csr_der: bytes) -> CMPMessageBuilder: ...              # [4]
    def body_genm(self, gen_msg_der: bytes) -> CMPMessageBuilder: ...           # [21]

    def build(self) -> CMPMessage: ...
    # Raises ValueError if sender or recipient are not set, or if any GeneralName
    # data is invalid.

OID constants

ConstantDescription
ID_PASSWORD_BASED_MACPassword-based MAC algorithm
ID_DHBASED_MACDH-based MAC algorithm
ID_KEM_BASED_MACKEM-based MAC algorithm
ID_KP_CM_KGACMP key-generation authority key purpose
ID_REG_CTRL_ALT_CERT_TEMPLATEAlternate certificate template control
ID_REG_CTRL_ALG_IDAlgorithm ID control
ID_REG_CTRL_RSA_KEY_LENRSA key length control

Usage

import synta.cmp as cmp
import synta.crmf as crmf

# Parse an incoming CMP message
msg = cmp.CMPMessage.from_der(data)
print(f"pvno: {msg.pvno}, body: {msg.body_type}")

# Build a CMP ir (Initialization Request) with a CRMF batch
crmf_der = batch.to_der()  # from CertReqMessagesBuilder.build()

ir_msg = (
    cmp.CMPMessageBuilder()
    .pvno(2)
    .sender_rfc822("alice@example.com")
    .recipient_directory_name(ca_name_der)
    .transaction_id(os.urandom(16))
    .sender_nonce(os.urandom(16))
    .body_ir(crmf_der)
    .build()
)
ir_der = ir_msg.to_der()

See also CRMF Messages for the request structures carried in CMP bodies.

PKCS#8 Private Key Structures

synta.pkcs8 provides OneAsymmetricKey (also exported as PrivateKeyInfo) for parsing DER-encoded private key envelopes produced by OpenSSL and other PKI tools (RFC 5958 / PKCS#8).

import synta.pkcs8 as pkcs8

OneAsymmetricKey

class OneAsymmetricKey:
    @staticmethod
    def from_der(data: bytes) -> OneAsymmetricKey: ...
    def to_der(self) -> bytes: ...

    version: int                          # 0 = v1 (PrivateKeyInfo), 1 = v2 (RFC 5958)
    private_key_algorithm: ObjectIdentifier  # the private-key algorithm OID
    private_key: bytes                    # raw key material (OCTET STRING value)
    attributes_der: bytes | None          # raw DER of [0] IMPLICIT attributes bag
    public_key_der: bytes | None          # raw DER of [1] IMPLICIT public key BIT STRING
    alg_parameters_der: bytes | None      # algorithm parameters DER, or None

PrivateKeyInfo = OneAsymmetricKey  # RFC 5958 / PKCS#8 alias

Usage

import synta.pkcs8 as pkcs8

# Parse a PKCS#8 key from DER bytes
with open("key.der", "rb") as f:
    key = pkcs8.OneAsymmetricKey.from_der(f.read())

print(f"version: {key.version}")
print(f"algorithm: {key.private_key_algorithm}")
print(f"key bytes: {key.private_key.hex()}")

# Algorithm parameters (e.g. curve OID for EC keys)
if key.alg_parameters_der:
    import synta.pkixalgs as pa
    ec_params = pa.ECParameters.from_der(key.alg_parameters_der)
    if ec_params.arm == "namedCurve":
        print(f"curve: {ec_params.named_curve_oid}")

# Check for optional public key component (v2 / RFC 5958)
if key.public_key_der:
    print(f"public key DER: {len(key.public_key_der)} bytes")

# Use the PrivateKeyInfo alias
key2 = pkcs8.PrivateKeyInfo.from_der(key_der)

For cryptographic operations (signing, decryption, key generation), use synta.PrivateKey which wraps an OpenSSL key. OneAsymmetricKey is a pure ASN.1 parser that does not invoke any cryptographic backend.

See also PKCS#9 OIDs for id-friendlyName and id-localKeyId bag attributes used in PKCS#12 archives, and PKCS Loaders for extracting raw PKCS#8 DER bytes from PKCS#12 archives via load_pkcs12_keys.

SPNEGO Negotiation Tokens

synta.spnego provides classes for the GSSAPI SPNEGO negotiation mechanism (RFC 4178). It handles both the full GSSAPI-wrapped form (leading 0x60 APPLICATION tag) and the plain CHOICE form (0xa0/0xa1).

Wire format note: GssapiSpnego.asn1 uses DEFINITIONS IMPLICIT TAGS. All context-tagged fields use IMPLICIT encoding (the universal tag is replaced, not wrapped).

import synta.spnego as spnego

NegTokenInit

Sent by the GSSAPI initiator to propose mechanisms and supply an optional initial token (RFC 4178 §4.2.1).

class NegTokenInit:
    @staticmethod
    def from_der(data: bytes) -> NegTokenInit: ...
    # Parse a DER-encoded NegTokenInit SEQUENCE.

    mech_types: list[str]       # proposed mechanism OIDs in dot-notation
    req_flags: bytes | None     # context flags bit-string bytes, or None
    mech_token: bytes | None    # initial token for preferred mech, or None
    mech_list_mic: bytes | None # MIC over mechanism list, or None

NegTokenResp

Sent by the GSSAPI acceptor to indicate negotiation state and supply a reply token (RFC 4178 §4.2.2). Compare neg_state against the NEG_STATE_* constants.

class NegTokenResp:
    @staticmethod
    def from_der(data: bytes) -> NegTokenResp: ...
    # Parse a DER-encoded NegTokenResp SEQUENCE.

    neg_state: int | None       # NEG_STATE_* constant, or None
    supported_mech: str | None  # selected mechanism OID (dot-notation), or None
    response_token: bytes | None
    mech_list_mic: bytes | None

NegotiationToken

The top-level CHOICE wrapper. from_der handles both the full GSSAPI-wrapped form and the raw CHOICE form.

class NegotiationToken:
    @staticmethod
    def from_der(data: bytes) -> NegotiationToken: ...
    # Accepts 0x60 (GSSAPI-wrapped), 0xa0 (NegTokenInit), or 0xa1 (NegTokenResp).

    variant: str                    # "NegTokenInit" or "NegTokenResp"
    neg_token_init: NegTokenInit | None
    neg_token_resp: NegTokenResp | None

Constants

spnego.NEG_STATE_ACCEPT_COMPLETED   # 0 — negotiation succeeded
spnego.NEG_STATE_ACCEPT_INCOMPLETE  # 1 — more tokens needed
spnego.NEG_STATE_REJECT             # 2 — negotiation rejected
spnego.NEG_STATE_REQUEST_MIC        # 3 — MIC is requested

spnego.SPNEGO_OID  # "1.3.6.1.5.5.2"  (string, not ObjectIdentifier)

Usage

import synta.spnego as spnego

# Parse a GSSAPI SPNEGO token from the network
tok = spnego.NegotiationToken.from_der(gssapi_bytes)
if tok.variant == "NegTokenInit":
    init = tok.neg_token_init
    print("Proposed mechanisms:", init.mech_types)
    if init.mech_token:
        print("Initial token:", len(init.mech_token), "bytes")
else:
    resp = tok.neg_token_resp
    if resp.neg_state == spnego.NEG_STATE_ACCEPT_COMPLETED:
        print("Negotiation complete; mech:", resp.supported_mech)
    elif resp.neg_state == spnego.NEG_STATE_REQUEST_MIC:
        print("MIC requested")

Microsoft PKI (AD CS) Extension Types

synta.ms_pki exposes two certificate extension types used by Microsoft Active Directory Certificate Services (AD CS), along with OID constants for Microsoft-specific extensions.

import synta.ms_pki as ms_pki

MSCSTemplateV2

Parsed from the extension value DER bytes of OID 1.3.6.1.4.1.311.21.7 (id-ms-certificate-template).

class MSCSTemplateV2:
    @staticmethod
    def from_der(data: bytes) -> MSCSTemplateV2: ...
    def to_der(self) -> bytes: ...

    template_id: ObjectIdentifier        # template OID
    template_major_version: int          # major version number
    template_minor_version: int | None   # minor version, or None if absent
import synta
import synta.ms_pki as ms_pki

ext_der = cert.get_extension_value_der("1.3.6.1.4.1.311.21.7")
if ext_der:
    tmpl = ms_pki.MSCSTemplateV2.from_der(ext_der)
    print(tmpl.template_id, tmpl.template_major_version)

RequestClientInfo

Parsed from the extension value DER bytes of OID 1.3.6.1.4.1.311.21.20 (id-ms-request-Client-Info). Carried in certificate requests to identify the enrolling Windows client.

class RequestClientInfo:
    @staticmethod
    def from_der(data: bytes) -> RequestClientInfo: ...

    client_id: int | None       # numeric enrollment type, or None
    machine_name: str | None    # NetBIOS or DNS machine name, or None
    user_name: str | None       # Windows user name (DOMAIN\user), or None
    process_name: str | None    # enrolling process (e.g. "certreq"), or None
ext_der = cert.get_extension_value_der("1.3.6.1.4.1.311.21.20")
if ext_der:
    info = ms_pki.RequestClientInfo.from_der(ext_der)
    print(info.machine_name, info.user_name)

OID constants

All are ObjectIdentifier instances.

ConstantOIDDescription
ID_MS_CERTSRV_CA_VERSION1.3.6.1.4.1.311.21.1CA version
ID_MS_CERTSRV_PREVIOUS_CERT_HASH1.3.6.1.4.1.311.21.2previous cert hash
ID_MS_CRL_VIRTUAL_BASE1.3.6.1.4.1.311.21.3CRL virtual base
ID_MS_CRL_NEXT_PUBLISH1.3.6.1.4.1.311.21.4CRL next publish time
ID_MS_KP_CA_EXCHANGE1.3.6.1.4.1.311.21.5CA key exchange
ID_MS_KP_KEY_RECOVERY_AGENT1.3.6.1.4.1.311.21.6key recovery agent
ID_MS_ENTERPRISE_OID_ROOT1.3.6.1.4.1.311.21.8enterprise OID root
ID_MS_APPLICATION_CERT_POLICIES1.3.6.1.4.1.311.21.10application cert policies
ID_MS_REQUEST_CLIENT_INFO1.3.6.1.4.1.311.21.20enrollment client info
ID_MS_ENCRYPTED_KEY_HASH1.3.6.1.4.1.311.21.21encrypted key hash
ID_MS_CERTSRV_CROSSCA_VERSION1.3.6.1.4.1.311.21.22cross-CA version
ID_MS_KP_CTL_USAGE_SIGNING1.3.6.1.4.1.311.10.3.1CTL usage signing
ID_MS_KP_TIME_STAMP_SIGNING1.3.6.1.4.1.311.10.3.2time stamp signing
ID_MS_KP_EFS_CRYPTO1.3.6.1.4.1.311.10.3.4EFS file encryption
ID_MS_KP_EFS_RECOVERY1.3.6.1.4.1.311.10.3.4.1EFS recovery
ID_MS_KP_KEY_RECOVERY1.3.6.1.4.1.311.10.3.11key recovery
ID_MS_KP_DOCUMENT_SIGNING1.3.6.1.4.1.311.10.3.12document signing
ID_MS_KP_LIFETIME_SIGNING1.3.6.1.4.1.311.10.3.13lifetime signing
ID_MS_AUTO_ENROLL_CTL_USAGE1.3.6.1.4.1.311.20.1auto-enrollment CTL

See also Well-known OIDs for the synta.oids Microsoft PKI constants (ID_MS_SAN_UPN, ID_MS_CERTIFICATE_TEMPLATE_NAME, ID_MS_CERTIFICATE_TEMPLATE, ID_MS_KP_SMARTCARD_LOGON, ID_MS_NTDS_REPLICATION).

Merkle Tree Certificates

synta.mtc implements the ASN.1 types from draft-ietf-plants-merkle-tree-certs. All classes are immutable (frozen) and constructed via from_der static methods.

import synta.mtc as mtc

Name fields such as issuer_der and subject_der are raw DER-encoded Name SEQUENCES; pass them to synta.parse_name_attrs() to decode the DN attributes.

ProofNode

A single node in a Merkle inclusion proof path.

class ProofNode:
    @staticmethod
    def from_der(data: bytes) -> ProofNode: ...
    is_left: bool   # True if this is the left sibling in the path
    hash: bytes     # raw hash bytes at this proof node

Subtree

A hash subtree covering leaf range [start, end).

class Subtree:
    @staticmethod
    def from_der(data: bytes) -> Subtree: ...
    start: int    # start leaf index (inclusive)
    end: int      # end leaf index (exclusive)
    value: bytes  # aggregated hash bytes for [start, end)

SubtreeProof

Left and right subtree lists for log compaction.

class SubtreeProof:
    @staticmethod
    def from_der(data: bytes) -> SubtreeProof: ...
    left_subtrees: list[Subtree] | None
    right_subtrees: list[Subtree] | None

InclusionProof

Merkle inclusion proof for a log entry.

class InclusionProof:
    @staticmethod
    def from_der(data: bytes) -> InclusionProof: ...
    log_entry_index: int              # leaf position of the certified entry
    tree_size: int                    # total leaves at proof time
    inclusion_path: list[ProofNode]   # ordered sibling hashes

LogID

Identifies a transparency log by its hash algorithm and public key.

class LogID:
    @staticmethod
    def from_der(data: bytes) -> LogID: ...
    hash_alg_oid: str       # hash algorithm OID (dot-notation)
    public_key_der: bytes   # DER-encoded SubjectPublicKeyInfo

CosignerID

Identifies a cosigner (witness) in a countersigned checkpoint.

class CosignerID:
    @staticmethod
    def from_der(data: bytes) -> CosignerID: ...
    hash_alg_oid: str
    public_key_der: bytes

Checkpoint

A signed tree head from a transparency log.

class Checkpoint:
    @staticmethod
    def from_der(data: bytes) -> Checkpoint: ...
    log_id: LogID
    tree_size: int
    timestamp: int      # milliseconds since Unix epoch
    root_hash: bytes
    signature: bytes

SubtreeSignature

A cosigner signature over a checkpoint and subtree proof.

class SubtreeSignature:
    @staticmethod
    def from_der(data: bytes) -> SubtreeSignature: ...
    cosigner_id: CosignerID
    tree_size: int
    timestamp: int
    root_hash: bytes
    signature: bytes

TbsCertificateLogEntry

The to-be-signed body of a Merkle Tree Certificate log entry.

class TbsCertificateLogEntry:
    @staticmethod
    def from_der(data: bytes) -> TbsCertificateLogEntry: ...

    issuer_der: bytes                            # raw Name DER
    validity_not_before: str                     # GeneralizedTime string
    validity_not_after: str                      # GeneralizedTime string
    subject_der: bytes                           # raw Name DER
    subject_public_key_algorithm_oid: str        # algorithm OID
    subject_public_key_hash: bytes               # hash of the SPKI
    issuer_unique_id: bytes | None
    subject_unique_id: bytes | None
    extensions_der: bytes | None                 # raw extensions DER

MerkleTreeCertEntry

Top-level CHOICE wrapper for a Merkle Tree Certificate entry.

class MerkleTreeCertEntry:
    @staticmethod
    def from_der(data: bytes) -> MerkleTreeCertEntry: ...
    variant: str                         # "StandaloneCertificate" or "LandmarkCertificate"
    standalone: StandaloneCertificate | None
    landmark: LandmarkCertificate | None

LandmarkID

Identifies a landmark certificate in the MTC ecosystem.

class LandmarkID:
    @staticmethod
    def from_der(data: bytes) -> LandmarkID: ...
    hash_alg_oid: str
    public_key_der: bytes
    tree_size: int

StandaloneCertificate

class StandaloneCertificate:
    @staticmethod
    def from_der(data: bytes) -> StandaloneCertificate: ...
    proof: InclusionProof
    checkpoint: Checkpoint
    subtree_proof: SubtreeProof | None
    cosignatures: list[SubtreeSignature]
    tbs_cert: TbsCertificateLogEntry
    cert_der: bytes | None          # optional original X.509 DER
    extensions_der: bytes | None

LandmarkCertificate

class LandmarkCertificate:
    @staticmethod
    def from_der(data: bytes) -> LandmarkCertificate: ...
    landmark_id: LandmarkID
    proof: InclusionProof
    checkpoint: Checkpoint
    tbs_cert: TbsCertificateLogEntry
    cert_der: bytes | None
    extensions_der: bytes | None

Usage

import synta
import synta.mtc as mtc

# Parse a Merkle Tree Certificate entry
entry = mtc.MerkleTreeCertEntry.from_der(data)
if entry.variant == "StandaloneCertificate":
    sc = entry.standalone
    print(f"tree size: {sc.checkpoint.tree_size}")
    print(f"leaf index: {sc.proof.log_entry_index}")
    issuer = synta.parse_name_attrs(sc.tbs_cert.issuer_der)
    subject = synta.parse_name_attrs(sc.tbs_cert.subject_der)
    print(f"issuer: {issuer}")
    print(f"subject: {subject}")

Well-known OIDs

synta.oids provides 70+ well-known OID constants as ObjectIdentifier instances (frozen, hashable). Import with import synta.oids as oids.

Algorithm OIDs

ConstantOIDStandard
RSA_ENCRYPTION1.2.840.113549.1.1.1PKCS #1
MD5_WITH_RSA1.2.840.113549.1.1.4PKCS #1
SHA1_WITH_RSA1.2.840.113549.1.1.5PKCS #1
SHA256_WITH_RSA1.2.840.113549.1.1.11RFC 4055
SHA384_WITH_RSA1.2.840.113549.1.1.12RFC 4055
SHA512_WITH_RSA1.2.840.113549.1.1.13RFC 4055
EC_PUBLIC_KEY1.2.840.10045.2.1RFC 5480
ECDSA_WITH_SHA11.2.840.10045.4.1ANSI X9.62
ECDSA_WITH_SHA2561.2.840.10045.4.3.2RFC 5758
ECDSA_WITH_SHA3841.2.840.10045.4.3.3RFC 5758
ECDSA_WITH_SHA5121.2.840.10045.4.3.4RFC 5758
ED255191.3.101.112RFC 8410
ED4481.3.101.113RFC 8410
ML_DSA_442.16.840.1.101.3.4.3.17FIPS 204
ML_DSA_652.16.840.1.101.3.4.3.18FIPS 204
ML_DSA_872.16.840.1.101.3.4.3.19FIPS 204
ML_KEM_5122.16.840.1.101.3.4.4.1FIPS 203
ML_KEM_7682.16.840.1.101.3.4.4.2FIPS 203
ML_KEM_10242.16.840.1.101.3.4.4.3FIPS 203
EC_CURVE_P2561.2.840.10045.3.1.7NIST P-256
EC_CURVE_P3841.3.132.0.34NIST P-384
EC_CURVE_P5211.3.132.0.35NIST P-521
EC_CURVE_SECP256K11.3.132.0.10Bitcoin curve

Hash algorithm OIDs

ConstantOIDStandard
SHA2242.16.840.1.101.3.4.2.4FIPS 180-4
SHA2562.16.840.1.101.3.4.2.1FIPS 180-4
SHA3842.16.840.1.101.3.4.2.2FIPS 180-4
SHA5122.16.840.1.101.3.4.2.3FIPS 180-4
SHA512_2242.16.840.1.101.3.4.2.5FIPS 180-4
SHA512_2562.16.840.1.101.3.4.2.6FIPS 180-4
SHA3_2242.16.840.1.101.3.4.2.7FIPS 202
SHA3_2562.16.840.1.101.3.4.2.8FIPS 202
SHA3_3842.16.840.1.101.3.4.2.9FIPS 202
SHA3_5122.16.840.1.101.3.4.2.10FIPS 202
SHAKE1282.16.840.1.101.3.4.2.11FIPS 202
SHAKE2562.16.840.1.101.3.4.2.12FIPS 202

SLH-DSA OIDs (FIPS 205)

SLH_DSA_SHA2_128F, SLH_DSA_SHA2_128S, SLH_DSA_SHA2_192F, SLH_DSA_SHA2_192S, SLH_DSA_SHA2_256F, SLH_DSA_SHA2_256S, SLH_DSA_SHAKE_128F, SLH_DSA_SHAKE_128S, SLH_DSA_SHAKE_192F, SLH_DSA_SHAKE_192S, SLH_DSA_SHAKE_256F, SLH_DSA_SHAKE_256S.

Prefix OIDs

These are prefix arcs for use with oid.components() rather than exact match.

ConstantOID prefixCovers
RSA1.2.840.113549.1.1All PKCS#1 signature algorithms
ECDSA_SIG1.2.840.10045.4All ECDSA signature algorithms
ECDSA_KEY1.2.840.10045.2EC public-key types
DSA1.2.840.10040.4DSA and DSA-with-hash algorithms
# Example: match any RSA algorithm
import synta.oids as oids
rsa_prefix = oids.RSA.components()
if cert.signature_algorithm_oid.components()[:len(rsa_prefix)] == rsa_prefix:
    print("RSA family")

X.509v3 extension OIDs

ConstantOIDRFC reference
SUBJECT_ALT_NAME2.5.29.17RFC 5280
ISSUER_ALT_NAME2.5.29.18RFC 5280
BASIC_CONSTRAINTS2.5.29.19RFC 5280
KEY_USAGE2.5.29.15RFC 5280
EXTENDED_KEY_USAGE2.5.29.37RFC 5280
SUBJECT_KEY_IDENTIFIER2.5.29.14RFC 5280
AUTHORITY_KEY_IDENTIFIER2.5.29.35RFC 5280
CERTIFICATE_POLICIES2.5.29.32RFC 5280
CRL_DISTRIBUTION_POINTS2.5.29.31RFC 5280
AUTHORITY_INFO_ACCESS1.3.6.1.5.5.7.1.1RFC 5280
CT_PRECERT_SCTS1.3.6.1.4.1.11129.2.4.2RFC 6962

Extended Key Usage (EKU) OIDs

ConstantOIDUse
KP_SERVER_AUTH1.3.6.1.5.5.7.3.1TLS server authentication
KP_CLIENT_AUTH1.3.6.1.5.5.7.3.2TLS client authentication
KP_CODE_SIGNING1.3.6.1.5.5.7.3.3Code signing
KP_EMAIL_PROTECTION1.3.6.1.5.5.7.3.4S/MIME
KP_TIME_STAMPING1.3.6.1.5.5.7.3.8RFC 3161 TSA
KP_OCSP_SIGNING1.3.6.1.5.5.7.3.9OCSP responder
ANY_EXTENDED_KEY_USAGE2.5.29.37.0Match any EKU

PKINIT OIDs (RFC 4556 / RFC 8636)

ConstantOIDDescription
ID_PKINIT_SAN1.3.6.1.5.2.2KRB5PrincipalName OtherName type-id
ID_PKINIT_KPCLIENT_AUTH1.3.6.1.5.2.3.4PKINIT client auth EKU
ID_PKINIT_KPKDC1.3.6.1.5.2.3.5PKINIT KDC EKU
ID_PKINIT_AUTH_DATA1.3.6.1.5.2.3.1PA-PK-AS-REQ content type
ID_PKINIT_DHKEY_DATA1.3.6.1.5.2.3.2DH key data content type
ID_PKINIT_RKEY_DATA1.3.6.1.5.2.3.3Reply key pack content type
ID_PKINIT_KDF1.3.6.1.5.2.3.6KDF algorithm arc (RFC 8636)
ID_PKINIT_KDF_AH_SHA11.3.6.1.5.2.3.6.1PKINIT KDF with SHA-1
ID_PKINIT_KDF_AH_SHA2561.3.6.1.5.2.3.6.2PKINIT KDF with SHA-256
ID_PKINIT_KDF_AH_SHA3841.3.6.1.5.2.3.6.4PKINIT KDF with SHA-384
ID_PKINIT_KDF_AH_SHA5121.3.6.1.5.2.3.6.3PKINIT KDF with SHA-512

Microsoft PKI OIDs

ConstantOIDWindows name
ID_MS_SAN_UPN1.3.6.1.4.1.311.20.2.3szOID_NT_PRINCIPAL_NAME — UPN in OtherName
ID_MS_CERTIFICATE_TEMPLATE_NAME1.3.6.1.4.1.311.20.2szOID_CERTIFICATE_TEMPLATE_NAME (v1)
ID_MS_CERTIFICATE_TEMPLATE1.3.6.1.4.1.311.21.7szOID_CERTIFICATE_TEMPLATE (v2)
ID_MS_KP_SMARTCARD_LOGON1.3.6.1.4.1.311.20.2.2szOID_MS_KP_SMARTCARD_LOGON EKU
ID_MS_NTDS_REPLICATION1.3.6.1.4.1.311.25.1szOID_NTDS_REPLICATION EKU

CMS content-type OIDs (RFC 5652)

ConstantOIDName
CMS_DATA1.2.840.113549.1.7.1id-data
CMS_SIGNED_DATA1.2.840.113549.1.7.2id-signedData
CMS_ENVELOPED_DATA1.2.840.113549.1.7.3id-envelopedData
CMS_DIGESTED_DATA1.2.840.113549.1.7.5id-digestedData
CMS_ENCRYPTED_DATA1.2.840.113549.1.7.6id-encryptedData
CMS_AUTH_DATA1.2.840.113549.1.9.16.1.2id-ct-authData
CMS_ORI1.2.840.113549.1.9.16.13OtherRecipientInfo arc (RFC 9629)
CMS_ORI_KEM1.2.840.113549.1.9.16.13.3KEMRecipientInfo (RFC 9629)

PKCS#9 attribute OIDs

ConstantOIDDescription
PKCS9_EMAIL_ADDRESS1.2.840.113549.1.9.1emailAddress
PKCS9_CONTENT_TYPE1.2.840.113549.1.9.3id-contentType
PKCS9_MESSAGE_DIGEST1.2.840.113549.1.9.4id-messageDigest
PKCS9_SIGNING_TIME1.2.840.113549.1.9.5id-signingTime
PKCS9_COUNTERSIGNATURE1.2.840.113549.1.9.6id-countersignature
PKCS9_CHALLENGE_PASSWORD1.2.840.113549.1.9.7id-challengePassword
PKCS9_EXTENSION_REQUEST1.2.840.113549.1.9.14id-extensionRequest
PKCS9_FRIENDLY_NAME1.2.840.113549.1.9.20id-friendlyName
PKCS9_LOCAL_KEY_ID1.2.840.113549.1.9.21id-localKeyId

OID helper functions

def identify_signature_algorithm(oid: ObjectIdentifier | str) -> str: ...
# Returns a display name such as "sha256WithRSAEncryption", "ecdsa-with-SHA256",
# "Ed25519", "ML-DSA-65", etc.  Returns "Other" for unknown OIDs.

def identify_public_key_algorithm(oid: ObjectIdentifier | str) -> str | None: ...
# Returns "RSA", "EC", "Ed25519", "ML-DSA-65", etc., or None for unknown OIDs.

def ec_curve_short_name(oid: ObjectIdentifier | str) -> str | None: ...
# Returns the ASN.1 short name, e.g. "prime256v1", "secp384r1".

def ec_curve_nist_name(oid: ObjectIdentifier | str) -> str | None: ...
# Returns the NIST name, e.g. "P-256", "P-384".  None for curves with no NIST name.

def ec_curve_key_bits(oid: ObjectIdentifier | str) -> int | None: ...
# Returns the field size in bits, e.g. 256, 384, 521.

def extension_oid_name(oid: ObjectIdentifier | str) -> str: ...
# Returns a display name, e.g. "X509v3 Subject Alternative Name".
# Returns the dotted-decimal string for unknown OIDs.

Usage

import synta.oids as oids

# Equality comparison against a string
assert oids.EC_PUBLIC_KEY == "1.2.840.10045.2.1"

# Use as a dict key (hashable)
lookup = {oids.SHA256: "SHA-256", oids.SHA384: "SHA-384"}
name = lookup.get(cert.signature_algorithm_oid, "unknown")

# OID helper functions
print(oids.identify_signature_algorithm(cert.signature_algorithm_oid))
print(oids.identify_public_key_algorithm(cert.public_key_algorithm_oid))

See also DN Attribute OIDs and PKCS#9 OIDs.

DN Attribute OIDs

synta.oids.attr provides OID constants for Distinguished Name (DN) attribute types.

import synta.oids.attr

Constants

All constants are ObjectIdentifier instances.

ConstantOIDRFC 4514 label
COMMON_NAME2.5.4.3CN
ORGANIZATION2.5.4.10O
ORG_UNIT2.5.4.11OU
COUNTRY2.5.4.6C
STATE2.5.4.8ST
LOCALITY2.5.4.7L
STREET2.5.4.9street
SERIAL_NUMBER2.5.4.5serialNumber
EMAIL_ADDRESS1.2.840.113549.1.9.1emailAddress
GIVEN_NAME2.5.4.42givenName
SURNAME2.5.4.4SN
TITLE2.5.4.12title
INITIALS2.5.4.43initials
ORG_IDENTIFIER2.5.4.97organizationIdentifier
USER_ID0.9.2342.19200300.100.1.1uid
DOMAIN_COMPONENT0.9.2342.19200300.100.1.25dc

Usage

import synta
import synta.oids.attr as attr

# Parse a certificate subject and look up specific attributes
cert = synta.Certificate.from_der(open("cert.der", "rb").read())
name_attrs = synta.parse_name_attrs(cert.subject_raw_der)

# name_attrs is a list of (dotted_oid_str, value_str) tuples
for oid_str, value in name_attrs:
    if oid_str == str(attr.COMMON_NAME):
        print(f"CN={value}")
    elif oid_str == str(attr.ORGANIZATION):
        print(f"O={value}")
    elif oid_str == str(attr.COUNTRY):
        print(f"C={value}")

# Use as hashable key
lookup = {attr.COMMON_NAME: "common name", attr.ORGANIZATION: "org"}

See also Well-known OIDs for the complete OID constant catalog.

PKCS#9 OID Constants

PKCS#9 attribute OIDs are available from two locations:

  • synta.pkcs9 — a dedicated module with the full PKCS#9 arc (13 constants)
  • synta.oids — the top-level OID catalog (9 of the most common constants)

From synta.oids

import synta.oids as oids

oids.PKCS9_EMAIL_ADDRESS        # 1.2.840.113549.1.9.1  — emailAddress
oids.PKCS9_CONTENT_TYPE         # 1.2.840.113549.1.9.3  — id-contentType
oids.PKCS9_MESSAGE_DIGEST       # 1.2.840.113549.1.9.4  — id-messageDigest
oids.PKCS9_SIGNING_TIME         # 1.2.840.113549.1.9.5  — id-signingTime
oids.PKCS9_COUNTERSIGNATURE     # 1.2.840.113549.1.9.6  — id-countersignature
oids.PKCS9_CHALLENGE_PASSWORD   # 1.2.840.113549.1.9.7  — id-challengePassword
oids.PKCS9_EXTENSION_REQUEST    # 1.2.840.113549.1.9.14 — id-extensionRequest
oids.PKCS9_FRIENDLY_NAME        # 1.2.840.113549.1.9.20 — id-friendlyName
oids.PKCS9_LOCAL_KEY_ID         # 1.2.840.113549.1.9.21 — id-localKeyId

From synta.pkcs9

synta.pkcs9 adds four more constants not in synta.oids:

import synta.pkcs9 as pkcs9

pkcs9.ID_PKCS_9                 # 1.2.840.113549.1.9    — id-pkcs-9 arc
pkcs9.ID_EMAIL_ADDRESS          # 1.2.840.113549.1.9.1  — emailAddress
pkcs9.ID_UNSTRUCTURED_NAME      # 1.2.840.113549.1.9.2  — unstructuredName
pkcs9.ID_CONTENT_TYPE           # 1.2.840.113549.1.9.3  — id-contentType
pkcs9.ID_MESSAGE_DIGEST         # 1.2.840.113549.1.9.4  — id-messageDigest
pkcs9.ID_SIGNING_TIME           # 1.2.840.113549.1.9.5  — id-signingTime
pkcs9.ID_COUNTERSIGNATURE       # 1.2.840.113549.1.9.6  — id-countersignature
pkcs9.ID_CHALLENGE_PASSWORD     # 1.2.840.113549.1.9.7  — id-challengePassword
pkcs9.ID_UNSTRUCTURED_ADDRESS   # 1.2.840.113549.1.9.8  — unstructuredAddress
pkcs9.ID_EXTENSION_REQUEST      # 1.2.840.113549.1.9.14 — id-extensionRequest
pkcs9.ID_SMIME                  # 1.2.840.113549.1.9.16 — id-smime arc
pkcs9.ID_FRIENDLY_NAME          # 1.2.840.113549.1.9.20 — id-friendlyName
pkcs9.ID_LOCAL_KEY_ID           # 1.2.840.113549.1.9.21 — id-localKeyId

Usage context

OIDTypical context
ID_CONTENT_TYPECMS SignedData signed attribute (RFC 5652 §11)
ID_MESSAGE_DIGESTCMS SignedData signed attribute (RFC 5652 §11)
ID_SIGNING_TIMECMS SignedData signed attribute (RFC 5652 §11)
ID_COUNTERSIGNATURECMS SignedData unsigned attribute
ID_EXTENSION_REQUESTPKCS#10 CSR attribute (RFC 2986 §5.4.2)
ID_CHALLENGE_PASSWORDPKCS#10 CSR attribute (RFC 2986 §5.4.1)
ID_FRIENDLY_NAMEPKCS#12 bag attribute (RFC 7292 §4.2)
ID_LOCAL_KEY_IDPKCS#12 bag attribute (RFC 7292 §4.2)

See also PKCS#9 Module for the protocol-level documentation and Well-known OIDs for the full OID catalog.

Performance

Measured with python/bench_certificate.py. Run after maturin develop --release or maturin build --release --interpreter python3 and installing the wheel. The benchmark uses Criterion’s Linear sampling mode (100 samples, linearly increasing iteration counts; see python/criterion_compat.py), so timings are directly comparable to the Rust Criterion benchmarks in synta-bench.

Note: Always verify the installed .so is a release build after maturin develop --release — the file size should be ≈1.4 MB (release) not ≈20 MB (debug). If in doubt, copy target/release/lib_synta.so manually to python/synta/_synta.abi3.so.

Parse-only (call only, no field access)

Two Python parse-only modes are benchmarked, corresponding to the two Rust variants:

Certificate setsynta lazy (from_der)synta_full eager (full_from_der)cryptography.x509Rust rust_shallowRust rust_typed
Traditional (~900 B, 5 certs)0.16 µs0.72–0.76 µs1.62–1.68 µs0.019–0.020 µs0.50–0.51 µs
ML-DSA-44 (3,992 B)0.17 µs0.74 µs1.55 µs0.020 µs0.54 µs
ML-DSA-65 (5,521 B)0.17 µs0.73 µs1.55 µs0.019 µs0.49 µs
ML-DSA-87 (7,479 B)0.17 µs0.73 µs1.54 µs0.020 µs0.49 µs

Comparison note: synta (from_der) and Rust rust_shallow perform the same 4-operation envelope scan. The ~0.14 µs gap between them (0.16 µs vs 0.019 µs) is pure PyO3 overhead: GIL acquisition, Py<PyBytes> setup, and constructing a PyCertificate struct with 17 OnceLock fields. synta_full (full_from_der) and Rust rust_typed both do a complete RFC 5280 decode; the ~0.23 µs gap is the same PyO3 overhead amortised over the full parse.

For parse-only workloads the honest comparison with cryptography.x509 (also lazy) is synta vs cryptography.x509~10× faster for traditional certs, ~9× faster for ML-DSA certs. For full-decode workloads the apples-to-apples comparison is synta_full vs cryptography.x509~2.3× faster.

from_der holds a Py<PyBytes> reference to the caller’s bytes object — no copy of the DER data. Parse time is flat across ML-DSA cert sizes: the large signature BIT STRING is not decoded at all during the shallow scan.

Parse + access all fields (cold cache — fresh cert each iteration)

Certificate setsynta (PyO3, 19 fields)cryptography.x509 (9 fields)
Traditional (~900 B, 5 certs)3.30–3.42 µs15.90–16.29 µs
ML-DSA-44 (3,992 B)3.71 µs13.41 µs
ML-DSA-65 (5,521 B)3.63 µs13.14 µs
ML-DSA-87 (7,479 B)3.71 µs13.38 µs

synta is ~4.8× faster than cryptography.x509 for traditional certs (parse+fields, 19 vs 9 fields). ML-DSA certs measure ~3.6× faster — the LAMPS WG test certs are simpler (fewer extensions) than the NIST PKITS certs, so cryptography.x509 processes fewer extension fields, reducing its parse+fields time. Requires cryptography ≥ 44.0 (FIPS 204) for ML-DSA public_key() decoding; older versions raise UnsupportedAlgorithm.

Field access only (warm cache — same cert, repeated access)

Certificate setsynta (PyO3, 19 fields)cryptography.x509 stockcryptography.x509 perf-opt
Traditional (~900 B, 5 certs)0.41–0.43 µs~14.8 µs0.91–0.95 µs
ML-DSA-44 (3,992 B)0.41 µs~11.2 µs2.21–2.28 µs
ML-DSA-65 (5,521 B)0.41 µs~11.2 µs2.21–2.28 µs
ML-DSA-87 (7,479 B)0.42 µs~11.1 µs2.21–2.28 µs

synta warm-cache access (~0.42 µs) is 19 clone_ref calls — essentially free per field.

Stock cryptography.x509 memoises only extensions (via PyOnceLock). The other 8 fields (subject, issuer, serial_number, not_valid_before_utc, not_valid_after_utc, signature, signature_hash_algorithm, public_key) are re-derived from the zero-copy in-memory ASN.1 structure on every Python access, each allocating a new Python object. Warm and cold times are therefore close (~11–15 µs).

cryptography PR #14441 adds OnceLock caching for issuer, subject, public_key, and signature_algorithm on Certificate, and caching on CertificationRequest, CertificateList, and OCSPResponse. This brings traditional warm access from ~14.8 µs down to 0.91–0.95 µs — a ~16× improvement. synta remains ~2.2× faster for traditional warm access and ~5× faster for ML-DSA (larger key objects have higher PyBytes copy cost even on the cached path). Parse-only and cold parse+fields times are unaffected by the caching changes.

PKCS#7 and PKCS#12 extraction

Measured with python/bench_pkcs.py. Benchmark IDs match the Rust Criterion IDs in synta-bench/benches/pkcs_formats.rs exactly (pkcs7/{synta,cryptography}/value, pkcs12/{synta,cryptography}/value).

Benchmark IDInputsyntacryptographySpeedup
pkcs7/…/amazon_roots1,848 B DER/BER, 2 certs0.82 µs (Rust) / 1.27 µs (Py)41.7 µs~33×
pkcs7/…/pem_isrg1,992 B PEM, 1 cert3.58 µs (Rust) / 4.14 µs (Py)32.1 µs~8×
pkcs12/…/unencrypted_3certs3,539 B, 3 certs1.10 µs (Rust) / 1.80 µs (Py)134 µs~75×
pkcs12/…/unencrypted_1cert_with_key756 B, 1 cert + key639 ns (Rust) / 0.96 µs (Py)

The unencrypted_1cert_with_key vector (cert-none-key-none.p12) uses a non-standard format that cryptography cannot parse; it is benchmarked as synta-only to exercise the key-bag-skipping code path.

The ~0.5–0.7 µs gap between Rust and Python times is the usual PyO3 overhead: GIL acquisition, PyBytes allocation for each returned certificate, and PyList construction. The pem_isrg Python time (4.14 µs) is higher than pure PKCS#7 parse (1.27 µs) because PEM decoding and base64 allocation are included in the timed loop.

cryptography comparison note: The amazon_roots DER benchmark triggers a UserWarning from cryptography — the file uses BER indefinite-length encoding (0x30 0x80…), which cryptography handles via an internal fallback with a deprecation warning. synta accepts BER transparently with no warning.

Architecture notes

Parse-only: synta.Certificate.from_der() holds a Py<PyBytes> strong reference to the caller’s bytes object (no copy). It performs only a 4-operation shallow envelope scan (outer SEQUENCE tag+length, TBSCertificate SEQUENCE tag+length) — equivalent to synta_certificate::validate_envelope() in Rust — and records the TBS byte range. The full recursive Certificate::decode() is deferred to the first getter call. synta.Certificate.full_from_der() performs the same shallow scan and then immediately triggers the full decode, so all 19 field caches are warm before any Python code accesses them.

Parse+fields: All 19 getters cache their Python object in an OnceLock<Py<T>>. String-returning getters (issuer, subject, signature_algorithm, signature_algorithm_oid, public_key_algorithm, public_key_algorithm_oid, not_before, not_after) store Py<PyString>. Bytes-returning getters (signature_value, public_key, tbs_bytes, issuer_raw_der, subject_raw_der) store Py<PyBytes>. Optional getters (signature_algorithm_params, public_key_algorithm_params, extensions_der) store Option<Py<PyBytes>>. to_der skips the lock entirely — direct clone_ref of the stored Py<PyBytes> that was passed to from_der.

#[pyclass(frozen)]: PyCertificate is declared frozen, which removes the 8-byte PyO3 borrow-tracking field (borrow_flag: Cell<isize>) from every instance and eliminates per-getter Cell::get/set calls. This gives a ~26% speedup on the warm-path field-access benchmark.

ML-DSA parse+fields: The first-call PyBytes copies of signature_value (2,420–4,627 bytes) and public_key (1,312–1,952 bytes) add ~0.3 µs over traditional certs (3.63–3.71 µs vs 3.30–3.42 µs). Subsequent accesses use clone_ref at ~0.41 µs total regardless of cert size.

For the full binding-layer comparison (Rust rust_shallow / rust_typed / rust_element / c_ffi / Python synta / synta_full / cryptography_x509, parse-only and parse+fields) see docs/performance.md.

See also Development for how to run benchmarks.

Development

Running Tests

# Build in development mode
python -m venv venv
source venv/bin/activate
maturin develop

# Run Python tests
python -m pytest tests/python/

# Or via the CI helper script
./contrib/ci/local-ci.sh python-test

Running Benchmarks

# Build release extension first (verify .so is ~1.4 MB, not ~20 MB debug)
maturin develop --release

# Certificate parsing and field-access benchmark (synta vs cryptography)
python python/bench_certificate.py

# Include per-field getter breakdown
python python/bench_certificate.py --per-field

# PKCS#7 / PKCS#12 certificate extraction benchmark (synta vs cryptography)
python python/bench_pkcs.py

# Save Criterion-compatible JSON alongside Rust benchmarks
# (default output: target/criterion/)
python python/bench_certificate.py --save-criterion
python python/bench_pkcs.py --save-criterion

# Custom output directory
python python/bench_certificate.py --save-criterion path/to/criterion

# Compare with a previous run using criterion-compare
# (first run writes new/, second run rotates new/→base/ then writes new/)
python python/bench_certificate.py --save-criterion
criterion-compare target/criterion

The benchmark certificate files are cloned by the Rust Criterion benchmarks on their first run. If tests/vectors/ is missing, run:

cargo bench -p synta-bench --bench bindings --features bench-bindings --no-run

The parse-only group prints three Python entries per certificate in the binding_comparison and binding_post_quantum groups:

binding_comparison/synta/cert_00_…       # from_der: shallow scan only (~0.15 µs)
binding_comparison/synta_full/cert_00_…  # full_from_der: complete RFC 5280 decode
binding_comparison/cryptography_x509/…   # envelope validation

The Rust benchmark (cargo bench --bench bindings) adds matching entries:

binding_comparison/rust_shallow/…        # validate_envelope: same 4-op scan as from_der
binding_comparison/rust_typed/…          # full Certificate::decode — apples-to-apples with synta_full

criterion_compat.py

python/criterion_compat.py is a standalone, import-ready module that provides the Criterion-compatible sampling and JSON-writing primitives. Import it directly from other benchmark or test scripts:

from criterion_compat import measure, save_criterion_files
from pathlib import Path

avg_us, iters, times_ns = measure(fn, warmup_s=3.0, measure_s=5.0)
print(f"avg: {avg_us:.2f} µs  ({sum(int(n) for n in iters)} iterations)")

save_criterion_files(
    Path("target/criterion"),
    group_id="my_group",
    function_id="my_fn",
    value_str="param_name",
    iters=iters,
    times_ns=times_ns,
)

See also Performance for benchmark result tables and Project Structure for the source tree layout.

Project Structure

synta-python/              # Python extension crate (cdylib → _synta)
├── Cargo.toml             # Declares crate-type = ["cdylib"]
└── src/
    ├── lib.rs             # #[pymodule] entry point, Encoding enum, pem_to_der/der_to_pem
    ├── types.rs           # ASN.1 primitive type wrappers (Integer, OID, BitString, …)
    ├── decoder.rs         # PyDecoder wrapper
    ├── encoder.rs         # PyEncoder wrapper
    ├── error.rs           # SyntaErr newtype + exception mapping
    ├── certificate/
    │   ├── mod.rs         # re-exports; PKI types live in synta-certificate
    │   ├── cert.rs        # PyCertificate
    │   ├── cert_builder.rs  # CertificateBuilder
    │   ├── csr_builder.rs   # CsrBuilder
    │   ├── name_builder.rs  # NameBuilder
    │   ├── pkix.rs        # CertificateList, OCSPResponse
    │   └── cms/           # synta.cms submodule (RFC 5652 / RFC 9629)
    │       ├── mod.rs     # register_cms_submodule, encode_element_opt helper
    │       ├── container.rs  # ContentInfo, IssuerAndSerialNumber
    │       ├── signed.rs  # SignedData, SignerInfo
    │       ├── enveloped.rs  # EnvelopedData, EncryptedData
    │       ├── digest.rs  # DigestedData, AuthenticatedData
    │       ├── kem.rs     # KEMRecipientInfo, CMSORIforKEMOtherInfo
    │       └── builder.rs # EnvelopedDataBuilder
    ├── ext_builders.rs    # synta.ext submodule: basic_constraints, key_usage, SKI/AKI, SAN/AIA/EKU builders
    ├── crypto.rs          # synta symmetric-crypto primitives (HMAC, PBKDF2, AES, Fernet, OTP)
    ├── crypto_keys.rs     # PublicKey, PrivateKey (RSA, EC, EdDSA)
    └── krb5.rs            # synta.krb5 submodule: Krb5PrincipalName, PKINIT classes

synta-certificate/src/python.rs  # ObjectIdentifier, Certificate, CertificationRequest,
                                 # CertificateList, OCSPResponse, PKCS loaders,
                                 # synta.oids / synta.oids.attr submodules

synta-krb5/src/python.rs         # EncryptionKey, Checksum, KDFAlgorithmId,
                                 # IssuerAndSerialNumber, ExternalPrincipalIdentifier,
                                 # PKAuthenticator, AuthPack, PaPkAsReq,
                                 # DHRepInfo, KDCDHKeyInfo, ReplyKeyPack, PaPkAsRep

python/                    # Python package source (installed by maturin)
├── bench_certificate.py   # Certificate parsing benchmark (synta vs cryptography)
├── bench_x509.py          # Port of cryptography's test_x509.py benchmarks
├── bench_pkcs.py          # PKCS#7 / PKCS#12 extraction benchmark (synta vs cryptography)
├── criterion_compat.py    # Criterion-compatible sampling + JSON output (importable)
└── synta/
    ├── __init__.py        # Package exports (Certificate, PKCS loaders, pem_to_der, …)
    └── py.typed           # PEP 561 marker

pyproject.toml             # Maturin configuration (manifest-path → synta-python)

Key source file roles

Source fileContents
src/lib.rsModule registration, Encoding enum, pem_to_der, der_to_pem, top-level exports
src/decoder.rsDecoder class wrapping synta::Decoder; includes decode_any_str()
src/encoder.rsEncoder class wrapping synta::Encoder
src/types.rsPython wrappers for all ASN.1 primitive types
src/error.rsSyntaError Python exception
src/certificate.rsPKI types: ObjectIdentifier, Certificate, CertificationRequest, CertificateList, OCSPResponse, PKCS loaders, synta.oids / synta.oids.attr submodules
src/pkinit.rsPKINIT classes: EncryptionKey, Checksum, KDFAlgorithmId, PKAuthenticator, AuthPack, PaPkAsReq/Rep, etc.
src/krb5.rssynta.krb5 submodule registration: Krb5PrincipalName, PKINIT classes, NT_* constants
src/x509_verification.rssynta.x509 submodule: TrustStore, VerificationPolicy, X509VerificationError, verify functions
src/certificate/pkixalgs.rssynta.pkixalgs submodule: DssParms, DssSigValue, EcdsaSigValue, ECParameters, OID constants
src/certificate/ac.rssynta.ac submodule: AttributeCertificate, RFC 5755 OID constants
src/certificate/crmf.rssynta.crmf submodule: CertReqMessages, CertReqMsg, registration-control OID constants
src/certificate/cmp.rssynta.cmp submodule: CMPMessage, MAC algorithm and key-purpose OID constants

All PyO3 bindings live directly in synta-python. The synta-certificate and synta-krb5 library crates have no PyO3 dependency; their types are wrapped in src/certificate.rs and src/pkinit.rs respectively.

See also Cargo Features and Development.

Cargo Features

The synta-python crate exposes the following Cargo features:

FeatureDefaultDescription
extension-moduleyes (maturin)Required by PyO3 for native extension module builds; set automatically by maturin
abi3-py38yes (maturin)Targets the stable Python 3.8 ABI; compatible with CPython 3.8–3.14+
opensslyesOpenSSL-backed crypto: PKCS#12 decryption (load_pkcs12_certificates with password, create_pkcs12 with password), CMS EnvelopedData.create / EnvelopedDataBuilder.build, EncryptedData.create / decrypt, PrivateKey.to_pkcs8_encrypted / from_pkcs8_encrypted, synta.x509 chain verification
deprecated-pkcs12-algorithmsnoEnables legacy PKCS#12 decryption algorithms (3DES, RC2) — requires openssl

Crate dependencies

CrateRole
syntaCore ASN.1 encoder/decoder
synta-certificateX.509 PKI types + OID constants + OpenSSL signature verifier
synta-krb5Kerberos V5 / PKINIT types
synta-x509-verificationRFC 5280 / CABF certificate chain validation (synta.x509)
pyo3 0.26Python/Rust interop (abi3-py38 feature)
openssl 0.10OpenSSL bindings for signature verification (synta.x509, synta.cms)

Building with optional features

# Default build (includes openssl feature)
maturin develop

# Build without openssl (disables PKCS#12 password support, x509 verification, CMS crypto)
maturin develop --no-default-features --features extension-module,abi3-py38

# Build with legacy PKCS#12 algorithm support
maturin develop --features deprecated-pkcs12-algorithms

See Installation for standard build instructions and Development for test and benchmark workflows.

Example Programs

Twenty-eight runnable programs in examples/ exercise every binding documented in this guide. Each program is self-contained and runs with:

uv run python3 examples/example_<name>.py

from the repository root (after maturin develop).

1. example_pem_helpers.py — PEM/DER conversion

Bindings: pem_to_der, der_to_pem, Certificate.from_pem, Certificate.to_pem, CertificationRequest.from_pem, CertificationRequest.to_pem, CertificateList.from_pem, CertificateList.to_pem.

  • Round-trip a single PEM certificate block through pem_to_der / der_to_pem (pem_to_der always returns list[bytes]).
  • Show pem_to_der with a multi-block PEM chain.
  • Use Certificate.from_pem on a single block and on a two-certificate chain.
  • Use Certificate.to_pem on a single cert and a list of certs.

2. example_certificate_fields.py — All Certificate properties

Bindings: Certificate.from_der, Certificate.full_from_der, all Certificate properties (serial_number, version, issuer, issuer_raw_der, subject, subject_raw_der, not_before, not_after, signature_algorithm, signature_algorithm_oid, signature_algorithm_params, signature_value, public_key_algorithm, public_key_algorithm_oid, public_key_algorithm_params, public_key, tbs_bytes), and Certificate.to_der() method.

  • Parse the same certificate with from_der (lazy) and full_from_der (eager).
  • Print every property with its Python type.
  • Verify to_der() round-trip (re-parse the returned bytes).
  • Show signature_algorithm_params is None for Ed25519 and non-None for RSA.

3. example_certificate_extensions.py — Extension access and SAN parsing

Bindings: Certificate.subject_alt_names, Certificate.extensions_der, Certificate.get_extension_value_der, synta.general_name tag constants, synta.parse_general_names, synta.parse_name_attrs, synta.oids.SUBJECT_ALT_NAME, synta.oids.BASIC_CONSTRAINTS, synta.oids.SUBJECT_KEY_IDENTIFIER, Decoder.decode_sequence, Decoder.peek_tag, Decoder.decode_implicit_tag, Decoder.decode_explicit_tag, Decoder.remaining_bytes, Decoder.decode_raw_tlv, Decoder.is_empty.

  • Call cert.subject_alt_names() for high-level SAN access; dispatch on synta.general_name constants (gn.DNS_NAME, gn.IP_ADDRESS, etc.); use ipaddress.ip_address(content) for IP rendering.
  • Verify that a certificate without a SAN extension returns an empty list.
  • Access extensions_der and iterate the raw extension TLVs.
  • Look up individual extensions by get_extension_value_der.
  • Parse SAN entries manually with peek_tag / decode_implicit_tag as a lower-level alternative to subject_alt_names().
  • Demonstrate peek_tag for CHOICE dispatch; remaining_bytes for primitive implicit values.
  • Look up BasicConstraints and decode the cA BOOLEAN.

4. example_certificate_pyca.py — PyCA interoperability

Bindings: Certificate.to_pyca, Certificate.from_pyca.

  • Parse a DER certificate with synta, then call .to_pyca() for cryptographic operations.
  • Load a PyCA certificate, convert to synta with from_pyca(), and compare fields.
  • Show ImportError message when cryptography is absent (caught and printed).

5. example_csr.py — PKCS#10 CSR parsing

Bindings: CertificationRequest.from_der, CertificationRequest.from_pem, CertificationRequest.to_pem, and all CertificationRequest properties.

  • Parse a CSR from DER and print every field.
  • Round-trip through PEM with from_pem / to_pem.
  • Verify subject_raw_der by re-decoding it as a raw Name SEQUENCE.

6. example_crl.py — CRL parsing

Bindings: CertificateList.from_der, CertificateList.from_pem, CertificateList.to_pem, and all CertificateList properties.

  • Parse a CRL and print issuer, dates, algorithm, revoked count.
  • Show next_update is None for CRLs that omit the field.
  • Round-trip through to_pem / from_pem.

7. example_ocsp.py — OCSP response parsing

Bindings: OCSPResponse.from_der, OCSPResponse.from_pem, OCSPResponse.to_pem, and all OCSPResponse properties.

  • Parse a successful OCSP response; print status, response_type_oid, length of response_bytes.
  • Parse a non-successful response (e.g. tryLater); confirm response_bytes is None.
  • Round-trip through to_pem / from_pem.

8. example_pkcs7.py — PKCS#7 certificate bundles

Bindings: load_der_pkcs7_certificates, load_pem_pkcs7_certificates.

  • Load a .p7b file and print the subject of each extracted certificate.
  • Demonstrate PEM-encoded PKCS#7 with load_pem_pkcs7_certificates.
  • Show ValueError for non-PKCS#7 input.

9. example_pkcs12.py — PKCS#12 archive parsing and creation

Bindings: load_pkcs12_certificates, load_pkcs12_keys, load_pkcs12, create_pkcs12.

  • Parse a password-less PKCS#12 file; print subjects of extracted certificates.
  • Extract private keys using load_pkcs12_keys; show raw PKCS#8 DER length.
  • Extract both with load_pkcs12; show the (certs, keys) tuple.
  • Parse an AES-256-CBC encrypted PKCS#12 with a password.
  • Show ValueError for wrong password (encrypted case, with openssl feature).
  • Build a PFX from extracted certificates using create_pkcs12 without a password.
  • Build a password-protected PFX with create_pkcs12 (requires openssl feature).
  • Build a cert+key PFX bundle.
  • Roundtrip: build an archive then parse it back and verify the extracted certificates.

10. example_pki_blocks.py — Format-agnostic PKI reader

Bindings: read_pki_blocks.

  • Pass PEM, DER, PKCS#7, and PKCS#12 bytes to read_pki_blocks and print the (label, len(der)) tuples returned.
  • Show PKCS#12 password handling.

11. example_objectidentifier.py — ObjectIdentifier constructors and operations

Bindings: ObjectIdentifier(str), ObjectIdentifier.from_components, ObjectIdentifier.from_der_value, ObjectIdentifier.components, ObjectIdentifier.__eq__, ObjectIdentifier.__hash__, Decoder.decode_oid, Encoder.encode_oid, Encoder.encode_oid_object.

  • Construct OIDs via all three constructors; verify they compare equal.
  • Show __eq__ working against a dotted string.
  • Use OIDs as dict keys (demonstrate hashability).
  • Round-trip through encode_oid / decode_oid.
  • Show from_der_value with the raw content bytes from an implicit-tag context.

12. example_oids_catalog.pysynta.oids constant groups

Bindings: every constant in synta.oids and synta.oids.attr, plus the helper functions identify_signature_algorithm, identify_public_key_algorithm.

  • Print algorithm OIDs (RSA, EC, EdDSA, ML-DSA, ML-KEM).
  • Print hash OIDs (SHA-2, SHA-3).
  • Print SLH-DSA OIDs.
  • Print prefix OIDs and demonstrate components()-based prefix matching.
  • Print X.509v3 extension OIDs.
  • Print EKU OIDs.
  • Print PKINIT OIDs.
  • Print MS PKI OIDs.
  • Print all nine PKCS#9 attribute OIDs (PKCS9_EMAIL_ADDRESS, PKCS9_CONTENT_TYPE, PKCS9_MESSAGE_DIGEST, PKCS9_SIGNING_TIME, PKCS9_COUNTERSIGNATURE, PKCS9_CHALLENGE_PASSWORD, PKCS9_EXTENSION_REQUEST, PKCS9_FRIENDLY_NAME, PKCS9_LOCAL_KEY_ID).
  • Print every synta.oids.attr DN attribute OID with its RFC 4514 label.

13. example_time_types.py — UtcTime and GeneralizedTime

Bindings: UtcTime(…), UtcTime properties (year, month, day, hour, minute, second), GeneralizedTime(…), GeneralizedTime properties, Encoder.encode_utc_time, Encoder.encode_utc_time_object, Encoder.encode_generalized_time, Encoder.encode_generalized_time_object, Decoder.decode_utc_time, Decoder.decode_generalized_time.

  • Construct both time types and inspect every property.
  • Round-trip each through encode / decode; verify string representations.
  • Show millisecond sub-second precision on GeneralizedTime.
  • Show ValueError for out-of-range UTCTime year (< 1950 or > 2049).
  • Demonstrate encode_utc_time_object and encode_generalized_time_object variants.

14. example_integer_advanced.py — Integer edge cases and bigint

Bindings: Integer(int), Integer.from_bytes, Integer.from_u64, Integer.to_int, Integer.to_i128, Integer.to_bytes, Encoder.encode_integer (bigint path), Encoder.encode_integer_object.

  • Construct integers via all three constructors.
  • Show to_int() succeeds for small values; raises OverflowError for 20-byte serials.
  • Use to_i128() for up to 16-byte values (i128 max = 2¹²⁷−1); show OverflowError beyond that.
  • Use to_bytes() for arbitrary-precision big-endian representation.
  • Encode a 20-byte certificate serial number via encode_integer(large_python_int).
  • Verify encode_integer_object round-trip.

15. example_string_types_advanced.py — Alternative string constructors

Bindings: TeletexString.from_latin1, TeletexString.from_str, GeneralString.from_ascii, GeneralString.from_str, UniversalString.from_bytes, BmpString.from_bytes, plus to_bytes() / as_str() on each.

  • Construct TeletexString from bytes and Latin-1 strings; GeneralString from bytes and ASCII strings.
  • Construct UniversalString and BmpString from raw UCS-4/UCS-2 byte buffers.
  • Show ValueError when non-BMP code points are passed to BmpString.
  • Round-trip each through the encoder / decoder.

16. example_decoder_advanced.py — Advanced Decoder operations

Bindings: Decoder.decode_set, Decoder.peek_tag, Decoder.decode_raw_tlv, Decoder.remaining_bytes, Decoder.decode_implicit_tag, Decoder.position, Decoder.remaining, Decoder.is_empty, Decoder.decode_any_str.

  • Build a hand-crafted SET and decode it with decode_set.
  • Use peek_tag in a loop to drive CHOICE dispatch.
  • Capture unknown elements with decode_raw_tlv.
  • Use decode_implicit_tag + remaining_bytes to decode a primitive implicit type (e.g. [2] IMPLICIT IA5String for a dNSName GeneralName).
  • Show position() and remaining() advancing through multi-element input.
  • Use decode_any_str() to decode a mixed-string SEQUENCE without tag inspection.

17. example_encoder_advanced.py — Advanced Encoder operations

Bindings: Encoder.encode_implicit_tag, Encoder.encode_set, Encoder.encode_explicit_tag, Encoder.encode_sequence, and all _object variants not demonstrated elsewhere.

  • Build a SET containing two PrintableStrings using encode_set.
  • Encode an implicit context tag [2] IMPLICIT IA5String (dNSName).
  • Encode a nested structure: SEQUENCE { [0] EXPLICIT SEQUENCE { INTEGER } }.
  • Demonstrate every _object encode variant with a typed object sourced from a decode.
  • Show ValueError for unknown tag class strings.

18. example_krb5_principal.py — Kerberos principal names

Bindings: synta.krb5.Krb5PrincipalName, synta.krb5.KRB5_PRINCIPAL_NAME_OID, and every NT_* constant.

  • Construct Krb5PrincipalName instances for each name type (NT_PRINCIPAL, NT_SRV_INST, NT_SRV_HST, NT_ENTERPRISE, NT_WELLKNOWN).
  • Encode each with to_der() and decode back with from_der().
  • Verify realm, name_type, and components survive the round-trip.
  • Show __eq__ and __repr__.
  • Print KRB5_PRINCIPAL_NAME_OID and demonstrate it matches synta.oids.ID_PKINIT_SAN.
  • Show ValueError for non-ASCII realm or component.

19. example_krb5_pkinit.py — PKINIT protocol classes

Bindings: EncryptionKey, Checksum, KDFAlgorithmId, IssuerAndSerialNumber, ExternalPrincipalIdentifier, PKAuthenticator, AuthPack, PaPkAsReq, DHRepInfo, KDCDHKeyInfo, ReplyKeyPack, PaPkAsRep.

For each class: parse hand-crafted DER bytes with from_der, access every property, and print a summary of key values.

20. example_error_handling.py — Exception catalogue

Bindings: synta.SyntaError, ValueError, OverflowError, EOFError.

  • Inspect SyntaError as a module exception class (MRO, issubclass check).
  • Demonstrate EOFError from empty input, tag-only input, and truncated value in Decoder.
  • Demonstrate ValueError from tag mismatch (decoding BOOLEAN as INTEGER).
  • Demonstrate ValueError from DER constraint violations (non-canonical BOOLEAN, non-minimal INTEGER).
  • Demonstrate OverflowError from Integer.to_int() and to_i128() on values exceeding the target type.
  • Demonstrate ValueError from constructor validation: BmpString with a non-BMP character, GeneralizedTime with an invalid month, ObjectIdentifier with a malformed string, Krb5PrincipalName with a non-ASCII realm, GeneralString.from_ascii with a non-ASCII character.

21. example_cms_encrypted_data.py — CMS EncryptedData round-trip

Bindings: synta.cms.EncryptedData, synta.cms.ID_AES128_CBC, synta.cms.ID_AES192_CBC, synta.cms.ID_AES256_CBC.

Requires the openssl Cargo feature (maturin develop --features openssl).

  • Create an EncryptedData with EncryptedData.create (AES-128-CBC) and inspect all properties: version, content_type, content_encryption_algorithm_oid, content_encryption_algorithm_params (IV extraction), encrypted_content.
  • Decrypt → replace text in plaintext → re-encrypt with a fresh random IV; verify the round-trip with to_der() / from_der().
  • Encrypt with each of AES-128/192/256-CBC using ID_AES128_CBC, ID_AES192_CBC, ID_AES256_CBC; confirm random IV differs across calls.
  • Verify synta-produced ciphertext with openssl enc -d (synta → openssl interop).
  • Inspect the EncryptedData DER structure with openssl asn1parse.
  • Encrypt raw bytes with openssl enc -e, wrap in an EncryptedData DER by hand, parse with from_der, and decrypt with synta (openssl → synta interop).

22. example_x509_verify.py — X.509 certificate chain verification

Bindings: synta.x509.TrustStore, synta.x509.VerificationPolicy, synta.x509.verify_server_certificate, synta.x509.verify_client_certificate, synta.x509.X509VerificationError.

Requires the openssl Cargo feature (maturin develop --features openssl).

  • Build a self-signed root CA DER in memory with openssl req -x509 and load it into a TrustStore; print store.len and repr(store).
  • Verify a leaf certificate signed by that root with verify_server_certificate; print the chain length and each certificate’s subject with synta.Certificate.from_der.
  • Demonstrate VerificationPolicy variants: single name, multi-name any, multi-name all, fixed validation_time, max_chain_depth, profile="rfc5280".
  • Demonstrate client certificate verification with verify_client_certificate.
  • Show that X509VerificationError is raised when the trust store does not contain the issuer, the server name does not match the SAN, and the validation time is outside the validity window.

23. example_general_name.py — Typed GeneralName API

Bindings: synta.general_name.DNSName, synta.general_name.IPAddress, synta.general_name.RFC822Name, synta.general_name.OtherName, synta.general_name.UniformResourceIdentifier, synta.parse_general_names.

  • Call cert.subject_alt_names() on a certificate with four SANs (two dNSName, one iPAddress, one rfc822Name); dispatch by isinstance.
  • Use cert.general_names(oid) to retrieve SANs by extension OID.
  • Use synta.parse_general_names(san_der) for low-level tag/bytes access.
  • Demonstrate integer tag constants (gn.DNS_NAME, gn.IP_ADDRESS, etc.).

24. example_pkixalgs.py — RFC 3279 algorithm parameter types

Bindings: synta.pkixalgs.DssParms, synta.pkixalgs.ECParameters, synta.pkixalgs.EcdsaSigValue, synta.pkixalgs.DssSigValue and OID constants.

  • Parse DssParms (DSA domain parameters P/Q/G) from hand-crafted DER; verify round-trip.
  • Parse ECParameters in namedCurve form; access arm and named_curve_oid.
  • Parse EcdsaSigValue (r and s components of an ECDSA signature).
  • Use OID constants: ID_DSA, ID_EC_PUBLIC_KEY, ECDSA_WITH_SHA256, PRIME256V1.

25. example_ac.py — RFC 5755 Attribute Certificates

Bindings: synta.ac.AttributeCertificate and related OID constants.

  • Build a minimal RFC 5755 v2 Attribute Certificate from scratch in DER.
  • Access all eight AttributeCertificate properties: version, holder, issuer, signature_algorithm, serial_number, not_before, not_after, attributes.
  • Demonstrate OID constants: ID_AT_ROLE, ID_PE_AC_AUDIT_IDENTITY, ID_CE_SUBJECT_DIRECTORY_ATTRIBUTES.

26. example_ms_pki.py — Microsoft PKI (AD CS) extension types

Bindings: synta.ms_pki.MSCSTemplateV2, synta.ms_pki.RequestClientInfo and OID constants.

  • Parse MSCSTemplateV2 with major version only and with both versions; verify template_id, template_major_version, template_minor_version, and to_der() round-trip.
  • Parse RequestClientInfo with all four fields present, with partial fields, and from an empty SEQUENCE.
  • Print all nineteen OID constants with their dot-notation values.

27. example_spnego.py — SPNEGO negotiation tokens (RFC 4178)

Bindings: synta.spnego.NegTokenInit, synta.spnego.NegTokenResp, synta.spnego.NegotiationToken, NEG_STATE_* constants, SPNEGO_OID.

DER encoding note: GssapiSpnego.asn1 uses DEFINITIONS IMPLICIT TAGS. The example builds all test vectors using IMPLICIT encoding helpers.

  • Parse NegTokenInit with mech_types (two OIDs) and mech_token; verify each property and confirm req_flags/mech_list_mic are absent.
  • Parse an empty NegTokenInit SEQUENCE; verify mech_types == [].
  • Parse NegotiationToken in the negTokenInit CHOICE arm (0xa0) and the negTokenResp arm (0xa1).
  • Parse a GSSAPI-wrapped token (0x60 APPLICATION tag) and verify that from_der strips the OID prefix automatically.
  • Parse NegTokenResp with neg_state 3 (NEG_STATE_REQUEST_MIC) and a response_token; verify supported_mech and mech_list_mic.
  • Verify all four NEG_STATE_* integer constants and SPNEGO_OID.

28. example_mtc.py — Merkle Tree Certificates

Bindings: synta.mtc.ProofNode, synta.mtc.Subtree, synta.mtc.SubtreeProof, synta.mtc.InclusionProof, synta.mtc.LogID, synta.mtc.CosignerID, synta.mtc.Checkpoint, synta.mtc.SubtreeSignature, synta.mtc.MerkleTreeCertEntry, synta.mtc.LandmarkID.

  • Parse ProofNode (left and right variants); verify is_left and hash.
  • Parse Subtree; verify start, end, and value.
  • Parse SubtreeProof with both subtree lists; verify element count.
  • Parse InclusionProof; verify log_entry_index, tree_size, and path length.
  • Parse LogID and CosignerID; verify hash_alg_oid and public_key_der.
  • Parse Checkpoint; verify all five properties.
  • Parse SubtreeSignature; verify cosigner_id, tree_size, root_hash.
  • Parse MerkleTreeCertEntry as a LandmarkCertificate CHOICE arm; verify landmark_id with tree_size.
  • Verify repr() output for LandmarkID.