Skip to content

symfonic.services.switching.keys

keys

KEY / LIB-EA — the envelope signing-key lifecycle, in both deployment modes.

Operated platforms provision keys from a secret manager; library mode derives one locally from an adopter-supplied secret. Both end up in the same keyset with the same three states, so verification behaves identically — library mode is not a reduced security tier.

Keyset

Keyset()

key_id → (material, state), with exactly one active key.

Key material never leaves this object: there is no accessor that returns it, __repr__ names only ids and states, and signing happens here rather than in the caller. A getter would be convenient exactly once and then live forever in a log line.

Source code in src/symfonic/services/switching/keys.py
def __init__(self) -> None:
    self._keys: dict[str, tuple[bytes, KeyState]] = {}
    self._available = True

retire

retire(key_id: str) -> None

KEY-4 — after the overlap window; its envelopes now verify-fail.

Source code in src/symfonic/services/switching/keys.py
def retire(self, key_id: str) -> None:
    """KEY-4 — after the overlap window; its envelopes now verify-fail."""
    material, _ = self._require(key_id)
    self._keys[key_id] = (material, KeyState.RETIRED)

rotate

rotate(key_id: str, *, secret: bytes) -> None

KEY-3 — the new key becomes active; the previous one verifies only.

Source code in src/symfonic/services/switching/keys.py
def rotate(self, key_id: str, *, secret: bytes) -> None:
    """KEY-3 — the new key becomes active; the previous one verifies only."""
    self.provision(key_id, secret=secret, state=KeyState.ACTIVE)

set_available

set_available(available: bool) -> None

KEY-5 — model secret-manager/keyset unavailability.

Source code in src/symfonic/services/switching/keys.py
def set_available(self, available: bool) -> None:
    """KEY-5 — model secret-manager/keyset unavailability."""
    self._available = available

derive_library_key

derive_library_key(secret: str, *, salt: bytes, info: bytes = b'symfonic-envelope') -> tuple[str, bytes]

LIB-EA-1 — HKDF-SHA256 from an adopter secret; key_id is its fingerprint.

LIB-EA-2 is the important half: there is no default secret. An adopter who configures nothing gets a construction error, not a package-wide shared key.

Source code in src/symfonic/services/switching/keys.py
def derive_library_key(
    secret: str, *, salt: bytes, info: bytes = b"symfonic-envelope"
) -> tuple[str, bytes]:
    """LIB-EA-1 — HKDF-SHA256 from an adopter secret; ``key_id`` is its fingerprint.

    LIB-EA-2 is the important half: there is no default secret. An adopter who
    configures nothing gets a construction error, not a package-wide shared key.
    """
    if not secret:
        raise ConfigurationError(
            "library mode requires an adopter-provided signing secret (explicit "
            "configuration parameter or the documented environment variable); "
            "there is no default secret and unsigned envelopes are forbidden."
        )
    prk = hmac.new(salt, secret.encode("utf-8"), hashlib.sha256).digest()
    material = hmac.new(prk, info + b"\x01", hashlib.sha256).digest()
    key_id = hashlib.sha256(material).hexdigest()[:16]
    return key_id, material