Skip to content

Chapter 25: Inventory first: CBOM

Consider a small backend service with five cryptographic uses. The first four are a public TLS endpoint terminated at api.example.com, a session token issued as a JSON Web Token (JWT) and signed with RS256, PBKDF2-HMAC-SHA256 password hashing on the user table, and HMAC-SHA256 authentication tags on outgoing webhooks. The fifth is an ECDSA-secp256k1 signing key the service uses to author Layer-1 blockchain transactions in its role as a chain operator. RS256 names an RSA PKCS #1 v1.5 signature over SHA-256 (Jones, 2015, sec. 3.1). Each use has a different primitive, a different failure mode under quantum attack, and a different ownership trail. A useful inventory names all five.

Parts II through IV implement four families of post-quantum primitives from scratch. Part V puts five schemes into production, from two of those four families. ML-KEM and ML-DSA come from the lattice chapters of Part II, and SLH-DSA from the hash-based chapters of Part III. The other two are XMSS and LMS, the stateful hash-based schemes Chapter 29 uses for firmware. NIST has standardized the first three in FIPS 203 to 205 and approved the stateful pair in SP 800-208. The code-based and isogeny schemes Part IV built are not deployment targets anywhere in this Part, which is a gap Part IV’s closing section addresses rather than one this chapter papers over. No team can swap a classical algorithm for a post-quantum one without first naming what runs where.

Chapter 1 framed the urgency through harvest now, decrypt later, Mosca’s inequality (Mosca, 2018), and three sets of policy and guidance timelines. This chapter uses the UK set to frame the inventory: three target dates from the National Cyber Security Centre (NCSC) (UK National Cyber Security Centre, 2025).

MilestoneDate
Discovery and assessment done, initial plan written2028
Highest-priority migration activities done2031
Migration complete2035

Chapter 30’s context section holds the full timetable, with what each instrument applies to and when, including the EU’s coordinated roadmap and the US instruments for National Security Systems and for civilian federal systems.

Chapter 25 turns that urgency into a concrete artifact. A Cryptography Bill of Materials (CBOM) is a machine-readable inventory of the cryptographic assets within a defined discovery scope, which in the CycloneDX bill-of-materials standard include algorithms, keys, certificates, and their relationships to software components (OWASP CycloneDX, 2025).

The inventory anchors the rest of the migration program. Lifecycle metadata feeds Mosca’s XX: how long the protected data or trust decision must remain secure (Chapter 1). Dependency counts, engineering estimates, and rollout complexity feed YY, the time the migration itself takes. ZZ is the time until a cryptographically relevant quantum computer exists. Chapter 1 notes it has no consensus date. The target dates above do not estimate ZZ; they set, for the organizations that adopt them, the latest acceptable date for each stage of the migration.

This chapter calls each of the service’s uses a touchpoint. It walks the five and writes a CycloneDX 1.6 generator for them by hand in standard-library Python. Each entry is classified against a deliberately simplified migration taxonomy derived from the NIST post-quantum standards at FIPS 203 (National Institute of Standards and Technology, 2024a), FIPS 204 (National Institute of Standards and Technology, 2024b), and FIPS 205 (National Institute of Standards and Technology, 2024c). The standalone code package at solutions/ch25-cbom/ carries the same logic and wraps the generator’s components in the CycloneDX document, which the chapter describes but does not print. Its pytest tests validate structural CycloneDX fields and the quantum-vulnerability propagation rule.

Block 1 declares the five touchpoints as plain Python records. The fields follow a fixed schema. Two are not self-evident from the key name: families, a short list of algorithm-family labels that the vulnerability lookup consumes, and exposure, the adversarial-visibility label (public or internal) that the priority matrix in Figure 25.1 reads.

