Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cms #17

Merged
merged 19 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# 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.

# Binaries for programs and plugins
*.exe
*.exe~
Expand All @@ -19,3 +29,12 @@

# Go workspace file
go.work

# Code Editors
.vscode
.idea
*.sublime-project
*.sublime-workspace

# Custom
coverage.txt
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/notaryproject/tspclient-go

go 1.20
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"`
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved

// 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
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
JeyJeyGao marked this conversation as resolved.
Show resolved Hide resolved
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
Loading