Skip to content

Commit

Permalink
feat: add cms (notaryproject#17)
Browse files Browse the repository at this point in the history
Signed-off-by: Junjie Gao <[email protected]>
  • Loading branch information
JeyJeyGao authored Jun 11, 2024
1 parent 657358b commit 53a48a2
Show file tree
Hide file tree
Showing 31 changed files with 1,412 additions and 0 deletions.
185 changes: 185 additions & 0 deletions internal/cms/cms.go
Original file line number Diff line number Diff line change
@@ -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
}
75 changes: 75 additions & 0 deletions internal/cms/errors.go
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 63 additions & 0 deletions internal/cms/errors_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading

0 comments on commit 53a48a2

Please sign in to comment.