Skip to content

Appendix D: Solutions for Chapter 29

This page collects solutions and editorial notes for the exercises in Chapter 29: PKI and code signing. Compute and derivation exercises have worked solutions; open-ended exercises have an editorial note describing what a strong answer addresses.

Editorial note. The classifier is a lookup table from signatureAlgorithm OID to {classical, single-PQ, composite}, which Chapter 29’s Block 1 prints in full. For the root that OID is the self-signature, which path validation does not verify. The production check is the anchor’s SubjectPublicKeyInfo, as the chapter notes. The chain warning fires when the leaf classifies as composite and any non-leaf classifies as classical.

What the exercise adds is the remediation message, and that is the part worth writing, because it needs a field Block 1 never touches. Block 1 classifies bare OID strings, so the worst it can say is “position 1”. The reference CertRef carries a subject and an issuer alongside the OID, and naming the offending certificate is what turns a verdict into a work item:

# Leaf-first chain of (subject, issuer, signatureAlgorithm OID).
COMPOSITE = "1.3.6.1.5.5.7.6.48" # id-MLDSA65-Ed25519-SHA512
RSA_SHA256 = "1.2.840.113549.1.1.11" # sha256WithRSAEncryption
chain = [
("CN=api.acme.example", "CN=Internal Issuer R1, O=Acme", COMPOSITE),
("CN=Internal Issuer R1, O=Acme", "CN=Acme Root R1", RSA_SHA256),
("CN=Acme Root R1", "CN=Acme Root R1", COMPOSITE),
]
def remediate(chain):
leaf_class = "composite" if chain[0][2] == COMPOSITE else "other"
if leaf_class != "composite":
return []
out = []
for depth, (subject, _issuer, oid) in enumerate(chain[1:], start=1):
if oid == RSA_SHA256:
out.append(f"depth {depth}: {subject} signed with classical "
f"OID {oid}; reissue under {COMPOSITE}")
return out
for line in remediate(chain):
print(line)
print("findings:", len(remediate(chain)))
# ==> depth 1: CN=Internal Issuer R1, O=Acme signed with classical OID 1.2.840.113549.1.1.11; reissue under 1.3.6.1.5.5.7.6.48
# ==> findings: 1

Two traps. The composite OID is 1.3.6.1.5.5.7.6.48, the value the chapter and the package both use. Earlier composite drafts used vendor arcs, and a remediation message naming the wrong one sends an operator to reissue under an OID no relying party will recognize. And the check must skip the leaf itself: a composite leaf is the precondition for the finding, not an instance of it. The exercise reinforces that a composite leaf gives no post-quantum protection if any link above it is classical, because forging the classical link forges the leaf transitively.

Editorial note. The JWKS fragment exposes one entry per kid. The classical entry has kty: "RSA" and standard RSA fields; the composite entry uses the chapter’s deployment-owned kty: "OKP-COMPOSITE" and alg: "Ed25519+ML-DSA-65". It carries the ML-DSA-65 and Ed25519 public keys as separate base64url members rather than one concatenated blob, in that order: mldsa_pk is the leading 1952 bytes of the composite key and ed_pk the trailing 32, matching the LAMPS draft’s mldsaPK || tradPK. Publishing them separately is what lets a verifier split them without knowing either length in advance. The JWT compact form is three base64url segments (header.payload.signature).

The verification result must be True for the round trip. Three checks run before any cryptography does, and each is a different failure. The kid in the JWT header selects the JWK, so an unknown kid raises KeyError rather than returning False. The selected JWK’s kty must be OKP-COMPOSITE, which rejects a token that points at the surviving RSA kid. And the JWK’s alg and the header’s alg must agree with each other and with the module constant. A header naming an algorithm the key was not published for is a confused-deputy signal, and it is the most common cause of a failing round trip when the keys themselves are correct. Note that this is a dispatch on kty and alg together, after a lookup by kid. None of the three is sufficient alone.

Editorial note. The wrapper extension introduces a counter_callback keyword argument with default None. When present, the wrapper calls the callback in place of the file-lock path. The pytest fake is a small class that holds a counter and increments on call. Testing the failure path requires a second fake that raises on call and confirms the wrapper surfaces RuntimeError rather than silently using a stale counter. The exercise reinforces that XMSS is unsafe under any state-tracking failure. The wrapper’s job is to make the failure modes explicit so the caller cannot accidentally sign with a stale counter.

Editorial note. The deprecation runbook is mostly supplied by the prompt; the writing task is filling in concrete signal thresholds. A strong runbook names a measurable threshold (e.g. “fewer than 100 daily certificate issuances under the classical root for one full reporting quarter”), a fleet-scan tool (e.g. “deploy a credential-rotation telemetry agent that reports validator pinning configurations to the central inventory”), and a rollback condition with an objective trigger (e.g. “composite chain causes more than 0.1 percent verification failure rate over 24 hours”). The retirement policy is two branches, and a runbook that names only one is incomplete, because the prompt asks for both.

Ordinary retirement. Expire-only, on the chapter’s deprecation signal: stop issuance, drain the remaining dependencies, and let the intermediate expire on its normal schedule, with no trust-store removal and no intermediate revocation until the fleet scan confirms no validator is still pinned to the classical root. The trap to avoid here is an active revoke before that confirmation. Every validator still pinned to the classical path fails closed the moment the anchor leaves its trust store, and the outage is as wide as the set of validators the scan has not yet cleared, which is why the scan is the gate rather than the calendar.

Known compromise. The second branch replaces the first rather than waiting on it. Revoke the intermediate through the root’s CRL and distribute trust-anchor removal to the fleet, on the incident-response timetable. The two actions are separate and both are needed: revocation retires the intermediate, and only trust-store removal retires the root, because a CRL cannot revoke the anchor that signs it. The availability cost the ordinary branch is built to avoid is here accepted and managed (a staged removal, an announced window, a composite path pre-positioned for the validators that can take one) rather than deferred. A trust path known to be compromised is not made safe by the fleet’s unreadiness to lose it, and an attacker holding the intermediate key can issue a certificate for any name the fleet still trusts.

Editorial note. Chapter 26 has no numbered sections. Its six areas are ### headings under ## Six architectural areas, and a strong answer names them as such.

Direct action: Lifecycle management. PKI rollout is fundamentally a lifecycle operation: provisioning the new chain, dual-running the classical and composite chains, retiring the classical chain on a measurable signal. That last step is exactly what Chapter 29’s deprecation rule specifies, and Chapter 26’s lifecycle area is where the rehash-on-read machinery for a superseded algorithm lives.

Adjacent but not the action: Governance and policy. The rollout requires governance to authorize the new OIDs, validate the audit trail, and approve the deprecation, but governance is not what the rollout itself changes; governance is the policy framework around the lifecycle work. Cryptographic observability is a defensible alternative answer for the adjacent half, since the deprecation signal in Chapter 29 is a measurement (the fraction of validation traffic resolving to the classical chain) and measuring it is observability work. Either is fine if the reasoning distinguishes what the rollout changes from what it depends on.

The fuller versions of these routines are in the pki_migration package under solutions/ch29-pki. From a clone of the companion repository, pytest tests/ch29 runs its suite. Appendix C has the setup.