feat: add cryptographic asset model and parser - #10970
Conversation
f9f77cf to
0e856fc
Compare
0e856fc to
fea60fe
Compare
| } | ||
|
|
||
| // Asset describes a format-neutral cryptographic asset. | ||
| type Asset struct { |
There was a problem hiding this comment.
Have you considered moving Asset and everything related to it into fanal/types?
All the fields in AnalysisResult are fields from ftypes:
trivy/pkg/fanal/analyzer/analyzer.go
Lines 180 to 186 in b7ce181
On top of that, we use
Asset as an analysis result, which also fits this change.The fields of
Asset and the other structs are exported, so there shouldn't be any problems with this move.I understand that the amount of code involved in this package is many times larger than for any type in
fanal/types, but in that case we could think about creating a subpackage.In addition, this would get rid of the circular import problem with
layers.
But I might have missed something.
| {name: "lowercase PEM", filePath: "certificates/server.pem", want: true}, | ||
| {name: "uppercase PEM", filePath: "certificates/server.PEM", want: true}, | ||
| {name: "mixed-case PEM", filePath: "certificates/server.PeM", want: true}, | ||
| {name: "lowercase DER", filePath: "certificates/server.der", want: true}, | ||
| {name: "uppercase DER", filePath: "certificates/server.DER", want: true}, | ||
| {name: "mixed-case DER", filePath: "certificates/server.DeR", want: true}, | ||
| {name: "lowercase CRT", filePath: "certificates/server.crt", want: true}, | ||
| {name: "uppercase CRT", filePath: "certificates/server.CRT", want: true}, | ||
| {name: "mixed-case CRT", filePath: "certificates/server.CrT", want: true}, | ||
| {name: "lowercase CER", filePath: "certificates/server.cer", want: true}, | ||
| {name: "uppercase CER", filePath: "certificates/server.CER", want: true}, | ||
| {name: "mixed-case CER", filePath: "certificates/server.CeR", want: true}, | ||
| {name: "lowercase KEY", filePath: "certificates/server.key", want: true}, | ||
| {name: "uppercase KEY", filePath: "certificates/server.KEY", want: true}, | ||
| {name: "mixed-case KEY", filePath: "certificates/server.KeY", want: true}, |
There was a problem hiding this comment.
We already have the tests for the Contains() function from set.NewCaseInsensitive.
So I think checking each ext once will be enough.
| }) | ||
|
|
||
| // Cryptographic assets | ||
| sort.SliceStable(r.CryptoAssets, func(i, j int) bool { |
There was a problem hiding this comment.
Could you explain in more detail why sort.SliceStable is needed here?
If we have assets with the same FilePath and Descriptor and we don't remove them as duplicates, then it might make sense to add more values for comparison.
I don't think we'll have enough entries to worry about the speed difference between sort.Slice and sort.SliceStable, but from a code consistency perspective, we could go with sort.Slice.
| case "", KeyFormatPKCS1, KeyFormatPKCS8, KeyFormatSEC1, KeyFormatPKIX: | ||
| default: | ||
| return xerrors.Errorf("unknown key format %q", a.Key.Format) | ||
| } | ||
| switch a.Key.Encoding { | ||
| case "", EncodingPEM, EncodingDER: |
There was a problem hiding this comment.
Are empty Format and Encoding values actually valid?
If I haven't missed anything, all the functions in the parser return both values, so an empty value isn't an expected case, and I'd return an error here.
| } | ||
| recognized = true | ||
| if err != nil { | ||
| logParseError(ctx, filePath, block.Type, err) |
There was a problem hiding this comment.
nit:
I checked the local cert file.
This file contains 128 certificates:
➜ cat /etc/ssl/cert.pem | grep "BEGIN CERTIFICATE" | wc -l
128If we assume there can be similar files with problems (with the new errors) for most of the certificates, log messages for each object can be noisy.
Perhaps we can show a single log for the file or, as we did in similar cases, a single info/warn log for the file and debug logs for each object.
| } | ||
|
|
||
| // No PEM block was decoded, so try the whole file as DER. | ||
| object, err := parseDERObject(content) |
There was a problem hiding this comment.
nit:
I don't really like the asymmetry we ended up with.
For PEM, everywhere in parsePEMObject we set cryptotypes.EncodingDER, but then we overwrite it in parsePEMBlock().
For DER, on the other hand, we take the already set value.
What if we make a symmetric function parseDERBlock() and change the encoding value there, removing it from the downstream functions?
| "EC PARAMETERS", | ||
| "DH PARAMETERS", |
There was a problem hiding this comment.
nit:
| "EC PARAMETERS", | |
| "DH PARAMETERS", | |
| "EC PARAMETERS", | |
| "DH PARAMETERS", | |
| "DSA PARAMETERS", |
|
|
||
| // MarshalJSON validates and encodes the descriptor as its canonical string. | ||
| func (d Descriptor) MarshalJSON() ([]byte, error) { | ||
| if err := d.Validate(); err != nil { |
There was a problem hiding this comment.
Validation on unmarshaling makes sense, but I'm not sure it's worth doing validation on marshaling.
It's better to validate when creating the descriptor.
|
|
||
| // Asset describes a format-neutral cryptographic asset. | ||
| type Asset struct { | ||
| descriptor *Descriptor |
There was a problem hiding this comment.
Maybe I don't see "the whole picture":
In this case we end up with duplication of the Kind, KeyType and Identity fields. One set is in the Asset itself, and another one is in the descriptor.
IIUC the main point is to fix the descriptor and its hash as an immutable object, but:
asset.Kind(and the other fields) can still be computed (e.g. after cloning), and then there will be a discrepancy;- we still build the string (
Descriptor.String()) on every call, which means we spend resources on computing the string anyway.
Since I don't fully understand the motivation behind this decision, let's discuss whether we can get rid of this field duplication.
| left := r.CryptoAssets[i].Descriptor().String() | ||
| right := r.CryptoAssets[j].Descriptor().String() |
There was a problem hiding this comment.
We call Descriptor().String() for every element, and each time we build the strings (using url.QueryEscape).
Maybe we could store the computed value for the object?
Related to !!!link comment.
| ) | ||
|
|
||
| // Certificate contains certificate-specific metadata. | ||
| type Certificate struct { |
There was a problem hiding this comment.
Should we add encoding field (as for Key), because we save this field in Parser:
https://github.com/knqyf263/trivy/blob/fea60fea765eede5c73cda310541f2e1d44bd016/pkg/crypto/parser/x509/parser.go#L119-L128
| if a.Certificate.Format != CertificateFormatX509 { | ||
| return xerrors.Errorf("unknown certificate format %q", a.Certificate.Format) | ||
| } | ||
| if a.Certificate.MaxPathLen < 0 { |
There was a problem hiding this comment.
IIUC possible -1 value.
https://github.com/golang/go/blob/c122d7e6c50f8f1950a99b8e7b2b36e8a54cffce/src/crypto/x509/x509.go#L825-L832
So we should check this value or check MaxPathLen + MaxPathLenZero.
| Kind Kind `json:",omitempty"` | ||
| KeyType KeyType `json:",omitempty"` | ||
| Identity Identity `json:",omitzero"` | ||
| Name string `json:",omitempty"` |
There was a problem hiding this comment.
If I understand correctly, we'll use Name as component.name in CycloneDX.
But where will we build it? In the Analyzer?
Description
Add the format-neutral cryptographic asset foundation for container image CBOM support.
This PR:
pkg/cryptoThe parser returns only public metadata for plaintext private keys and does not retain private-key material in its results. Encrypted PKCS#8 containers are treated as opaque assets and identified by their SHA-256 digest.
This PR does not add user-facing scanner behavior. Analyzer and extractor integration, scanner registration, and CycloneDX output will be implemented in follow-up PRs.
Related issues
Related discussion
Checklist