# Block 1: the five cryptographic touchpoints.
TOUCHPOINTS = [
{
"name": "tls_endpoint_api",
"location": "edge/api.example.com",
"algorithm": "ECDHE-ECDSA-AES256-GCM-SHA384",
"primitive": "key-agree",
"parameters": {
"tls_version": "1.2",
"curve": "P-256",
"aead": "AES-256-GCM",
"signature": "ECDSA-P-256",
},
"families": ["ECDHE", "ECDSA", "AES", "SHA-384"],
"exposure": "public",
"deployed": "2022-06-15",
"owner": "platform-team",
},
{
"name": "jwt_signing",
"location": "auth/token-service",
"algorithm": "RS256",
"primitive": "signature",
"parameters": {"rsa_modulus_bits": 2048, "hash": "SHA-256"},
"families": ["RSA", "SHA-256"],
"exposure": "internal",
"deployed": "2023-01-10",
"owner": "auth-team",
},
{
"name": "password_hashing",
"location": "auth/user-service",
"algorithm": "PBKDF2-HMAC-SHA256",
"primitive": "kdf",
"parameters": {
"iterations": 600_000,
"salt_bytes": 16,
"dk_bytes": 32,
"hash": "SHA-256",
},
"families": ["PBKDF2", "HMAC", "SHA-256"],
"exposure": "internal",
"deployed": "2024-03-01",
"owner": "auth-team",
},
{
"name": "webhook_hmac",
"location": "webhooks/outgoing-signer",
"algorithm": "HMAC-SHA256",
"primitive": "mac",
"parameters": {"key_bytes": 32, "hash": "SHA-256"},
"families": ["HMAC", "SHA-256"],
"exposure": "internal",
"deployed": "2023-08-20",
"owner": "integrations-team",
},
{
"name": "blockchain_validator_sig",
"location": "chain/validator-keystore",
"algorithm": "ECDSA-secp256k1",
"primitive": "signature",
"parameters": {"curve": "secp256k1", "hash": "SHA-256"},
"families": ["ECDSA", "SHA-256"],
"exposure": "public",
"deployed": "2024-01-15",
"owner": "chain-ops",
},
]
print(len(TOUCHPOINTS), [t["primitive"] for t in TOUCHPOINTS])
# ==> 5 ['key-agree', 'signature', 'kdf', 'mac', 'signature']

Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch25/, one file per block. Appendix C covers the clone and the environment they run on.

The five records cover four of the fifteen CycloneDX primitive labels, namely key-agree, signature, kdf, and mac. The JWT signer and the validator key are both signature primitives. The records span Shor-vulnerable and Grover-only algorithm families, which is enough to produce an ordered priority list for the entries. The exposure label is adversarial visibility, not key location. blockchain_validator_sig keeps its private key in an internal keystore but signs into a public consensus ecosystem, so it is labeled public; jwt_signing is reachable only inside the trust boundary, so it is internal.

Any CBOM entry has to answer four questions at once.

AxisWhat the entry has to pin down
IdentityThe algorithm and its parameters. RSA-2048 is a different entry from RSA-4096, and TLS 1.2 with a P-256 curve is a different entry from TLS 1.3 with X25519.
LocationWhere the use sits in the architecture, so an owner can be assigned and the migration touches the right code. edge/api.example.com and auth/token-service are both software, but they belong to different teams on different release cadences.
Risk profileQuantum vulnerability combined with exposure. What shape that risk takes depends on what the primitive protects, which is the next paragraph.
LifecycleWhen the entry went live, how long its keys have to stay confidential, and when it is scheduled for rotation or retirement.

The risk axis is the one that repays care. Harvest now, decrypt later (HNDL, from Chapter 1) applies to confidentiality-bearing key-establishment and encryption: recorded ciphertext is decrypted later once the key exchange is broken. Public-facing entries have the most-exposed HNDL surface. Internal traffic is still harvestable by an attacker with east-west network access, so the threat is not bounded by the perimeter. Signature and authentication entries carry a different risk: a future quantum attacker forges signatures, impersonates a service, or invalidates long-term trust, with no recorded-traffic step required. A CBOM should let a reader separate confidentiality risk from authenticity and long-term-verification risk.

