-
Notifications
You must be signed in to change notification settings - Fork 377
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.
@@ -167,6 +170,22 @@ type prSigstoreSignedFulcio struct { | |||
SubjectEmail string `json:"subjectEmail,omitempty"` | |||
} | |||
|
|||
// prSigstoreSignedPKI contains non-fulcio certificate PKI configuration options for prSigstoreSigned | |||
type prSigstoreSignedPKI struct { |
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.
There should be some public type callers can use to refer to the value. Following the other examples here, probably an interface with a private method, and public functions to construct it (NewPRSigstoreSignedPKI
etc.) using the “functional option” pattern.
signature/policy_types.go
Outdated
CAIntermediatesPath string `json:"caIntermediatesPath"` | ||
// CAIntermediatesData contains accepted CA intermediate certificates in PEM format, all of that base64-encoded. Only one of CAIntermediatesPath or CAIntermediatesData can be specified, not both. | ||
CAIntermediatesData []byte `json:"caIntermediatesData"` | ||
// SubjectEmail specifies the expected email address imposed on the subject to which the certificate was issued. Exactly one of SubjectEmail and Hostname 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.
(Nit: A blank line above would separate the two kinds of fields.)
signature/policy_types.go
Outdated
// SubjectEmail specifies the expected email address imposed on the subject to which the certificate was issued. Exactly one of SubjectEmail and Hostname must be specified. | ||
SubjectEmail string `json:"subjectEmail"` | ||
// Hostname specifies the expected hostname imposed on the subject to which the certificate was issued. Exactly one of SubjectEmail and Hostname must be specified. | ||
Hostname string `json:"hostname"` |
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.
Hostname string `json:"hostname"` | |
SubjectHostname string `json:"subjectHostname"` |
d558162
to
b3a0606
Compare
Signed-off-by: Qi Wang <[email protected]>
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.
untrustedIntermediatePool := x509.NewCertPool() | ||
if len(untrustedIntermediateChainBytes) > 0 { | ||
untrustedIntermediateChain, err := cryptoutils.UnmarshalCertificatesFromPEM(untrustedIntermediateChainBytes) | ||
if err != nil { | ||
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading certificate chain: %v", err)) | ||
} | ||
if len(untrustedIntermediateChain) > 1 { | ||
for _, untrustedIntermediateCert := range untrustedIntermediateChain { | ||
if isIntermediateCA(untrustedIntermediateCert) { | ||
untrustedIntermediatePool.AddCert(untrustedIntermediateCert) | ||
} | ||
} | ||
} | ||
} | ||
|
||
if len(pkiTrustRoot.caIntermediateCertificates) > 0 { | ||
trustedIntermediates, err := cryptoutils.UnmarshalCertificatesFromPEM(pkiTrustRoot.caIntermediateCertificates) | ||
if err != nil { | ||
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading trusted intermediate certificates: %v", err)) | ||
} | ||
for _, trustedIntermediateCert := range trustedIntermediates { | ||
untrustedIntermediatePool.AddCert(trustedIntermediateCert) | ||
} | ||
} |
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.
I think this can all happen in prSigstoreSignedPKI.prepareTrustRoot
:
- With incorrect configurations we don’t even start parsing the high-risk signature itself.
- It’s a long-term goal to store the trust root data in
PolicyContext
instead of doing that work on every single signature verification. (That’s certainly not this PR, just that structuring the code to make that possible would be nice.)
} | ||
|
||
// isIntermediateCA checks if the certificate is an intermediate CA | ||
func isIntermediateCA(cert *x509.Certificate) bool { |
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.
It’s not immediately clear to me that this is necessary, I’d expect the crypto/x509
code to validate the intermediates are usable. Is there a specific reason to filter them this way? If so, it would be nice to document that.
case trustRoot.publicKeys != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations. | ||
return sarRejected, errors.New("Internal inconsistency: Both a public key and Fulcio CA specified") | ||
case trustRoot.publicKeys == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations. | ||
return sarRejected, errors.New("Internal inconsistency: Neither a public key nor a Fulcio CA specified") | ||
case trustRoot.publicKeys != nil && trustRoot.pki != nil: // newPRSigstoreSigned rejects such combinations. | ||
return sarRejected, errors.New("Internal inconsistency: Both a public key and PKI specified") | ||
case trustRoot.fulcio != nil && trustRoot.pki != nil: // newPRSigstoreSigned rejects such combinations. | ||
return sarRejected, errors.New("Internal inconsistency: Both Fulcio CA and PKI specified") | ||
case trustRoot.publicKeys == nil && trustRoot.fulcio == nil && trustRoot.pki == nil: // newPRSigstoreSigned rejects such combinations. | ||
return sarRejected, errors.New("Internal inconsistency: A public key, Fulcio, or 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.
This O(N^2) item list is getting large — maybe switching this to check counting keySources == 1
would be easier to follow.
return nil, err | ||
} | ||
if caIntermediatesCertPEMs != nil { | ||
pki.caIntermediateCertificates = caIntermediatesCertPEMs[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.
A sanity check that len() == 1
would be nice here.
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. |
OCPNODE-2338