diff --git a/go.mod b/go.mod index b7bec727..9e27e3c8 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/fxamacker/cbor/v2 v2.7.0 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058 github.com/veraison/go-cose v1.1.0 golang.org/x/crypto v0.24.0 ) diff --git a/go.sum b/go.sum index 04011b06..b3cfbd4a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058 h1:FlGmQAwbf78rw12fXT4+9EkmD9+ZWuqH08v0fE3sqHc= +github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/internal/oid/oid.go b/internal/oid/oid.go new file mode 100644 index 00000000..18ccb937 --- /dev/null +++ b/internal/oid/oid.go @@ -0,0 +1,31 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oid + +import "encoding/asn1" + +// KeyUsage (id-ce-keyUsage) is defined in RFC 5280 +// +// Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.3 +var KeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} + +// ExtKeyUsage (id-ce-extKeyUsage) is defined in RFC 5280 +// +// Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.12 +var ExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} + +// Timestamping (id-kp-timeStamping) is defined in RFC 3161 2.3 +// +// Reference: https://datatracker.ietf.org/doc/html/rfc3161#section-2.3 +var Timestamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} diff --git a/internal/timestamp/testdata/TimeStampToken.p7s b/internal/timestamp/testdata/TimeStampToken.p7s new file mode 100644 index 00000000..c036aac2 Binary files /dev/null and b/internal/timestamp/testdata/TimeStampToken.p7s differ diff --git a/internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s b/internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s new file mode 100644 index 00000000..5da09e0b Binary files /dev/null and b/internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s differ diff --git a/internal/timestamp/testdata/TimeStampTokenWithInvalidTSTInfo.p7s b/internal/timestamp/testdata/TimeStampTokenWithInvalidTSTInfo.p7s new file mode 100644 index 00000000..153ea92f Binary files /dev/null and b/internal/timestamp/testdata/TimeStampTokenWithInvalidTSTInfo.p7s differ diff --git a/internal/timestamp/testdata/tsaRootCert.crt b/internal/timestamp/testdata/tsaRootCert.crt new file mode 100644 index 00000000..3492b955 Binary files /dev/null and b/internal/timestamp/testdata/tsaRootCert.crt differ diff --git a/internal/timestamp/timestamp.go b/internal/timestamp/timestamp.go new file mode 100644 index 00000000..bfb4ce89 --- /dev/null +++ b/internal/timestamp/timestamp.go @@ -0,0 +1,65 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package timestamp provides functionalities of timestamp countersignature +package timestamp + +import ( + "crypto/x509" + + "github.com/notaryproject/notation-core-go/signature" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" +) + +// Timestamp generates a timestamp request and sends to TSA. It then validates +// the TSA certificate chain against Notary Project certificate and signature +// algorithm requirements. +// On success, it returns the full bytes of the timestamp token received from +// TSA. +// +// Reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signature-specification.md#leaf-certificates +func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byte, error) { + tsaRequest, err := tspclient.NewRequest(opts) + if err != nil { + return nil, err + } + ctx := req.Context() + resp, err := req.Timestamper.Timestamp(ctx, tsaRequest) + if err != nil { + return nil, err + } + token, err := resp.SignedToken() + if err != nil { + return nil, err + } + info, err := token.Info() + if err != nil { + return nil, err + } + timestamp, err := info.Validate(opts.Content) + if err != nil { + return nil, err + } + tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{ + CurrentTime: timestamp.Value, + Roots: req.TSARootCAs, + }) + if err != nil { + return nil, err + } + if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil { + return nil, err + } + return resp.TimestampToken.FullBytes, nil +} diff --git a/internal/timestamp/timestamp_test.go b/internal/timestamp/timestamp_test.go new file mode 100644 index 00000000..6c1da88f --- /dev/null +++ b/internal/timestamp/timestamp_test.go @@ -0,0 +1,200 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package timestamp + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/asn1" + "errors" + "os" + "strings" + "testing" + + "github.com/notaryproject/notation-core-go/signature" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" + "github.com/notaryproject/tspclient-go/pki" +) + +const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" + +func TestTimestamp(t *testing.T) { + rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.crt") + if err != nil || len(rootCerts) == 0 { + t.Fatal("failed to read root CA certificate:", err) + } + rootCert := rootCerts[0] + rootCAs := x509.NewCertPool() + rootCAs.AddCert(rootCert) + + // --------------- Success case ---------------------------------- + timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) + if err != nil { + t.Fatal(err) + } + req := &signature.SignRequest{ + Timestamper: timestamper, + TSARootCAs: rootCAs, + } + opts := tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA256, + } + _, err = Timestamp(req, opts) + if err != nil { + t.Fatal(err) + } + + // ------------- Failure cases ------------------------ + opts = tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA1, + } + expectedErr := "malformed timestamping request: unsupported hashing algorithm: SHA-1" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{}, + TSARootCAs: rootCAs, + } + opts = tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA256, + NoNonce: true, + } + expectedErr = "failed to timestamp" + _, err = Timestamp(req, opts) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("expected error message to contain %s, but got %v", expectedErr, err) + } + + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + respWithRejectedStatus: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "invalid timestamping response: invalid response with status code 2: rejected" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + invalidTSTInfo: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:24 set:false omitEmpty:false} Time @89" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + opts = tspclient.RequestOptions{ + Content: []byte("mismatch"), + HashAlgorithm: crypto.SHA256, + NoNonce: true, + } + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + failValidate: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "invalid TSTInfo: mismatched message" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + opts = tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA256, + NoNonce: true, + } + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + invalidSignature: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "failed to verify signed token: cms verification failure: crypto/rsa: verification error" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) +} + +func assertErrorEqual(expected string, err error, t *testing.T) { + if err == nil || expected != err.Error() { + t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) + } +} + +type dummyTimestamper struct { + respWithRejectedStatus bool + invalidTSTInfo bool + failValidate bool + invalidSignature bool +} + +func (d dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) { + if d.respWithRejectedStatus { + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusRejection, + }, + }, nil + } + if d.invalidTSTInfo { + token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidTSTInfo.p7s") + if err != nil { + return nil, err + } + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusGranted, + }, + TimestampToken: asn1.RawValue{ + FullBytes: token, + }, + }, nil + } + if d.failValidate { + token, err := os.ReadFile("testdata/TimeStampToken.p7s") + if err != nil { + return nil, err + } + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusGranted, + }, + TimestampToken: asn1.RawValue{ + FullBytes: token, + }, + }, nil + } + if d.invalidSignature { + token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidSignature.p7s") + if err != nil { + return nil, err + } + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusGranted, + }, + TimestampToken: asn1.RawValue{ + FullBytes: token, + }, + }, nil + } + return nil, errors.New("failed to timestamp") +} diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index d3def3c0..359ce7e9 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -36,11 +36,24 @@ import ( "golang.org/x/crypto/ocsp" ) +// Purpose is an enum for purpose of the certificate chain whose OCSP status +// is checked +type Purpose int + +const ( + // PurposeCodeSigning means the certificate chain is a code signing chain + PurposeCodeSigning Purpose = iota + + // PurposeTimestamping means the certificate chain is a timestamping chain + PurposeTimestamping +) + // Options specifies values that are needed to check OCSP revocation type Options struct { - CertChain []*x509.Certificate - SigningTime time.Time - HTTPClient *http.Client + CertChain []*x509.Certificate + CertChainPurpose Purpose // default value is `PurposeCodeSigning` + SigningTime time.Time + HTTPClient *http.Client } const ( @@ -64,8 +77,17 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { // Since this is using authentic signing time, signing time may be zero. // Thus, it is better to pass nil here than fail for a cert's NotBefore // being after zero time - if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil { - return nil, result.InvalidChainError{Err: err} + switch opts.CertChainPurpose { + case PurposeCodeSigning: + if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil { + return nil, result.InvalidChainError{Err: err} + } + case PurposeTimestamping: + if err := coreX509.ValidateTimestampingCertChain(opts.CertChain); err != nil { + return nil, result.InvalidChainError{Err: err} + } + default: + return nil, result.InvalidChainError{Err: fmt.Errorf("unknown certificate chain purpose %v", opts.CertChainPurpose)} } certResults := make([]*result.CertRevocationResult, len(opts.CertChain)) diff --git a/revocation/ocsp/ocsp_test.go b/revocation/ocsp/ocsp_test.go index 4afca12b..14d43df6 100644 --- a/revocation/ocsp/ocsp_test.go +++ b/revocation/ocsp/ocsp_test.go @@ -474,6 +474,7 @@ func TestCheckStatusErrors(t *testing.T) { noHTTPLeaf.OCSPServer = []string{"ldap://ds.example.com:123/chain_ocsp/0"} noHTTPChain := []*x509.Certificate{noHTTPLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert} + timestampSigningCertErr := result.InvalidChainError{Err: errors.New("timestamp signing certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 3,O=Notary,L=Seattle,ST=WA,C=US\" must have and only have Timestamping as extended key usage")} backwardsChainErr := result.InvalidChainError{Err: errors.New("leaf certificate with subject \"CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate")} chainRootErr := result.InvalidChainError{Err: errors.New("root certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\" is not self-signed. Certificate chain must end with a valid self-signed root certificate")} expiredRespErr := GenericError{Err: errors.New("expired OCSP response")} @@ -531,6 +532,38 @@ func TestCheckStatusErrors(t *testing.T) { } }) + t.Run("check codesigning cert with PurposeTimestamping", func(t *testing.T) { + opts := Options{ + CertChain: okChain, + CertChainPurpose: PurposeTimestamping, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, + } + certResults, err := CheckStatus(opts) + if err == nil || err.Error() != timestampSigningCertErr.Error() { + t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err) + } + if certResults != nil { + t.Error("Expected certResults to be nil when there is an error") + } + }) + + t.Run("check with unknwon CertChainPurpose", func(t *testing.T) { + opts := Options{ + CertChain: okChain, + CertChainPurpose: 2, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, + } + certResults, err := CheckStatus(opts) + if err == nil || err.Error() != "invalid chain: expected chain to be correct and complete: unknown certificate chain purpose 2" { + t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err) + } + if certResults != nil { + t.Error("Expected certResults to be nil when there is an error") + } + }) + t.Run("timeout", func(t *testing.T) { timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond} opts := Options{ diff --git a/revocation/revocation.go b/revocation/revocation.go index 287935bb..801a0780 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -35,16 +35,30 @@ type Revocation interface { // revocation is an internal struct used for revocation checking type revocation struct { - httpClient *http.Client + httpClient *http.Client + certChainPurpose ocsp.Purpose } -// New constructs a revocation object +// New constructs a revocation object for code signing certificate chain func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } return &revocation{ - httpClient: httpClient, + httpClient: httpClient, + certChainPurpose: ocsp.PurposeCodeSigning, + }, nil +} + +// NewTimestamp contructs a revocation object for timestamping certificate +// chain +func NewTimestamp(httpClient *http.Client) (Revocation, error) { + if httpClient == nil { + return nil, errors.New("invalid input: a non-nil httpClient must be specified") + } + return &revocation{ + httpClient: httpClient, + certChainPurpose: ocsp.PurposeTimestamping, }, nil } @@ -56,10 +70,12 @@ func New(httpClient *http.Client) (Revocation, error) { // https://github.com/notaryproject/notation-core-go/issues/125 func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { return ocsp.CheckStatus(ocsp.Options{ - CertChain: certChain, - SigningTime: signingTime, - HTTPClient: r.httpClient, + CertChain: certChain, + CertChainPurpose: r.certChainPurpose, + SigningTime: signingTime, + HTTPClient: r.httpClient, }) + // TODO: add CRL support // https://github.com/notaryproject/notation-core-go/issues/125 } diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index d6b2adb4..f9d4f4e5 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -99,6 +99,14 @@ func TestNew(t *testing.T) { } } +func TestNewTimestamp(t *testing.T) { + expectedErrMsg := "invalid input: a non-nil httpClient must be specified" + _, err := NewTimestamp(nil) + if err == nil || err.Error() != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, err) + } +} + func TestCheckRevocationStatusForSingleCert(t *testing.T) { revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() revokableIssuerTuple := testhelper.GetRSARootCertificate() @@ -459,6 +467,239 @@ func TestCheckRevocationStatusForChain(t *testing.T) { }) } +func TestCheckRevocationStatusForTimestampChain(t *testing.T) { + zeroTime := time.Time{} + testChain := testhelper.GetRevokableRSATimestampChain(6) + revokableChain := make([]*x509.Certificate, 6) + for i, tuple := range testChain { + revokableChain[i] = tuple.Cert + revokableChain[i].NotBefore = zeroTime + } + + t.Run("empty chain", func(t *testing.T) { + r, err := NewTimestamp(&http.Client{Timeout: 5 * time.Second}) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate([]*x509.Certificate{}, time.Now()) + expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} + if err == nil || err.Error() != expectedErr.Error() { + t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) + } + if certResults != nil { + t.Error("Expected certResults to be nil when there is an error") + } + }) + t.Run("check non-revoked chain", func(t *testing.T) { + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + getOKCertResult(revokableChain[2].OCSPServer[0]), + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check chain with 1 Unknown cert", func(t *testing.T) { + // 3rd cert will be unknown, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 revoked cert", func(t *testing.T) { + // 3rd cert will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 unknown and 1 revoked cert", func(t *testing.T) { + // 3rd cert will be unknown, 5th will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 future revoked cert", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + // 3rd cert will be future revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + getOKCertResult(revokableChain[2].OCSPServer[0]), + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 unknown and 1 future revoked cert", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + // 3rd cert will be unknown, 5th will be future revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 revoked cert before signing time", func(t *testing.T) { + // 3rd cert will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now().Add(time.Hour)) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 revoked cert after zero signing time", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + // 3rd cert will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) + if !zeroTime.IsZero() { + t.Errorf("exected zeroTime.IsZero() to be true") + } + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now().Add(time.Hour)) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) +} + func TestCheckRevocationErrors(t *testing.T) { leafCertTuple := testhelper.GetRSALeafCertificate() rootCertTuple := testhelper.GetRSARootCertificate() diff --git a/signature/cose/conformance_test.go b/signature/cose/conformance_test.go index 37d82243..72b25026 100644 --- a/signature/cose/conformance_test.go +++ b/signature/cose/conformance_test.go @@ -57,7 +57,7 @@ func TestConformance(t *testing.T) { // testSign does conformance check on COSE_Sign1_Tagged func testSign(t *testing.T, sign1 *sign1) { - signRequest, err := getSignReq(sign1) + signRequest, err := getSignReq() if err != nil { t.Fatalf("getSignReq() failed. Error = %s", err) } @@ -90,7 +90,7 @@ func testSign(t *testing.T, sign1 *sign1) { // testVerify does conformance check by decoding COSE_Sign1_Tagged object // into Sign1Message func testVerify(t *testing.T, sign1 *sign1) { - signRequest, err := getSignReq(sign1) + signRequest, err := getSignReq() if err != nil { t.Fatalf("getSignReq() failed. Error = %s", err) } @@ -124,7 +124,7 @@ func testVerify(t *testing.T, sign1 *sign1) { verifySignerInfo(&content.SignerInfo, signRequest, t) } -func getSignReq(sign1 *sign1) (*signature.SignRequest, error) { +func getSignReq() (*signature.SignRequest, error) { certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey) if err != nil { diff --git a/signature/cose/envelope.go b/signature/cose/envelope.go index 0c461d71..d5b7b247 100644 --- a/signature/cose/envelope.go +++ b/signature/cose/envelope.go @@ -24,8 +24,10 @@ import ( "time" "github.com/fxamacker/cbor/v2" + "github.com/notaryproject/notation-core-go/internal/timestamp" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/base" + "github.com/notaryproject/tspclient-go" "github.com/veraison/go-cose" ) @@ -76,7 +78,7 @@ const ( // Unprotected Headers // https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md const ( - headerLabelTimeStampSignature = "io.cncf.notary.timestampSignature" + headerLabelTimestampSignature = "io.cncf.notary.timestampSignature" headerLabelSigningAgent = "io.cncf.notary.signingAgent" ) @@ -234,10 +236,26 @@ func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } - // generate unprotected headers of COSE envelope + // generate unprotected headers of COSE envelope. generateUnprotectedHeaders(req, signer, msg.Headers.Unprotected) - // TODO: needs to add headerKeyTimeStampSignature. + // timestamping + if req.SigningScheme == signature.SigningSchemeX509 && req.Timestamper != nil { + hash, err := hashFromCOSEAlgorithm(signer.Algorithm()) + if err != nil { + return nil, &signature.TimestampError{Detail: err} + } + timestampOpts := tspclient.RequestOptions{ + Content: msg.Signature, + HashAlgorithm: hash, + } + timestampToken, err := timestamp.Timestamp(req, timestampOpts) + if err != nil { + return nil, &signature.TimestampError{Detail: err} + } + // on success, embed the timestamp token to Unprotected header + msg.Headers.Unprotected[headerLabelTimestampSignature] = timestampToken + } // encode Sign1Message into COSE_Sign1_Tagged object encoded, err := msg.MarshalCBOR() @@ -368,7 +386,10 @@ func (e *envelope) signerInfo() (*signature.SignerInfo, error) { signerInfo.UnsignedAttributes.SigningAgent = h } - // TODO: needs to add headerKeyTimeStampSignature. + // populate signerInfo.UnsignedAttributes.TimestampSignature + if timestamepToken, ok := e.base.Headers.Unprotected[headerLabelTimestampSignature].([]byte); ok { + signerInfo.UnsignedAttributes.TimestampSignature = timestamepToken + } return &signerInfo, nil } @@ -701,3 +722,17 @@ func generateRawProtectedCBORMap(rawProtected cbor.RawMessage) (map[any]cbor.Raw return headerMap, nil } + +// hashFromCOSEAlgorithm maps the cose algorithm supported by go-cose to hash +func hashFromCOSEAlgorithm(alg cose.Algorithm) (crypto.Hash, error) { + switch alg { + case cose.AlgorithmPS256, cose.AlgorithmES256: + return crypto.SHA256, nil + case cose.AlgorithmPS384, cose.AlgorithmES384: + return crypto.SHA384, nil + case cose.AlgorithmPS512, cose.AlgorithmES512: + return crypto.SHA512, nil + default: + return 0, fmt.Errorf("unsupported cose algorithm %s", alg) + } +} diff --git a/signature/cose/envelope_test.go b/signature/cose/envelope_test.go index e91697b3..b9f2c11c 100644 --- a/signature/cose/envelope_test.go +++ b/signature/cose/envelope_test.go @@ -25,11 +25,15 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/signaturetest" "github.com/notaryproject/notation-core-go/testhelper" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" "github.com/veraison/go-cose" ) const ( payloadString = "{\"targetArtifact\":{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\",\"size\":16724,\"annotations\":{\"io.wabbit-networks.buildId\":\"123\"}}}" + + rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" ) var ( @@ -123,6 +127,49 @@ func TestSign(t *testing.T) { } } } + + t.Run("with timestmap countersignature request", func(t *testing.T) { + signRequest, err := newSignRequest("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) + if err != nil { + t.Fatal(err) + } + rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") + if err != nil || len(rootCerts) == 0 { + t.Fatal("failed to read root CA certificate:", err) + } + rootCert := rootCerts[0] + rootCAs := x509.NewCertPool() + rootCAs.AddCert(rootCert) + signRequest.TSARootCAs = rootCAs + encoded, err := env.Sign(signRequest) + if err != nil || encoded == nil { + t.Fatalf("Sign() failed. Error = %s", err) + } + content, err := env.Content() + if err != nil { + t.Fatal(err) + } + timestampToken := content.SignerInfo.UnsignedAttributes.TimestampSignature + if len(timestampToken) == 0 { + t.Fatal("expected timestamp token to be present") + } + signedToken, err := tspclient.ParseSignedToken(timestampToken) + if err != nil { + t.Fatal(err) + } + info, err := signedToken.Info() + if err != nil { + t.Fatal(err) + } + _, err = info.Validate(content.SignerInfo.Signature) + if err != nil { + t.Fatal(err) + } + }) } func TestSignErrors(t *testing.T) { @@ -288,6 +335,29 @@ func TestSignErrors(t *testing.T) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) + + t.Run("when invalid tsa url is provided", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") + if err != nil { + t.Fatal(err) + } + expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"") + encoded, err := env.Sign(signRequest) + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + var timestampErr *signature.TimestampError + if !errors.As(err, ×tampErr) { + t.Fatal("expected signature.TimestampError") + } + if encoded != nil { + t.Fatal("expected nil signature envelope") + } + }) } func TestVerifyErrors(t *testing.T) { @@ -801,6 +871,44 @@ func TestGenerateExtendedAttributesError(t *testing.T) { } } +func TestHashFunc(t *testing.T) { + hash, err := hashFromCOSEAlgorithm(cose.AlgorithmPS256) + if err != nil || hash.String() != "SHA-256" { + t.Fatalf("expected SHA-256, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmPS384) + if err != nil || hash.String() != "SHA-384" { + t.Fatalf("expected SHA-384, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmPS512) + if err != nil || hash.String() != "SHA-512" { + t.Fatalf("expected SHA-512, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES256) + if err != nil || hash.String() != "SHA-256" { + t.Fatalf("expected SHA-256, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES384) + if err != nil || hash.String() != "SHA-384" { + t.Fatalf("expected SHA-384, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES512) + if err != nil || hash.String() != "SHA-512" { + t.Fatalf("expected SHA-512, but got %s", hash) + } + + _, err = hashFromCOSEAlgorithm(cose.AlgorithmEd25519) + expectedErrMsg := "unsupported cose algorithm EdDSA" + if err == nil || err.Error() != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, err) + } +} + func newSignRequest(signingScheme string, keyType signature.KeyType, size int) (*signature.SignRequest, error) { signer, err := signaturetest.GetTestLocalSigner(keyType, size) if err != nil { diff --git a/signature/errors.go b/signature/errors.go index d106c180..ead7c2c1 100644 --- a/signature/errors.go +++ b/signature/errors.go @@ -142,3 +142,28 @@ type DuplicateKeyError struct { func (e *DuplicateKeyError) Error() string { return fmt.Sprintf("repeated key: %q exists.", e.Key) } + +// TimestampError is any error related to RFC3161 Timestamp. +type TimestampError struct { + Msg string + Detail error +} + +// Error returns the formatted error message. +func (e *TimestampError) Error() string { + if e.Msg != "" && e.Detail != nil { + return fmt.Sprintf("timestamp: %s. Error: %s", e.Msg, e.Detail.Error()) + } + if e.Msg != "" { + return fmt.Sprintf("timestamp: %s", e.Msg) + } + if e.Detail != nil { + return fmt.Sprintf("timestamp: %s", e.Detail.Error()) + } + return "timestamp error" +} + +// Unwrap returns the detail error of e. +func (e *TimestampError) Unwrap() error { + return e.Detail +} diff --git a/signature/errors_test.go b/signature/errors_test.go index 5662c50f..da2fbe74 100644 --- a/signature/errors_test.go +++ b/signature/errors_test.go @@ -177,3 +177,34 @@ func TestEnvelopeKeyRepeatedError(t *testing.T) { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } + +func TestTimestampError(t *testing.T) { + err := &TimestampError{Msg: "test error", Detail: errors.New("test inner error")} + expectMsg := "timestamp: test error. Error: test inner error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } + + err = &TimestampError{Msg: "test error"} + expectMsg = "timestamp: test error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } + + err = &TimestampError{Detail: errors.New("test inner error")} + expectMsg = "timestamp: test inner error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } + unwrappedErr := err.Unwrap() + expectMsg = "test inner error" + if unwrappedErr.Error() != expectMsg { + t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) + } + + err = &TimestampError{} + expectMsg = "timestamp error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } +} diff --git a/signature/internal/base/envelope.go b/signature/internal/base/envelope.go index 41c685e0..8a7609bf 100644 --- a/signature/internal/base/envelope.go +++ b/signature/internal/base/envelope.go @@ -53,7 +53,6 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { if err != nil { return nil, err } - if err := validateCertificateChain( content.SignerInfo.CertificateChain, &content.SignerInfo.SignedAttributes.SigningTime, @@ -62,7 +61,9 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { return nil, err } + // store the raw signature e.Raw = raw + return e.Raw, nil } diff --git a/signature/internal/base/envelope_test.go b/signature/internal/base/envelope_test.go index e0be51c3..1498a0f8 100644 --- a/signature/internal/base/envelope_test.go +++ b/signature/internal/base/envelope_test.go @@ -22,10 +22,12 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" + "github.com/notaryproject/tspclient-go" ) var ( errMsg = "error msg" + invalidTimestamper tspclient.Timestamper invalidSigningAgent = "test/1" validSigningAgent = "test/0" invalidContentType = "text/plain" @@ -35,13 +37,13 @@ var ( time08_02 time.Time time08_03 time.Time timeLayout = "2006-01-02" - signiningSchema = signature.SigningScheme("notary.x509") + signiningSchema = signature.SigningScheme("notary.x509") validSignerInfo = &signature.SignerInfo{ Signature: validBytes, SignatureAlgorithm: signature.AlgorithmPS384, SignedAttributes: signature.SignedAttributes{ - SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, - Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, }, CertificateChain: []*x509.Certificate{ @@ -62,8 +64,8 @@ var ( ContentType: validContentType, Content: validBytes, }, - SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, - Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ @@ -82,8 +84,8 @@ var ( ContentType: validContentType, Content: validBytes, }, - SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, - Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ @@ -97,19 +99,42 @@ var ( }, SigningAgent: invalidSigningAgent, } + reqWithInvalidTSAurl = &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + SigningScheme: signiningSchema, + Signer: &mockSigner{ + keySpec: signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 3072, + }, + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + }, + SigningAgent: validSigningAgent, + Timestamper: invalidTimestamper, + } ) func init() { time08_02, _ = time.Parse(timeLayout, "2020-08-02") time08_03, _ = time.Parse(timeLayout, "2020-08-03") + invalidTimestamper, _ = tspclient.NewHTTPTimestamper(nil, "invalid") } // Mock an internal envelope that implements signature.Envelope. type mockEnvelope struct { - payload *signature.Payload - signerInfo *signature.SignerInfo - content *signature.EnvelopeContent - failVerify bool + payload *signature.Payload + signerInfo *signature.SignerInfo + content *signature.EnvelopeContent + failTimestamp bool + failVerify bool } // Sign implements Sign of signature.Envelope. @@ -118,6 +143,9 @@ func (e mockEnvelope) Sign(req *signature.SignRequest) ([]byte, error) { case invalidSigningAgent: return nil, errors.New(errMsg) case validSigningAgent: + if e.failTimestamp { + return validBytes, &signature.TimestampError{} + } return validBytes, nil } return nil, nil @@ -234,6 +262,21 @@ func TestSign(t *testing.T) { expect: validBytes, expectErr: false, }, + { + name: "failed to timestamp", + req: reqWithInvalidTSAurl, + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + content: &signature.EnvelopeContent{ + SignerInfo: *validSignerInfo, + }, + failTimestamp: true, + }, + }, + expect: nil, + expectErr: true, + }, } for _, tt := range tests { @@ -246,6 +289,17 @@ func TestSign(t *testing.T) { if !reflect.DeepEqual(sig, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, sig) } + + if tt.name == "failed to timestamp" { + var timestampErr *signature.TimestampError + if !errors.As(err, ×tampErr) { + t.Fatal("expecting error to be signature.TimestampError") + } + expectedErrMsg := "timestamp error" + if timestampErr.Error() != expectedErrMsg { + t.Fatalf("expected error %s, but got %v", expectedErrMsg, err) + } + } }) } } @@ -456,8 +510,8 @@ func TestValidateSignRequest(t *testing.T) { ContentType: validContentType, Content: validBytes, }, - SigningTime: time08_02, - Expiry: time08_03, + SigningTime: time08_02, + Expiry: time08_03, SigningScheme: signiningSchema, Signer: &mockSigner{ certs: []*x509.Certificate{ diff --git a/signature/jws/envelope.go b/signature/jws/envelope.go index eb280488..f0bcd2c0 100644 --- a/signature/jws/envelope.go +++ b/signature/jws/envelope.go @@ -20,8 +20,10 @@ import ( "fmt" "github.com/golang-jwt/jwt/v4" + "github.com/notaryproject/notation-core-go/internal/timestamp" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/base" + "github.com/notaryproject/tspclient-go" ) // MediaTypeEnvelope defines the media type name of JWS envelope. @@ -91,11 +93,17 @@ func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } + // timestamping + if err := timestampJWS(env, req, signedAttrs[headerKeySigningScheme].(string)); err != nil { + return nil, err + } + encoded, err := json.Marshal(env) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } e.base = env + return encoded, nil } @@ -220,3 +228,34 @@ func sign(payload jwt.MapClaims, headers map[string]interface{}, method signingM } return compact, certs, nil } + +// timestampJWS timestamps a JWS envelope +func timestampJWS(env *jwsEnvelope, req *signature.SignRequest, signingScheme string) error { + if signingScheme != string(signature.SigningSchemeX509) || req.Timestamper == nil { + return nil + } + primitiveSignature, err := base64.RawURLEncoding.DecodeString(env.Signature) + if err != nil { + return &signature.TimestampError{Detail: err} + } + ks, err := req.Signer.KeySpec() + if err != nil { + return &signature.TimestampError{Detail: err} + } + hash := ks.SignatureAlgorithm().Hash() + if hash == 0 { + return &signature.TimestampError{Msg: fmt.Sprintf("got hash value 0 from key spec %+v", ks)} + } + timestampOpts := tspclient.RequestOptions{ + Content: primitiveSignature, + HashAlgorithm: hash, + } + timestampToken, err := timestamp.Timestamp(req, timestampOpts) + if err != nil { + return &signature.TimestampError{Detail: err} + } + + // on success, embed the timestamp token to TimestampSignature + env.Header.TimestampSignature = timestampToken + return nil +} diff --git a/signature/jws/envelope_test.go b/signature/jws/envelope_test.go index f93643bf..4d765165 100644 --- a/signature/jws/envelope_test.go +++ b/signature/jws/envelope_test.go @@ -32,8 +32,12 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/signaturetest" "github.com/notaryproject/notation-core-go/testhelper" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" ) +const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" + // remoteMockSigner is used to mock remote signer type remoteMockSigner struct { privateKey crypto.PrivateKey @@ -226,8 +230,8 @@ func TestNewEnvelope(t *testing.T) { } } -// Test the same key exists both in extended signed attributes and protected header func TestSignFailed(t *testing.T) { + // Test the same key exists both in extended signed attributes and protected header t.Run("extended attribute conflict with protected header keys", func(t *testing.T) { _, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttrRepeated) checkErrorEqual(t, "attribute key:cty repeated", err.Error()) @@ -253,6 +257,32 @@ func TestSignFailed(t *testing.T) { _, err = e.Sign(signReq) checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) }) + + t.Run("invalid tsa url", func(t *testing.T) { + env := envelope{} + signer, err := signaturetest.GetTestLocalSigner(signature.KeyTypeRSA, 3072) + checkNoError(t, err) + + signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) + checkNoError(t, err) + + signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") + if err != nil { + t.Fatal(err) + } + expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"") + encoded, err := env.Sign(signReq) + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + var timestampErr *signature.TimestampError + if !errors.As(err, ×tampErr) { + t.Fatal("expected signature.TimestampError") + } + if encoded != nil { + t.Fatal("expected nil signature envelope") + } + }) } func TestSigningScheme(t *testing.T) { @@ -302,6 +332,52 @@ func TestSignVerify(t *testing.T) { } } +func TestSignWithTimestamp(t *testing.T) { + signer, err := signaturetest.GetTestLocalSigner(signature.KeyTypeRSA, 3072) + checkNoError(t, err) + + signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) + checkNoError(t, err) + + signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) + if err != nil { + t.Fatal(err) + } + rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") + if err != nil || len(rootCerts) == 0 { + t.Fatal("failed to read root CA certificate:", err) + } + rootCert := rootCerts[0] + rootCAs := x509.NewCertPool() + rootCAs.AddCert(rootCert) + signReq.TSARootCAs = rootCAs + env := envelope{} + encoded, err := env.Sign(signReq) + if err != nil || encoded == nil { + t.Fatalf("Sign() failed. Error = %s", err) + } + content, err := env.Content() + if err != nil { + t.Fatal(err) + } + timestampToken := content.SignerInfo.UnsignedAttributes.TimestampSignature + if len(timestampToken) == 0 { + t.Fatal("expected timestamp token to be present") + } + signedToken, err := tspclient.ParseSignedToken(timestampToken) + if err != nil { + t.Fatal(err) + } + info, err := signedToken.Info() + if err != nil { + t.Fatal(err) + } + _, err = info.Validate(content.SignerInfo.Signature) + if err != nil { + t.Fatal(err) + } +} + func TestVerify(t *testing.T) { t.Run("break json format", func(t *testing.T) { encoded, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttr) @@ -601,3 +677,13 @@ func TestEmptyEnvelope(t *testing.T) { } }) } + +func isErrEqual(wanted, got error) bool { + if wanted == nil && got == nil { + return true + } + if wanted != nil && got != nil { + return wanted.Error() == got.Error() + } + return false +} diff --git a/signature/jws/types.go b/signature/jws/types.go index bfbb204b..b2e34455 100644 --- a/signature/jws/types.go +++ b/signature/jws/types.go @@ -73,7 +73,7 @@ type jwsProtectedHeader struct { // jwsUnprotectedHeader contains the set of unprotected headers. type jwsUnprotectedHeader struct { - // RFC3161 time stamp token Base64-encoded. + // RFC3161 timestamp token Base64-encoded. TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"` // List of X.509 Base64-DER-encoded certificates diff --git a/signature/types.go b/signature/types.go index e324b92f..ab53bee8 100644 --- a/signature/types.go +++ b/signature/types.go @@ -14,9 +14,12 @@ package signature import ( + "context" "crypto/x509" "errors" "time" + + "github.com/notaryproject/tspclient-go" ) // SignatureMediaType list the supported media-type for signatures. @@ -101,6 +104,41 @@ type SignRequest struct { // SigningScheme defines the Notary Project Signing Scheme used by the signature. SigningScheme SigningScheme + + // Timestamper denotes the timestamper for RFC 3161 timestamping + Timestamper tspclient.Timestamper + + // TSARootCAs is the set of caller trusted TSA root certificates + TSARootCAs *x509.CertPool + + // ctx is the caller context. It should only be modified via WithContext. + // It is unexported to prevent people from using Context wrong + // and mutating the contexts held by callers of the same request. + ctx context.Context +} + +// Context returns the SignRequest's context. To change the context, use +// [SignRequest.WithContext]. +// +// The returned context is always non-nil; it defaults to the +// background context. +func (r *SignRequest) Context() context.Context { + if r.ctx != nil { + return r.ctx + } + return context.Background() +} + +// WithContext returns a shallow copy of r with its context changed +// to ctx. The provided ctx must be non-nil. +func (r *SignRequest) WithContext(ctx context.Context) *SignRequest { + if ctx == nil { + panic("nil context") + } + r2 := new(SignRequest) + *r2 = *r + r2.ctx = ctx + return r2 } // EnvelopeContent represents a combination of payload to be signed and a parsed diff --git a/signature/types_test.go b/signature/types_test.go new file mode 100644 index 00000000..f8ff5625 --- /dev/null +++ b/signature/types_test.go @@ -0,0 +1,53 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signature + +import ( + "context" + "fmt" + "testing" +) + +func TestSignRequestContext(t *testing.T) { + r := &SignRequest{ + ctx: context.WithValue(context.Background(), "k1", "v1"), + } + + ctx := r.Context() + if ctx.Value("k1") != "v1" { + t.Fatal("expected k1:v1 in ctx") + } + + r = &SignRequest{} + ctx = r.Context() + if fmt.Sprint(ctx) != "context.Background" { + t.Fatal("expected context.Background") + } +} + +func TestSignRequestWithContext(t *testing.T) { + r := &SignRequest{} + ctx := context.WithValue(context.Background(), "k1", "v1") + r = r.WithContext(ctx) + if r.ctx.Value("k1") != "v1" { + t.Fatal("expected k1:v1 in request ctx") + } + + defer func() { + if rc := recover(); rc == nil { + t.Errorf("expected to be panic") + } + }() + r.WithContext(nil) // should panic +} diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 1e9eb0e3..54a31c0e 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -22,12 +22,15 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "fmt" "math/big" mrand "math/rand" "strconv" "sync" "time" + + "github.com/notaryproject/notation-core-go/internal/oid" ) var ( @@ -81,7 +84,22 @@ func GetRevokableRSAChain(size int) []RSACertTuple { chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false) + } + return chain +} + +// GetRevokableRSATimestampChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm. +// The leaf certificate is a timestamp certificate. +func GetRevokableRSATimestampChain(size int) []RSACertTuple { + setupCertificates() + chain := make([]RSACertTuple, size) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) + for i := size - 2; i > 0; i-- { + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) + } + if size > 1 { + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true) } return chain } @@ -148,13 +166,13 @@ func getRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { } func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { - template := getCertTemplate(issuer == nil, true, cn) + template := getCertTemplate(issuer == nil, true, false, cn) template.OCSPServer = []string{"http://example.com/ocsp"} return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple { - template := getCertTemplate(previous == nil, true, cn) + template := getCertTemplate(previous == nil, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign @@ -164,7 +182,7 @@ func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) - template := getCertTemplate(true, true, cn) + template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign @@ -172,8 +190,8 @@ func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { return getRSACertTupleWithTemplate(template, pk, nil) } -func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int) RSACertTuple { - template := getCertTemplate(false, true, cn) +func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp bool) RSACertTuple { + template := getCertTemplate(false, codesign, timestamp, cn) template.BasicConstraintsValid = true template.IsCA = false template.KeyUsage = x509.KeyUsageDigitalSignature @@ -183,7 +201,7 @@ func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index in func getRSACertWithoutEKUTuple(cn string, issuer *RSACertTuple) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) - template := getCertTemplate(issuer == nil, false, cn) + template := getCertTemplate(issuer == nil, false, false, cn) return getRSACertTupleWithTemplate(template, pk, issuer) } @@ -200,18 +218,18 @@ func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple { func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple { // Even though we are creating self-signed root, we are using false for 'isRoot' to not // add root CA's basic constraint, KU and EKU. - template := getCertTemplate(false, true, cn) + template := getCertTemplate(false, true, false, cn) privKey, _ := rsa.GenerateKey(rand.Reader, 3072) return getRSACertTupleWithTemplate(template, privKey, nil) } func GetRSACertTupleWithPK(privKey *rsa.PrivateKey, cn string, issuer *RSACertTuple) RSACertTuple { - template := getCertTemplate(issuer == nil, true, cn) + template := getCertTemplate(issuer == nil, true, false, cn) return getRSACertTupleWithTemplate(template, privKey, issuer) } func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple { - template := getCertTemplate(false, true, cn) + template := getCertTemplate(false, true, false, cn) return getRSACertTupleWithTemplate(template, privKey, nil) } @@ -231,7 +249,7 @@ func getRSACertTupleWithTemplate(template *x509.Certificate, privKey *rsa.Privat } func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCertTuple) ECCertTuple { - template := getCertTemplate(issuer == nil, true, cn) + template := getCertTemplate(issuer == nil, true, false, cn) var certBytes []byte if issuer != nil { @@ -247,7 +265,7 @@ func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCer } } -func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certificate { +func getCertTemplate(isRoot bool, setCodeSignEKU, setTimestampEKU bool, cn string) *x509.Certificate { template := &x509.Certificate{ Subject: pkix.Name{ Organization: []string{"Notary"}, @@ -262,6 +280,15 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific if setCodeSignEKU { template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning} + } else if setTimestampEKU { + ekuValue, _ := asn1.Marshal([]asn1.ObjectIdentifier{oid.Timestamping}) + template.ExtraExtensions = []pkix.Extension{ + { + Id: oid.ExtKeyUsage, + Critical: true, + Value: ekuValue, + }, + } } if isRoot { @@ -275,7 +302,6 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific template.SerialNumber = big.NewInt(int64(mrand.Intn(200))) template.NotAfter = time.Now().AddDate(0, 0, 1) } - return template } diff --git a/x509/cert_validations.go b/x509/cert_validations.go deleted file mode 100644 index 8dd7d0ed..00000000 --- a/x509/cert_validations.go +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright The Notary Project Authors. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package x509 - -import ( - "bytes" - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" - "errors" - "fmt" - "strings" - "time" -) - -// ValidateCodeSigningCertChain takes an ordered code-signing certificate chain -// and validates issuance from leaf to root -// Validates certificates according to this spec: -// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements -func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { - return validateCertChain(certChain, 0, signingTime) -} - -// ValidateTimeStampingCertChain takes an ordered time-stamping certificate -// chain and validates issuance from leaf to root -// Validates certificates according to this spec: -// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements -func ValidateTimeStampingCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { - return validateCertChain(certChain, x509.ExtKeyUsageTimeStamping, signingTime) -} - -func validateCertChain(certChain []*x509.Certificate, expectedLeafEku x509.ExtKeyUsage, signingTime *time.Time) error { - if len(certChain) < 1 { - return errors.New("certificate chain must contain at least one certificate") - } - - // For self-signed signing certificate (not a CA) - if len(certChain) == 1 { - cert := certChain[0] - if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { - return signedTimeError - } - if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { - return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) - } - if err := validateLeafCertificate(cert, expectedLeafEku); err != nil { - return fmt.Errorf("invalid self-signed certificate. Error: %w", err) - } - return nil - } - - for i, cert := range certChain { - if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { - return signedTimeError - } - if i == len(certChain)-1 { - selfSigned, selfSignedError := isSelfSigned(cert) - if selfSignedError != nil { - return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) - } - if !selfSigned { - return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) - } - } else { - // This is to avoid extra/redundant multiple root cert at the end - // of certificate-chain - selfSigned, selfSignedError := isSelfSigned(cert) - // not checking selfSignedError != nil here because we expect - // a non-nil err. For a non-root certificate, it shouldn't be - // self-signed, hence CheckSignatureFrom would return a non-nil - // error. - if selfSignedError == nil && selfSigned { - if i == 0 { - return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) - } - return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) - } - parentCert := certChain[i+1] - issuedBy, issuedByError := isIssuedBy(cert, parentCert) - if issuedByError != nil { - return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) - } - if !issuedBy { - return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) - } - } - - if i == 0 { - if err := validateLeafCertificate(cert, expectedLeafEku); err != nil { - return err - } - } else { - if err := validateCACertificate(cert, i-1); err != nil { - return err - } - } - } - return nil -} - -func isSelfSigned(cert *x509.Certificate) (bool, error) { - return isIssuedBy(cert, cert) -} - -func isIssuedBy(subject *x509.Certificate, issuer *x509.Certificate) (bool, error) { - if err := subject.CheckSignatureFrom(issuer); err != nil { - return false, err - } - return bytes.Equal(issuer.RawSubject, subject.RawIssuer), nil -} - -func validateSigningTime(cert *x509.Certificate, signingTime *time.Time) error { - if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) { - return fmt.Errorf("certificate with subject %q was invalid at signing time of %s. Certificate is valid from [%s] to [%s]", - cert.Subject, signingTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC()) - } - return nil -} - -func validateCACertificate(cert *x509.Certificate, expectedPathLen int) error { - if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { - return err - } - return validateCAKeyUsage(cert) -} - -func validateLeafCertificate(cert *x509.Certificate, expectedEku x509.ExtKeyUsage) error { - if err := validateLeafBasicConstraints(cert); err != nil { - return err - } - if err := validateLeafKeyUsage(cert); err != nil { - return err - } - if err := validateExtendedKeyUsage(cert, expectedEku); err != nil { - return err - } - return validateKeyLength(cert) -} - -func validateCABasicConstraints(cert *x509.Certificate, expectedPathLen int) error { - if !cert.BasicConstraintsValid || !cert.IsCA { - return fmt.Errorf("certificate with subject %q: ca field in basic constraints must be present, critical, and set to true", cert.Subject) - } - maxPathLen := cert.MaxPathLen - isMaxPathLenPresent := maxPathLen > 0 || (maxPathLen == 0 && cert.MaxPathLenZero) - if isMaxPathLenPresent && maxPathLen < expectedPathLen { - return fmt.Errorf("certificate with subject %q: expected path length of %d but certificate has path length %d instead", cert.Subject, expectedPathLen, maxPathLen) - } - return nil -} - -func validateLeafBasicConstraints(cert *x509.Certificate) error { - if cert.BasicConstraintsValid && cert.IsCA { - return fmt.Errorf("certificate with subject %q: if the basic constraints extension is present, the ca field must be set to false", cert.Subject) - } - return nil -} - -func validateCAKeyUsage(cert *x509.Certificate) error { - if err := validateKeyUsagePresent(cert); err != nil { - return err - } - if cert.KeyUsage&x509.KeyUsageCertSign == 0 { - return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) - } - return nil -} - -func validateLeafKeyUsage(cert *x509.Certificate) error { - if err := validateKeyUsagePresent(cert); err != nil { - return err - } - if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { - return fmt.Errorf("The certificate with subject %q is invalid. The key usage must have the bit positions for \"Digital Signature\" set", cert.Subject) - } - - var invalidKeyUsages []string - if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"KeyEncipherment"`) - } - if cert.KeyUsage&x509.KeyUsageDataEncipherment != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"DataEncipherment"`) - } - if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"KeyAgreement"`) - } - if cert.KeyUsage&x509.KeyUsageCertSign != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"CertSign"`) - } - if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"CRLSign"`) - } - if cert.KeyUsage&x509.KeyUsageEncipherOnly != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"EncipherOnly"`) - } - if cert.KeyUsage&x509.KeyUsageDecipherOnly != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"DecipherOnly"`) - } - if len(invalidKeyUsages) > 0 { - return fmt.Errorf("The certificate with subject %q is invalid. The key usage must be \"Digital Signature\" only, but found %s", cert.Subject, strings.Join(invalidKeyUsages, ", ")) - } - return nil -} - -func validateKeyUsagePresent(cert *x509.Certificate) error { - keyUsageExtensionOid := []int{2, 5, 29, 15} - - var hasKeyUsageExtension bool - for _, ext := range cert.Extensions { - if ext.Id.Equal(keyUsageExtensionOid) { - if !ext.Critical { - return fmt.Errorf("certificate with subject %q: key usage extension must be marked critical", cert.Subject) - } - hasKeyUsageExtension = true - break - } - } - if !hasKeyUsageExtension { - return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) - } - return nil -} - -func validateExtendedKeyUsage(cert *x509.Certificate, expectedEku x509.ExtKeyUsage) error { - if len(cert.ExtKeyUsage) <= 0 { - return nil - } - - excludedEkus := []x509.ExtKeyUsage{ - x509.ExtKeyUsageAny, - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - x509.ExtKeyUsageEmailProtection, - x509.ExtKeyUsageOCSPSigning, - } - - if expectedEku == 0 { - excludedEkus = append(excludedEkus, x509.ExtKeyUsageTimeStamping) - } else if expectedEku == x509.ExtKeyUsageTimeStamping { - excludedEkus = append(excludedEkus, x509.ExtKeyUsageCodeSigning) - } - - var hasExpectedEku bool - for _, certEku := range cert.ExtKeyUsage { - if certEku == expectedEku { - hasExpectedEku = true - continue - } - for _, excludedEku := range excludedEkus { - if certEku == excludedEku { - return fmt.Errorf("certificate with subject %q: extended key usage must not contain %s eku", cert.Subject, ekuToString(excludedEku)) - } - } - } - - if expectedEku != 0 && !hasExpectedEku { - return fmt.Errorf("certificate with subject %q: extended key usage must contain %s eku", cert.Subject, ekuToString(expectedEku)) - } - return nil -} - -func validateKeyLength(cert *x509.Certificate) error { - switch key := cert.PublicKey.(type) { - case *rsa.PublicKey: - if key.N.BitLen() < 2048 { - return fmt.Errorf("certificate with subject %q: rsa public key length must be 2048 bits or higher", cert.Subject) - } - case *ecdsa.PublicKey: - if key.Params().N.BitLen() < 256 { - return fmt.Errorf("certificate with subject %q: ecdsa public key length must be 256 bits or higher", cert.Subject) - } - } - return nil -} - -func ekuToString(eku x509.ExtKeyUsage) string { - switch eku { - case x509.ExtKeyUsageAny: - return "Any" - case x509.ExtKeyUsageServerAuth: - return "ServerAuth" - case x509.ExtKeyUsageClientAuth: - return "ClientAuth" - case x509.ExtKeyUsageOCSPSigning: - return "OCSPSigning" - case x509.ExtKeyUsageEmailProtection: - return "EmailProtection" - case x509.ExtKeyUsageCodeSigning: - return "CodeSigning" - case x509.ExtKeyUsageTimeStamping: - return "TimeStamping" - default: - return fmt.Sprintf("%d", int(eku)) - } -} diff --git a/x509/codesigning_cert_validations.go b/x509/codesigning_cert_validations.go new file mode 100644 index 00000000..e074cef8 --- /dev/null +++ b/x509/codesigning_cert_validations.go @@ -0,0 +1,173 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "crypto/x509" + "errors" + "fmt" + "time" + + "github.com/notaryproject/notation-core-go/internal/oid" +) + +// ValidateCodeSigningCertChain takes an ordered code signing certificate chain +// and validates issuance from leaf to root +// Validates certificates according to this spec: +// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements +func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { + if len(certChain) < 1 { + return errors.New("certificate chain must contain at least one certificate") + } + + // For self-signed signing certificate (not a CA) + if len(certChain) == 1 { + cert := certChain[0] + if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { + return signedTimeError + } + if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { + return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) + } + if err := validateCodeSigningLeafCertificate(cert); err != nil { + return fmt.Errorf("invalid self-signed certificate. Error: %w", err) + } + return nil + } + + for i, cert := range certChain { + if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { + return signedTimeError + } + if i == len(certChain)-1 { + selfSigned, selfSignedError := isSelfSigned(cert) + if selfSignedError != nil { + return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) + } + if !selfSigned { + return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) + } + } else { + // This is to avoid extra/redundant multiple root cert at the end + // of certificate-chain + selfSigned, selfSignedError := isSelfSigned(cert) + // not checking selfSignedError != nil here because we expect + // a non-nil err. For a non-root certificate, it shouldn't be + // self-signed, hence CheckSignatureFrom would return a non-nil + // error. + if selfSignedError == nil && selfSigned { + if i == 0 { + return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) + } + return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) + } + parentCert := certChain[i+1] + issuedBy, issuedByError := isIssuedBy(cert, parentCert) + if issuedByError != nil { + return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) + } + if !issuedBy { + return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) + } + } + + if i == 0 { + if err := validateCodeSigningLeafCertificate(cert); err != nil { + return err + } + } else { + if err := validateCodeSigningCACertificate(cert, i-1); err != nil { + return err + } + } + } + return nil +} + +func validateCodeSigningCACertificate(cert *x509.Certificate, expectedPathLen int) error { + if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { + return err + } + return validateCodeSigningCAKeyUsage(cert) +} + +func validateCodeSigningLeafCertificate(cert *x509.Certificate) error { + if err := validateLeafBasicConstraints(cert); err != nil { + return err + } + if err := validateCodeSigningLeafKeyUsage(cert); err != nil { + return err + } + if err := validateCodeSigningExtendedKeyUsage(cert); err != nil { + return err + } + return validateSignatureAlgorithm(cert) +} + +func validateCodeSigningCAKeyUsage(cert *x509.Certificate) error { + if err := validateCodeSigningKeyUsagePresent(cert); err != nil { + return err + } + if cert.KeyUsage&x509.KeyUsageCertSign == 0 { + return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) + } + return nil +} + +func validateCodeSigningLeafKeyUsage(cert *x509.Certificate) error { + if err := validateCodeSigningKeyUsagePresent(cert); err != nil { + return err + } + return validateLeafKeyUsage(cert) +} + +func validateCodeSigningKeyUsagePresent(cert *x509.Certificate) error { + var hasKeyUsageExtension bool + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid.KeyUsage) { + if !ext.Critical { + return fmt.Errorf("certificate with subject %q: key usage extension must be marked critical", cert.Subject) + } + hasKeyUsageExtension = true + break + } + } + if !hasKeyUsageExtension { + return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) + } + return nil +} + +func validateCodeSigningExtendedKeyUsage(cert *x509.Certificate) error { + if len(cert.ExtKeyUsage) == 0 { + return nil + } + + excludedEkus := []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageEmailProtection, + x509.ExtKeyUsageTimeStamping, + x509.ExtKeyUsageOCSPSigning, + } + + for _, certEku := range cert.ExtKeyUsage { + for _, excludedEku := range excludedEkus { + if certEku == excludedEku { + return fmt.Errorf("certificate with subject %q: extended key usage must not contain %s eku", cert.Subject, ekuToString(excludedEku)) + } + } + } + return nil +} diff --git a/x509/cert_validations_test.go b/x509/codesigning_cert_validations_test.go similarity index 84% rename from x509/cert_validations_test.go rename to x509/codesigning_cert_validations_test.go index a11289f7..dcb2e230 100644 --- a/x509/cert_validations_test.go +++ b/x509/codesigning_cert_validations_test.go @@ -17,10 +17,12 @@ import ( "crypto/x509" "crypto/x509/pkix" _ "embed" - "encoding/asn1" + "errors" + "os" "testing" "time" + "github.com/notaryproject/notation-core-go/internal/oid" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -100,25 +102,6 @@ var codeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + "cwtsQn/iENuvFcfRHcFhvRjEFrIP+Ugx\n" + "-----END CERTIFICATE-----" -var timeStampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIC5TCCAc2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + - "cm1lZGlhdGUyMCAXDTIyMDYzMDE5MjAwNFoYDzMwMjExMDMxMTkyMDA0WjAbMRkw\n" + - "FwYDVQQDDBBUaW1lU3RhbXBpbmdMZWFmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" + - "MIIBCgKCAQEAyx2ispY5C5sQCiLAuCUTp4wv+fpgHwzE4an8eqi+Jrm0tEabTdzP\n" + - "IdZFRYPZbgRx+D9DKeN76f+rt51G9gOX77fYWyIXgnVL4UAYNlQj58hqZ0IO22vT\n" + - "nIFiDbJoSPuamQaLZNuluiirUwJv1uqSQiEnWHC4LhKwNOo4UHH5S3XkkYRpdFBF\n" + - "Tm4uOTaQJA9dfCh+0wbe7ZlEjDiuk1GTSQu69EPIl4IK7aEWqdvk2z1Pg4YkgJZX\n" + - "mWzkECNayUiBeHj7lL5ZnyZeki2l77WzXe/j5dgQ9E2+63hfBew+O/XeS/Tm/TyQ\n" + - "0P8bQre6vbn9820Cpyg82fd1+5bwYedwVwIDAQABozUwMzAOBgNVHQ8BAf8EBAMC\n" + - "B4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0B\n" + - "AQsFAAOCAQEAB9Z80K17p4J3VCqVcKyhgkzzYPoKiBWFThVwxS2+TKY0x4zezSAT\n" + - "69Nmf7NkVH4XyvCEUfgdWYst4t41rH3b5MTMOc5/nPeMccDWT0eZRivodF5hFWZd\n" + - "2QSFiMHmfUhnglY0ocLbfKeI/QoSGiPyBWO0SK6qOszRi14lP0TpgvgNDtMY/Jj5\n" + - "AyINT6o0tyYJvYE23/7ysT3U6pq50M4vOZiSuRys83As/qvlDIDKe8OVlDt6xRvr\n" + - "fqdMFWSk6Iay2OCfYcjUbTutMzSI7dvhDivn5FKnNA6M7QD1lqb7V9fymgrQTsth\n" + - "We9tUxypXgMjYN74QEHYxEAIfNOTeBppWw==\n" + - "-----END CERTIFICATE-----" - var unrelatedCertPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + @@ -183,7 +166,6 @@ var rootCert = parseCertificateFromString(rootCertPem) var intermediateCert1 = parseCertificateFromString(intermediateCertPem1) var intermediateCert2 = parseCertificateFromString(intermediateCertPem2) var codeSigningCert = parseCertificateFromString(codeSigningLeafPem) -var timeStampingCert = parseCertificateFromString(timeStampingLeafPem) var unrelatedCert = parseCertificateFromString(unrelatedCertPem) var intermediateCertInvalidPathLen = parseCertificateFromString(intermediateCertInvalidPathLenPem) var codeSigningLeafInvalidPathLen = parseCertificateFromString(codeSigningLeafInvalidPathLenPem) @@ -211,15 +193,6 @@ func TestValidCodeSigningChain(t *testing.T) { } } -func TestValidTimeStampingChain(t *testing.T) { - certChain := []*x509.Certificate{timeStampingCert, intermediateCert2, intermediateCert1, rootCert} - signingTime := time.Now() - - if err := ValidateTimeStampingCertChain(certChain, &signingTime); err != nil { - t.Fatal(err) - } -} - func TestFailEmptyChain(t *testing.T) { signingTime := time.Now() err := ValidateCodeSigningCertChain(nil, &signingTime) @@ -340,13 +313,13 @@ func TestInvalidSelfSignedSigningCertificate(t *testing.T) { // ---------------- CA Validations ---------------- func TestValidCa(t *testing.T) { - if err := validateCACertificate(rootCert, 2); err != nil { + if err := validateCodeSigningCACertificate(rootCert, 2); err != nil { t.Fatal(err) } } func TestFailInvalidPathLenCa(t *testing.T) { - err := validateCACertificate(rootCert, 3) + err := validateCodeSigningCACertificate(rootCert, 3) assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t) } @@ -370,7 +343,7 @@ var noBasicConstraintsCaPem = "-----BEGIN CERTIFICATE-----\n" + var noBasicConstraintsCa = parseCertificateFromString(noBasicConstraintsCaPem) func TestFailNoBasicConstraintsCa(t *testing.T) { - err := validateCACertificate(noBasicConstraintsCa, 3) + err := validateCodeSigningCACertificate(noBasicConstraintsCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } @@ -394,7 +367,7 @@ var basicConstraintsNotCaPem = "-----BEGIN CERTIFICATE-----\n" + var basicConstraintsNotCa = parseCertificateFromString(basicConstraintsNotCaPem) func TestFailBasicConstraintsNotCa(t *testing.T) { - err := validateCACertificate(basicConstraintsNotCa, 3) + err := validateCodeSigningCACertificate(basicConstraintsNotCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } @@ -418,7 +391,7 @@ var kuNotCriticalCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" + var kuNotCriticalCertSignCa = parseCertificateFromString(kuNotCriticalCertSignCaPem) func TestFailKuNotCriticalCertSignCa(t *testing.T) { - err := validateCACertificate(kuNotCriticalCertSignCa, 3) + err := validateCodeSigningCACertificate(kuNotCriticalCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be marked critical", err, t) } @@ -442,7 +415,7 @@ var kuMissingCaPem = "-----BEGIN CERTIFICATE-----\n" + var kuMissingCa = parseCertificateFromString(kuMissingCaPem) func TestFailKuMissingCa(t *testing.T) { - err := validateCACertificate(kuMissingCa, 3) + err := validateCodeSigningCACertificate(kuMissingCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t) } @@ -466,11 +439,11 @@ var kuNotCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" + var kuNotCertSignCa = parseCertificateFromString(kuNotCertSignCaPem) func TestFailKuNotCertSignCa(t *testing.T) { - err := validateCACertificate(kuNotCertSignCa, 3) + err := validateCodeSigningCACertificate(kuNotCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage must have the bit positions for key cert sign set", err, t) } -// ---------------- Code-Signing + Time-Stamping Leaf Validations ---------------- +// ---------------- Code-Signing Validations ---------------- var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICtzCCAZ+gAwIBAgIJAL+FUPhO8J8cMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + @@ -492,7 +465,7 @@ var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" + var validNoOptionsLeaf = parseCertificateFromString(validNoOptionsLeafPem) func TestValidNoOptionsLeaf(t *testing.T) { - if err := validateLeafCertificate(validNoOptionsLeaf, x509.ExtKeyUsageCodeSigning); err != nil { + if err := validateCodeSigningLeafCertificate(validNoOptionsLeaf); err != nil { t.Fatal(err) } } @@ -517,7 +490,7 @@ var caTrueLeafPem = "-----BEGIN CERTIFICATE-----\n" + var caTrueLeaf = parseCertificateFromString(caTrueLeafPem) func TestFailCaTrueLeaf(t *testing.T) { - err := validateLeafCertificate(caTrueLeaf, x509.ExtKeyUsageCodeSigning) + err := validateCodeSigningLeafCertificate(caTrueLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": if the basic constraints extension is present, the ca field must be set to false", err, t) } @@ -541,8 +514,8 @@ var kuNoDigitalSignatureLeafPem = "-----BEGIN CERTIFICATE-----\n" + var kuNoDigitalSignatureLeaf = parseCertificateFromString(kuNoDigitalSignatureLeafPem) func TestFailKuNoDigitalSignatureLeaf(t *testing.T) { - err := validateLeafCertificate(kuNoDigitalSignatureLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("The certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) + err := validateCodeSigningLeafCertificate(kuNoDigitalSignatureLeaf) + assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) } var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" + @@ -565,8 +538,8 @@ var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" + var kuWrongValuesLeaf = parseCertificateFromString(kuWrongValuesLeafPem) func TestFailKuWrongValuesLeaf(t *testing.T) { - err := validateLeafCertificate(kuWrongValuesLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("The certificate with subject \"CN=Hello\" is invalid. The key usage must be \"Digital Signature\" only, but found \"CertSign\", \"CRLSign\"", err, t) + err := validateCodeSigningLeafCertificate(kuWrongValuesLeaf) + assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must be \"Digital Signature\" only, but found \"CertSign\", \"CRLSign\"", err, t) } var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + @@ -584,8 +557,8 @@ var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + var rsaKeyTooSmallLeaf = parseCertificateFromString(rsaKeyTooSmallLeafPem) func TestFailRsaKeyTooSmallLeaf(t *testing.T) { - err := validateLeafCertificate(rsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": rsa public key length must be 2048 bits or higher", err, t) + err := validateCodeSigningLeafCertificate(rsaKeyTooSmallLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": rsa key size 1024 bits is not supported", err, t) } var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + @@ -600,141 +573,21 @@ var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + var ecdsaKeyTooSmallLeaf = parseCertificateFromString(ecdsaKeyTooSmallLeafPem) func TestFailEcdsaKeyTooSmallLeaf(t *testing.T) { - err := validateLeafCertificate(ecdsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa public key length must be 256 bits or higher", err, t) + err := validateCodeSigningLeafCertificate(ecdsaKeyTooSmallLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa key size 224 bits is not supported", err, t) } // ---------------- Code-Signing Leaf Validations ---------------- func TestValidFullOptionsCodeLeaf(t *testing.T) { - if err := validateLeafCertificate(codeSigningCert, x509.ExtKeyUsageCodeSigning); err != nil { + if err := validateCodeSigningLeafCertificate(codeSigningCert); err != nil { t.Fatal(err) } } -var ekuWrongValuesCodeLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIC6jCCAdKgAwIBAgIJAKZJHdWFNYPlMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMDEwM1oYDzIxMjIwNjAxMDMwMTAzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2t\n" + - "EFpNOJkX7B78d9ahTl5MXGWyKIjgfg1PhkYwHKHJWBiqHa1OUewfUG4ouVuaAvJ+\n" + - "GPzcxt23/J3jK+3/szrzpBNv1f0vgIa+mqaRQDW2m/wfWw3kpcwxlRcL7GnCeHbv\n" + - "gRFDXQW6MhKgGgKdQ5ezV+p01eF+CzMhUe+bZO+mvgxj36MJHzLMFHyh3x4/+z4x\n" + - "qRKmj4uUqJ2FJLlQEk92vPE/N3r7rEWa6gd4mBZ+DsZSrCbVPXchS2mCkeg70qxA\n" + - "4840qVLZ5eFxtqnTEUNytu3ug/8ydV9VmuT+C5fQYUp3Fl7D1QxHxWYTVTKdenCY\n" + - "jxcJHW1cUWZQlgPTLq8CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + - "MCgGCCsGAQUFBwMDBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + - "SIb3DQEBCwUAA4IBAQBRfpNRu79i47yp73LWTKrnZRiLC4JAI3I3w5TTx8m2tYkq\n" + - "tkSCP3Sn4y6VjKqo9Xtlt/bBLypw7XAOZOUZLEaoCjwRmAwq74VHAxDZO1LfFlKd\n" + - "au8G3xhKjc5prOMJ2g4DELOcyDoLDlwYqQ/jfG/t8b0P37yakFVffSzIA7D0BjmS\n" + - "OnWrGOJO/IJZjiaTdQkg+n5jk4FNqhwW91em64/M3MOmib3plnl89MgR90kuvQOV\n" + - "ctDBylt8M61MgnbzeunAq4aKYJc4IeeIH++g4F3/pqyoC95sAZP+A6+LkmBDOcyE\n" + - "5wUmNtUsL9xxKIUCvPR1JtiLNxHrfendWiuJnW1M\n" + - "-----END CERTIFICATE-----" -var ekuWrongValuesCodeLeaf = parseCertificateFromString(ekuWrongValuesCodeLeafPem) - -func TestFailEkuWrongValuesCodeLeaf(t *testing.T) { - err := validateLeafCertificate(ekuWrongValuesCodeLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) -} - -var ekuMissingCodeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + - "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + - "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + - "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + - "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + - "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + - "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + - "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + - "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + - "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + - "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + - "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + - "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + - "-----END CERTIFICATE-----" -var ekuMissingCodeSigningLeaf = parseCertificateFromString(ekuMissingCodeSigningLeafPem) - -func TestFailEkuMissingCodeSigningLeaf(t *testing.T) { - err := validateLeafCertificate(ekuMissingCodeSigningLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) -} - -// ---------------- Time-Stamping Leaf Validations ---------------- - -func TestValidFullOptionsTimeLeaf(t *testing.T) { - if err := validateLeafCertificate(timeStampingCert, x509.ExtKeyUsageTimeStamping); err != nil { - t.Fatal(err) - } -} - -var ekuWrongValuesTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOZe\n" + - "9zjKWNlFD/HGrkaAI9mh9Fw1gF8S2tphQD/aPd9IS4HJJEQRkKz5oeHj2g1Y6TEk\n" + - "plODrKlnoLe+ZFNFFD4xMVV55aQSJDTljCLPwIZt2VewlaAhIImYihOJvJFST1zW\n" + - "K2NW4eLxt0awbE/YzL6beH4A6UsrcXcnN0KKiu6YD1/d5TezJoTQBMo6fboltuce\n" + - "P/+RMxyqpvip7nyFF3Yrmhumb7DKJrmSfSjdziI5QoUqzqVgqJ8pXMRb3ZOKb499\n" + - "d9RRxGkox93iOdSSlaP3FEl8VK9KqnD+MNhjVZbeYTfjm9UVdp91VLP1E/yfMXz+\n" + - "fZhYkublK6v3GWSEcb0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + - "MCgGCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + - "SIb3DQEBCwUAA4IBAQCaQZ+ws93F1azT6SKBYvBRBCj07+2DtNI83Q53GxrVy2vU\n" + - "rP1ULX7beY87amy6kQcqnQ0QSaoLK+CDL88pPxR2PBzCauz70rMRY8O/KrrLcfwd\n" + - "D5HM9DcbneqXQyfh0ZQpt0wK5wux0MFh2sAEv76jgYBMHq2zc+19skAW/oBtTUty\n" + - "i/IdOVeO589KXwJzEJmKiswN9zKo9KGgAlKS05zohjv40AOCAs+8Q2lOJjRMq4Ji\n" + - "z21qor5e/5+NnGY+2p4A7PbN+QnDdRC3y16dESRN50o5x6CwUWQO74+uRjrAWYCm\n" + - "f/Y7qdOf5zZbY21n8KnLcFOsKhwv4t40Y/LQqN/L\n" + - "-----END CERTIFICATE-----" -var ekuWrongValuesTimeLeaf = parseCertificateFromString(ekuWrongValuesTimeLeafPem) - -func TestFailEkuWrongValuesTimeLeaf(t *testing.T) { - err := validateLeafCertificate(ekuWrongValuesTimeLeaf, x509.ExtKeyUsageTimeStamping) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) -} - -var ekuMissingTimeStampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + - "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + - "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + - "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + - "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + - "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + - "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + - "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + - "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + - "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + - "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + - "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + - "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + - "-----END CERTIFICATE-----" -var ekuMissingTimeStampingLeaf = parseCertificateFromString(ekuMissingTimeStampingLeafPem) - -func TestFailEkuMissingTimeStampingLeaf(t *testing.T) { - err := validateLeafCertificate(ekuMissingTimeStampingLeaf, x509.ExtKeyUsageTimeStamping) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) -} - -// ---------------- Utility Methods ---------------- - -func parseCertificateFromString(certPem string) *x509.Certificate { - stringAsBytes := []byte(certPem) - cert, _ := parseCertificates(stringAsBytes) - return cert[0] -} - -func assertErrorEqual(expected string, err error, t *testing.T) { - if err == nil || expected != err.Error() { - t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) - } -} - func TestValidateLeafKeyUsage(t *testing.T) { extensions := []pkix.Extension{{ - Id: asn1.ObjectIdentifier{2, 5, 29, 15}, // OID for KeyUsage + Id: oid.KeyUsage, Critical: true, }} @@ -768,7 +621,7 @@ func TestValidateLeafKeyUsage(t *testing.T) { KeyUsage: x509.KeyUsageCertSign, Extensions: extensions, }, - expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", + expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", }, { name: "Invalid KeyEncipherment usage", @@ -777,7 +630,7 @@ func TestValidateLeafKeyUsage(t *testing.T) { KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, Extensions: extensions, }, - expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\"", + expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\"", }, { name: "Multiple Invalid usages", @@ -786,7 +639,7 @@ func TestValidateLeafKeyUsage(t *testing.T) { KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly, Extensions: extensions, }, - expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\", \"DataEncipherment\", \"KeyAgreement\", \"CertSign\", \"CRLSign\", \"EncipherOnly\", \"DecipherOnly\"", + expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\", \"DataEncipherment\", \"KeyAgreement\", \"CertSign\", \"CRLSign\", \"EncipherOnly\", \"DecipherOnly\"", }, } @@ -803,3 +656,81 @@ func TestValidateLeafKeyUsage(t *testing.T) { }) } } + +var ekuWrongValuesCodeLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIIC6jCCAdKgAwIBAgIJAKZJHdWFNYPlMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMDEwM1oYDzIxMjIwNjAxMDMwMTAzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2t\n" + + "EFpNOJkX7B78d9ahTl5MXGWyKIjgfg1PhkYwHKHJWBiqHa1OUewfUG4ouVuaAvJ+\n" + + "GPzcxt23/J3jK+3/szrzpBNv1f0vgIa+mqaRQDW2m/wfWw3kpcwxlRcL7GnCeHbv\n" + + "gRFDXQW6MhKgGgKdQ5ezV+p01eF+CzMhUe+bZO+mvgxj36MJHzLMFHyh3x4/+z4x\n" + + "qRKmj4uUqJ2FJLlQEk92vPE/N3r7rEWa6gd4mBZ+DsZSrCbVPXchS2mCkeg70qxA\n" + + "4840qVLZ5eFxtqnTEUNytu3ug/8ydV9VmuT+C5fQYUp3Fl7D1QxHxWYTVTKdenCY\n" + + "jxcJHW1cUWZQlgPTLq8CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + + "MCgGCCsGAQUFBwMDBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + + "SIb3DQEBCwUAA4IBAQBRfpNRu79i47yp73LWTKrnZRiLC4JAI3I3w5TTx8m2tYkq\n" + + "tkSCP3Sn4y6VjKqo9Xtlt/bBLypw7XAOZOUZLEaoCjwRmAwq74VHAxDZO1LfFlKd\n" + + "au8G3xhKjc5prOMJ2g4DELOcyDoLDlwYqQ/jfG/t8b0P37yakFVffSzIA7D0BjmS\n" + + "OnWrGOJO/IJZjiaTdQkg+n5jk4FNqhwW91em64/M3MOmib3plnl89MgR90kuvQOV\n" + + "ctDBylt8M61MgnbzeunAq4aKYJc4IeeIH++g4F3/pqyoC95sAZP+A6+LkmBDOcyE\n" + + "5wUmNtUsL9xxKIUCvPR1JtiLNxHrfendWiuJnW1M\n" + + "-----END CERTIFICATE-----" +var ekuWrongValuesCodeLeaf = parseCertificateFromString(ekuWrongValuesCodeLeafPem) + +func TestFailEkuWrongValuesCodeLeaf(t *testing.T) { + err := validateCodeSigningLeafCertificate(ekuWrongValuesCodeLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) +} + +var ekuMissingCodeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + + "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + + "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + + "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + + "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + + "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + + "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + + "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + + "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + + "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + + "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + + "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + + "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + + "-----END CERTIFICATE-----" +var ekuMissingCodeSigningLeaf = parseCertificateFromString(ekuMissingCodeSigningLeafPem) + +func TestFailEkuMissingCodeSigningLeaf(t *testing.T) { + err := validateCodeSigningLeafCertificate(ekuMissingCodeSigningLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) +} + +// ---------------- Utility Methods ---------------- + +func parseCertificateFromString(certPem string) *x509.Certificate { + stringAsBytes := []byte(certPem) + cert, _ := parseCertificates(stringAsBytes) + return cert[0] +} + +func assertErrorEqual(expected string, err error, t *testing.T) { + if err == nil || expected != err.Error() { + t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) + } +} + +func readSingleCertificate(path string) (*x509.Certificate, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + certs, err := parseCertificates(data) + if err != nil { + return nil, err + } + if len(certs) == 0 { + return nil, errors.New("no certificate in file") + } + return certs[0], nil +} diff --git a/x509/helper.go b/x509/helper.go new file mode 100644 index 00000000..91d34430 --- /dev/null +++ b/x509/helper.go @@ -0,0 +1,127 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "bytes" + "crypto/x509" + "fmt" + "strings" + "time" + + "github.com/notaryproject/notation-core-go/signature" +) + +func isSelfSigned(cert *x509.Certificate) (bool, error) { + return isIssuedBy(cert, cert) +} + +func isIssuedBy(subject *x509.Certificate, issuer *x509.Certificate) (bool, error) { + if err := subject.CheckSignatureFrom(issuer); err != nil { + return false, err + } + return bytes.Equal(issuer.RawSubject, subject.RawIssuer), nil +} + +func validateSigningTime(cert *x509.Certificate, signingTime *time.Time) error { + if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) { + return fmt.Errorf("certificate with subject %q was invalid at signing time of %s. Certificate is valid from [%s] to [%s]", + cert.Subject, signingTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC()) + } + return nil +} + +func validateCABasicConstraints(cert *x509.Certificate, expectedPathLen int) error { + if !cert.BasicConstraintsValid || !cert.IsCA { + return fmt.Errorf("certificate with subject %q: ca field in basic constraints must be present, critical, and set to true", cert.Subject) + } + maxPathLen := cert.MaxPathLen + isMaxPathLenPresent := maxPathLen > 0 || (maxPathLen == 0 && cert.MaxPathLenZero) + if isMaxPathLenPresent && maxPathLen < expectedPathLen { + return fmt.Errorf("certificate with subject %q: expected path length of %d but certificate has path length %d instead", cert.Subject, expectedPathLen, maxPathLen) + } + return nil +} + +func validateLeafBasicConstraints(cert *x509.Certificate) error { + if cert.BasicConstraintsValid && cert.IsCA { + return fmt.Errorf("certificate with subject %q: if the basic constraints extension is present, the ca field must be set to false", cert.Subject) + } + return nil +} + +func validateLeafKeyUsage(cert *x509.Certificate) error { + if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + return fmt.Errorf("the certificate with subject %q is invalid. The key usage must have the bit positions for \"Digital Signature\" set", cert.Subject) + } + + var invalidKeyUsages []string + if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"KeyEncipherment"`) + } + if cert.KeyUsage&x509.KeyUsageDataEncipherment != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"DataEncipherment"`) + } + if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"KeyAgreement"`) + } + if cert.KeyUsage&x509.KeyUsageCertSign != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"CertSign"`) + } + if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"CRLSign"`) + } + if cert.KeyUsage&x509.KeyUsageEncipherOnly != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"EncipherOnly"`) + } + if cert.KeyUsage&x509.KeyUsageDecipherOnly != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"DecipherOnly"`) + } + if len(invalidKeyUsages) > 0 { + return fmt.Errorf("the certificate with subject %q is invalid. The key usage must be \"Digital Signature\" only, but found %s", cert.Subject, strings.Join(invalidKeyUsages, ", ")) + } + return nil +} + +func validateSignatureAlgorithm(cert *x509.Certificate) error { + keySpec, err := signature.ExtractKeySpec(cert) + if err != nil { + return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err) + } + if keySpec.SignatureAlgorithm() == 0 { + return fmt.Errorf("certificate with subject %q: unsupported signature algorithm with key spec %+v", cert.Subject, keySpec) + } + return nil +} + +func ekuToString(eku x509.ExtKeyUsage) string { + switch eku { + case x509.ExtKeyUsageAny: + return "Any" + case x509.ExtKeyUsageServerAuth: + return "ServerAuth" + case x509.ExtKeyUsageClientAuth: + return "ClientAuth" + case x509.ExtKeyUsageOCSPSigning: + return "OCSPSigning" + case x509.ExtKeyUsageEmailProtection: + return "EmailProtection" + case x509.ExtKeyUsageCodeSigning: + return "CodeSigning" + case x509.ExtKeyUsageTimeStamping: + return "Timestamping" + default: + return fmt.Sprintf("%d", int(eku)) + } +} diff --git a/x509/testdata/timestamp_intermediate.crt b/x509/testdata/timestamp_intermediate.crt new file mode 100644 index 00000000..7c375380 Binary files /dev/null and b/x509/testdata/timestamp_intermediate.crt differ diff --git a/x509/testdata/timestamp_leaf.crt b/x509/testdata/timestamp_leaf.crt new file mode 100644 index 00000000..fdc6e067 Binary files /dev/null and b/x509/testdata/timestamp_leaf.crt differ diff --git a/x509/testdata/timestamp_root.crt b/x509/testdata/timestamp_root.crt new file mode 100644 index 00000000..99bcc84b Binary files /dev/null and b/x509/testdata/timestamp_root.crt differ diff --git a/x509/timestamp_cert_validations.go b/x509/timestamp_cert_validations.go new file mode 100644 index 00000000..adb46e60 --- /dev/null +++ b/x509/timestamp_cert_validations.go @@ -0,0 +1,161 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "crypto/x509" + "errors" + "fmt" + + "github.com/notaryproject/notation-core-go/internal/oid" +) + +// ValidateTimestampingCertChain takes an ordered time stamping certificate +// chain and validates issuance from leaf to root +// Validates certificates according to this spec: +// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements +func ValidateTimestampingCertChain(certChain []*x509.Certificate) error { + if len(certChain) < 1 { + return errors.New("certificate chain must contain at least one certificate") + } + + // For self-signed signing certificate (not a CA) + if len(certChain) == 1 { + cert := certChain[0] + if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { + return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) + } + if err := validateTimestampingLeafCertificate(cert); err != nil { + return fmt.Errorf("invalid self-signed certificate. Error: %w", err) + } + return nil + } + + for i, cert := range certChain { + if i == len(certChain)-1 { + selfSigned, selfSignedError := isSelfSigned(cert) + if selfSignedError != nil { + return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) + } + if !selfSigned { + return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) + } + } else { + // This is to avoid extra/redundant multiple root cert at the end + // of certificate-chain + selfSigned, selfSignedError := isSelfSigned(cert) + // not checking selfSignedError != nil here because we expect + // a non-nil err. For a non-root certificate, it shouldn't be + // self-signed, hence CheckSignatureFrom would return a non-nil + // error. + if selfSignedError == nil && selfSigned { + if i == 0 { + return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) + } + return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) + } + parentCert := certChain[i+1] + issuedBy, issuedByError := isIssuedBy(cert, parentCert) + if issuedByError != nil { + return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) + } + if !issuedBy { + return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) + } + } + + if i == 0 { + if err := validateTimestampingLeafCertificate(cert); err != nil { + return err + } + } else { + if err := validateTimestampingCACertificate(cert, i-1); err != nil { + return err + } + } + } + return nil +} + +func validateTimestampingCACertificate(cert *x509.Certificate, expectedPathLen int) error { + if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { + return err + } + return validateTimestampingCAKeyUsage(cert) +} + +func validateTimestampingLeafCertificate(cert *x509.Certificate) error { + if err := validateLeafBasicConstraints(cert); err != nil { + return err + } + if err := validateTimestampingLeafKeyUsage(cert); err != nil { + return err + } + if err := validateTimestampingExtendedKeyUsage(cert); err != nil { + return err + } + return validateSignatureAlgorithm(cert) +} + +func validateTimestampingCAKeyUsage(cert *x509.Certificate) error { + if err := validateTimestampingKeyUsagePresent(cert); err != nil { + return err + } + if cert.KeyUsage&x509.KeyUsageCertSign == 0 { + return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) + } + return nil +} + +func validateTimestampingLeafKeyUsage(cert *x509.Certificate) error { + if err := validateTimestampingKeyUsagePresent(cert); err != nil { + return err + } + return validateLeafKeyUsage(cert) +} + +func validateTimestampingKeyUsagePresent(cert *x509.Certificate) error { + var hasKeyUsageExtension bool + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid.KeyUsage) { + hasKeyUsageExtension = true + break + } + } + if !hasKeyUsageExtension { + return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) + } + return nil +} + +func validateTimestampingExtendedKeyUsage(cert *x509.Certificate) error { + // RFC 3161 2.3: The corresponding certificate MUST contain only one + // instance of the extended key usage field extension. And it MUST be + // marked as critical. + if len(cert.ExtKeyUsage) != 1 || + cert.ExtKeyUsage[0] != x509.ExtKeyUsageTimeStamping || + len(cert.UnknownExtKeyUsage) != 0 { + return fmt.Errorf("timestamp signing certificate with subject %q must have and only have %s as extended key usage", cert.Subject, ekuToString(x509.ExtKeyUsageTimeStamping)) + } + // check if Extended Key Usage extension is marked critical + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid.ExtKeyUsage) { + if !ext.Critical { + return fmt.Errorf("timestamp signing certificate with subject %q must have extended key usage extension marked as critical", cert.Subject) + } + break + } + } + return nil +} diff --git a/x509/timestamp_cert_validations_test.go b/x509/timestamp_cert_validations_test.go new file mode 100644 index 00000000..cfede2da --- /dev/null +++ b/x509/timestamp_cert_validations_test.go @@ -0,0 +1,217 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" +) + +func TestValidTimestampingChain(t *testing.T) { + timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") + if err != nil { + t.Fatal(err) + } + timestamp_intermediate, err := readSingleCertificate("testdata/timestamp_intermediate.crt") + if err != nil { + t.Fatal(err) + } + timestamp_root, err := readSingleCertificate("testdata/timestamp_root.crt") + if err != nil { + t.Fatal(err) + } + certChain := []*x509.Certificate{timestamp_leaf, timestamp_intermediate, timestamp_root} + err = ValidateTimestampingCertChain(certChain) + if err != nil { + t.Fatal(err) + } +} + +func TestInvalidTimestampingChain(t *testing.T) { + timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") + if err != nil { + t.Fatal(err) + } + timestamp_intermediate, err := readSingleCertificate("testdata/timestamp_intermediate.crt") + if err != nil { + t.Fatal(err) + } + timestamp_root, err := readSingleCertificate("testdata/timestamp_root.crt") + if err != nil { + t.Fatal(err) + } + + expectedErr := "certificate chain must contain at least one certificate" + err = ValidateTimestampingCertChain([]*x509.Certificate{}) + assertErrorEqual(expectedErr, err, t) + + certChain := []*x509.Certificate{timestamp_leaf, intermediateCert2, intermediateCert1, rootCert} + expectedErr = "invalid certificates or certificate with subject \"CN=DigiCert Timestamp 2023,O=DigiCert\\\\, Inc.,C=US\" is not issued by \"CN=Intermediate2\". Error: crypto/rsa: verification error" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_leaf} + expectedErr = "invalid self-signed certificate. subject: \"CN=DigiCert Timestamp 2023,O=DigiCert\\\\, Inc.,C=US\". Error: crypto/rsa: verification error" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_leaf, timestamp_intermediate} + expectedErr = "root certificate with subject \"CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA,O=DigiCert\\\\, Inc.,C=US\" is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: crypto/rsa: verification error" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_root, timestamp_root} + expectedErr = "leaf certificate with subject \"CN=DigiCert Trusted Root G4,OU=www.digicert.com,O=DigiCert Inc,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_leaf, timestamp_intermediate, timestamp_root, timestamp_root} + expectedErr = "intermediate certificate with subject \"CN=DigiCert Trusted Root G4,OU=www.digicert.com,O=DigiCert Inc,C=US\" is self-signed. Certificate chain must not contain self-signed intermediate certificate" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) +} + +var ekuNonCriticalTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIIC5TCCAc2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + + "cm1lZGlhdGUyMCAXDTIyMDYzMDE5MjAwNFoYDzMwMjExMDMxMTkyMDA0WjAbMRkw\n" + + "FwYDVQQDDBBUaW1lU3RhbXBpbmdMZWFmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" + + "MIIBCgKCAQEAyx2ispY5C5sQCiLAuCUTp4wv+fpgHwzE4an8eqi+Jrm0tEabTdzP\n" + + "IdZFRYPZbgRx+D9DKeN76f+rt51G9gOX77fYWyIXgnVL4UAYNlQj58hqZ0IO22vT\n" + + "nIFiDbJoSPuamQaLZNuluiirUwJv1uqSQiEnWHC4LhKwNOo4UHH5S3XkkYRpdFBF\n" + + "Tm4uOTaQJA9dfCh+0wbe7ZlEjDiuk1GTSQu69EPIl4IK7aEWqdvk2z1Pg4YkgJZX\n" + + "mWzkECNayUiBeHj7lL5ZnyZeki2l77WzXe/j5dgQ9E2+63hfBew+O/XeS/Tm/TyQ\n" + + "0P8bQre6vbn9820Cpyg82fd1+5bwYedwVwIDAQABozUwMzAOBgNVHQ8BAf8EBAMC\n" + + "B4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0B\n" + + "AQsFAAOCAQEAB9Z80K17p4J3VCqVcKyhgkzzYPoKiBWFThVwxS2+TKY0x4zezSAT\n" + + "69Nmf7NkVH4XyvCEUfgdWYst4t41rH3b5MTMOc5/nPeMccDWT0eZRivodF5hFWZd\n" + + "2QSFiMHmfUhnglY0ocLbfKeI/QoSGiPyBWO0SK6qOszRi14lP0TpgvgNDtMY/Jj5\n" + + "AyINT6o0tyYJvYE23/7ysT3U6pq50M4vOZiSuRys83As/qvlDIDKe8OVlDt6xRvr\n" + + "fqdMFWSk6Iay2OCfYcjUbTutMzSI7dvhDivn5FKnNA6M7QD1lqb7V9fymgrQTsth\n" + + "We9tUxypXgMjYN74QEHYxEAIfNOTeBppWw==\n" + + "-----END CERTIFICATE-----" +var ekuNonCriticalTimeLeafCert = parseCertificateFromString(ekuNonCriticalTimeLeafPem) + +func TestTimestampLeafWithNonCriticalEKU(t *testing.T) { + expectedErr := "timestamp signing certificate with subject \"CN=TimeStampingLeaf\" must have extended key usage extension marked as critical" + err := validateTimestampingLeafCertificate(ekuNonCriticalTimeLeafCert) + assertErrorEqual(expectedErr, err, t) +} + +var ekuWrongValuesTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOZe\n" + + "9zjKWNlFD/HGrkaAI9mh9Fw1gF8S2tphQD/aPd9IS4HJJEQRkKz5oeHj2g1Y6TEk\n" + + "plODrKlnoLe+ZFNFFD4xMVV55aQSJDTljCLPwIZt2VewlaAhIImYihOJvJFST1zW\n" + + "K2NW4eLxt0awbE/YzL6beH4A6UsrcXcnN0KKiu6YD1/d5TezJoTQBMo6fboltuce\n" + + "P/+RMxyqpvip7nyFF3Yrmhumb7DKJrmSfSjdziI5QoUqzqVgqJ8pXMRb3ZOKb499\n" + + "d9RRxGkox93iOdSSlaP3FEl8VK9KqnD+MNhjVZbeYTfjm9UVdp91VLP1E/yfMXz+\n" + + "fZhYkublK6v3GWSEcb0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + + "MCgGCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + + "SIb3DQEBCwUAA4IBAQCaQZ+ws93F1azT6SKBYvBRBCj07+2DtNI83Q53GxrVy2vU\n" + + "rP1ULX7beY87amy6kQcqnQ0QSaoLK+CDL88pPxR2PBzCauz70rMRY8O/KrrLcfwd\n" + + "D5HM9DcbneqXQyfh0ZQpt0wK5wux0MFh2sAEv76jgYBMHq2zc+19skAW/oBtTUty\n" + + "i/IdOVeO589KXwJzEJmKiswN9zKo9KGgAlKS05zohjv40AOCAs+8Q2lOJjRMq4Ji\n" + + "z21qor5e/5+NnGY+2p4A7PbN+QnDdRC3y16dESRN50o5x6CwUWQO74+uRjrAWYCm\n" + + "f/Y7qdOf5zZbY21n8KnLcFOsKhwv4t40Y/LQqN/L\n" + + "-----END CERTIFICATE-----" +var ekuWrongValuesTimeLeaf = parseCertificateFromString(ekuWrongValuesTimeLeafPem) + +func TestFailEkuWrongValuesTimeLeaf(t *testing.T) { + err := validateTimestampingLeafCertificate(ekuWrongValuesTimeLeaf) + assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) +} + +var ekuMissingTimestampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + + "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + + "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + + "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + + "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + + "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + + "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + + "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + + "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + + "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + + "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + + "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + + "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + + "-----END CERTIFICATE-----" +var ekuMissingTimestampingLeaf = parseCertificateFromString(ekuMissingTimestampingLeafPem) + +func TestFailEkuMissingTimestampingLeaf(t *testing.T) { + err := validateTimestampingLeafCertificate(ekuMissingTimestampingLeaf) + assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) +} + +func TestTimestampingFailNoBasicConstraintsCa(t *testing.T) { + err := validateTimestampingCACertificate(noBasicConstraintsCa, 3) + assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) +} + +func TestTimestampingFailKuMissingCa(t *testing.T) { + err := validateTimestampingCACertificate(kuMissingCa, 3) + assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t) +} + +func TestTimestampingFailInvalidPathLenCa(t *testing.T) { + err := validateTimestampingCACertificate(rootCert, 3) + assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t) +} + +func TestTimestampingFailKuNotCertSignCa(t *testing.T) { + err := validateTimestampingCACertificate(kuNotCertSignCa, 3) + assertErrorEqual("certificate with subject \"CN=Hello\": key usage must have the bit positions for key cert sign set", err, t) +} + +func TestTimestampingFailWrongExtendedKeyUsage(t *testing.T) { + err := validateTimestampingLeafCertificate(validNoOptionsLeaf) + assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) +} + +func TestValidateTimestampingLeafCertificate(t *testing.T) { + err := validateTimestampingLeafCertificate(caTrueLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": if the basic constraints extension is present, the ca field must be set to false", err, t) + + err = validateTimestampingLeafCertificate(kuNoDigitalSignatureLeaf) + assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) + + cert := &x509.Certificate{ + Subject: pkix.Name{CommonName: "Test CN"}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + err = validateTimestampingLeafCertificate(cert) + assertErrorEqual("certificate with subject \"CN=Test CN\": key usage extension must be present", err, t) +} + +func TestEkuToString(t *testing.T) { + if ekuToString(x509.ExtKeyUsageAny) != "Any" { + t.Fatalf("expected Any") + } + if ekuToString(x509.ExtKeyUsageClientAuth) != "ClientAuth" { + t.Fatalf("expected ClientAuth") + } + if ekuToString(x509.ExtKeyUsageEmailProtection) != "EmailProtection" { + t.Fatalf("expected EmailProtection") + } + if ekuToString(x509.ExtKeyUsageCodeSigning) != "CodeSigning" { + t.Fatalf("expected CodeSigning") + } + if ekuToString(x509.ExtKeyUsageIPSECUser) != "7" { + t.Fatalf("expected 7") + } +}