For this toy application, the quantum axis reduces to three cases. Shor’s algorithm gives quantum polynomial-time attacks on factoring and discrete log, which breaks RSA, DSA, and classical Diffie-Hellman directly (Shor, 1994). The same approach extends to elliptic-curve discrete log, so ECDSA and ECDH fall under the same bound. Grover’s algorithm gives a square-root speedup on unstructured search (from 2n2^n queries to 2n/22^{n/2}), which halves the exponent of the brute-force key search against a symmetric primitive (Grover, 1996). The migration targets are the first three finalized NIST post-quantum standards: ML-KEM (FIPS 203 (National Institute of Standards and Technology, 2024a)), ML-DSA (FIPS 204 (National Institute of Standards and Technology, 2024b)), and SLH-DSA (FIPS 205 (National Institute of Standards and Technology, 2024c)).

For US National Security Systems (NSS) specifically, NSA CNSA 2.0 fixes the main parameter choices: ML-KEM-1024 for key establishment, ML-DSA-87 for general digital signatures, AES-256, and SHA-384 or SHA-512. For software and firmware signing, CNSA 2.0 also approves the stateful hash-based LMS and XMSS profiles from SP 800-208. NSA encouraged those early because validated implementations were available before ML-DSA was widely validated. SLH-DSA is standardized by NIST but is not part of CNSA 2.0 for NSS (US National Security Agency, 2026).

A short lookup is enough for the toy application. Production tooling also includes protocol-level tags such as TLS version and ML-KEM hybrid mode, and flags deprecated primitives separately. The pedagogical slice here names the families and returns one of four labels (vulnerable, grover-only, quantum-safe, or unknown). The family table is deliberately coarse. A production scanner classifies a hash function by use case (preimage resistance, collision resistance, MAC or KDF construction, password hashing, transcript hashing, signature-domain separation), because SHA-256 has no single universal security interpretation.

The first step is the vulnerability lookup. Block 2 declares the three-family table, the per-family status function, and the worst-case propagation rule for a touchpoint that combines several families. The rule is that a touchpoint is only as strong as its weakest component. An ECDHE-ECDSA-AES cipher suite is vulnerable as a whole: an attacker who derives the session key offline owns the session no matter how strong the bulk cipher is. That holds for components in different roles, as in a cipher suite. A correctly constructed hybrid runs two components in one role, and its result holds while either does, under the conditions Chapter 27 sets out. This lookup does not model that case.

# Block 2: the three-family quantum-vulnerability lookup.
VULNERABLE = "vulnerable"
GROVER_ONLY = "grover-only"
QUANTUM_SAFE = "quantum-safe"
UNKNOWN = "unknown"
FAMILIES = {
"RSA": VULNERABLE, "DSA": VULNERABLE, "ECDSA": VULNERABLE,
"ECDHE": VULNERABLE, "ECDH": VULNERABLE, "DH": VULNERABLE,
"AES": GROVER_ONLY, "SHA-256": GROVER_ONLY,
"SHA-384": GROVER_ONLY, "SHA-512": GROVER_ONLY,
"HMAC": GROVER_ONLY, "PBKDF2": GROVER_ONLY,
"ML-KEM": QUANTUM_SAFE, "ML-DSA": QUANTUM_SAFE,
"SLH-DSA": QUANTUM_SAFE,
}
def status(family):
return FAMILIES.get(family, UNKNOWN)
def touchpoint_status(families):
if not families: return UNKNOWN # an empty entry is a gap, not safe
ordered = [status(f) for f in families]
if VULNERABLE in ordered: return VULNERABLE
if UNKNOWN in ordered: return UNKNOWN
if GROVER_ONLY in ordered: return GROVER_ONLY
return QUANTUM_SAFE
print(status("RSA"), status("HMAC"), status("ML-KEM"), status("Frobnitz"))
# ==> vulnerable grover-only quantum-safe unknown
print(touchpoint_status(["ECDHE", "ECDSA", "AES", "SHA-384"]))
# ==> vulnerable
print(touchpoint_status(["HMAC", "SHA-256"]))
# ==> grover-only

