Appendix D: Solutions for Chapter 25
This page collects solutions and editorial notes for the exercises in Chapter 25: Inventory first: CBOM. Compute and derivation exercises have worked solutions; open-ended exercises have an editorial note describing what a strong answer addresses.
The fuller versions of these routines are in the cbom package under solutions/ch25-cbom. From a clone of the companion repository, pytest tests/ch25 runs its suite. Appendix C has the setup.
Exercise 1
Section titled “Exercise 1”Editorial note. The exercise is a mechanical extension of Block 1: the chapter already declares five touchpoints, so the ML-KEM-768 endpoint is the sixth, and the summary should then list six. Two details decide whether it comes out tagged quantum-safe.
The vulnerability table is keyed by algorithm family, not by parameter set. status("ML-KEM-768") returns unknown, because ML-KEM-768 is not a key. The new record’s families list has to read ["ML-KEM"]. Block 2’s full table already carries "ML-KEM": QUANTUM_SAFE, so it needs no edit at all. Block 4’s reduced table does, since it omits the post-quantum rows.
The second detail is the one worth pausing on. Adding a hash to the family list, as every other record in Block 1 does, sends the entry to grover-only rather than quantum-safe, because touchpoint_status returns the worst case across the list. That is the same trap as Exercise 4 below, and it is the propagation rule working correctly rather than a bug.
Exercise 2
Section titled “Exercise 2”Editorial note. The filter is a one-liner over cbom["components"] selecting entries whose properties carries the quantum-status property with value "vulnerable". Against the five-touchpoint CBOM it returns three entries: the TLS endpoint, the JWT signer, and the validator key. The two Grover-only entries (password_hashing and webhook_hmac) are excluded. The exercise makes the point that CBOM consumers answer migration questions with simple property filters once the inventory is well-formed. The hard part is keeping the inventory accurate, not querying it.
The block below runs Exercises 2, 3 and 4 against the CycloneDX document the chapter’s generator builds, not against the raw touchpoint list. That is the interface the three questions name: each answer reads cbom["components"], turns a component’s properties list into a name-value lookup, and joins on bom-ref.
# Appendix D, Chapter 25: Exercises 2, 3 and 4 over CycloneDX documents.import syssys.path.insert(0, "solutions/ch25-cbom/src")from cbom.app import TOUCHPOINTSfrom cbom.generator import build_cbom
def props(component): # CycloneDX properties are a name-value list, not a mapping. return {p["name"]: p["value"] for p in component.get("properties", [])}
STATUS = "encryptorium:quantum-status"
def only_vulnerable(cbom): return [c for c in cbom["components"] if props(c).get(STATUS) == "vulnerable"]
def priority(component): p = props(component) state, exposure = p[STATUS], p["encryptorium:exposure"] if state == "quantum-safe": return "DONE" if state == "grover-only": return "LOW" if state == "vulnerable": return "HIGH" if exposure == "public" else "MEDIUM" return "UNKNOWN"
def diff_cboms(before, after): b = {c["bom-ref"]: props(c) for c in before["components"]} a = {c["bom-ref"]: props(c) for c in after["components"]} changed = sorted((r, b[r][STATUS], a[r][STATUS]) for r in b.keys() & a.keys() if b[r][STATUS] != a[r][STATUS]) return changed, sorted(a.keys() - b.keys()), sorted(b.keys() - a.keys())
before = build_cbom(TOUCHPOINTS)
# Exercise 2: the three vulnerable components, as components.print([c["bom-ref"] for c in only_vulnerable(before)])# ==> ['crypto:tls_endpoint_api', 'crypto:jwt_signing', 'crypto:blockchain_validator_sig']
# Exercise 3: the priority of all five entries.for c in before["components"]: print(priority(c), c["bom-ref"])# ==> HIGH crypto:tls_endpoint_api# ==> MEDIUM crypto:jwt_signing# ==> LOW crypto:password_hashing# ==> LOW crypto:webhook_hmac# ==> HIGH crypto:blockchain_validator_sig
# Exercise 4: two documents, joined on bom-ref, under both readings# of what the migrated jwt_signing record lists.for families in (["ML-DSA"], ["ML-DSA", "SHA-256"]): migrated = [dict(t, algorithm="ML-DSA-65", families=families) if t["name"] == "jwt_signing" else t for t in TOUCHPOINTS] print(families, diff_cboms(before, build_cbom(migrated)))# ==> ['ML-DSA'] ([('crypto:jwt_signing', 'vulnerable', 'quantum-safe')], [], [])# ==> ['ML-DSA', 'SHA-256'] ([('crypto:jwt_signing', 'vulnerable', 'grover-only')], [], [])Exercise 3
Section titled “Exercise 3”Editorial note. The scoring rule is a four-way decision tree on (quantum-status, exposure), and the block under Exercise 2 runs it over the five entries. The results are HIGH for the TLS endpoint, MEDIUM for the JWT signer, LOW for password hashing, LOW for the webhook HMAC, and HIGH for the blockchain validator signing key. Only the two vulnerable public-facing entries score HIGH, which is the point of scoring on the pair rather than on the status alone: the JWT signer is exactly as broken by Shor’s algorithm as the TLS endpoint and is reachable by fewer attackers. A strong solution uses an explicit lookup table rather than nested ifs, since the rule is likely to evolve as new categories (“vulnerable-but-end-of-life”, “grover-public-large-data”) emerge. The harder problem is exposure attribution: determining whether a service is truly public-facing requires network topology data that source-level CBOM does not capture, so most teams attribute exposure manually at first and tighten the rule once a runtime probe is in place.
Exercise 4
Section titled “Exercise 4”Editorial note. The diff is a join on bom-ref followed by a property comparison, and it takes two documents rather than two touchpoint lists: bom-ref is the only identifier a consumer of someone else’s CBOM can rely on, since the touchpoint name is an input to the generator and not a field of the standard. The expected output names crypto:jwt_signing and nothing else, because migrating one touchpoint changes one row.
What it reports that row as is the interesting part, and the block under Exercise 2 runs both readings. If the migrated record’s families list reads ["ML-DSA"], the status goes vulnerable to quantum-safe. If it keeps the hash, ["ML-DSA", "SHA-256"], the status goes vulnerable to grover-only, because touchpoint_status returns the worst case across the list and SHA-256 is a Grover-only row. The second is the more likely edit, since every other record in Block 1 lists its hash. Neither answer is wrong about the code. A solution that asserts quantum-safe without saying which family list it assumed has skipped the step the exercise is for.
A complete solution also reports new components (introduced post-migration) and removed components (the inverse), which is why diff_cboms returns three lists and not one. Both are empty here and neither is empty in a real migration, where an ML-DSA library is added or a deprecated RSA-only path is removed.
Exercise 5
Section titled “Exercise 5”Editorial note. Common misses include cryptography embedded in third-party native binaries (where source scanning sees only the FFI call, not the primitive), language constructs that compile away (Java keystores, .NET CMS APIs), and cryptography on the wire from external services the application calls. Mitigations that avoid rewrites: runtime tracing of TLS handshakes via eBPF or strace, network capture at egress that records the negotiated group and cipher suite from each handshake’s clear ServerHello (ALPN identifies the application protocol, not the cryptography; in TLS 1.3 every handshake message after the ServerHello is encrypted, the Certificate included, so the certificate signature algorithm needs endpoint TLS telemetry, decryption material, or a separate active inventory of the servers’ certificates (Rescorla, 2026)), dependency-tree scanning that resolves transitive crypto libraries, OS-level audit subsystems that log key-material access.
The exercise asks which of the chapter’s four failure modes the mitigation addresses, so a strong answer names one. Dependency-tree scanning goes after shadow cryptography. Runtime tracing and egress capture go after shadow cryptography and out-of-band cryptography at once, because both see the primitive actually negotiated rather than the one the source appears to request. Regenerating from live configuration goes after stale entries. No mitigation that only reads source addresses any of the four, which is the point.
NIST SP 1800-38B supports this without using these words for it. It calls for automated tools to “identify the cryptographic algorithms used in hardware and software modules, libraries, and embedded code”. It then scopes its own lab demonstration to “three core protocols”, namely TLS, SSH, and IPsec (National Cybersecurity Center of Excellence (NCCoE), 2023).