diff --git a/internal/cms/cms.go b/internal/cms/cms.go new file mode 100644 index 0000000..b11b19f --- /dev/null +++ b/internal/cms/cms.go @@ -0,0 +1,185 @@ +// 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 cms verifies Signed-Data defined in RFC 5652 Cryptographic Message +// Syntax (CMS) / PKCS7 +// +// References: +// - RFC 5652 Cryptographic Message Syntax (CMS): https://datatracker.ietf.org/doc/html/rfc5652 +package cms + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "math/big" +) + +// ContentInfo struct is used to represent the content of a CMS message, +// which can be encrypted, signed, or both. +// +// References: RFC 5652 3 ContentInfo Type +// +// ContentInfo ::= SEQUENCE { +// contentType ContentType, +// content [0] EXPLICIT ANY DEFINED BY contentType } +type ContentInfo struct { + // ContentType field specifies the type of the content, which can be one of + // several predefined types, such as data, signedData, envelopedData, or + // encryptedData. Only signedData is supported currently. + ContentType asn1.ObjectIdentifier + + // Content field contains the actual content of the message. + Content asn1.RawValue `asn1:"explicit,tag:0"` +} + +// SignedData struct is used to represent a signed CMS message, which contains +// one or more signatures that are used to verify the authenticity and integrity +// of the message. +// +// Reference: RFC 5652 5.1 SignedData +// +// SignedData ::= SEQUENCE { +// version CMSVersion, +// digestAlgorithms DigestAlgorithmIdentifiers, +// encapContentInfo EncapsulatedContentInfo, +// certificates [0] IMPLICIT CertificateSet OPTIONAL, +// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL, +// signerInfos SignerInfos } +type SignedData struct { + // Version field specifies the syntax version number of the SignedData. + Version int + + // DigestAlgorithmIdentifiers field specifies the digest algorithms used + // by one or more signatures in SignerInfos. + DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"` + + // EncapsulatedContentInfo field specifies the content that is signed. + EncapsulatedContentInfo EncapsulatedContentInfo + + // Certificates field contains the certificates that are used to verify the + // signatures in SignerInfos. + Certificates asn1.RawValue `asn1:"optional,tag:0"` + + // CRLs field contains the Certificate Revocation Lists that are used to + // verify the signatures in SignerInfos. + CRLs []x509.RevocationList `asn1:"optional,tag:1"` + + // SignerInfos field contains one or more signatures. + SignerInfos []SignerInfo `asn1:"set"` +} + +// EncapsulatedContentInfo struct is used to represent the content of a CMS +// message. +// +// References: RFC 5652 5.2 EncapsulatedContentInfo +// +// EncapsulatedContentInfo ::= SEQUENCE { +// eContentType ContentType, +// eContent [0] EXPLICIT OCTET STRING OPTIONAL } +type EncapsulatedContentInfo struct { + // ContentType is an object identifier. The object identifier uniquely + // specifies the content type. + ContentType asn1.ObjectIdentifier + + // Content field contains the actual content of the message. + Content []byte `asn1:"explicit,optional,tag:0"` +} + +// SignerInfo struct is used to represent a signature and related information +// that is needed to verify the signature. +// +// Reference: RFC 5652 5.3 SignerInfo +// +// SignerInfo ::= SEQUENCE { +// version CMSVersion, +// sid SignerIdentifier, +// digestAlgorithm DigestAlgorithmIdentifier, +// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL, +// signatureAlgorithm SignatureAlgorithmIdentifier, +// signature SignatureValue, +// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL } +// +// Only version 1 is supported. As defined in RFC 5652 5.3, SignerIdentifier +// is IssuerAndSerialNumber when version is 1. +type SignerInfo struct { + // Version field specifies the syntax version number of the SignerInfo. + Version int + + // SignerIdentifier field specifies the signer's certificate. Only IssuerAndSerialNumber + // is supported currently. + SignerIdentifier IssuerAndSerialNumber + + // DigestAlgorithm field specifies the digest algorithm used by the signer. + DigestAlgorithm pkix.AlgorithmIdentifier + + // SignedAttributes field contains a collection of attributes that are + // signed. + SignedAttributes Attributes `asn1:"optional,tag:0"` + + // SignatureAlgorithm field specifies the signature algorithm used by the + // signer. + SignatureAlgorithm pkix.AlgorithmIdentifier + + // Signature field contains the actual signature. + Signature []byte + + // UnsignedAttributes field contains a collection of attributes that are + // not signed. + UnsignedAttributes Attributes `asn1:"optional,tag:1"` +} + +// IssuerAndSerialNumber struct is used to identify a certificate. +// +// Reference: RFC 5652 5.3 SignerIdentifier +// +// IssuerAndSerialNumber ::= SEQUENCE { +// issuer Name, +// serialNumber CertificateSerialNumber } +type IssuerAndSerialNumber struct { + // Issuer field identifies the certificate issuer. + Issuer asn1.RawValue + + // SerialNumber field identifies the certificate. + SerialNumber *big.Int +} + +// Attribute struct is used to represent a attribute with type and values. +// +// Reference: RFC 5652 5.3 SignerInfo +// +// Attribute ::= SEQUENCE { +// attrType OBJECT IDENTIFIER, +// attrValues SET OF AttributeValue } +type Attribute struct { + // Type field specifies the type of the attribute. + Type asn1.ObjectIdentifier + + // Values field contains the actual value of the attribute. + Values asn1.RawValue `asn1:"set"` +} + +// Attributes ::= SET SIZE (0..MAX) OF Attribute +type Attributes []Attribute + +// Get tries to find the attribute by the given identifier, parse and store +// the result in the value pointed to by out. +func (a Attributes) Get(identifier asn1.ObjectIdentifier, out any) error { + for _, attribute := range a { + if identifier.Equal(attribute.Type) { + _, err := asn1.Unmarshal(attribute.Values.Bytes, out) + return err + } + } + return ErrAttributeNotFound +} diff --git a/internal/cms/errors.go b/internal/cms/errors.go new file mode 100644 index 0000000..8e21afa --- /dev/null +++ b/internal/cms/errors.go @@ -0,0 +1,75 @@ +// 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 cms + +import "errors" + +// ErrNotSignedData is returned if wrong content is provided when signed +// data is expected. +var ErrNotSignedData = errors.New("cms: content type is not signed-data") + +// ErrAttributeNotFound is returned if attribute is not found in a given set. +var ErrAttributeNotFound = errors.New("attribute not found") + +// Verification errors +var ( + ErrSignerInfoNotFound = VerificationError{Message: "signerInfo not found"} + ErrCertificateNotFound = VerificationError{Message: "certificate not found"} +) + +// SyntaxError indicates that the ASN.1 data is invalid. +type SyntaxError struct { + Message string + Detail error +} + +// Error returns error message. +func (e SyntaxError) Error() string { + msg := "cms: syntax error" + if e.Message != "" { + msg += ": " + e.Message + } + if e.Detail != nil { + msg += ": " + e.Detail.Error() + } + return msg +} + +// Unwrap returns the internal error. +func (e SyntaxError) Unwrap() error { + return e.Detail +} + +// VerificationError indicates verification failures. +type VerificationError struct { + Message string + Detail error +} + +// Error returns error message. +func (e VerificationError) Error() string { + msg := "cms verification failure" + if e.Message != "" { + msg += ": " + e.Message + } + if e.Detail != nil { + msg += ": " + e.Detail.Error() + } + return msg +} + +// Unwrap returns the internal error. +func (e VerificationError) Unwrap() error { + return e.Detail +} diff --git a/internal/cms/errors_test.go b/internal/cms/errors_test.go new file mode 100644 index 0000000..fb572c8 --- /dev/null +++ b/internal/cms/errors_test.go @@ -0,0 +1,63 @@ +// 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 cms + +import ( + "errors" + "testing" +) + +func TestSyntaxError(t *testing.T) { + tests := []struct { + name string + err SyntaxError + wantMsg string + }{ + {"No detail", SyntaxError{Message: "test"}, "cms: syntax error: test"}, + {"With detail", SyntaxError{Message: "test", Detail: errors.New("detail")}, "cms: syntax error: test: detail"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotMsg := tt.err.Error(); gotMsg != tt.wantMsg { + t.Errorf("SyntaxError.Error() = %v, want %v", gotMsg, tt.wantMsg) + } + if gotDetail := tt.err.Unwrap(); gotDetail != tt.err.Detail { + t.Errorf("SyntaxError.Unwrap() = %v, want %v", gotDetail, tt.err.Detail) + } + }) + } +} + +func TestVerificationError(t *testing.T) { + tests := []struct { + name string + err VerificationError + wantMsg string + }{ + {"No detail", VerificationError{Message: "test"}, "cms verification failure: test"}, + {"With detail", VerificationError{Message: "test", Detail: errors.New("detail")}, "cms verification failure: test: detail"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotMsg := tt.err.Error(); gotMsg != tt.wantMsg { + t.Errorf("VerificationError.Error() = %v, want %v", gotMsg, tt.wantMsg) + } + if gotDetail := tt.err.Unwrap(); gotDetail != tt.err.Detail { + t.Errorf("VerificationError.Unwrap() = %v, want %v", gotDetail, tt.err.Detail) + } + }) + } +} diff --git a/internal/cms/signed.go b/internal/cms/signed.go new file mode 100644 index 0000000..a2e5d57 --- /dev/null +++ b/internal/cms/signed.go @@ -0,0 +1,324 @@ +// 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 cms + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/asn1" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/notaryproject/tspclient-go/internal/encoding/asn1/ber" + "github.com/notaryproject/tspclient-go/internal/hashutil" + "github.com/notaryproject/tspclient-go/internal/oid" +) + +// ParsedSignedData is a parsed SignedData structure for golang friendly types. +type ParsedSignedData struct { + // Content is the content of the EncapsulatedContentInfo. + Content []byte + + // ContentType is the content type of the EncapsulatedContentInfo. + ContentType asn1.ObjectIdentifier + + // Certificates is the list of certificates in the SignedData. + Certificates []*x509.Certificate + + // CRLs is the list of certificate revocation lists in the SignedData. + CRLs []x509.RevocationList + + // SignerInfos is the list of signer information in the SignedData. + SignerInfos []SignerInfo +} + +// ParseSignedData parses ASN.1 BER-encoded SignedData structure to golang +// friendly types. +// +// Only supported SignedData version is 3. +func ParseSignedData(berData []byte) (*ParsedSignedData, error) { + data, err := ber.ConvertToDER(berData) + if err != nil { + return nil, SyntaxError{Message: "invalid signed data: failed to convert from BER to DER", Detail: err} + } + var contentInfo ContentInfo + if _, err := asn1.Unmarshal(data, &contentInfo); err != nil { + return nil, SyntaxError{Message: "invalid content info: failed to unmarshal DER to ContentInfo", Detail: err} + } + if !oid.SignedData.Equal(contentInfo.ContentType) { + return nil, ErrNotSignedData + } + + var signedData SignedData + if _, err := asn1.Unmarshal(contentInfo.Content.Bytes, &signedData); err != nil { + return nil, SyntaxError{Message: "invalid signed data", Detail: err} + } + + if signedData.Version != 3 { + return nil, SyntaxError{Message: fmt.Sprintf("unsupported signed data version: got %d, want 3", signedData.Version)} + } + + certs, err := x509.ParseCertificates(signedData.Certificates.Bytes) + if err != nil { + return nil, SyntaxError{Message: "failed to parse X.509 certificates from signed data. Only X.509 certificates are supported", Detail: err} + } + + return &ParsedSignedData{ + Content: signedData.EncapsulatedContentInfo.Content, + ContentType: signedData.EncapsulatedContentInfo.ContentType, + Certificates: certs, + CRLs: signedData.CRLs, + SignerInfos: signedData.SignerInfos, + }, nil +} + +// Verify attempts to verify the content in the parsed signed data against the signer +// information. The `Intermediates` in the verify options will be ignored and +// re-contrusted using the certificates in the parsed signed data. +// If more than one signature is present, the successful validation of any signature +// implies that the content in the parsed signed data is valid. +// On successful verification, the list of signing certificates that successfully +// verify is returned. +// If all signatures fail to verify, the last error is returned. +// +// References: +// - RFC 5652 5 Signed-data Content Type +// - RFC 5652 5.4 Message Digest Calculation Process +// - RFC 5652 5.6 Signature Verification Process +// +// WARNING: this function doesn't do any revocation checking. +func (d *ParsedSignedData) Verify(ctx context.Context, opts x509.VerifyOptions) ([][]*x509.Certificate, error) { + if len(d.SignerInfos) == 0 { + return nil, ErrSignerInfoNotFound + } + if len(d.Certificates) == 0 { + return nil, ErrCertificateNotFound + } + + intermediates := x509.NewCertPool() + for _, cert := range d.Certificates { + intermediates.AddCert(cert) + } + opts.Intermediates = intermediates + verifiedSignerMap := map[string][]*x509.Certificate{} + var lastErr error + for _, signerInfo := range d.SignerInfos { + signingCertificate := d.GetCertificate(signerInfo.SignerIdentifier) + if signingCertificate == nil { + lastErr = ErrCertificateNotFound + continue + } + + certChain, err := d.VerifySigner(ctx, &signerInfo, signingCertificate, opts) + if err != nil { + lastErr = err + continue + } + + thumbprint, err := hashutil.ComputeHash(crypto.SHA256, signingCertificate.Raw) + if err != nil { + lastErr = err + continue + } + verifiedSignerMap[hex.EncodeToString(thumbprint)] = certChain + } + if len(verifiedSignerMap) == 0 { + return nil, lastErr + } + + verifiedSigningCertChains := make([][]*x509.Certificate, 0, len(verifiedSignerMap)) + for _, certChain := range verifiedSignerMap { + verifiedSigningCertChains = append(verifiedSigningCertChains, certChain) + } + return verifiedSigningCertChains, nil +} + +// VerifySigner verifies the signerInfo against the user specified signingCertificate. +// +// This function should be used when: +// +// 1. The certificates field of d is missing. This function allows the caller to provide +// a signing certificate to verify the signerInfo. +// +// 2. The caller doesn't trust the signer identifier (unsigned field) of signerInfo +// to identify signing certificate. This function allows such caller to use their trusted +// signing certificate. +// +// Note: the intermediate certificates (if any) and root certificates in the verify +// options MUST be set by the caller. The certificates field of d is not used in this function. +// +// References: +// - RFC 5652 5 Signed-data Content Type +// - RFC 5652 5.4 Message Digest Calculation Process +// - RFC 5652 5.6 Signature Verification Process +// +// WARNING: this function doesn't do any revocation checking. +func (d *ParsedSignedData) VerifySigner(ctx context.Context, signerInfo *SignerInfo, signingCertificate *x509.Certificate, opts x509.VerifyOptions) ([]*x509.Certificate, error) { + if signerInfo == nil { + return nil, VerificationError{Message: "VerifySigner failed: signer info is required"} + } + + if signingCertificate == nil { + return nil, VerificationError{Message: "VerifySigner failed: signing certificate is required"} + } + + if signerInfo.Version != 1 { + // Only IssuerAndSerialNumber is supported currently + return nil, VerificationError{Message: fmt.Sprintf("invalid signer info version: only version 1 is supported; got %d", signerInfo.Version)} + } + + return d.verify(signerInfo, signingCertificate, &opts) +} + +// verify verifies the trust in a top-down manner. +// +// References: +// - RFC 5652 5.4 Message Digest Calculation Process +// - RFC 5652 5.6 Signature Verification Process +func (d *ParsedSignedData) verify(signerInfo *SignerInfo, cert *x509.Certificate, opts *x509.VerifyOptions) ([]*x509.Certificate, error) { + // verify signer certificate + certChains, err := cert.Verify(*opts) + if err != nil { + return nil, VerificationError{Detail: err} + } + + // verify signature + if err := d.verifySignature(signerInfo, cert); err != nil { + return nil, err + } + + // verify attribute + return d.verifySignedAttributes(signerInfo, certChains) +} + +// verifySignature verifies the signature with a trusted certificate. +// +// References: +// - RFC 5652 5.4 Message Digest Calculation Process +// - RFC 5652 5.6 Signature Verification Process +func (d *ParsedSignedData) verifySignature(signerInfo *SignerInfo, cert *x509.Certificate) error { + // verify signature + algorithm := oid.ToSignatureAlgorithm( + signerInfo.DigestAlgorithm.Algorithm, + signerInfo.SignatureAlgorithm.Algorithm, + ) + if algorithm == x509.UnknownSignatureAlgorithm { + return VerificationError{Message: "unknown signature algorithm"} + } + + signed := d.Content + if len(signerInfo.SignedAttributes) > 0 { + encoded, err := asn1.MarshalWithParams(signerInfo.SignedAttributes, "set") + if err != nil { + return VerificationError{Message: "invalid signed attributes", Detail: err} + } + signed = encoded + } + + if err := cert.CheckSignature(algorithm, signed, signerInfo.Signature); err != nil { + return VerificationError{Detail: err} + } + return nil +} + +// verifySignedAttributes verifies the signed attributes. +// +// References: +// - RFC 5652 5.3 SignerInfo Type +// - RFC 5652 5.6 Signature Verification Process +func (d *ParsedSignedData) verifySignedAttributes(signerInfo *SignerInfo, chains [][]*x509.Certificate) ([]*x509.Certificate, error) { + if len(chains) == 0 { + return nil, VerificationError{Message: "Failed to verify signed attributes because the certificate chain is empty."} + } + + // verify attributes if present + if len(signerInfo.SignedAttributes) == 0 { + if d.ContentType.Equal(oid.Data) { + return nil, nil + } + // signed attributes MUST be present if the content type of the + // EncapsulatedContentInfo value being signed is not id-data. + return nil, VerificationError{Message: "missing signed attributes"} + } + + var contentType asn1.ObjectIdentifier + if err := signerInfo.SignedAttributes.Get(oid.ContentType, &contentType); err != nil { + return nil, VerificationError{Message: "invalid content type", Detail: err} + } + if !d.ContentType.Equal(contentType) { + return nil, VerificationError{Message: fmt.Sprintf("mismatch content type: found %q in signer info, and %q in signed data", contentType, d.ContentType)} + } + + var expectedDigest []byte + if err := signerInfo.SignedAttributes.Get(oid.MessageDigest, &expectedDigest); err != nil { + return nil, VerificationError{Message: "invalid message digest", Detail: err} + } + hash, ok := oid.ToHash(signerInfo.DigestAlgorithm.Algorithm) + if !ok { + return nil, VerificationError{Message: "unsupported digest algorithm"} + } + actualDigest, err := hashutil.ComputeHash(hash, d.Content) + if err != nil { + return nil, VerificationError{Message: "hash failure", Detail: err} + } + if !bytes.Equal(expectedDigest, actualDigest) { + return nil, VerificationError{Message: "mismatch message digest"} + } + + // sanity check on signing time + var signingTime time.Time + if err := signerInfo.SignedAttributes.Get(oid.SigningTime, &signingTime); err != nil { + if errors.Is(err, ErrAttributeNotFound) { + return chains[0], nil + } + return nil, VerificationError{Message: "invalid signing time", Detail: err} + } + + // verify signing time is within the validity period of all certificates + // in the chain. As long as one chain is valid, the signature is valid. + for _, chain := range chains { + if isSigningTimeValid(chain, signingTime) { + return chain, nil + } + } + + return nil, VerificationError{Message: fmt.Sprintf("signing time, %s, is outside certificate's validity period", signingTime)} +} + +// GetCertificate finds the certificate by issuer name and issuer-specific +// serial number. +// Reference: RFC 5652 5 Signed-data Content Type +func (d *ParsedSignedData) GetCertificate(ref IssuerAndSerialNumber) *x509.Certificate { + for _, cert := range d.Certificates { + if bytes.Equal(cert.RawIssuer, ref.Issuer.FullBytes) && cert.SerialNumber.Cmp(ref.SerialNumber) == 0 { + return cert + } + } + return nil +} + +// isSigningTimeValid helpes to check if signingTime is within the validity +// period of all certificates in the chain +func isSigningTimeValid(chain []*x509.Certificate, signingTime time.Time) bool { + for _, cert := range chain { + if signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter) { + return false + } + } + return true +} diff --git a/internal/cms/signed_test.go b/internal/cms/signed_test.go new file mode 100644 index 0000000..2148991 --- /dev/null +++ b/internal/cms/signed_test.go @@ -0,0 +1,417 @@ +// 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 cms + +import ( + "context" + "crypto/x509" + "os" + "reflect" + "testing" + "time" +) + +func TestVerifySignedData(t *testing.T) { + ctx := context.Background() + // parse signed data + sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s") + if err != nil { + t.Fatal("failed to read test signature:", err) + } + signed, err := ParseSignedData(sigBytes) + if err != nil { + t.Fatal("ParseSignedData() error =", err) + } + + // basic check on parsed signed data + if got := len(signed.Certificates); got != 4 { + t.Fatalf("len(Certificates) = %v, want %v", got, 4) + } + if got := len(signed.SignerInfos); got != 1 { + t.Fatalf("len(Signers) = %v, want %v", got, 1) + } + + // verify with no root CAs and should fail + roots := x509.NewCertPool() + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, + CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), + } + if _, err := signed.Verify(ctx, opts); err == nil { + t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true) + } else if vErr, ok := err.(VerificationError); !ok { + t.Errorf("ParseSignedData.Verify() error = %v, want VerificationError", err) + } else if _, ok := vErr.Detail.(x509.UnknownAuthorityError); !ok { + t.Errorf("ParseSignedData.Verify() VerificationError.Detail = %v, want UnknownAuthorityError", err) + } + + // verify with proper root CA + rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt") + if err != nil { + t.Fatal("failed to read root CA certificate:", err) + } + if ok := roots.AppendCertsFromPEM(rootCABytes); !ok { + t.Fatal("failed to load root CA certificate") + } + verifiedCertChains, err := signed.Verify(ctx, opts) + if err != nil { + t.Fatal("ParseSignedData.Verify() error =", err) + } + if !reflect.DeepEqual(verifiedCertChains[0], signed.Certificates) { + t.Fatalf("ParseSignedData.Verify() = %v, want %v", verifiedCertChains, signed.Certificates[:1]) + } +} + +func TestParseSignedData(t *testing.T) { + t.Run("invalid berData", func(t *testing.T) { + _, err := ParseSignedData([]byte("invalid")) + if err == nil { + t.Fatal("ParseSignedData() error = nil, wantErr true") + } + }) + + t.Run("invalid contentInfo", func(t *testing.T) { + _, err := ParseSignedData([]byte{0x30, 0x00}) + if err == nil { + t.Fatal("ParseSignedData() error = nil, wantErr true") + } + }) + + t.Run("content type is not signed data", func(t *testing.T) { + _, err := ParseSignedData([]byte{ + 0x30, 0x12, 0x06, 0x0b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x06, 0xa0, 0x03, 0x04, 0x01, 0x78, + }) + if err != ErrNotSignedData { + t.Errorf("ParseSignedData() error = %v, wantErr %v", err, ErrNotSignedData) + } + }) + + t.Run("invalid signed data content", func(t *testing.T) { + _, err := ParseSignedData([]byte{ + 0x30, 0x10, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x03, 0x04, 0x01, 0x78, + }) + + if err == nil { + t.Fatal("ParseSignedData() error = nil, wantErr true") + } + }) + + t.Run("invalid certificate", func(t *testing.T) { + // parse signed data + sigBytes, err := os.ReadFile("testdata/TimeStampTokenWithInvalidCertificates.p7s") + if err != nil { + t.Fatal("failed to read test signature:", err) + } + _, err = ParseSignedData(sigBytes) + if err == nil { + t.Fatal("ParseSignedData() error = nil, wantErr true") + } + }) +} + +func TestVerify(t *testing.T) { + testData := []struct { + name string + filePath string + wantErr bool + }{ + { + name: "without certificate", + filePath: "testdata/TimeStampTokenWithoutCertificate.p7s", + wantErr: true, + }, + { + name: "without signer info", + filePath: "testdata/TimeStampTokenWithoutSigner.p7s", + wantErr: true, + }, + { + name: "signer version is 2", + filePath: "testdata/TimeStampTokenWithSignerVersion2.p7s", + wantErr: true, + }, + { + name: "unknown signer issuer", + filePath: "testdata/TimeStampTokenWithUnknownSignerIssuer.p7s", + wantErr: true, + }, + { + name: "sha1 leaf cert", + filePath: "testdata/Sha1SignedData.p7s", + wantErr: true, + }, + { + name: "invalid signature", + filePath: "testdata/TimeStampTokenWithInvalidSignature.p7s", + wantErr: true, + }, + { + name: "id-data content type without signed attributes", + filePath: "testdata/SignedDataWithoutSignedAttributes.p7s", + wantErr: false, + }, + { + name: "an invalid and a valid signer info", + filePath: "testdata/TimeStampTokenWithAnInvalidAndAValidSignerInfo.p7s", + wantErr: false, + }, + } + + for _, testcase := range testData { + t.Run(testcase.name, func(t *testing.T) { + ctx := context.Background() + // parse signed data + sigBytes, err := os.ReadFile(testcase.filePath) + if err != nil { + t.Fatal("failed to read test signature:", err) + } + signed, err := ParseSignedData(sigBytes) + if err != nil { + t.Fatal("ParseSignedData() error =", err) + } + + // verify with no root CAs and should fail + roots := x509.NewCertPool() + certLen := len(signed.Certificates) + if certLen > 0 { + roots.AddCert(signed.Certificates[certLen-1]) + } + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, + CurrentTime: time.Date(2024, 1, 9, 0, 0, 0, 0, time.UTC), + } + _, err = signed.Verify(ctx, opts) + if testcase.wantErr != (err != nil) { + t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true) + } + }) + } +} + +func TestVerifySignerInvalidSignerInfo(t *testing.T) { + ctx := context.Background() + testData := []struct { + name string + filePath string + wantErr bool + }{ + { + name: "signer version is not 1", + filePath: "testdata/TimeStampTokenWithSignerVersion2.p7s", + wantErr: true, + }, + } + for _, testcase := range testData { + t.Run(testcase.name, func(t *testing.T) { + // parse signed data + sigBytes, err := os.ReadFile(testcase.filePath) + if err != nil { + t.Fatal("failed to read test signature:", err) + } + signed, err := ParseSignedData(sigBytes) + if err != nil { + t.Fatal("ParseSignedData() error =", err) + } + + // verify with no root CAs and should fail + roots := x509.NewCertPool() + certLen := len(signed.Certificates) + if certLen > 0 { + roots.AddCert(signed.Certificates[certLen-1]) + } + intermediates := x509.NewCertPool() + for _, cert := range signed.Certificates { + intermediates.AddCert(cert) + } + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, + CurrentTime: time.Date(2024, 1, 9, 0, 0, 0, 0, time.UTC), + Intermediates: intermediates, + } + _, err = signed.VerifySigner(ctx, &signed.SignerInfos[0], signed.Certificates[0], opts) + // err = err , err == nil, false, want error == false + if testcase.wantErr != (err != nil) { + t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, testcase.wantErr) + } + }) + } +} + +func TestVerifySigner(t *testing.T) { + ctx := context.Background() + // parse signed data + sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s") + if err != nil { + t.Fatal("failed to read test signature:", err) + } + signed, err := ParseSignedData(sigBytes) + if err != nil { + t.Fatal("ParseSignedData() error =", err) + } + + roots := x509.NewCertPool() + certLen := len(signed.Certificates) + if certLen > 0 { + roots.AddCert(signed.Certificates[certLen-1]) + } + intermediates := x509.NewCertPool() + for _, cert := range signed.Certificates { + intermediates.AddCert(cert) + } + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, + CurrentTime: time.Date(2024, 1, 9, 0, 0, 0, 0, time.UTC), + Intermediates: intermediates, + } + + t.Run("valid user provided signing certificate", func(t *testing.T) { + // verify with no root CAs and should fail + _, err = signed.VerifySigner(ctx, &signed.SignerInfos[0], signed.Certificates[0], opts) + if err != nil { + t.Errorf("ParseSignedData.Verify() error = %v, want nil", err) + } + }) + + t.Run("invalid user provided signing certificate", func(t *testing.T) { + // verify with no root CAs and should fail + _, err = signed.VerifySigner(ctx, &signed.SignerInfos[0], signed.Certificates[1], opts) + if err == nil { + t.Errorf("ParseSignedData.Verify() error = %v, want error", err) + } + }) + + t.Run("signerInfo is nil", func(t *testing.T) { + _, err = signed.VerifySigner(ctx, nil, signed.Certificates[0], opts) + if err == nil { + t.Error("ParseSignedData.Verify() error = nil, want error") + } + }) + + t.Run("certificate is nil", func(t *testing.T) { + // verify with no root CAs and should fail + _, err = signed.VerifySigner(ctx, &signed.SignerInfos[0], nil, opts) + if err == nil { + t.Error("ParseSignedData.Verify() error = nil, want error") + } + }) +} + +func TestVerifyAttributes(t *testing.T) { + testData := []struct { + name string + filePath string + wantErr bool + }{ + { + name: "without content type", + filePath: "testdata/TimeStampTokenWithoutSignedAttributeContentType.p7s", + wantErr: true, + }, + { + name: "with invalid content type", + filePath: "testdata/TimeStampTokenInvalidSignedAttributeContentType.p7s", + wantErr: true, + }, + { + name: "without signed attributes digest", + filePath: "testdata/TimeStampTokenWithoutSignedAttributeDigest.p7s", + wantErr: true, + }, + { + name: "with SHA1 hash", + filePath: "testdata/TimeStampTokenWithSignedAttributeSHA1.p7s", + wantErr: true, + }, + { + name: "with invalid signing time", + filePath: "testdata/TimeStampTokenWithInvalidSigningTime.p7s", + wantErr: true, + }, + { + name: "valid signing time", + filePath: "testdata/TimeStampTokenWithSigningTime.p7s", + wantErr: false, + }, + { + name: "signing time before expected", + filePath: "testdata/TimeStampTokenWithSigningTimeBeforeExpected.p7s", + wantErr: true, + }, + { + name: "timestamp token without signed attributes", + filePath: "testdata/TimeStampTokenWithoutSignedAttributes.p7s", + wantErr: true, + }, + } + + for _, testcase := range testData { + t.Run(testcase.name, func(t *testing.T) { + // parse signed data + sigBytes, err := os.ReadFile(testcase.filePath) + if err != nil { + t.Fatal("failed to read test signature:", err) + } + signed, err := ParseSignedData(sigBytes) + if err != nil { + t.Fatal("ParseSignedData() error =", err) + } + + _, err = signed.verifySignedAttributes(&signed.SignerInfos[0], [][]*x509.Certificate{signed.Certificates}) + if testcase.wantErr && err == nil { + t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true) + } else if !testcase.wantErr && err != nil { + t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, false) + } + }) + } +} + +func TestVerifyCorruptedSignedData(t *testing.T) { + ctx := context.Background() + // parse signed data + sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s") + if err != nil { + t.Fatal("failed to read test signature:", err) + } + signed, err := ParseSignedData(sigBytes) + if err != nil { + t.Fatal("ParseSignedData() error =", err) + } + + // corrupt the content + signed.Content = []byte("corrupted data") + + roots := x509.NewCertPool() + rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt") + if err != nil { + t.Fatal("failed to read root CA certificate:", err) + } + if ok := roots.AppendCertsFromPEM(rootCABytes); !ok { + t.Fatal("failed to load root CA certificate") + } + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, + CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), + } + if _, err := signed.Verify(ctx, opts); err == nil { + t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true) + } else if _, ok := err.(VerificationError); !ok { + t.Errorf("ParseSignedData.Verify() error = %v, want VerificationError", err) + } +} diff --git a/internal/cms/testdata/GlobalSignRootCA.crt b/internal/cms/testdata/GlobalSignRootCA.crt new file mode 100644 index 0000000..8afb219 --- /dev/null +++ b/internal/cms/testdata/GlobalSignRootCA.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- diff --git a/internal/cms/testdata/Sha1SignedData.p7s b/internal/cms/testdata/Sha1SignedData.p7s new file mode 100644 index 0000000..c2cde5e Binary files /dev/null and b/internal/cms/testdata/Sha1SignedData.p7s differ diff --git a/internal/cms/testdata/SignedDataWithoutSignedAttributes.p7s b/internal/cms/testdata/SignedDataWithoutSignedAttributes.p7s new file mode 100644 index 0000000..67ea549 Binary files /dev/null and b/internal/cms/testdata/SignedDataWithoutSignedAttributes.p7s differ diff --git a/internal/cms/testdata/TimeStampToken.p7s b/internal/cms/testdata/TimeStampToken.p7s new file mode 100644 index 0000000..c036aac Binary files /dev/null and b/internal/cms/testdata/TimeStampToken.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenInvalidSignedAttributeContentType.p7s b/internal/cms/testdata/TimeStampTokenInvalidSignedAttributeContentType.p7s new file mode 100644 index 0000000..588cc96 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenInvalidSignedAttributeContentType.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithAnInvalidAndAValidSignerInfo.p7s b/internal/cms/testdata/TimeStampTokenWithAnInvalidAndAValidSignerInfo.p7s new file mode 100644 index 0000000..fc73aa5 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithAnInvalidAndAValidSignerInfo.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithInvalidCertificates.p7s b/internal/cms/testdata/TimeStampTokenWithInvalidCertificates.p7s new file mode 100644 index 0000000..37fea8b Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithInvalidCertificates.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithInvalidSignature.p7s b/internal/cms/testdata/TimeStampTokenWithInvalidSignature.p7s new file mode 100644 index 0000000..5da09e0 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithInvalidSignature.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithInvalidSigningTime.p7s b/internal/cms/testdata/TimeStampTokenWithInvalidSigningTime.p7s new file mode 100644 index 0000000..a71677e Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithInvalidSigningTime.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithSignedAttributeSHA1.p7s b/internal/cms/testdata/TimeStampTokenWithSignedAttributeSHA1.p7s new file mode 100644 index 0000000..ab08251 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithSignedAttributeSHA1.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithSignerVersion2.p7s b/internal/cms/testdata/TimeStampTokenWithSignerVersion2.p7s new file mode 100644 index 0000000..f21df64 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithSignerVersion2.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithSigningTime.p7s b/internal/cms/testdata/TimeStampTokenWithSigningTime.p7s new file mode 100644 index 0000000..ef7401f Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithSigningTime.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithSigningTimeBeforeExpected.p7s b/internal/cms/testdata/TimeStampTokenWithSigningTimeBeforeExpected.p7s new file mode 100644 index 0000000..275840e Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithSigningTimeBeforeExpected.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithUnknownSignerIssuer.p7s b/internal/cms/testdata/TimeStampTokenWithUnknownSignerIssuer.p7s new file mode 100644 index 0000000..7f5db55 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithUnknownSignerIssuer.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithoutCertificate.p7s b/internal/cms/testdata/TimeStampTokenWithoutCertificate.p7s new file mode 100644 index 0000000..6a86a79 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithoutCertificate.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithoutSignedAttributeContentType.p7s b/internal/cms/testdata/TimeStampTokenWithoutSignedAttributeContentType.p7s new file mode 100644 index 0000000..10744e1 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithoutSignedAttributeContentType.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithoutSignedAttributeDigest.p7s b/internal/cms/testdata/TimeStampTokenWithoutSignedAttributeDigest.p7s new file mode 100644 index 0000000..c3115a8 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithoutSignedAttributeDigest.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithoutSignedAttributes.p7s b/internal/cms/testdata/TimeStampTokenWithoutSignedAttributes.p7s new file mode 100644 index 0000000..b2865b7 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithoutSignedAttributes.p7s differ diff --git a/internal/cms/testdata/TimeStampTokenWithoutSigner.p7s b/internal/cms/testdata/TimeStampTokenWithoutSigner.p7s new file mode 100644 index 0000000..f3888c2 Binary files /dev/null and b/internal/cms/testdata/TimeStampTokenWithoutSigner.p7s differ diff --git a/internal/hashutil/hash.go b/internal/hashutil/hash.go new file mode 100644 index 0000000..5e7ba5c --- /dev/null +++ b/internal/hashutil/hash.go @@ -0,0 +1,28 @@ +// 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 hashutil provides utilities for hash. +package hashutil + +import "crypto" + +// ComputeHash computes the digest of the message with the given hash algorithm. +// Callers should check the availability of the hash algorithm before invoking. +func ComputeHash(hash crypto.Hash, message []byte) ([]byte, error) { + h := hash.New() + _, err := h.Write(message) + if err != nil { + return nil, err + } + return h.Sum(nil), nil +} diff --git a/internal/hashutil/hash_test.go b/internal/hashutil/hash_test.go new file mode 100644 index 0000000..a0db797 --- /dev/null +++ b/internal/hashutil/hash_test.go @@ -0,0 +1,35 @@ +// 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 hashutil provides utilities for hash. +package hashutil + +import ( + "crypto" + "crypto/sha256" + "testing" +) + +func TestComputeHash(t *testing.T) { + message := []byte("test message") + expectedHash := sha256.Sum256(message) + + hash, err := ComputeHash(crypto.SHA256, message) + if err != nil { + t.Fatalf("ComputeHash returned an error: %v", err) + } + + if string(hash) != string(expectedHash[:]) { + t.Errorf("ComputeHash returned incorrect hash: got %x, want %x", hash, expectedHash) + } +} diff --git a/internal/oid/algorithm.go b/internal/oid/algorithm.go new file mode 100644 index 0000000..6baff1d --- /dev/null +++ b/internal/oid/algorithm.go @@ -0,0 +1,57 @@ +// 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 ( + "crypto/x509" + "encoding/asn1" +) + +// ToSignatureAlgorithm converts ASN.1 digest and signature algorithm +// identifiers to golang signature algorithms. +func ToSignatureAlgorithm(digestAlg, sigAlg asn1.ObjectIdentifier) x509.SignatureAlgorithm { + switch { + case RSA.Equal(sigAlg): + switch { + case SHA256.Equal(digestAlg): + return x509.SHA256WithRSA + case SHA384.Equal(digestAlg): + return x509.SHA384WithRSA + case SHA512.Equal(digestAlg): + return x509.SHA512WithRSA + } + case RSAPSS.Equal(sigAlg): + switch { + case SHA256.Equal(digestAlg): + return x509.SHA256WithRSAPSS + case SHA384.Equal(digestAlg): + return x509.SHA384WithRSAPSS + case SHA512.Equal(digestAlg): + return x509.SHA512WithRSAPSS + } + case SHA256WithRSA.Equal(sigAlg): + return x509.SHA256WithRSA + case SHA384WithRSA.Equal(sigAlg): + return x509.SHA384WithRSA + case SHA512WithRSA.Equal(sigAlg): + return x509.SHA512WithRSA + case ECDSAWithSHA256.Equal(sigAlg): + return x509.ECDSAWithSHA256 + case ECDSAWithSHA384.Equal(sigAlg): + return x509.ECDSAWithSHA384 + case ECDSAWithSHA512.Equal(sigAlg): + return x509.ECDSAWithSHA512 + } + return x509.UnknownSignatureAlgorithm +} diff --git a/internal/oid/algorithm_test.go b/internal/oid/algorithm_test.go new file mode 100644 index 0000000..7b341d9 --- /dev/null +++ b/internal/oid/algorithm_test.go @@ -0,0 +1,51 @@ +// 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 ( + "crypto/x509" + "encoding/asn1" + "testing" +) + +func TestToSignatureAlgorithm(t *testing.T) { + tests := []struct { + name string + digestAlg asn1.ObjectIdentifier + sigAlg asn1.ObjectIdentifier + wantResult x509.SignatureAlgorithm + }{ + {"SHA256WithRSA", SHA256, RSA, x509.SHA256WithRSA}, + {"SHA384WithRSA", SHA384, RSA, x509.SHA384WithRSA}, + {"SHA512WithRSA", SHA512, RSA, x509.SHA512WithRSA}, + {"SHA256WithRSAPSS", SHA256, RSAPSS, x509.SHA256WithRSAPSS}, + {"SHA384WithRSAPSS", SHA384, RSAPSS, x509.SHA384WithRSAPSS}, + {"SHA512WithRSAPSS", SHA512, RSAPSS, x509.SHA512WithRSAPSS}, + {"SHA256WithRSA direct", SHA256WithRSA, SHA256WithRSA, x509.SHA256WithRSA}, + {"SHA384WithRSA direct", SHA384WithRSA, SHA384WithRSA, x509.SHA384WithRSA}, + {"SHA512WithRSA direct", SHA512WithRSA, SHA512WithRSA, x509.SHA512WithRSA}, + {"ECDSAWithSHA256", ECDSAWithSHA256, ECDSAWithSHA256, x509.ECDSAWithSHA256}, + {"ECDSAWithSHA384", ECDSAWithSHA384, ECDSAWithSHA384, x509.ECDSAWithSHA384}, + {"ECDSAWithSHA512", ECDSAWithSHA512, ECDSAWithSHA512, x509.ECDSAWithSHA512}, + {"UnknownSignatureAlgorithm", asn1.ObjectIdentifier{1, 2, 3}, asn1.ObjectIdentifier{4, 5, 6}, x509.UnknownSignatureAlgorithm}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := ToSignatureAlgorithm(tt.digestAlg, tt.sigAlg); gotResult != tt.wantResult { + t.Errorf("ToSignatureAlgorithm() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} diff --git a/internal/oid/hash.go b/internal/oid/hash.go new file mode 100644 index 0000000..2b943e5 --- /dev/null +++ b/internal/oid/hash.go @@ -0,0 +1,36 @@ +// 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 ( + "crypto" + "encoding/asn1" +) + +// ToHash converts ASN.1 digest algorithm identifier to golang crypto hash +// if it is available. +func ToHash(alg asn1.ObjectIdentifier) (crypto.Hash, bool) { + var hash crypto.Hash + switch { + case SHA256.Equal(alg): + hash = crypto.SHA256 + case SHA384.Equal(alg): + hash = crypto.SHA384 + case SHA512.Equal(alg): + hash = crypto.SHA512 + default: + return hash, false + } + return hash, hash.Available() +} diff --git a/internal/oid/hash_test.go b/internal/oid/hash_test.go new file mode 100644 index 0000000..cd746f4 --- /dev/null +++ b/internal/oid/hash_test.go @@ -0,0 +1,46 @@ +// 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 ( + "crypto" + "encoding/asn1" + "testing" +) + +func TestToHash(t *testing.T) { + tests := []struct { + name string + alg asn1.ObjectIdentifier + wantHash crypto.Hash + wantExists bool + }{ + {"SHA256", SHA256, crypto.SHA256, true}, + {"SHA384", SHA384, crypto.SHA384, true}, + {"SHA512", SHA512, crypto.SHA512, true}, + {"Unknown", asn1.ObjectIdentifier{1, 2, 3}, crypto.Hash(0), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotHash, gotExists := ToHash(tt.alg) + if gotHash != tt.wantHash { + t.Errorf("ToHash() gotHash = %v, want %v", gotHash, tt.wantHash) + } + if gotExists != tt.wantExists { + t.Errorf("ToHash() gotExists = %v, want %v", gotExists, tt.wantExists) + } + }) + } +} diff --git a/internal/oid/oid.go b/internal/oid/oid.go new file mode 100644 index 0000000..db94cde --- /dev/null +++ b/internal/oid/oid.go @@ -0,0 +1,74 @@ +// 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 collects object identifiers for crypto algorithms. +package oid + +import "encoding/asn1" + +// OIDs for hash algorithms +var ( + // SHA256 (id-sha256) is defined in RFC 8017 B.1 Hash Functions + SHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} + + // SHA384 (id-sha384) is defined in RFC 8017 B.1 Hash Functions + SHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} + + // SHA512 (id-sha512) is defined in RFC 8017 B.1 Hash Functions + SHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} +) + +// OIDs for signature algorithms +var ( + // RSA is defined in RFC 8017 C ASN.1 Module + RSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + + // SHA256WithRSA is defined in RFC 8017 C ASN.1 Module + SHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} + + // SHA384WithRSA is defined in RFC 8017 C ASN.1 Module + SHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12} + + // SHA512WithRSA is defined in RFC 8017 C ASN.1 Module + SHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13} + + // RSAPSS is defined in RFC 8017 C ASN.1 Module + RSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} + + // ECDSAWithSHA256 is defined in RFC 5758 3.2 ECDSA Signature Algorithm + ECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} + + // ECDSAWithSHA384 is defined in RFC 5758 3.2 ECDSA Signature Algorithm + ECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3} + + // ECDSAWithSHA512 is defined in RFC 5758 3.2 ECDSA Signature Algorithm + ECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4} +) + +// OIDs defined in RFC 5652 Cryptographic Message Syntax (CMS) +var ( + // Data (id-data) is defined in RFC 5652 4 Data Content Type + Data = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1} + + // SignedData (id-signedData) is defined in RFC 5652 5.1 SignedData Type + SignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2} + + // ContentType (id-ct-contentType) is defined in RFC 5652 3 General Syntax + ContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} + + // MessageDigest (id-messageDigest) is defined in RFC 5652 11.2 Message Digest + MessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} + + // SigningTime (id-signingTime) is defined in RFC 5652 11.3 Signing Time + SigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5} +)