The second step is the CBOM generator. Block 3 builds a single CycloneDX component for one touchpoint and prints it. The CycloneDX specification, which also defines the SBOM format, extends its schema with a cryptographic-asset component type and a cryptoProperties sub-object for crypto asset type, primitive, parameter-set identifier, execution environment, and security-level metadata (the component and cryptoProperties definitions in OWASP CycloneDX, 2024). Each component is of type cryptographic-asset with cryptoProperties.assetType = "algorithm" and algorithm details under cryptoProperties.algorithmProperties. Organization-specific fields go into a properties list of name-value pairs, the standard CycloneDX escape hatch. The four-way migration status this chapter assigns (vulnerable, grover-only, quantum-safe, unknown) is not a native CycloneDX field; it is carried as an organization-specific encryptorium:quantum-status property.

# Block 3: build the CycloneDX component for one touchpoint.
import json
VULNERABLE = "vulnerable"
# Reduced table for this single JWT example; the full package uses
# the shared three-family table from Block 2.
FAMILIES = {"RSA": VULNERABLE, "SHA-256": "grover-only"}
def status(family): return FAMILIES.get(family, "unknown")
def touchpoint_status(families):
if not families: return "unknown"
ordered = [status(f) for f in families]
if VULNERABLE in ordered: return VULNERABLE
if "unknown" in ordered: return "unknown"
return "grover-only"
def parameter_set(params):
# Sort keys so the identifier is stable across versions and a
# CBOM-to-CBOM diff is meaningful.
return "; ".join(f"{k}={params[k]}" for k in sorted(params))
def component(touchpoint):
return {
"type": "cryptographic-asset",
"bom-ref": f"crypto:{touchpoint['name']}",
"name": touchpoint["algorithm"],
"cryptoProperties": {
"assetType": "algorithm",
"algorithmProperties": {
"primitive": touchpoint["primitive"],
"parameterSetIdentifier": parameter_set(touchpoint["parameters"]),
"executionEnvironment": "software-plain-ram",
},
},
"properties": [
{"name": "encryptorium:location", "value": touchpoint["location"]},
{"name": "encryptorium:exposure", "value": touchpoint["exposure"]},
{"name": "encryptorium:owner", "value": touchpoint["owner"]},
{"name": "encryptorium:deployed", "value": touchpoint["deployed"]},
{"name": "encryptorium:quantum-status", "value": touchpoint_status(touchpoint["families"])},
{"name": "encryptorium:families", "value": ",".join(touchpoint["families"])},
],
}
jwt = {
"name": "jwt_signing", "location": "auth/token-service",
"algorithm": "RS256", "primitive": "signature",
"parameters": {"rsa_modulus_bits": 2048, "hash": "SHA-256"},
"families": ["RSA", "SHA-256"], "exposure": "internal",
"deployed": "2023-01-10", "owner": "auth-team",
}
print(json.dumps(component(jwt), indent=2))
# ==> {
# ==> "type": "cryptographic-asset",
# ==> "bom-ref": "crypto:jwt_signing",
# ==> "name": "RS256",
# ==> "cryptoProperties": {
# ==> "assetType": "algorithm",
# ==> "algorithmProperties": {
# ==> "primitive": "signature",
# ==> "parameterSetIdentifier": "hash=SHA-256; rsa_modulus_bits=2048",
# ==> "executionEnvironment": "software-plain-ram"
# ==> }
# ==> },
# ==> "properties": [
# ==> {
# ==> "name": "encryptorium:location",
# ==> "value": "auth/token-service"
# ==> },
# ==> {
# ==> "name": "encryptorium:exposure",
# ==> "value": "internal"
# ==> },
# ==> {
# ==> "name": "encryptorium:owner",
# ==> "value": "auth-team"
# ==> },
# ==> {
# ==> "name": "encryptorium:deployed",
# ==> "value": "2023-01-10"
# ==> },
# ==> {
# ==> "name": "encryptorium:quantum-status",
# ==> "value": "vulnerable"
# ==> },
# ==> {
# ==> "name": "encryptorium:families",
# ==> "value": "RSA,SHA-256"
# ==> }
# ==> ]
# ==> }

