synta 0.1.9

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# 33. `example_acme_rfc8737.py` — ACME TLS-ALPN-01 Extension (RFC 8737)

[← Example index](index.md) · [`example_acme_rfc8737.py` on Codeberg](https://codeberg.org/abbra/synta/src/branch/main/examples/example_acme_rfc8737.py)

Bindings: `synta.acme.AcmeAuthorization`, `synta.acme.ID_PE_ACME_IDENTIFIER`,
`synta.oids.PE_ACME_IDENTIFIER`, `synta.digest`.

- Compute the RFC 8737 §3 key authorization digest: concatenate the ACME
  token and the base64url-encoded JWK thumbprint, then SHA-256 the result
  using `synta.digest("sha256", ...)`.
- Construct an `AcmeAuthorization` from the 32-byte digest; verify
  `hex_digest`, `len`, and `bytes()` conversion.
- Perform a DER encoding and decoding round-trip via `to_der()` /
  `from_der()`; confirm the OCTET STRING TLV is 34 bytes (2 header + 32 value).
- Look up the `id-pe-acmeIdentifier` OID (1.3.6.1.5.5.7.1.31) via both
  `acme.ID_PE_ACME_IDENTIFIER` and `oids.PE_ACME_IDENTIFIER`.
- Show how the extension value is embedded in a validation certificate per
  RFC 8737 §3: the `extnValue` OCTET STRING wraps `auth.to_der()`; the
  extension MUST be marked critical and the certificate MUST NOT be served
  outside of acme-tls/1 ALPN negotiation.

## Source

```python
#!/usr/bin/env python3
"""
Example: ACME TLS-ALPN-01 Extension (RFC 8737)

Demonstrates:
  - Computing the key authorization digest as specified in RFC 8737 §3
  - Constructing an AcmeAuthorization value from a 32-byte SHA-256 digest
  - DER encoding and decoding round-trip
  - Looking up the id-pe-acmeIdentifier OID (1.3.6.1.5.5.7.1.31)
  - How the extension value would sit inside a validation certificate

Run with:
    python3 examples/example_acme_rfc8737.py

Import note: this example uses only synta — no external crypto library is needed.
The synta.digest() function provides SHA-256 hashing.
"""

from __future__ import annotations

import synta
import synta.acme as acme
import synta.oids as oids


def section(title: str) -> None:
    print(f"\n{'─' * 60}\n{title}\n{'─' * 60}")


# ── Step 1: Simulate ACME protocol inputs ─────────────────────────────────────
#
# RFC 8737 §3 defines the key authorization string as:
#   keyAuthorization = token || '.' || base64url(thumbprint(accountKey))
#
# For demonstration purposes we use fixed ASCII strings that mimic the
# format of a real ACME token and base64url-encoded JWK thumbprint.  In a
# real ACME client, `token` comes from the challenge object returned by the
# ACME server, and `thumbprint` is the SHA-256 of the canonicalized JWK.

# 43-character URL-safe base64 token (no padding, as produced by ACME servers)
DEMO_TOKEN = "LoqXcYV8q5ONbJQxbmR7SCkF3nLudld73GnNwqiTvjU"

# Simulated base64url-encoded JWK thumbprint (43 characters, no padding)
DEMO_THUMBPRINT_B64URL = "XNpDJCeS4be1RGe8XCfv_BnFjFm8Hm-u5IfmE3QUIA"


def demo_compute_digest() -> bytes:
    section("Step 1 — Compute key authorization digest (RFC 8737 §3)")

    # RFC 8737 §3 key authorization construction:
    #   keyAuthorization = token || '.' || base64url(SHA-256(accountKey JWK))
    # The thumbprint MUST use base64url without padding per RFC 7638.
    key_auth = f"{DEMO_TOKEN}.{DEMO_THUMBPRINT_B64URL}"
    print(f"  keyAuthorization string : {key_auth!r}")
    print(f"  length                  : {len(key_auth)} characters")

    # SHA-256(keyAuthorization) — the 32-byte value that goes into the extension
    digest_bytes = synta.digest("sha256", key_auth.encode("ascii"))
    print(f"  SHA-256 digest (hex)    : {digest_bytes.hex()}")
    print(f"  digest length           : {len(digest_bytes)} bytes")

    assert len(digest_bytes) == 32, "SHA-256 digest must be 32 bytes"
    return digest_bytes


def demo_construct_authorization(digest_bytes: bytes) -> acme.AcmeAuthorization:
    section("Step 2 — Construct AcmeAuthorization from digest")

    auth = acme.AcmeAuthorization(digest_bytes)
    print(f"  repr                    : {repr(auth)}")
    print(f"  hex_digest              : {auth.hex_digest}")
    print(f"  len                     : {len(auth)} bytes")

    raw = bytes(auth)
    assert len(raw) == 32
    assert raw == digest_bytes
    print(f"  bytes(auth) matches digest input : OK")

    return auth


def demo_der_round_trip(auth: acme.AcmeAuthorization) -> None:
    section("Step 3 — DER encoding and decoding round-trip")

    # to_der() returns the OCTET STRING TLV: tag 0x04, length 0x20, then 32 bytes
    der = auth.to_der()
    print(f"  DER (hex)               : {der.hex()}")
    print(f"  DER length              : {len(der)} bytes  (= 2 bytes TLV + 32 bytes value)")

    assert len(der) == 34

    # Parse the DER back into an AcmeAuthorization
    auth2 = acme.AcmeAuthorization.from_der(der)
    print(f"  from_der repr           : {repr(auth2)}")
    assert auth == auth2, "round-trip produced different value"
    print(f"  Round-trip              : OK")

    # from_der also accepts raw OctetString DER written by the encoder directly
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_octet_string(bytes(auth))
    raw_der = enc.finish()
    auth3 = acme.AcmeAuthorization.from_der(raw_der)
    assert auth3 == auth
    print(f"  from_der via Encoder    : OK")


def demo_oid_constant() -> None:
    section("Step 4 — OID lookup: id-pe-acmeIdentifier")

    # synta.acme exports the OID as an ObjectIdentifier
    oid = acme.ID_PE_ACME_IDENTIFIER
    print(f"  acme.ID_PE_ACME_IDENTIFIER : {oid}")
    assert str(oid) == "1.3.6.1.5.5.7.1.31"

    # The same OID is available in synta.oids under PE_ACME_IDENTIFIER
    oid2 = oids.PE_ACME_IDENTIFIER
    print(f"  oids.PE_ACME_IDENTIFIER    : {oid2}")
    assert str(oid2) == "1.3.6.1.5.5.7.1.31"

    print(f"  OID arc: id-pkix.id-pe.31 (id-pe-acmeIdentifier)")
    print(f"  OID assertion             : OK")


def demo_extension_note(auth: "acme.AcmeAuthorization") -> None:
    section("Step 5 — How to embed in a validation certificate (note)")

    # RFC 8737 §3 specifies that the ACME validation certificate MUST:
    #  (a) include a dNSName SAN equal to the domain being validated,
    #  (b) include the acmeIdentifier extension marked critical,
    #  (c) set extnValue to the DER encoding of Authorization.
    #
    # The extension's extnValue OCTET STRING wraps the Authorization DER,
    # so the actual bytes placed in the certificate are auth.to_der().
    ext_value_der = auth.to_der()

    print(
        "  Per RFC 8737 §3, the validation certificate must carry:\n"
        "    - SAN: exactly one dNSName equal to the domain under validation\n"
        "    - Extension OID: 1.3.6.1.5.5.7.1.31 (id-pe-acmeIdentifier)\n"
        "    - Critical: TRUE (must be marked critical)\n"
        "    - extnValue: the DER encoding of the Authorization OCTET STRING"
    )
    print(f"\n  extnValue content (hex) : {ext_value_der.hex()}")
    print(f"  extnValue length        : {len(ext_value_der)} bytes")

    # Show how to decode the extension value from a parsed certificate:
    # (assuming cert.get_extension_value_der("1.3.6.1.5.5.7.1.31") returns it)
    recovered = acme.AcmeAuthorization.from_der(ext_value_der)
    assert recovered == auth
    print(f"  Decode from cert extnValue : OK")
    print(
        "\n  Note: the certificate MUST NOT be served outside of acme-tls/1\n"
        "  ALPN negotiation (RFC 8737 §3 / RFC 7301)."
    )


def main() -> None:
    print("=== ACME TLS-ALPN-01 Extension Example (RFC 8737) ===")

    digest_bytes = demo_compute_digest()
    auth = demo_construct_authorization(digest_bytes)
    demo_der_round_trip(auth)
    demo_oid_constant()
    demo_extension_note(auth)

    print("\nAll steps completed successfully.")


if __name__ == "__main__":
    main()
```