diff --git a/internal/crypto/cms/cms.go b/internal/crypto/cms/cms.go new file mode 100644 index 00000000..58219272 --- /dev/null +++ b/internal/crypto/cms/cms.go @@ -0,0 +1,182 @@ +// 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 signatures in Cryptographic Message Syntax (CMS) / PKCS7 +// defined in RFC 5652. +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 +} + +// Attributes struct is used to represent a collection of attributes. +// +// 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"` +} + +// Attribute ::= SET SIZE (1..MAX) OF Attribute +type Attributes []Attribute + +// TryGet tries to find the attribute by the given identifier, parse and store +// the result in the value pointed to by out. +func (a *Attributes) TryGet(identifier asn1.ObjectIdentifier, out interface{}) 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/encoding/asn1/asn1.go b/internal/crypto/cms/encoding/ber/ber.go similarity index 99% rename from internal/encoding/asn1/asn1.go rename to internal/crypto/cms/encoding/ber/ber.go index 87a4b833..9fbd93ce 100644 --- a/internal/encoding/asn1/asn1.go +++ b/internal/crypto/cms/encoding/ber/ber.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package asn1 decodes BER-encoded ASN.1 data structures and encodes in DER. +// Package ber decodes BER-encoded ASN.1 data structures and encodes in DER. // Note: // - DER is a subset of BER. // - Indefinite length is not supported. @@ -21,7 +21,7 @@ // Reference: // - http://luca.ntop.org/Teaching/Appunti/asn1.html // - ISO/IEC 8825-1:2021 -package asn1 +package ber import ( "bytes" diff --git a/internal/encoding/asn1/asn1_test.go b/internal/crypto/cms/encoding/ber/ber_test.go similarity index 99% rename from internal/encoding/asn1/asn1_test.go rename to internal/crypto/cms/encoding/ber/ber_test.go index e4a11567..541b87c7 100644 --- a/internal/encoding/asn1/asn1_test.go +++ b/internal/crypto/cms/encoding/ber/ber_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package asn1 +package ber import ( "reflect" diff --git a/internal/encoding/asn1/common.go b/internal/crypto/cms/encoding/ber/common.go similarity index 99% rename from internal/encoding/asn1/common.go rename to internal/crypto/cms/encoding/ber/common.go index 080b5ddb..8d2ecd87 100644 --- a/internal/encoding/asn1/common.go +++ b/internal/crypto/cms/encoding/ber/common.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package asn1 +package ber import ( "io" diff --git a/internal/encoding/asn1/common_test.go b/internal/crypto/cms/encoding/ber/common_test.go similarity index 99% rename from internal/encoding/asn1/common_test.go rename to internal/crypto/cms/encoding/ber/common_test.go index 672d168f..424a4da9 100644 --- a/internal/encoding/asn1/common_test.go +++ b/internal/crypto/cms/encoding/ber/common_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package asn1 +package ber import ( "bytes" diff --git a/internal/encoding/asn1/constructed.go b/internal/crypto/cms/encoding/ber/constructed.go similarity index 99% rename from internal/encoding/asn1/constructed.go rename to internal/crypto/cms/encoding/ber/constructed.go index cd73a684..be8126a6 100644 --- a/internal/encoding/asn1/constructed.go +++ b/internal/crypto/cms/encoding/ber/constructed.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package asn1 +package ber import "bytes" diff --git a/internal/encoding/asn1/primitive.go b/internal/crypto/cms/encoding/ber/primitive.go similarity index 99% rename from internal/encoding/asn1/primitive.go rename to internal/crypto/cms/encoding/ber/primitive.go index 5cfbf300..de746487 100644 --- a/internal/encoding/asn1/primitive.go +++ b/internal/crypto/cms/encoding/ber/primitive.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package asn1 +package ber import "bytes" diff --git a/internal/crypto/cms/errors.go b/internal/crypto/cms/errors.go new file mode 100644 index 00000000..b66178f8 --- /dev/null +++ b/internal/crypto/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" + +// ErrExpectSignedData is returned if wrong content is provided when signed +// data is expected. +var ErrExpectSignedData = errors.New("cms: signed data expected") + +// ErrAttributeNotFound is returned if attribute is not found in a given set. +var ErrAttributeNotFound = errors.New("attribute not found") + +// Verification errors +var ( + ErrSignerNotFound = VerificationError{Message: "signer 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/crypto/cms/signed.go b/internal/crypto/cms/signed.go new file mode 100644 index 00000000..6b3eeb94 --- /dev/null +++ b/internal/crypto/cms/signed.go @@ -0,0 +1,246 @@ +// 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" + "crypto" + "crypto/x509" + "encoding/asn1" + "encoding/hex" + "time" + + "github.com/notaryproject/notation-core-go/internal/crypto/cms/encoding/ber" + "github.com/notaryproject/notation-core-go/internal/crypto/hashutil" + "github.com/notaryproject/notation-core-go/internal/crypto/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 + + // Signers is the list of signer information in the SignedData. + Signers []SignerInfo +} + +// ParseSignedData parses ASN.1 BER-encoded SignedData structure to golang friendly types. +func ParseSignedData(data []byte) (*ParsedSignedData, error) { + data, err := ber.ConvertBERToDER(data) + if err != nil { + return nil, SyntaxError{Message: "invalid signed data", Detail: err} + } + var contentInfo ContentInfo + if _, err := asn1.Unmarshal(data, &contentInfo); err != nil { + return nil, SyntaxError{Message: "invalid content info", Detail: err} + } + if !oid.SignedData.Equal(contentInfo.ContentType) { + return nil, ErrExpectSignedData + } + + var signedData SignedData + if _, err := asn1.Unmarshal(contentInfo.Content.Bytes, &signedData); err != nil { + return nil, SyntaxError{Message: "invalid signed data", Detail: err} + } + certs, err := x509.ParseCertificates(signedData.Certificates.Bytes) + if err != nil { + return nil, SyntaxError{Message: "invalid signed data", Detail: err} + } + + return &ParsedSignedData{ + Content: signedData.EncapsulatedContentInfo.Content, + ContentType: signedData.EncapsulatedContentInfo.ContentType, + Certificates: certs, + CRLs: signedData.CRLs, + Signers: 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(opts x509.VerifyOptions) ([]*x509.Certificate, error) { + if len(d.Signers) == 0 { + return nil, ErrSignerNotFound + } + 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 _, signer := range d.Signers { + cert, err := d.verify(&signer, &opts) + if err != nil { + lastErr = err + continue + } + thumbprint, err := hashutil.ComputeHash(crypto.SHA256, cert.Raw) + if err != nil { + return nil, err + } + verifiedSignerMap[hex.EncodeToString(thumbprint)] = cert + } + if len(verifiedSignerMap) == 0 { + return nil, lastErr + } + + verifiedSigners := make([]*x509.Certificate, 0, len(verifiedSignerMap)) + for _, cert := range verifiedSignerMap { + verifiedSigners = append(verifiedSigners, cert) + } + return verifiedSigners, nil +} + +// 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(signer *SignerInfo, opts *x509.VerifyOptions) (*x509.Certificate, error) { + // find signer certificate + cert := d.getCertificate(&signer.SignerIdentifier) + if cert == nil { + return nil, ErrCertificateNotFound + } + + // verify signer certificate + if _, err := cert.Verify(*opts); err != nil { + return cert, VerificationError{Detail: err} + } + + // verify signature + if err := d.verifySignature(signer, cert); err != nil { + return nil, err + } + + // verify attribute + return cert, d.verifyAttributes(signer, cert) +} + +// 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(signer *SignerInfo, cert *x509.Certificate) error { + // verify signature + algorithm := oid.ToSignatureAlgorithm( + signer.DigestAlgorithm.Algorithm, + signer.SignatureAlgorithm.Algorithm, + ) + if algorithm == x509.UnknownSignatureAlgorithm { + return VerificationError{Message: "unknown signature algorithm"} + } + + signed := d.Content + if len(signer.SignedAttributes) > 0 { + encoded, err := asn1.MarshalWithParams(signer.SignedAttributes, "set") + if err != nil { + return VerificationError{Message: "invalid signed attributes", Detail: err} + } + signed = encoded + } + + if err := cert.CheckSignature(algorithm, signed, signer.Signature); err != nil { + return VerificationError{Detail: err} + } + return nil +} + +// verifyAttributes verifies the signed attributes. +// +// References: +// - RFC 5652 5.6 Signature Verification Process +func (d *ParsedSignedData) verifyAttributes(signer *SignerInfo, cert *x509.Certificate) error { + // verify attributes if present + if len(signer.SignedAttributes) == 0 { + return nil + } + + var contentType asn1.ObjectIdentifier + if err := signer.SignedAttributes.TryGet(oid.ContentType, &contentType); err != nil { + return VerificationError{Message: "invalid content type", Detail: err} + } + if !d.ContentType.Equal(contentType) { + return VerificationError{Message: "mismatch content type"} + } + + var expectedDigest []byte + if err := signer.SignedAttributes.TryGet(oid.MessageDigest, &expectedDigest); err != nil { + return VerificationError{Message: "invalid message digest", Detail: err} + } + hash, ok := oid.ToHash(signer.DigestAlgorithm.Algorithm) + if !ok { + return VerificationError{Message: "unsupported digest algorithm"} + } + actualDigest, err := hashutil.ComputeHash(hash, d.Content) + if err != nil { + return VerificationError{Message: "hash failure", Detail: err} + } + if !bytes.Equal(expectedDigest, actualDigest) { + return VerificationError{Message: "mismatch message digest"} + } + + // sanity check on signing time + var signingTime time.Time + if err := signer.SignedAttributes.TryGet(oid.SigningTime, &signingTime); err != nil { + if err == ErrAttributeNotFound { + return nil + } + return VerificationError{Message: "invalid signing time", Detail: err} + } + if signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter) { + return VerificationError{Message: "signature signed when cert is inactive"} + } + return nil +} + +// 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 +} diff --git a/internal/crypto/cms/signed_test.go b/internal/crypto/cms/signed_test.go new file mode 100644 index 00000000..6b7961f2 --- /dev/null +++ b/internal/crypto/cms/signed_test.go @@ -0,0 +1,108 @@ +// 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 ( + "crypto/x509" + "os" + "reflect" + "testing" + "time" +) + +func TestVerifySignedData(t *testing.T) { + // 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.Signers); 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(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") + } + verifiedSigners, err := signed.Verify(opts) + if err != nil { + t.Fatal("ParseSignedData.Verify() error =", err) + } + if !reflect.DeepEqual(verifiedSigners, signed.Certificates[:1]) { + t.Fatalf("ParseSignedData.Verify() = %v, want %v", verifiedSigners, signed.Certificates[:1]) + } +} + +func TestVerifyCorruptedSignedData(t *testing.T) { + // 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") + + // verify with no root CAs and should fail + 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(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/crypto/cms/testdata/GlobalSignRootCA.crt b/internal/crypto/cms/testdata/GlobalSignRootCA.crt new file mode 100644 index 00000000..8afb2190 --- /dev/null +++ b/internal/crypto/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/crypto/cms/testdata/TimeStampToken.p7s b/internal/crypto/cms/testdata/TimeStampToken.p7s new file mode 100644 index 00000000..c036aac2 Binary files /dev/null and b/internal/crypto/cms/testdata/TimeStampToken.p7s differ diff --git a/internal/crypto/hashutil/hash.go b/internal/crypto/hashutil/hash.go new file mode 100644 index 00000000..797faf66 --- /dev/null +++ b/internal/crypto/hashutil/hash.go @@ -0,0 +1,30 @@ +// 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/crypto/oid/algorithm.go b/internal/crypto/oid/algorithm.go new file mode 100644 index 00000000..c567f4f6 --- /dev/null +++ b/internal/crypto/oid/algorithm.go @@ -0,0 +1,54 @@ +// 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 SHA1.Equal(digestAlg): + return x509.SHA1WithRSA + case SHA256.Equal(digestAlg): + return x509.SHA256WithRSA + case SHA384.Equal(digestAlg): + return x509.SHA384WithRSA + case SHA512.Equal(digestAlg): + return x509.SHA512WithRSA + } + case SHA1WithRSA.Equal(sigAlg): + return x509.SHA1WithRSA + case SHA256WithRSA.Equal(sigAlg): + return x509.SHA256WithRSA + case SHA384WithRSA.Equal(sigAlg): + return x509.SHA384WithRSA + case SHA512WithRSA.Equal(sigAlg): + return x509.SHA512WithRSA + case ECDSAWithSHA1.Equal(sigAlg): + return x509.ECDSAWithSHA1 + 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/crypto/oid/hash.go b/internal/crypto/oid/hash.go new file mode 100644 index 00000000..70076c39 --- /dev/null +++ b/internal/crypto/oid/hash.go @@ -0,0 +1,55 @@ +// 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" + "fmt" +) + +// 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 SHA1.Equal(alg): + hash = crypto.SHA1 + 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() +} + +// FromHash returns corresponding ASN.1 OID for the given Hash algorithm. +func FromHash(alg crypto.Hash) (asn1.ObjectIdentifier, error) { + var id asn1.ObjectIdentifier + switch alg { + case crypto.SHA256: + id = SHA256 + case crypto.SHA384: + id = SHA384 + case crypto.SHA512: + id = SHA512 + default: + return nil, fmt.Errorf("unsupported hashing algorithm: %s", alg) + } + return id, nil +} diff --git a/internal/crypto/oid/oid.go b/internal/crypto/oid/oid.go new file mode 100644 index 00000000..4c4cec87 --- /dev/null +++ b/internal/crypto/oid/oid.go @@ -0,0 +1,80 @@ +// 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 ( + // SHA1 (id-sha1) is defined in RFC 8017 B.1 Hash Functions + SHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} + + // 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} + + // SHA1WithRSA is defined in RFC 8017 C ASN.1 Module + SHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5} + + // 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} + + // ECDSAWithSHA1 is defined in ANSI X9.62 + ECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1} + + // 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 ( + // 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} +) + +// TSTInfo (id-ct-TSTInfo) is defined in RFC 3161 2.4.2 Response Format +var TSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4} diff --git a/internal/crypto/pki/pki.go b/internal/crypto/pki/pki.go new file mode 100644 index 00000000..7a8bac17 --- /dev/null +++ b/internal/crypto/pki/pki.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 pki contains certificate management protocol structures +// defined in RFC 2510. +package pki + +import "encoding/asn1" + +// PKIStatus is defined in RFC 2510 3.2.3. +const ( + StatusGranted = 0 // you got exactly what you asked for + StatusGrantedWithMods = 1 // you got something like what you asked for + StatusRejection = 2 // you don't get it, more information elsewhere in the message + StatusWaiting = 3 // the request body part has not yet been processed, expect to hear more later + StatusRevocationWarning = 4 // this message contains a warning that a revocation is imminent + StatusRevocationNotification = 5 // notification that a revocation has occurred + StatusKeyUpdateWarning = 6 // update already done for the oldCertId specified in the key update request message +) + +// PKIFailureInfo is defined in RFC 2510 3.2.3 and RFC 3161 2.4.2. +const ( + FailureInfoBadAlg = 0 // unrecognized or unsupported Algorithm Identifier + FailureInfoBadMessageCheck = 1 // integrity check failed (e.g., signature did not verify) + FailureInfoBadRequest = 2 // transaction not permitted or supported + FailureInfoBadTime = 3 // messageTime was not sufficiently close to the system time, as defined by local policy + FailureInfoBadCertID = 4 // no certificate could be found matching the provided criteria + FailureInfoBadDataFormat = 5 // the data submitted has the wrong format + FailureInfoWrongAuthority = 6 // the authority indicated in the request is different from the one creating the response token + FailureInfoIncorrectData = 7 // the requester's data is incorrect (used for notary services) + FailureInfoMissingTimeStamp = 8 // the timestamp is missing but should be there (by policy) + FailureInfoBadPOP = 9 // the proof-of-possession failed + FailureInfoTimeNotAvailable = 14 // the TSA's time source is not available + FailureInfoUnacceptedPolicy = 15 // the requested TSA policy is not supported by the TSA. + FailureInfoUnacceptedExtension = 16 // the requested extension is not supported by the TSA. + FailureInfoAddInfoNotAvailable = 17 // the additional information requested could not be understood or is not available + FailureInfoSystemFailure = 25 // the request cannot be handled due to system failure +) + +// StatusInfo contains status codes and failure information for PKI messages. +// +// PKIStatusInfo ::= SEQUENCE { +// status PKIStatus, +// statusString PKIFreeText OPTIONAL, +// failInfo PKIFailureInfo OPTIONAL } +// +// PKIStatus ::= INTEGER +// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String +// PKIFailureInfo ::= BIT STRING +// Reference: RFC 2510 3.2.3 Status codes and Failure Information for PKI messages. +type StatusInfo struct { + Status int + StatusString []string `asn1:"optional,utf8"` + FailInfo asn1.BitString `asn1:"optional"` +}