The third step wraps all five components in a CycloneDX envelope, which follows the CycloneDX specification (root properties in OWASP CycloneDX, 2024): bomFormat, specVersion, serialNumber, version, a metadata block, and a components array. The build_cbom function in the ch25-cbom package under solutions/ builds it, and the full document is in that package and in the test suite. Block 4 does not build the envelope. It prints the inventory summary from reduced touchpoint records instead: each touchpoint, its primitive, its exposure, and its quantum status, which is what feeds the next step of the migration program.

# Block 4: print the inventory summary.
TOUCHPOINTS = [
{"name": "tls_endpoint_api", "primitive": "key-agree",
"exposure": "public", "families": ["ECDHE", "ECDSA", "AES", "SHA-384"]},
{"name": "jwt_signing", "primitive": "signature",
"exposure": "internal", "families": ["RSA", "SHA-256"]},
{"name": "password_hashing", "primitive": "kdf",
"exposure": "internal", "families": ["PBKDF2", "HMAC", "SHA-256"]},
{"name": "webhook_hmac", "primitive": "mac",
"exposure": "internal", "families": ["HMAC", "SHA-256"]},
{"name": "blockchain_validator_sig", "primitive": "signature",
"exposure": "public", "families": ["ECDSA", "SHA-256"]},
]
FAMILIES = {
"ECDHE": "vulnerable", "ECDSA": "vulnerable", "AES": "grover-only",
"SHA-384": "grover-only", "RSA": "vulnerable", "SHA-256": "grover-only",
"PBKDF2": "grover-only", "HMAC": "grover-only",
}
def touchpoint_status(families):
if not families: return "unknown"
ordered = [FAMILIES.get(f, "unknown") for f in families]
if "vulnerable" in ordered: return "vulnerable"
if "unknown" in ordered: return "unknown"
if "grover-only" in ordered: return "grover-only"
return "quantum-safe"
print(f"{'touchpoint':<24} {'primitive':<12} {'exposure':<10} {'status'}")
for t in TOUCHPOINTS:
print(f"{t['name']:<24} {t['primitive']:<12} {t['exposure']:<10} "
f"{touchpoint_status(t['families'])}")
# ==> touchpoint primitive exposure status
# ==> tls_endpoint_api key-agree public vulnerable
# ==> jwt_signing signature internal vulnerable
# ==> password_hashing kdf internal grover-only
# ==> webhook_hmac mac internal grover-only
# ==> blockchain_validator_sig signature public vulnerable

Three of the five touchpoints are Shor-vulnerable and two carry only a Grover-reduced margin, but the two Grover-reduced readings are not the same. webhook_hmac has a clean symmetric reading: with a uniformly random 256-bit MAC key and full-length HMAC-SHA256 tags, Grover key search still leaves roughly a 21282^{128} query margin. password_hashing is different. PBKDF2-HMAC-SHA256 uses only symmetric primitives, so Shor does not break it, but its real security is bounded by password entropy, salt handling, iteration count, and the rate-limiting and breach model around the verifier, not by the 32-byte derived-key length. Setting dk_bytes = 32 does not give a human password a 128-bit post-quantum margin. Collision resistance is a separate question: the Brassard-Høyer-Tapp (BHT) algorithm finds a collision in an nn-bit hash in approximately 2n/32^{n/3} queries with quantum-accessible memory on the same scale (Brassard et al., 1998). The chapter does not claim a quantum margin for that case.

Figure 25.1 positions each touchpoint on a quantum-vulnerability axis against an exposure axis, with each of its six cells labeled by migration priority. Four labels cover the six cells, because LOW and DONE each appear twice. Of the two HIGH entries, the blockchain touchpoint carries forward into the blockchain Part: Ch 37 walks the L1 transaction signing migration, Ch 39 covers consensus and staking signatures, Ch 40 addresses the smart-contract verifier contracts that ZK rollups deploy. The other, the TLS endpoint, is Chapter 28’s.

