CBOM: design & implementation plan (crypto files) #10933
Replies: 2 comments
-
|
I reviewed the proposal against the CycloneDX 1.7 model and existing CBOM implementations. I think there are a few points we should clarify or adjust.
|
Beta Was this translation helpful? Give feedback.
-
|
Consumer-side perspective, since I built an evaluator that grades CycloneDX CBOMs against the EO 14412 deadlines — this design decides what's gradable downstream, so a few notes from the receiving end. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
1. Summary
Idea. Add CBOM (Cryptography Bill of Materials) generation to Trivy: while scanning an image, find cryptographic files (certificates, keys, and — later — keystores), extract their properties, and add them as
cryptographic-assetcomponents to the SBOM. Every asset is tagged with its layer, so system cryptography (base image, trusted roots, OS packages) is separated from the user's, and the report consumer filters for their own task.Phase 1. A new
cryptoscanner, enabled manually and working only when scanning an image with CycloneDX output, plus an X.509 analyzer that extracts certificates and keys (PEM/DER), with no new dependencies — stdlib only (crypto/x509+encoding/pem).Out of phase 1 (section 10): the remaining formats (PKCS#12, JKS, OpenSSH, OpenPGP, CRL) — the same inventory extended by a new analyzer; detection of weak and deprecated cryptography.
2. Scope
In scope: inventory of cryptographic files — certificates, keystores, keys. Covers 3 of the 4 CycloneDX asset types:
algorithm,certificate,related-crypto-material.Out of scope:
The inventory records that an asset is present and where it came from, but not the fact of its use.
3. File selection and detection
Formats
Certificates
.der,.crt,.cer,.pemKeys
.key,.der,.pemDetection strategy
Required) — a cheap pre-filter without reading content:.pem,.der,.crt,.cer,.key.Analyze):-----BEGINpresent → PEM, otherwise DER;PEM labels:
Specifications
-----BEGIN …-----labels: RFC 7468namedCurve— RFC 54804. Parsing and data extraction
Analyzer model
Three layers, in processing order
raw bytes → object → CryptoAsset:analyzer.analyzerinterface:Requiredselects a file by extension without reading content;Analyzereads the content and returns anAnalysisResultwith crypto assets.*x509.Certificate, etc.): moves its fields into theCryptoAssetmodel (subject, algorithm, key size, curve, validity period). Shared, reused across analyzers. The certificate extractor returns one or more components: the certificate itself and, when expanded, the public key, the key algorithm, and the signature algorithm.To cut boilerplate, a shared
Parsertype (Parse(ctx, r) ([]CryptoAsset, error)) and acrypto.Analyze(input, parser)function wrapping the parser call in anAnalysisResultcan be introduced, by analogy withanalyzer/language/analyze.go.Analyzers
x509— X.509 material in PEM or DER encoding.Required:.pem,.der,.crt,.cer,.key.Analyze: detects the encoding by content (text-----BEGIN...-----→ PEM, otherwise DER), strips the PEM wrapper, decodes ASN.1, and dispatches to the Certificate or Key extractor by header/structure.crypto/x509+encoding/pem.Files with multiple blocks (chains, bundles, cert+key)
A single file is
0..Nobjects, not one: a certificate chain, a CA bundle (hundreds ofCERTIFICATEblocks), a certificate and key together. The parser walks every block via therestthatpem.Decodereturns, calling the extractor on each object; a corrupt block is logged and skipped without aborting the rest. For DER a file usually contains a single ASN.1 object.crypto/x509.ParseCertificaterejects some certificates that do not conform to the spec — we skip those: for example, EC with explicit parameters (stdlib supports only namedCurve). DSA is a special case: stdlib parses a DSA public key (PublicKeyAlgorithm == DSA, "Only supported for parsing"); only verifying its signature is unsupported. So we inventory a DSA key like any other — the fact that a DSA signature cannot be verified has nothing to do with parsing the key.Shared extractors
The Certificate and Key extractors live in the shared
pkg/cryptopackage and are used by thex509analyzer (the extracted fields themselves are in the next section).Extracted data
Certificate → certificateProperties
The signature algorithm and the public key are emitted as separate assets — that is what
relatedCryptographicAssetspoints to.Key (PKCS#1/PKCS#8/SEC1) → relatedCryptoMaterialProperties
Sensitive data
Only metadata is extracted from private keys (type, size, curve, format, whether encrypted) — the secret material never reaches the CBOM. The
Valuefield ofrelatedCryptoMaterialPropertiesis not filled for private keys (for public keys it is allowed). Certificates and public keys are not sensitive.Encrypted private keys
The
-----BEGIN ENCRYPTED PRIVATE KEY-----label is anEncryptedPrivateKeyInfo(RFC 5958 §3): the key is encrypted with a password (KDF + cipher). We cannot decrypt it — stdlib has noEncryptedPrivateKeyInfodecoder — so in phase 1 we inventory it as-is.Because of the encryption, the SPKI is unavailable, so the usual
sha256(SPKI)cannot be computed as theID. Instead we take the hash of the DER of the encrypted container itself:(see "Identity scheme" in section 6). This
IDidentifies a specific encrypted blob, not the key inside it: the same key re-encrypted with a different salt gets a differentID, and it can no longer be linked to its certificate or public key.The key's own properties —
size,curve,algorithm— are inside the encrypted material and unavailable, so in phase 1 an encrypted key is inventoried as arelated-crypto-materialasset withtype: private-keyandstate: encryptedonly.Extracting what protects the key (
securedBy: the PBES2 scheme — KDF, PRF, cipher, iteration count) is deferred to phase 2 (section 10): it is a strength signal rather than inventory, and it is the only part that would require hand-rolled ASN.1 below stdlib's typed API. In phase 1SecuredBystaysnil.algorithmProperties — assembled from an OID and a registry
An algorithm has no parsing source of its own. In X.509/PKCS it never exists as a standalone object — only as an
AlgorithmIdentifierinside a certificate or key: an OID plus optional parameters. There is no name, no indication of whether it is a signature or a hash, no family, no strength level in bytes — an OID is a pure identifier. So the algorithm component is derived and assembled from three sources:AlgorithmIdentifier— the component's identity;primitive, family, hash, strength levels) — from theOID → propertiesregistry (pkg/crypto/algorithm.go), a static table; none of this is in the file itself;parameters.From these sources the extractor fills algorithmProperties:
primitive(signature, encryption, hash, etc.);parameterSetIdentifier;ellipticCurve— a value from the closed CycloneDXellipticCurvesEnum(cryptography-defs.schema.json), with a namespace prefix (nist/P-256,nist/P-384,nist/P-521,other/Ed25519,other/Ed448); taken from the registry.classicalSecurityLevelandnistQuantumSecurityLevelare not filled in phase 1 — these are strength estimates, not identity: there is no official source for them (they are not incryptography-defs.jsoneither), andclassicalSecurityLeveladditionally depends on key size (RSA-2048 ≈ 112, RSA-3072 ≈ 128), i.e. it requires a(family, size/curve) → levellookup table. Both fields and that table are added in phase 2 together with weak-crypto detection (section 5); until then they are absent fromAlgoPropsas well.An unknown OID does not abort parsing: the registry does not find it, and the component is created minimally — the OID itself plus
primitive: unknown.The finished algorithm becomes a separate component with
ID = OID+ normalized parameters when those change the asset (RSA size, EC curve — see "Identity scheme" in section 6). ArelatedCryptographicAssets{type:"algorithm", ref}reference points to it from the Certificate/Key, and identical(OID, parameters)are merged into one component.Algorithm registry (
pkg/crypto/algorithm.go)The
OID → propertiesregistry is a static table and the only source of the descriptive part of an algorithm component. It has two layers with different sources of truth:Our layer (
OID → family). Links an OID from anAlgorithmIdentifierto an algorithm family and defines where to takeparamSetfrom. CycloneDX does not provide this: the official data (see below) contains no OIDs at all, while our identity is built precisely on the OID. Maintained by hand.Official layer. The descriptive fields tied to a family are extracted from the official CycloneDX file
cryptography-defs.json:family → {primitive, name pattern, standards}from thealgorithmsarray;curve name → ellipticCurvesEnum valuefrom theellipticCurvesarray (name+namespacefields).The extraction is done by a separate mage target, modeled on the SPDX license generation (
magefiles/spdx.go, targetSPDX.UpdateLicenseEntries): the target downloadscryptography-defs.json, selects the needed fields, and writes a committed data file intopkg/cryptothat is wired in via//go:embedand read at startup. Generation instead of manual entry guarantees thatprimitiveand curve values match the schema enum and do not drift from it on updates.Record structure
Matching (OID + parsed object → algorithm component)
Family,Primitive,Hash.id-ecPublicKeyby itself does not define the key's purpose (signing or agreement), soprimitivestaysunknownand the curve is kept; ECDSA is not assigned without evidence of purpose. Determining ECDSA/ECDH viaKeyUsageis deferred to phase 2 (section 10).paramSetbyParamFrom:rsaModulusBits— the modulus size from the key ("2048","4096");ecNamedCurve— the enum curve value fromcurveEnumbypub.Curve.Params().Name(stdlib does not expose the raw namedCurve OID — it resolves it to anelliptic.Curve);none— empty (the parameter is already in the OID itself, as with EdDSA).ID = crypto:algorithm:<OID>[:<paramSet>](section 6).paramSetdistinguishes by strength only those components whose parameter lies outside the OID (RSA size, EC curve); a signature OID (ecdsa-with-SHA256, etc.) already distinguishes the hash itself, so itsparamSetis empty.Nameis assembled from the family template in the official layer with known parts substituted (e.g.RSA-2048,ECDSA-P-256-SHA-256).Unknown OID
Parsing is not aborted: the OID is not found in the table — the component is created minimally (the OID itself,
primitive: unknown),paramSetempty. The same for a curve absent fromcurveEnum: theellipticCurvefield stays empty rather than being filled with a value invalid for the enum.Initial set (phase 1)
Public-key algorithms (
SubjectPublicKeyInfo):plength)The set corresponds to the four
x509.PublicKeyAlgorithmvalues stdlib recognizes (RSA, DSA, ECDSA, Ed25519). Ed448 (1.3.101.113) is out of the set — Go does not support it.Signature algorithms (
Certificate.SignatureAlgorithm):Named curves (
curveEnum):nist/P-256,nist/P-384,nist/P-521.The set covers the material that stdlib (
crypto/x509) actually parses in phase 1; extending it comes down to adding rows to the tables.5. Weak and deprecated crypto detection
Deferred out of phase 1. This also includes the
classicalSecurityLevelandnistQuantumSecurityLevelfields (section 4) and the(family, size/curve) → strength levellookup table on which they and the detection rules (SHA-1, RSA < 2048, weak curves, expired certificates) are built.6. Internal data model
The analyzer returns
[]CryptoAsset— an intermediate representation that is then translated into a BOM component (section 8). One file yields several assets (certificate + algorithm(s) + key) that are merged byIDat the encoding stage. The type:The field composition of
CertProps,KeyProps, andAlgoPropsis defined below, and their semantics are described in section 4 ("Extracted data").Identity scheme (
ID)The
IDserves both as the deduplication key and (at encode time) as thebom-refvalue. It is derived from content, so the same asset from different files and layers is merged into one component, and relationships resolve across files. Formula by type:IDcertificatecrypto:certificate:<sha256(DER)>related-crypto-material(key)crypto:related-crypto-material:<private|public>:<sha256(SPKI)>sha256(SPKI). Theprivate/publicmarker keeps them distinct assets — otherwise the presence of a private key would be masked by the public one.algorithmcrypto:algorithm:<OID>[:<paramSet>]namedCurvein the parameters (RFC 5480 §2.1.1), while theAlgorithmIdentifieritself is identical for RSA-2048 and RSA-4096 (or P-256 and P-384) (RFC 5280 §4.1.1.2). So we addparamSet— the normalized key size or curve name — to theID; otherwise algorithms of different strength would merge into one component. For EdDSA the parameter is already in the OID (Ed25519 ≠ Ed448), soparamSetis empty.Two special cases of the scheme:
ID = crypto:related-crypto-material:private:enc:<sha256(EncryptedPrivateKeyInfo)>(see "Encrypted private keys" in section 4).paramSetis taken from the curve name:crypto:algorithm:1.2.840.10045.2.1:nist/P-256.A filled example (a self-signed certificate):
Through
Relatedthe certificate references two more assets — the signature algorithm (ID = crypto:algorithm:<OID>[:paramSet]) and the related-crypto-material with the public key (ID = crypto:related-crypto-material:public:<sha256(SPKI)>); the public key, in turn, references its own key algorithm. Each merges with same-named assets from other files (see "Identity scheme").The types are neutral (no cyclonedx-go): they are reused by both the findings layer and
core.Component(as it already reusesftypes.PkgIdentifier), and only the cdx marshaler translates them tocdx.CryptoProperties. This keepsfanal/types(imported by hundreds of packages) and the neutralsbom/coremodel independent of the output format.Required changes:
CryptoAsset,CryptoProps(CertProps/KeyProps/AlgoProps,Related),CryptoRelationinpkg/fanal/types;[]CryptoAssetfield inAnalysisResult(+ handling inMerge/isEmpty),ArtifactDetail,types.Result(the chain — section 9);CryptoProperties *ftypes.CryptoPropsfield incore.Component(sbom/core) — reusing the neutral type fromftypes(section 8).7. Provenance and attribution
An image or filesystem sees the whole tree, not only the user's artifacts, so every asset carries its path (occurrence) and is tagged with a layer.
Layer attribution
Trivy tags files by layer (
DiffID). For packages the layer is a component property (aquasecurity:trivy:LayerDiffID/LayerDigest:sbom/io/encode.go→decode.go), and a single such property is enough because a package component is bound to one layer — packages are not merged by content across layers. Crypto assets, by contrast, are merged byIDacross layers (section 6: a CA bundle of 130 certificates in the base and the user layer → 130 components, not 260), so one component has several layers and a single component property cannot hold them. Therefore the layer is a property of the occurrence: in CycloneDX that isEvidenceOccurrence.AdditionalContext(a field next tolocation, per-occurrence). The inventory stays complete, and the consumer filters by layer (base image / user). The alternative — not merging across layers and repeating the package scheme — was rejected: it brings back the very duplicates the content-derivedIDwas introduced to remove.Noise
/etc/ssl/certs/..., 130+ public roots) — the main source of noise. Such certificates are detected and marked as a system trust store but are not removed from the report; the criterion is in the subsection below.testdata/, sample certificates inside packages) — see the open question in section 11.Detecting the system trust store
A system root is a root CA that lives in the OS trust store. Neither signal alone singles it out, so we emit both, and what we find is marked, not removed — the threshold is left to the consumer.
BasicConstraints: CA:TRUEand self-signed (Issuer == Subject); an intermediate CA isCA:TRUEbut not self-signed; everything else is an end-entity (leaf) certificate. The signal is derived from the certificate itself, so it is path-independent, identical for all occurrences of a component (same DER → sameID), and works per certificate, including inside bundles. It answers "is this a root CA?", but not "is this the system store?": by content a public root is indistinguishable from a user's — a corporate internal CA is also self-signedCA:TRUE. The classification is computed in the extractor fromBasicConstraintsandIssuer/Subject. CycloneDX has no dedicated role field — it is derived from already-emitted standard fields (basicConstraintsincertificateExtensions,subjectNamevsissuerName) — so the marshaler duplicates it as a ready component propertyaquasecurity:trivy:CertificateRole(root-ca|intermediate-ca|leaf), the same mechanism as package properties: the consumer filters by a single property instead of parsing extensions itself./etc/ssl/certs,/etc/pki/tls/certs,/etc/pki/ca-trust,/usr/share/ca-certificates,/usr/local/share/ca-certificates) and typical bundle names (ca-certificates.crt,ca-bundle.crt,tls-ca-bundle.pem). The path is already inoccurrence.location, so no separate field is introduced.Trivy only exposes both attributes; the "this is noise" decision is made by the report consumer, combining them for their own task: a typical filter is role
root-caand a trust-store path, which drops the 130+ public roots while keeping a user root that lies outside the system directories. Neutrality is needed because "noise" depends on the task: when auditing your own CA the roots are wanted, when hunting for weak end-entity certificates they get in the way. Whether the marked roots stay in the output by default — and the flag that implies — is left open (section 11).8. CycloneDX output mapping
cyclonedx-go v0.11.0(Trivy emits CycloneDX 1.7) already contains the needed types —cdx.CryptoProperties(CertificateProperties,RelatedCryptoMaterialProperties,AlgorithmProperties) and thecdx.ComponentTypeCryptographicAssetcomponent type; no spec upgrade is required.The CBOM is not a separate document: crypto assets become additional
components(typecryptographic-asset) in the same CycloneDX report as the SBOM.Mapping chain
A crypto asset goes through the same path as the other BOM components — the
sbom/coredomain model and the marshaler. Three places need to be added:pkg/sbom/core/bom.go— thecore.Componentdomain model currently has no field for cryptography. Add:CryptoProperties *ftypes.CryptoPropsfield (the neutral type fromftypes) tocore.Component;core.ComponentType—TypeCryptographicAsset.pkg/sbom/io/encode.go—Encodebuilds acore.BOMfromreport.Results. Add anencodeCryptoAssetsbranch:CryptoAsset.ID→ onecore.Componentper unique ID; each sourceCryptoAsset(its ownFilePath+Layer) yields its ownevidence.occurrences[]:location = FilePath, the layer goes intoAdditionalContext(section 7), i.e. per occurrence, not per component;component.PkgIdentifier.BOMRef = asset.ID(the marshaler writesbom-reffrom this field,marshal.go:126);component.CryptoProperties = &asset.Props— the neutral type is carried over whole, together withProps.Related;dependsOn, see section 7) — via a separate mechanism through relationships incore.BOM.pkg/sbom/cyclonedx/marshal.go:componentType()— addcore.TypeCryptographicAsset → cdx.ComponentTypeCryptographicAsset;MarshalComponent()— translatecomponent.CryptoProperties(ftypes.CryptoProps) intocdx.CryptoProperties:CertProps/KeyProps/AlgoProps→ the corresponding nested objects,Related→relatedCryptographicAssets(ref = target.ID). This is the only place where the neutral model meets the cdx types.The marshaler writes
relatedCryptographicAssetsreferences directly fromRelated— they do not go throughcomponentIDs(UUID→bom-ref), unlike top-leveldependencies. Sorefmust literally match the target'sbom-ref; this is guaranteed byBOMRef = ID(step 2) andRelated.Ref = target.ID.SPDX does not support
cryptoProperties, so the mapping is implemented for CycloneDX only; the crypto field exists in the sharedsbom/coredomain model, but the SPDX marshaler ignores it.Component example
The
CryptoAssetfrom section 6 (a certificate) in CycloneDX:{ "type": "cryptographic-asset", "bom-ref": "crypto:certificate:ab12…", "name": "CN=example.com", "cryptoProperties": { "assetType": "certificate", "certificateProperties": { "subjectName": "CN=example.com", "issuerName": "CN=example.com", "notValidBefore": "2024-01-01T00:00:00Z", "notValidAfter": "2025-01-01T00:00:00Z", "certificateFormat": "X.509", "fingerprint": { "alg": "SHA-256", "content": "ab12…" }, "relatedCryptographicAssets": [ { "type": "algorithm", "ref": "crypto:algorithm:1.2.840.113549.1.1.11" }, { "type": "publicKey", "ref": "crypto:related-crypto-material:public:cd34…" } ] } }, "evidence": { "occurrences": [{ "location": "etc/ssl/certs/server.crt" }] } }FilePathgoes intoevidence.occurrences[].location, the rest intocryptoProperties. The algorithm (crypto:algorithm:…) and the public key (crypto:related-crypto-material:public:…) are separate neighboring components of the same document, referenced byrelatedCryptographicAssets.9. Integration into analysis and scanning
The crypto analyzers are enabled only for the
imagetarget and thecyclonedxformat.A new
cryptoscanner type, available only for image and enabled manually: inNewImageCommandadd it to the allowed values of the--scannersflag but not to the default set (unlike k8s, where the default set is overridden):If the crypto scanner is not enabled,
disabledAnalyzersadds the crypto analyzers (listed inconst.go) toDisabledAnalyzers. For an image without thecyclonedxformat — a warning or error incheckOptions.In
pkg/fanal/analyzercreate acryptopackage, with the analyzers (analyzer.analyzer) in subpackages:Requiredselects by extension (section 3),Analyzeparses and returns crypto material. For files with no recognizable extension — an open question in section 11.The parser lives next to its analyzer; the shared extractors (X.509, keys), reused by several analyzers, are moved into the shared
pkg/cryptopackage. A sharedParser/Analyzeto cut boilerplate — see section 4.Registration is via blank-import: by analogy with
analyzer/config/all, create a nested aggregatoranalyzer/crypto/alland add it to the existinganalyzer/all/import.go(already imported inpkg/scan/local/service.go).Package tree:
Threading crypto assets to the report
The analysis result goes through the same chain as
Secrets/Licenses, but diverges at the end (crypto assets are BOM components, not findings):AnalysisResult(a new field, see section 6) — filled by the analyzer, handled inMerge/isEmpty.ftypes.ArtifactDetail— a new field; filled in the applier (pkg/fanal/applier/docker.go):ApplyLayerssets each asset'sLayer = {DiffID, Digest}for the layer it was found in (as for licenses/secrets), rather than looking up the layer by files as for packages. This is the layer of the specific occurrence; it is not reduced onto the shared component but goes into the occurrence at encode time (sections 7–8).types.Result— a new field; filled inpkg/scan/local/service.go.sbom/io/encode.goencoder builds components from this (section 8).10. Delivery stages
Out of phase 1
dependsOn) — a separate post-processing step, reconstructed imprecisely.id_*,*.pub,authorized_keys,known_hosts) — the nearest follow-up as a separate PR: high value, zero new dependencies (x/crypto/sshis already in the tree). Deferred for architectural independence (its own format, its own extractor, detection by file name), not for cost..p12/.pfx) — keystores. The built-inx/crypto/pkcs12library is frozen and too limited: it reads only a single certificate and key (not all container entries), does not support modern encryption (AES, the OpenSSL 3 default), and only certificate stores. Full support requires a new dependency,sslmate/go-pkcs12, so the format is deferred out of phase 1..jks/.keystore) — a separate PR: high value (Java/enterprise), low complexity, but requires a new external dependency (keystore-go). Phase 1 stays free of new dependencies..crl) — rare as a standalone file, and its cryptographic signal (the CA's signature algorithm) duplicates the CA certificate itself. Parsing is cheap (x509.ParseRevocationList), but the value is low..gpg/.pgp/.asc, keyrings) — common (apt/rpm), but requires a new dependency (x/crypto/openpgpdeprecated → theProtonMail/go-cryptofork) and a separate packet-based parser. Deferred for cost, not rarity.sha256WithRSAEncryptionsignature algorithm can be decomposed into two assets plus a relationship: SHA-256 as a hash primitive, RSA as a pke primitive, andsha256WithRSAEncryptionitself as a signature primitive referencing the first two. With the current focus on inventory rather than detection, decomposition is not required.securedBy) — in phase 1 an encrypted private key is inventoried asstate: encryptedonly; decomposing what protects it (the PBES2 scheme: KDF, PRF, cipher, iteration count) is deferred to phase 2. It is a strength signal, it is the only part that needs hand-rolled ASN.1 below stdlib's typed API, and it would require the registry to gain cipher/KDF OIDs that the phase-1 initial set does not have. Details below.KeyUsage— in phase 1 the EC key's purpose is not determined (primitive: unknown); determining signing vs. key agreement via the certificate'sKeyUsageis deferred to phase 2 as its first item, right after the inventory. Details below.ECDSA/ECDH split via
KeyUsageThe OID
1.2.840.10045.2.1(id-ecPublicKey, RFC 5480 §2.1.1) in SubjectPublicKeyInfo marks only "an elliptic-curve key", but does not indicate the purpose — signing (ECDSA) or key agreement (ECDH). In phase 1 the purpose is not determined:primitiveisunknown,familyis left empty, the curve is kept. Without confirmed purpose ECDSA is not assigned, so as not to attribute a wrong primitive to the key.Determining the purpose is deferred to phase 2 as its first item: until this step is done,
primitiveis empty for all EC keys, including the most common case — end-entity TLS certificates on the P-256 curve. It relies on the certificate'sKeyUsage:keyAgreement→ ECDH (key-agree),digitalSignature/keyCertSign→ ECDSA (signature); for a standalone key file, whereKeyUsageis absent, the purpose staysunknown. This is the only registry record whosefamilyandprimitiveare resolved not statically from the table but fromKeyUsagein the certificate extractor, so it is implemented in thex509extractor itself, without a separate file or analyzer.The split also affects identity: the SPKI OID is the same for ECDSA and ECDH, so the purpose is not reflected in the
IDby the OID alone. In phase 1 theIDis built on the plain OID (crypto:algorithm:1.2.840.10045.2.1:nist/P-256). In phase 2, so that ECDSA and ECDH components on the same curve do not merge into one with a contradictoryprimitive, theIDof a determined purpose is built from the family name:The key component itself (
related-crypto-materialbysha256(SPKI)) merges across files independently — only the algorithm reference differs.Encrypted-key protection (
securedBy)Without the password, the top-level
AlgorithmIdentifierof anEncryptedPrivateKeyInfois readable (for PBES2 — the KDF, its PRF, and the cipher), andsecuredByis filled from it. The model carries a singlemechanismand a singlealgorithmRef, so they are distributed like this: the cipher becomes a separatealgorithmcomponent referenced viaalgorithmRef, andmechanismholds a string with the whole scheme, including the KDF and PRF:The
mechanismstring format is an internal convention, not a standard: in CycloneDX it is free text with no grammar. It is assembled deterministically so the value is stable across scans: OIDs are run through anOID → nametable (an unknown OID is emitted as-is, as a dotted string), plusiterationCountas a strength signal. The example values are theopenssl genpkeydefault (HMAC-SHA1, 2048 iterations).The top-level
AlgorithmIdentifieris parsed with stdlib (encoding/asn1), without decryption and without external dependencies — theEncryptedPrivateKeyInfo/PBES2-params/PBKDF2-paramsstructures are described by hand:A subtlety with
prf: inPBKDF2-paramsthis field is declaredDEFAULT algid-hmacWithSHA1(RFC 8018, Appendix A.2), and by the DER rule (X.690 §11.5) a value equal to the default is omitted when encoding — so openssl does not write the field at all for HMAC-SHA1, and a spec-conformant decoder must treat its absence as HMAC-SHA1.encoding/asn1does not expandDEFAULTitself (it leaves the zero value), so the substitution in code is not a heuristic but a literal following of the RFC.x509.ParsePKCS8PrivateKeyis inapplicable here: it fails on an encrypted block.Because the cipher is emitted as a separate
algorithmcomponent, this step also requires the registry (section 4) to gain symmetric cipher and KDF OIDs (AES-256-CBC, PBKDF2, ...), which the phase-1 initial set does not include.11. Open questions
Filtering test paths. Should
testdata//testpaths (a frequent source of sample cryptography) be filtered out, and would that lose real assets?System trust store: default and flag. The detection criterion and marking approach are chosen (section 7); the default is not. Two options, and each implies a flag:
So the real question is which default; a flag is likely needed either way. Also open: whether the list of trust-store directories is complete across distributions.
Beta Was this translation helpful? Give feedback.
All reactions