-
Notifications
You must be signed in to change notification settings - Fork 386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
policy.json BYOPKI signature verification API #2579
base: main
Are you sure you want to change the base?
Conversation
Hi @mtrmac , I’ve created this draft PR for the policy.json API change, could you take a look and provide your initial thoughts on the structure of the API update? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Looks good overall.
Earlier discussion openshift/enhancements#1658 .
Doing both the API and implementation in one PR works for me, having the API available but broken is not really helping anything.
b3a0606
to
f8585cd
Compare
@mtrmac Could you please take a look? It's ready for review. I am unsure about how I’m handling the intermediate certificates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few-minute first pass — this looks very good. I didn’t read this carefully, and I skipped the tests completely, for now.
Combining the policy-provided and signature-provided intermediate certificates makes sense to me, but I’ll at least check what Cosign does.
Cc: @Honny1 as well — c/image/signature is a somewhat separate part of the codebase, and as security-critical with a lot of paranoia. Worth understanding the structure and idioms. |
f8585cd
to
f70f17a
Compare
@mtrmac Do you think the PKI code needs a file like fulcio_cert_stub.go and should include build tags? I'm not fully clear on the purpose of this compile configuration. |
The Here, the only similar dependency seems to be |
I checked some cosign code, maybe I got lost in the code path, I did see it process the signature-provided intermediate certificates 🤔 |
@mtrmac could you review? |
f70f17a
to
897b425
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just two nits, otherwise LGTM
897b425
to
f020a87
Compare
@containers/image-maintainers can we get this merged? |
@mtrmac Could you review this PR? |
f020a87
to
c9ea5b9
Compare
Rebased with the base branch. |
now tracked in RUN-2436 |
@QiWang19 could you rebase it again? (Maybe this would help to get this merged 😄 ) |
Signed-off-by: Qi Wang <[email protected]>
c9ea5b9
to
5e5e968
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very good overall, ~1.5 comment on the substance of the code; almost all are about testing or documentation.
@@ -350,6 +358,11 @@ Both `oidcIssuer` and `subjectEmail` are mandatory, | |||
exactly specifying the expected identity provider, | |||
and the identity of the user obtaining the Fulcio certificate. | |||
|
|||
If `pki` is present, the signature must be based on a non-Fulcio X.509 certificate. | |||
One of `caRootsPath` and `caRootsData` must be specified, containing the public key of the CA. | |||
Only one of `caIntermediatesPath` and `caIntermediatesData` can be present, containing the public key of the intermediate CA. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn’t this
Only one of `caIntermediatesPath` and `caIntermediatesData` can be present, containing the public key of the intermediate CA. | |
Only one of `caIntermediatesPath` and `caIntermediatesData` can be present, containing certificates of intermediate CAs. |
?
If `pki` is present, the signature must be based on a non-Fulcio X.509 certificate. | ||
One of `caRootsPath` and `caRootsData` must be specified, containing the public key of the CA. | ||
Only one of `caIntermediatesPath` and `caIntermediatesData` can be present, containing the public key of the intermediate CA. | ||
One of `subjectEmail` and `subjectHostname` must be specified, exactly specifying the expected identity provider, and the identity of the user obtaining the certificate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of `subjectEmail` and `subjectHostname` must be specified, exactly specifying the expected identity provider, and the identity of the user obtaining the certificate. | |
One of `subjectEmail` and `subjectHostname` must be specified, exactly specifying the expected identity of the user obtaining the certificate. |
and the latter part perhaps “the expected subject of the certificate”, or “expected identity to which the certificate was issued”.
@@ -407,6 +420,18 @@ selectively allow individual transports and scopes as desired. | |||
"rekorPublicKeyPath": "/path/to/rekor.pub", | |||
} | |||
], | |||
/* A Sigstore-signed repository using a certificate generated by the Bring Your Own Public Key Infrastructure (BYOPKI).*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like
/* A Sigstore-signed repository using a certificate generated by the Bring Your Own Public Key Infrastructure (BYOPKI).*/ | |
/* A Sigstore-signed repository using a certificate generated by a custom public-key infrastructure. */ |
? I’m not sure that BYOPKI is a widely-used term, and by its nature there is no “the” BYOPKI, every user has its own.
@@ -120,7 +120,7 @@ type prSigstoreSigned struct { | |||
// KeyDatas is a set of trusted keys, base64-encoded. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified. | |||
KeyDatas [][]byte `json:"keyDatas,omitempty"` | |||
|
|||
// Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified. | |||
// Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas, Fulcio, and PKI must be specified. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also the comments for KeyPath
, KeyPaths
, KeyData
, KeyDatas
need to be updated.
@@ -141,6 +141,9 @@ type prSigstoreSigned struct { | |||
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified). | |||
RekorPublicKeyDatas [][]byte `json:"rekorPublicKeyDatas,omitempty"` | |||
|
|||
// PKI specifies which PKI-generated certificates are accepted. Exactly one of KeyPath, KeyData, Fulcio, PKI must be specified. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also KeyPaths
, KeyDatas
.
untrustedLeafCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(untrustedCertificateBytes) | ||
if err != nil { | ||
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("parsing leaf certificate: %v", err)) | ||
} | ||
switch len(untrustedLeafCerts) { | ||
case 0: | ||
return nil, internal.NewInvalidSignatureError("no certificate found in signature certificate data") | ||
case 1: | ||
break // OK | ||
default: | ||
return nil, internal.NewInvalidSignatureError("unexpected multiple certificates present in signature certificate data") | ||
} | ||
untrustedCertificate := untrustedLeafCerts[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The failure cases should have tests, similar to the ones in fulcio_cert.go
.
Alternatively (preferably?) extract the code+test in fulcio_cert.go
into a separate function, and share the code.
if pkiTrustRoot.subjectEmail != "" { | ||
if !slices.Contains(untrustedCertificate.EmailAddresses, pkiTrustRoot.subjectEmail) { | ||
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %q not found (got %q)", | ||
pkiTrustRoot.subjectEmail, | ||
untrustedCertificate.EmailAddresses)) | ||
} | ||
} | ||
|
||
if pkiTrustRoot.subjectHostname != "" { | ||
if err = untrustedCertificate.VerifyHostname(pkiTrustRoot.subjectHostname); err != nil { | ||
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Unexpected subject hostname: %v", err)) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should establish trust in the certificate contents (that it is correctly signed) first, only then it makes sense to read any of the data fields.
So, (roots are done); intermediates first; individual certificate next; validate certificate against root+intermediates; then read contents.
} | ||
} | ||
|
||
var trustedAndUntrustedIntermediatePool *x509.CertPool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See elsewhere about “trusted intermediates”.
Anyway, trusted+untrusted = untrusted, so naming this untrustedIntermediatePool
would more accurately convey the risk.
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading certificate chain: %v", err)) | ||
} | ||
if len(untrustedIntermediateChain) > 1 { | ||
for _, untrustedIntermediateCert := range untrustedIntermediateChain { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Fulcio code only processes [:len(untrustedIntermediateChain)-1]
(because the last one is supposed to be the root which we already have), doesn’t it make sense to parse the same annotation the same way here?
Otherwise the len()>1
could cause us to ignore single-element chains for no clear reason.
}, certBytes, chainBytes) | ||
require.NoError(t, err) | ||
assertPublicKeyMatchesCert(t, certBytes, pk) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should test more failures, similar to the existing Fulcio code:
- Failures to parse intermediates
- Failure with intermediates with neither signature nor config, success with intermediates in signature only / config only
- Failures to extract the leaf certificate (or perhaps extract that into a helper, see elsewhere)
- For both email and subject: missing SAN entry; multiple entries, one matches; one entry, mismatch; multiple entries, all mismatches
I think this is for I think the relevant code path for reading signatures from the image is https://github.com/sigstore/cosign/blob/737c83c71cd42ba8db843f84beadcd59b3aa6e0a/pkg/cosign/verify.go#L822-L841 . It is convoluted, but, effectively, if the configuration contains intermediates, the ones from the signature are ignored. I think that behavior is unintuitive — it means that adding more configuration (possibly 100% redundant configuration) can turn accepted signatures into rejected signatures. That code comes from sigstore/cosign#1804 , which effectively proposes to use TUF to stop accepting intermediate certificates without revoking them in any way. I don’t understand the use of intermediate certificates in this manner; it seems to me that shipping these intermediates “as trusted roots” would work well, then TUF could be used to ship a configuration update where the compromised certificate is simply dropped. For comparison, Go’s TLS implementation takes certificates only from the protocol data, it does not allow users to provide intermediates at all. For BYOPKI, I wouldn’t expect TUF to be used. If I’m not missing something subtle about this, let’s leave the behavior as is. |
OCPNODE-2338