Inventory priority matrix. Two-axis priority matrix. The exposure axis runs from internal to public. The quantum-vulnerability axis runs from quantum-safe through grover-only to vulnerable. The five mock touchpoints are placed as points: the TLS endpoint (tls_endpoint_api) and the blockchain validator signing key (blockchain_validator_sig) share the upper-right high-priority cell, the JWT signer (jwt_signing) is in the vulnerable-internal medium cell, the password hasher (password_hashing) and webhook HMAC (webhook_hmac) both sit in the internal grover-only low-priority cell, since both are internal-facing. The public grover-only cell carries no point. exposure internal public quantum vulnerability vulnerable grover-only quantum-safe MEDIUM HIGH LOW LOW DONE DONE tls_endpoint_api blockchain_validator_sig jwt_signing password_hashing webhook_hmac
Figure 25.1. The five mock touchpoints placed on a two-axis priority matrix. The public-facing vulnerable cell is HIGH priority and carries both tls_endpoint_api and blockchain_validator_sig; internal-vulnerable is MEDIUM. Both grover-only entries are internal, so they sit together in the left-hand LOW cell; quantum-safe entries are done.

The CBOM document that build_cbom returns in the ch25-cbom package under solutions/ carries the same six encryptorium: properties per component as Block 3. The test at tests/ch25/test_cbom_schema.py checks that every component has the required CycloneDX fields and all six property names.

Production tooling in the CycloneDX ecosystem handles a full schema, schema validation, and richer metadata. The cyclonedx-python-lib package provides data models, validators, and Vulnerability Exploitability eXchange (VEX) document modeling (OWASP CycloneDX, 2026b). Signing a BOM is a separate tool’s job: the CycloneDX command-line tool carries sign and verify commands (OWASP CycloneDX, 2026a).

This chapter targets 1.6 rather than the current version. CycloneDX 1.7 was released on 2025-10-21, and specVersion is a per-document field, so a 1.6 document stays valid. Every field used here is defined identically in both. 1.7 adds algorithmFamily and ellipticCurve to algorithmProperties, and key-wrap to the primitive enum, none of which this generator emits.

Four failure modes produce a CBOM document that looks complete while hiding risk. NIST SP 1800-38B, the NCCoE Quantum Readiness: Cryptographic Discovery practice guide, frames cryptographic discovery as a multifaceted problem and motivates these categories, particularly the gaps around library and framework dependencies (National Cybersecurity Center of Excellence (NCCoE), 2023). NIST IR 8547 is the companion transition-planning report (algorithm deprecation schedules and PQC replacements), not a discovery guide (Moody et al., 2024).

Shadow cryptography. Third-party libraries often bring their own cryptographic primitives. A JWT library may hash with SHA-1 internally for key-id derivation even while the signing algorithm is RS256. A logging dependency may use a weak PRF for request correlation. Source scanning limited to top-level code misses both. A useful CBOM program audits library versions against a curated database of known primitive uses, or instruments the runtime to log algorithm identifiers from inside the library stack.

Stale entries. Algorithms are deprecated on a cadence (3DES, SHA-1, RSA-1024, TLS 1.0), but deployed code is slow to follow. An entry that records “TLS 1.2” when the endpoint actually negotiates TLS 1.3 is stale. An entry that records “TLS 1.2” when the endpoint still accepts TLS 1.0 hides risk. Periodic regeneration from live configuration, not from a one-time manual survey, is the only reliable fix.

Parameter drift. An RSA-1024 key generated for a lab test five years ago can remain in a key vault long after anyone remembers why. The inventory must capture not only the algorithm but every parameter that affects strength: key length, curve choice, hash output size, iteration count, nonce size. A bare entry “RSA signing” is close to useless; an entry “RS256 / RSA-2048 / SHA-256” is auditable.

Out-of-band cryptography. HSM-managed keys, enclaves, and hardware security tokens do not always expose their algorithms to source scanning. A CBOM that covers only the application layer will omit operations performed inside the HSM. Filling this gap requires a second inventory pass against the HSM vendor’s key catalogue and a cross-reference back to the application CBOM by key identifier.

