This issue outlines how the core revocation functionality will be built into notation-core-go, and how it will be integrated with notation-go. This additional functionality will provide a mechanism to check the revocation status of certificates in a way that can be easily integrated into other projects, and to conduct revocation checks as part of the Verify process.
These changes will address the following issue:
- Implement OCSP Support (#124)
CRL Support will not be implemented until later.
The outlined mechanisms are based upon previously defined specifications (https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#certificate-revocation-evaluation) as well as some ideas from existing solutions (https://pkg.go.dev/github.com/cloudflare/cfssl/revoke and https://github.com/grpc/grpc-go/blob/52ca9571068d/security/advancedtls/crl.go) that don’t fully match our use case.
Implementation is planned to be completed in two PRs. The first will introduce the revocation package in notation-core-go. The second PR will follow, which will incorporate the revocation package into notation-go. These sections are separated by a horizontal line.
Notation-core-go
The following will be added as a new package within the x509 package via a new file, revocation.go:
// Internal struct
type revocation struct {
httpClient *http.Client
ocspTimeoutThreshold time.Duration
}
// Options struct
// Users can specify any options they want for constructing a revocation object
// If an option is unspecified or nil, a default value will be assigned in the NewRevocation function
type RevocationOptions struct {
ocspTimeoutThreshold time.Duration
}
// Constructor to return a revocation object that substitutes default values for any that are passed as nil
func NewRevocation(httpClient *http.Client, options RevocationOptions) *revocation
// Errors:
// RevokedInOCSPError is returned when the certificate's status for OCSP is ocsp.Revoked (or ocsp.Unknown?)
type RevokedInOCSPError struct{}
func (e RevokedInOCSPError) Error() string {
return "certificate is revoked via OCSP"
}
// CheckOCSPError is returned when there is an error during the OCSP revocation check, not necessarily a revocation
type CheckOCSPError struct {
Err error
}
func (e CheckOCSPError) Error() string {
if e.Err != nil {
return fmt.Sprintf("error checking revocation via OCSP: %s", e.Err.Error())
} else {
return "error checking revocation via OCSP"
}
}
// NoOCSPServerError is returned when the OCSPServer is not specified or is not an HTTP URL.
type NoOCSPServerError struct{}
func (e NoOCSPServerError) Error() string {
return "no valid OCSPServer found"
}
// OCSPTimeoutError is returned when the connection attempt to a OCSP URL exceeds the specified threshold
type OCSPTimeoutError struct{}
func (e OCSPTimeoutError) Error() string {
return "exceeded timeout threshold for OCSP check"
}
// It may be necessary to add other errors to the ones specified as new errors/results are discovered during development
// Methods:
// Checks OCSP, then CRL status, returns nil if all certs in the chain are not revoked
// If any are revoked, it will return one of the defined errors in this file
// If there is some other error, it will return that error
// NOTE: Will only perform OCSP until CRL is implemented
func (r *revocation) Validate(certChain []*x509.Certificate) (error)
// Only checks OCSP, same returns as above
func (r *revocation) OCSPStatus(certChain []*x509.Certificate) (error)
Since testing this will require validating against OCSP server responses, a mock client needs to be created to mock these responses. This will be added to the testhelper package in a new file named httptest.go:
// Creates a mock HTTP Client (more accurately, a spy) that intercepts requests to /issuer and /ocsp endpoints
// The client's responses are dependent on the cert and desiredOCSPStatus
func MockClient(cert *x509.Certificate, key *rsa.PrivateKey, desiredOCSPStatus ocsp.ResponseStatus) *http.Client
In addition to the mocked client, we will also need a new test certificate that specifies an OCSPServer in the certificate template. This value will be necessary to confirm revocation status with the mocked client. The following will be added to the testhelper package in the existing certificatetest.go file:
var revokableRSALeaf RSACertTuple
// GetRevokableRSALeafCertificate returns leaf certificate that specifies a local OCSPServer signed using RSA algorithm
func GetRevokableRSALeafCertificate() RSACertTuple
Notation-go
The following will be added/modified in the verifier package within the existing verifier.go file:
// Since a section has already been added to Verify as a placeholder for revocation, it will be modified to incorporate the verifyRevocation function
func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts notation.VerifyOptions) (*notation.VerificationOutcome, error)
// The verifyRevocation function will use the revocation.Validate() function defined in notation-core-go to verify the revocation status
func verifyRevocation(outcome *notation.VerificationOutcome) *notation.ValidationResult
TypeRevocation has already been added to the ValidationTypes in the trustpolicy, so no change is needed there.