A CBOM enables migration tracking through a versioned, machine-readable record. Its bom-ref and encryptorium:quantum-status fields survive an automated diff across versions of the document, which turns “what changed since last quarter” into a mechanical query rather than a manual review. Once migration is under way, NCSC’s guidance expects progress to be measured, for instance by quantifying how many software clients use post-quantum cryptography and identifying those that do not (UK National Cyber Security Centre, 2025). A CBOM that regenerates after each migration step is this chapter’s proposal for producing that measurement, not a mechanism NCSC prescribes.

Figure 25.2 traces the pipeline from source code to CBOM to risk score to migration roadmap and back to progress tracking.

The source-to-roadmap pipeline. A horizontal flow diagram. Box one, source code and configuration. Arrow to box two, CBOM JSON. Arrow to box three, risk-and-priority score. Arrow to box four, migration roadmap. Arrow to box five, progress tracking. A dashed feedback arrow runs from progress tracking back to the source-code box, representing the cycle that regenerates the CBOM after each migration step. source code + deploy config + HSM catalogue CBOM JSON CycloneDX 1.6 cryptographic-asset risk score quantum-status + exposure roadmap priority-ordered per-entry plan tracking before / after CBOM diff regenerate after each migration step
Figure 25.2. From source and deployment artifacts to a CBOM document, a risk score, a priority-ordered roadmap, and a before-and-after tracking loop. The dashed arrow marks the regeneration step that turns the CBOM from a one-shot survey into a continuous audit trail.

A CBOM does not automate migration. It does not check whether an implementation is correct, whether a library version is affected by a CVE, or whether a deployment has rolled out safely. CBOM and CycloneDX VEX are designed to be paired: the CBOM inventories the assets, a VEX document tracks which known vulnerabilities are actually exploitable in context. Side-channel resistance is not captured in a CBOM (timing leaks, Rowhammer, EM emanations all live outside the schema). A CBOM is the input artifact for those checks, not a substitute.

At the start of a migration program the choice is coverage against completeness. NCSC’s 2028 milestone is discovery and assessment with an initial migration plan, and NCSC says that discovery “is not intended to be a formal asset register” (UK National Cyber Security Centre, 2025). The form of the record is therefore the organization’s choice, and this chapter’s is a CBOM filed early and regenerated as coverage grows, rather than held back until it covers the whole estate. A CBOM with explicit “not yet inventoried” entries for the remaining estate is auditable; a gap that is silently omitted is not.

One final caveat on the toy generator. The output of build_cbom in the ch25-cbom package under solutions/ is a minimal CycloneDX 1.6 subset: bomFormat, specVersion, version, serialNumber, metadata, and a components array of cryptographic-asset entries. It omits VEX linkages, external-reference bundles, licensing fields, signatures on the BOM itself, and the full vendor / hash / evidence tree that a production toolchain populates. The TLS row also collapses a protocol profile into one teaching entry: a production CBOM models the cipher-suite configuration and the underlying key-agreement, signature, AEAD, and hash assets as separate cryptographic-asset components linked by bom-ref. The tests/ch25/test_cbom_schema.py suite confirms the structural subset is correct. The document is not a complete description of the application.

  1. Extend the toy application with a sixth touchpoint for ML-KEM-768 key encapsulation at a new endpoint api.example.com/v2/kem. Update Block 1 with the new record and the vulnerability table so the generator tags the entry as quantum-safe. Print the resulting CBOM summary.

  2. Write a filter function only_vulnerable(cbom) that returns the list of components whose encryptorium:quantum-status property equals "vulnerable". Run it against the five-touchpoint CBOM and confirm the three expected entries.

  3. Define a migration-priority score. A reasonable rule: a vulnerable touchpoint that is public-facing scores HIGH; a vulnerable touchpoint that is internal scores MEDIUM; a grover-only touchpoint of any exposure scores LOW; a quantum-safe touchpoint scores DONE. The exposure field is already on each Block 1 record and is emitted as the encryptorium:exposure property. Implement the scorer over (encryptorium:quantum-status, encryptorium:exposure) and print the priority for all five entries.

  4. Given two CBOM documents (the state before and after migrating jwt_signing from RS256 to ML-DSA-65), write a diff function that reports each entry whose encryptorium:quantum-status changed and what it changed to.

  5. Name one cryptographic use the source-only inventory described in this chapter would miss, and propose one mitigation that does not require rewriting the application. Indicate which of the four failure modes in Section “What makes an inventory wrong” the mitigation addresses, and whether it overlaps with the discovery challenges NIST SP 1800-38B names (National Cybersecurity Center of Excellence (NCCoE), 2023).

Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 25. A separate track, for rebuilding rather than reading. Chapter 25 prints the vulnerability lookup and the per-touchpoint component builder in full, so the package exercises/ch25-cbom hands you both and stubs only build_cbom, the CycloneDX envelope this chapter describes but never prints. Grade your version against the suite that proves the reference one with PQC_IMPL=exercises pytest tests/ch25.

Brassard, G., Høyer, P., & Tapp, A. (1998). Quantum Cryptanalysis of Hash and Claw-Free Functions. LATIN ’98: Theoretical Informatics, 1380, 163–169. https://doi.org/10.1007/bfb0054319
Grover, L. K. (1996). A fast quantum mechanical algorithm for database search. Proceedings of the 28th Annual ACM Symposium on Theory of Computing (STOC), 212–219. https://doi.org/10.1145/237814.237866
Jones, M. B. (2015). JSON Web Algorithms (JWA). IETF RFC 7518. https://doi.org/10.17487/RFC7518
Moody, D., Perlner, R., Regenscheid, A., Robinson, A., & Cooper, D. (2024). Transition to Post-Quantum Cryptography Standards (Internal Report NIST IR 8547 (Initial Public Draft)). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.IR.8547.ipd
Mosca, M. (2018). Cybersecurity in an era with quantum computers: will we be ready? IEEE Security & Privacy, 16(5), 38–41. https://doi.org/10.1109/MSP.2018.3761723
National Cybersecurity Center of Excellence (NCCoE). (2023). Quantum Readiness: Cryptographic Discovery (NIST Special Publication SP 1800-38B (Preliminary Draft)). National Institute of Standards and Technology. https://www.nccoe.nist.gov/sites/default/files/2023-12/pqc-migration-nist-sp-1800-38b-preliminary-draft.pdf
National Institute of Standards and Technology. (2024a). FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.203
National Institute of Standards and Technology. (2024b). FIPS 204: Module-Lattice-Based Digital Signature Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.204
National Institute of Standards and Technology. (2024c). FIPS 205: Stateless Hash-Based Digital Signature Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.205
OWASP CycloneDX. (2024). CycloneDX Bill of Materials Standard, version 1.6: JSON schema. bom-1.6.schema.json, released with specification version 1.6 on 9 April 2024. https://cyclonedx.org/schema/bom-1.6.schema.json
OWASP CycloneDX. (2025). Cryptography Bill of Materials (CBOM) capability. Open specification at cyclonedx.org. https://cyclonedx.org/capabilities/cbom/
OWASP CycloneDX. (2026a). CycloneDX CLI. GitHub repository README, release v0.33.1 of 23 July 2026. https://github.com/CycloneDX/cyclonedx-cli/blob/v0.33.1/README.md
OWASP CycloneDX. (2026b). CycloneDX Python Library documentation, version 11.12.0. cyclonedx-python-lib, released on PyPI 13 August 2026. https://cyclonedx-python-library.readthedocs.io/en/v11.12.0/
Shor, P. W. (1994). Algorithms for quantum computation: discrete logarithms and factoring. Proceedings of the 35th Annual Symposium on Foundations of Computer Science (FOCS), 124–134. https://doi.org/10.1109/SFCS.1994.365700
UK National Cyber Security Centre. (2025). Timelines for migration to post-quantum cryptography. NCSC guidance. https://www.ncsc.gov.uk/guidance/pqc-migration-timelines
US National Security Agency. (2026). Commercial National Security Algorithm Suite 2.0 and Quantum Computing Frequently Asked Questions, Version 2.2. NSA Cybersecurity Information Sheet. https://media.defense.gov/2022/Sep/07/2003071836/-1/-1/0/CSI_CNSA_2.0__FAQ_v2.2.PDF

Last updated: