diff --git a/signature/algorithm.go b/signature/algorithm.go new file mode 100644 index 00000000..32ee7c17 --- /dev/null +++ b/signature/algorithm.go @@ -0,0 +1,109 @@ +package signature + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "fmt" +) + +// Algorithm defines the signature algorithm. +type Algorithm int + +// Signature algorithms supported by this library. +// +// Reference: https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + AlgorithmPS256 Algorithm = 1 + iota // RSASSA-PSS with SHA-256 + AlgorithmPS384 // RSASSA-PSS with SHA-384 + AlgorithmPS512 // RSASSA-PSS with SHA-512 + AlgorithmES256 // ECDSA on secp256r1 with SHA-256 + AlgorithmES384 // ECDSA on secp384r1 with SHA-384 + AlgorithmES512 // ECDSA on secp521r1 with SHA-512 +) + +// KeyType defines the key type. +type KeyType int + +const ( + KeyTypeRSA KeyType = 1 + iota // KeyType RSA + KeyTypeEC // KeyType EC +) + +// KeySpec defines a key type and size. +type KeySpec struct { + Type KeyType + Size int +} + +// Hash returns the hash function of the algorithm. +func (alg Algorithm) Hash() crypto.Hash { + switch alg { + case AlgorithmPS256, AlgorithmES256: + return crypto.SHA256 + case AlgorithmPS384, AlgorithmES384: + return crypto.SHA384 + case AlgorithmPS512, AlgorithmES512: + return crypto.SHA512 + } + return 0 +} + +// ExtractKeySpec extracts KeySpec from the signing certificate. +func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) { + switch key := signingCert.PublicKey.(type) { + case *rsa.PublicKey: + switch bitSize := key.Size() << 3; bitSize { + case 2048, 3072, 4096: + return KeySpec{ + Type: KeyTypeRSA, + Size: bitSize, + }, nil + default: + return KeySpec{}, &UnsupportedSigningKeyError{ + Msg: fmt.Sprintf("rsa key size %d is not supported", bitSize), + } + } + case *ecdsa.PublicKey: + switch bitSize := key.Curve.Params().BitSize; bitSize { + case 256, 384, 521: + return KeySpec{ + Type: KeyTypeEC, + Size: bitSize, + }, nil + default: + return KeySpec{}, &UnsupportedSigningKeyError{ + Msg: fmt.Sprintf("ecdsa key size %d is not supported", bitSize), + } + } + } + return KeySpec{}, &UnsupportedSigningKeyError{ + Msg: "invalid public key type", + } +} + +// SignatureAlgorithm returns the signing algorithm associated with the KeySpec. +func (k KeySpec) SignatureAlgorithm() Algorithm { + switch k.Type { + case KeyTypeEC: + switch k.Size { + case 256: + return AlgorithmES256 + case 384: + return AlgorithmES384 + case 521: + return AlgorithmES512 + } + case KeyTypeRSA: + switch k.Size { + case 2048: + return AlgorithmPS256 + case 3072: + return AlgorithmPS384 + case 4096: + return AlgorithmPS512 + } + } + return 0 +} diff --git a/signature/algorithm_test.go b/signature/algorithm_test.go new file mode 100644 index 00000000..40066772 --- /dev/null +++ b/signature/algorithm_test.go @@ -0,0 +1,231 @@ +package signature + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "reflect" + "strconv" + "testing" + + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestHash(t *testing.T) { + tests := []struct { + name string + alg Algorithm + expect crypto.Hash + }{ + { + name: "PS256", + alg: AlgorithmPS256, + expect: crypto.SHA256, + }, + { + name: "ES256", + alg: AlgorithmES256, + expect: crypto.SHA256, + }, + { + name: "PS384", + alg: AlgorithmPS384, + expect: crypto.SHA384, + }, + { + name: "ES384", + alg: AlgorithmES384, + expect: crypto.SHA384, + }, + { + name: "PS512", + alg: AlgorithmPS512, + expect: crypto.SHA512, + }, + { + name: "ES512", + alg: AlgorithmES512, + expect: crypto.SHA512, + }, + { + name: "UnsupportedAlgorithm", + alg: 0, + expect: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hash := tt.alg.Hash() + if hash != tt.expect { + t.Fatalf("Expected %v, got %v", tt.expect, hash) + } + }) + } +} + +func TestExtractKeySpec(t *testing.T) { + type testCase struct { + name string + cert *x509.Certificate + expect KeySpec + expectErr bool + } + // invalid cases + tests := []testCase{ + { + name: "RSA wrong size", + cert: testhelper.GetUnsupportedRSACert().Cert, + expect: KeySpec{}, + expectErr: true, + }, + { + name: "ECDSA wrong size", + cert: testhelper.GetUnsupportedECCert().Cert, + expect: KeySpec{}, + expectErr: true, + }, + { + name: "Unsupported type", + cert: &x509.Certificate{ + PublicKey: ed25519.PublicKey{}, + }, + expect: KeySpec{}, + expectErr: true, + }, + } + + // append valid RSA cases + for _, k := range []int{2048, 3072, 4096} { + rsaRoot := testhelper.GetRSARootCertificate() + priv, _ := rsa.GenerateKey(rand.Reader, k) + + certTuple := testhelper.GetRSACertTupleWithPK( + priv, + "Test RSA_"+strconv.Itoa(priv.Size()), + &rsaRoot, + ) + tests = append(tests, testCase{ + name: "RSA " + strconv.Itoa(k), + cert: certTuple.Cert, + expect: KeySpec{ + Type: KeyTypeRSA, + Size: k, + }, + expectErr: false, + }) + } + + // append valid EDCSA cases + for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { + ecdsaRoot := testhelper.GetECRootCertificate() + priv, _ := ecdsa.GenerateKey(curve, rand.Reader) + bitSize := priv.Params().BitSize + + certTuple := testhelper.GetECDSACertTupleWithPK( + priv, + "Test EC_"+strconv.Itoa(bitSize), + &ecdsaRoot, + ) + tests = append(tests, testCase{ + name: "EC " + strconv.Itoa(bitSize), + cert: certTuple.Cert, + expect: KeySpec{ + Type: KeyTypeEC, + Size: bitSize, + }, + expectErr: false, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keySpec, err := ExtractKeySpec(tt.cert) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(keySpec, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, keySpec) + } + }) + } +} + +func TestSignatureAlgorithm(t *testing.T) { + tests := []struct { + name string + keySpec KeySpec + expect Algorithm + }{ + { + name: "EC 256", + keySpec: KeySpec{ + Type: KeyTypeEC, + Size: 256, + }, + expect: AlgorithmES256, + }, + { + name: "EC 384", + keySpec: KeySpec{ + Type: KeyTypeEC, + Size: 384, + }, + expect: AlgorithmES384, + }, + { + name: "EC 521", + keySpec: KeySpec{ + Type: KeyTypeEC, + Size: 521, + }, + expect: AlgorithmES512, + }, + { + name: "RSA 2048", + keySpec: KeySpec{ + Type: KeyTypeRSA, + Size: 2048, + }, + expect: AlgorithmPS256, + }, + { + name: "RSA 3072", + keySpec: KeySpec{ + Type: KeyTypeRSA, + Size: 3072, + }, + expect: AlgorithmPS384, + }, + { + name: "RSA 4096", + keySpec: KeySpec{ + Type: KeyTypeRSA, + Size: 4096, + }, + expect: AlgorithmPS512, + }, + { + name: "Unsupported key spec", + keySpec: KeySpec{ + Type: 0, + Size: 0, + }, + expect: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + alg := tt.keySpec.SignatureAlgorithm() + if alg != tt.expect { + t.Errorf("unexpected signature algorithm: %v, expect: %v", alg, tt.expect) + } + }) + } +} diff --git a/signature/envelope.go b/signature/envelope.go new file mode 100644 index 00000000..1260dd41 --- /dev/null +++ b/signature/envelope.go @@ -0,0 +1,96 @@ +// Package signature provides operations for types that implement +// signature.Envelope or signature.Signer. +// +// An Envelope is a structure that creates and verifies a signature using the +// specified signing algorithm with required validation. To register a new +// envelope, call RegisterEnvelopeType first during the initialization. +// +// A Signer is a structure used to sign payload generated after signature +// envelope created. The underlying signing logic is provided by the underlying +// local crypto library or the external signing plugin. +package signature + +import "fmt" + +// Envelope provides functions to basic functions to manipulate signatures. +type Envelope interface { + // Sign generates and sign the envelope according to the sign request. + Sign(req *SignRequest) ([]byte, error) + + // Verify verifies the envelope and returns its enclosed payload and signer + // info. + Verify() (*Payload, *SignerInfo, error) + + // Payload returns the payload of the envelope. + // Payload is trusted only after the successful call to `Verify()`. + Payload() (*Payload, error) + + // SignerInfo returns the signer information of the envelope. + // SignerInfo is trusted only after the successful call to `Verify()`. + SignerInfo() (*SignerInfo, error) +} + +// NewEnvelopeFunc defines a function to create a new Envelope. +type NewEnvelopeFunc func() Envelope + +// ParseEnvelopeFunc defines a function that takes envelope bytes to create +// an Envelope. +type ParseEnvelopeFunc func([]byte) (Envelope, error) + +// envelopeFunc wraps functions to create and parsenew envelopes. +type envelopeFunc struct { + newFunc NewEnvelopeFunc + parseFunc ParseEnvelopeFunc +} + +// envelopeFuncs maps envelope media type to corresponding constructors and +// parsers. +var envelopeFuncs map[string]envelopeFunc + +// RegisterEnvelopeType registers newFunc and parseFunc for the given mediaType. +// Those functions are intended to be called when creating a new envelope. +// It will be called while inializing the built-in envelopes(JWS/COSE). +func RegisterEnvelopeType(mediaType string, newFunc NewEnvelopeFunc, parseFunc ParseEnvelopeFunc) error { + if newFunc == nil || parseFunc == nil { + return fmt.Errorf("required functions not provided") + } + if envelopeFuncs == nil { + envelopeFuncs = make(map[string]envelopeFunc) + } + + envelopeFuncs[mediaType] = envelopeFunc{ + newFunc: newFunc, + parseFunc: parseFunc, + } + return nil +} + +// RegisteredEnvelopeTypes lists registered envelope media types. +func RegisteredEnvelopeTypes() []string { + var types []string + + for envelopeType := range envelopeFuncs { + types = append(types, envelopeType) + } + + return types +} + +// NewEnvelope generates an envelope of given media type. +func NewEnvelope(mediaType string) (Envelope, error) { + envelopeFunc, ok := envelopeFuncs[mediaType] + if !ok { + return nil, &UnsupportedSignatureFormatError{MediaType: mediaType} + } + return envelopeFunc.newFunc(), nil +} + +// ParseEnvelope generates an envelope by given envelope bytes with specified +// media type. +func ParseEnvelope(mediaType string, envelopeBytes []byte) (Envelope, error) { + envelopeFunc, ok := envelopeFuncs[mediaType] + if !ok { + return nil, &UnsupportedSignatureFormatError{MediaType: mediaType} + } + return envelopeFunc.parseFunc(envelopeBytes) +} diff --git a/signature/envelope_test.go b/signature/envelope_test.go new file mode 100644 index 00000000..d04a4ec7 --- /dev/null +++ b/signature/envelope_test.go @@ -0,0 +1,199 @@ +package signature + +import ( + "reflect" + "testing" +) + +// mock an envelope that implements signature.Envelope. +type testEnvelope struct { +} + +// Sign implements Sign of signature.Envelope. +func (e testEnvelope) Sign(req *SignRequest) ([]byte, error) { + return nil, nil +} + +// Verify implements Verify of signature.Envelope. +func (e testEnvelope) Verify() (*Payload, *SignerInfo, error) { + return nil, nil, nil +} + +// Payload implements Payload of signature.Envelope. +func (e testEnvelope) Payload() (*Payload, error) { + return nil, nil +} + +// SignerInfo implements SignerInfo of signature.Envelope. +func (e testEnvelope) SignerInfo() (*SignerInfo, error) { + return nil, nil +} + +var ( + testNewFunc = func() Envelope { + return testEnvelope{} + } + testParseFunc = func([]byte) (Envelope, error) { + return testEnvelope{}, nil + } +) + +func TestRegisterEnvelopeType(t *testing.T) { + tests := []struct { + name string + mediaType string + newFunc NewEnvelopeFunc + parseFunc ParseEnvelopeFunc + expectErr bool + }{ + { + name: "nil newFunc", + mediaType: testMediaType, + newFunc: nil, + parseFunc: testParseFunc, + expectErr: true, + }, + { + name: "nil newParseFunc", + mediaType: testMediaType, + newFunc: testNewFunc, + parseFunc: nil, + expectErr: true, + }, + { + name: "valid funcs", + mediaType: testMediaType, + newFunc: testNewFunc, + parseFunc: testParseFunc, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := RegisterEnvelopeType(tt.mediaType, tt.newFunc, tt.parseFunc) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestRegisteredEnvelopeTypes(t *testing.T) { + tests := []struct { + name string + envelopeFuncs map[string]envelopeFunc + expect []string + }{ + { + name: "empty map", + envelopeFuncs: make(map[string]envelopeFunc), + expect: nil, + }, + { + name: "nonempty map", + envelopeFuncs: map[string]envelopeFunc{ + testMediaType: {}, + }, + expect: []string{testMediaType}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envelopeFuncs = tt.envelopeFuncs + types := RegisteredEnvelopeTypes() + + if !reflect.DeepEqual(types, tt.expect) { + t.Errorf("got types: %v, expect types: %v", types, tt.expect) + } + }) + } +} + +func TestNewEnvelope(t *testing.T) { + tests := []struct { + name string + mediaType string + envelopeFuncs map[string]envelopeFunc + expect Envelope + expectErr bool + }{ + { + name: "unsupported media type", + mediaType: testMediaType, + envelopeFuncs: make(map[string]envelopeFunc), + expect: nil, + expectErr: true, + }, + { + name: "valid media type", + mediaType: testMediaType, + envelopeFuncs: map[string]envelopeFunc{ + testMediaType: { + newFunc: testNewFunc, + }, + }, + expect: testEnvelope{}, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envelopeFuncs = tt.envelopeFuncs + envelope, err := NewEnvelope(tt.mediaType) + + if (err != nil) != tt.expectErr { + t.Errorf("got error: %v, expected error? %v", err, tt.expectErr) + } + if envelope != tt.expect { + t.Errorf("got envelope: %v, expected envelope? %v", envelope, tt.expect) + } + }) + } +} + +func TestParseEnvelope(t *testing.T) { + tests := []struct { + name string + mediaType string + envelopeFuncs map[string]envelopeFunc + expect Envelope + expectErr bool + }{ + { + name: "unsupported media type", + mediaType: testMediaType, + envelopeFuncs: make(map[string]envelopeFunc), + expect: nil, + expectErr: true, + }, + { + name: "valid media type", + mediaType: testMediaType, + envelopeFuncs: map[string]envelopeFunc{ + testMediaType: { + parseFunc: testParseFunc, + }, + }, + expect: testEnvelope{}, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envelopeFuncs = tt.envelopeFuncs + envelope, err := ParseEnvelope(tt.mediaType, nil) + + if (err != nil) != tt.expectErr { + t.Errorf("got error: %v, expected error? %v", err, tt.expectErr) + } + if envelope != tt.expect { + t.Errorf("got envelope: %v, expected envelope? %v", envelope, tt.expect) + } + }) + } +} diff --git a/signature/errors.go b/signature/errors.go new file mode 100644 index 00000000..47f81fe2 --- /dev/null +++ b/signature/errors.go @@ -0,0 +1,126 @@ +package signature + +import ( + "fmt" +) + +// MalformedSignatureError is used when Signature envelope is malformed. +type MalformedSignatureError struct { + Msg string +} + +// Error returns the error message or the default message if not provided. +func (e *MalformedSignatureError) Error() string { + if e.Msg != "" { + return e.Msg + } + return "signature envelope format is malformed" +} + +// UnsupportedSigningKeyError is used when a signing key is not supported. +type UnsupportedSigningKeyError struct { + Msg string +} + +// Error returns the error message or the default message if not provided. +func (e *UnsupportedSigningKeyError) Error() string { + if e.Msg != "" { + return e.Msg + } + return "signing key is not supported" +} + +// MalformedArgumentError is used when an argument to a function is malformed. +type MalformedArgumentError struct { + Param string + Err error +} + +// Error returns the error message. +func (e *MalformedArgumentError) Error() string { + if e.Err != nil { + return fmt.Sprintf("%q param is malformed. Error: %s", e.Param, e.Err.Error()) + } + return fmt.Sprintf("%q param is malformed", e.Param) +} + +// Unwrap returns the unwrapped error +func (e *MalformedArgumentError) Unwrap() error { + return e.Err +} + +// MalformedSignRequestError is used when SignRequest is malformed. +type MalformedSignRequestError struct { + Msg string +} + +// Error returns the error message or the default message if not provided. +func (e *MalformedSignRequestError) Error() string { + if e.Msg != "" { + return e.Msg + } + return "SignRequest is malformed" +} + +// SignatureAlgoNotSupportedError is used when signing algo is not supported. +type SignatureAlgoNotSupportedError struct { + Alg string +} + +// Error returns the formatted error message. +func (e *SignatureAlgoNotSupportedError) Error() string { + return fmt.Sprintf("signature algorithm %q is not supported", e.Alg) +} + +// SignatureIntegrityError is used when the signature associated is no longer +// valid. +type SignatureIntegrityError struct { + Err error +} + +// Error returns the formatted error message. +func (e *SignatureIntegrityError) Error() string { + return fmt.Sprintf("signature is invalid. Error: %s", e.Err.Error()) +} + +// Unwrap unwraps the internal error. +func (e *SignatureIntegrityError) Unwrap() error { + return e.Err +} + +// SignatureEnvelopeNotFoundError is used when signature envelope is not present. +type SignatureEnvelopeNotFoundError struct{} + +// Error returns the default error message. +func (e *SignatureEnvelopeNotFoundError) Error() string { + return "signature envelope is not present" +} + +// SignatureAuthenticityError is used when signature is not generated using +// trusted certificates. +type SignatureAuthenticityError struct{} + +// Error returns the default error message. +func (e *SignatureAuthenticityError) Error() string { + return "signature is not produced by a trusted signer" +} + +// UnsupportedSignatureFormatError is used when Signature envelope is not supported. +type UnsupportedSignatureFormatError struct { + MediaType string +} + +// Error returns the formatted error message. +func (e *UnsupportedSignatureFormatError) Error() string { + return fmt.Sprintf("signature envelope format with media type %q is not supported", e.MediaType) +} + +// EnvelopeKeyRepeatedError is used when repeated key name found in the envelope. +type EnvelopeKeyRepeatedError struct { + Key string +} + +// Error returns the formatted error message. +func (e *EnvelopeKeyRepeatedError) Error() string { + return fmt.Sprintf("repeated key: %q exists in the envelope.", e.Key) +} diff --git a/signature/errors_test.go b/signature/errors_test.go new file mode 100644 index 00000000..3dd5d9bb --- /dev/null +++ b/signature/errors_test.go @@ -0,0 +1,202 @@ +package signature + +import ( + "errors" + "fmt" + "testing" +) + +const ( + errMsg = "error msg" + testParam = "test param" + testAlg = "test algorithm" + testMediaType = "test media type" +) + +func TestMalformedSignatureError(t *testing.T) { + tests := []struct { + name string + err *MalformedSignatureError + expect string + }{ + { + name: "err msg set", + err: &MalformedSignatureError{Msg: errMsg}, + expect: errMsg, + }, + { + name: "err msg not set", + err: &MalformedSignatureError{}, + expect: "signature envelope format is malformed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := tt.err.Error() + if msg != tt.expect { + t.Errorf("Expected %s but got %s", tt.expect, msg) + } + }) + } +} + +func TestUnsupportedSigningKeyError(t *testing.T) { + tests := []struct { + name string + err *UnsupportedSigningKeyError + expect string + }{ + { + name: "err msg set", + err: &UnsupportedSigningKeyError{Msg: errMsg}, + expect: errMsg, + }, + { + name: "err msg not set", + err: &UnsupportedSigningKeyError{}, + expect: "signing key is not supported", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := tt.err.Error() + if msg != tt.expect { + t.Errorf("Expected %s but got %s", tt.expect, msg) + } + }) + } +} + +func TestMalformedArgumentError(t *testing.T) { + tests := []struct { + name string + err *MalformedArgumentError + expect string + }{ + { + name: "err set", + err: &MalformedArgumentError{ + Param: testParam, + Err: errors.New(errMsg), + }, + expect: fmt.Sprintf("%q param is malformed. Error: %s", testParam, errMsg), + }, + { + name: "err not set", + err: &MalformedArgumentError{Param: testParam}, + expect: fmt.Sprintf("%q param is malformed", testParam), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := tt.err.Error() + if msg != tt.expect { + t.Errorf("Expected %s but got %s", tt.expect, msg) + } + }) + } +} + +func TestMalformedArgumentError_Unwrap(t *testing.T) { + err := &MalformedArgumentError{ + Param: testParam, + Err: errors.New(errMsg), + } + unwrappedErr := err.Unwrap() + if unwrappedErr.Error() != errMsg { + t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) + } +} + +func TestMalformedSignRequestError(t *testing.T) { + tests := []struct { + name string + err *MalformedSignRequestError + expect string + }{ + { + name: "err msg set", + err: &MalformedSignRequestError{Msg: errMsg}, + expect: errMsg, + }, + { + name: "err msg not set", + err: &MalformedSignRequestError{}, + expect: "SignRequest is malformed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := tt.err.Error() + if msg != tt.expect { + t.Errorf("Expected %s but got %s", tt.expect, msg) + } + }) + } +} + +func TestSignatureAlgoNotSupportedError(t *testing.T) { + err := &SignatureAlgoNotSupportedError{ + Alg: testAlg, + } + + expectMsg := fmt.Sprintf("signature algorithm %q is not supported", testAlg) + if err.Error() != expectMsg { + t.Errorf("Expected %s but got %s", expectMsg, err.Error()) + } +} + +func TestSignatureIntegrityError(t *testing.T) { + unwrappedErr := errors.New(errMsg) + err := &SignatureIntegrityError{ + Err: unwrappedErr, + } + + expectMsg := fmt.Sprintf("signature is invalid. Error: %s", errMsg) + if err.Error() != expectMsg { + t.Errorf("Expected %s but got %s", expectMsg, err.Error()) + } + if err.Unwrap() != unwrappedErr { + t.Errorf("Expected %v but got %v", unwrappedErr, err.Unwrap()) + } +} + +func TestSignatureEnvelopeNotFoundError(t *testing.T) { + err := &SignatureEnvelopeNotFoundError{} + expectMsg := "signature envelope is not present" + + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } +} + +func TestSignatureAuthenticityError(t *testing.T) { + err := &SignatureAuthenticityError{} + expectMsg := "signature is not produced by a trusted signer" + + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } +} + +func TestUnsupportedSignatureFormatError(t *testing.T) { + err := &UnsupportedSignatureFormatError{MediaType: testMediaType} + expectMsg := fmt.Sprintf("signature envelope format with media type %q is not supported", testMediaType) + + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } +} + +func TestEnvelopeKeyRepeatedError(t *testing.T) { + err := &EnvelopeKeyRepeatedError{Key: errMsg} + expectMsg := fmt.Sprintf("repeated key: %q exists in the envelope.", errMsg) + + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } +} diff --git a/signature/internal/base/envelope.go b/signature/internal/base/envelope.go new file mode 100644 index 00000000..ad256192 --- /dev/null +++ b/signature/internal/base/envelope.go @@ -0,0 +1,235 @@ +package base + +import ( + "crypto/x509" + "fmt" + "time" + + "github.com/notaryproject/notation-core-go/signature" + nx509 "github.com/notaryproject/notation-core-go/x509" +) + +// Envelope represents a general envelope wrapping a raw signature and envelope +// in specific format. +// Envelope manipulates the common validation shared by internal envelopes. +type Envelope struct { + signature.Envelope // internal envelope in a specific format(e.g. Cose, JWS) + Raw []byte // raw signature +} + +// Sign generates signature in terms of given SignRequest. +// +// Reference: https://github.com/notaryproject/notaryproject/blob/main/signing-and-verification-workflow.md#signing-steps +func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { + // Canonicalize request. + req.SigningTime = req.SigningTime.Truncate(time.Second) + req.Expiry = req.Expiry.Truncate(time.Second) + err := validateSignRequest(req) + if err != nil { + return nil, err + } + + raw, err := e.Envelope.Sign(req) + if err != nil { + return nil, err + } + + // validate certificate chain + signerInfo, err := e.Envelope.SignerInfo() + if err != nil { + return nil, err + } + + if err := validateCertificateChain( + signerInfo.CertificateChain, + signerInfo.SignedAttributes.SigningTime, + signerInfo.SignatureAlgorithm, + ); err != nil { + return nil, err + } + + e.Raw = raw + return e.Raw, nil +} + +// Verify performs integrity and other signature specification related +// validations. +// It returns the payload to be signed and SignerInfo object containing the +// information about the signature. +// +// Reference: https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#steps +func (e *Envelope) Verify() (*signature.Payload, *signature.SignerInfo, error) { + // validation before the core verify process. + if len(e.Raw) == 0 { + return nil, nil, &signature.MalformedSignatureError{} + } + + // core verify process. + payload, signerInfo, err := e.Envelope.Verify() + if err != nil { + return nil, nil, err + } + + // validation after the core verify process. + if err = validatePayload(payload); err != nil { + return nil, nil, err + } + + if err = validateSignerInfo(signerInfo); err != nil { + return nil, nil, err + } + + return payload, signerInfo, nil +} + +// Payload returns the validated payload to be signed. +func (e *Envelope) Payload() (*signature.Payload, error) { + if len(e.Raw) == 0 { + return nil, &signature.MalformedSignatureError{Msg: "raw signature is empty"} + } + payload, err := e.Envelope.Payload() + if err != nil { + return nil, err + } + + if err = validatePayload(payload); err != nil { + return nil, err + } + return payload, nil +} + +// SignerInfo returns validated information about the signature envelope. +func (e *Envelope) SignerInfo() (*signature.SignerInfo, error) { + if len(e.Raw) == 0 { + return nil, &signature.MalformedSignatureError{Msg: "raw signature is empty"} + } + + signerInfo, err := e.Envelope.SignerInfo() + if err != nil { + return nil, &signature.MalformedSignatureError{ + Msg: fmt.Sprintf("signature envelope format is malformed. error: %s", err), + } + } + + if err := validateSignerInfo(signerInfo); err != nil { + return nil, err + } + + return signerInfo, nil +} + +// validatePayload performs validation of the payload. +func (e *Envelope) validatePayload() error { + payload, err := e.Envelope.Payload() + if err != nil { + return err + } + + return validatePayload(payload) +} + +// validateSignRequest performs basic set of validations on SignRequest struct. +func validateSignRequest(req *signature.SignRequest) error { + if err := validatePayload(&req.Payload); err != nil { + return err + } + + if err := validateSigningTime(req.SigningTime, req.Expiry); err != nil { + return err + } + + if req.Signer == nil { + return &signature.MalformedSignatureError{Msg: "signer is nil"} + } + + _, err := req.Signer.KeySpec() + return err +} + +// validateSignerInfo performs basic set of validations on SignerInfo struct. +func validateSignerInfo(info *signature.SignerInfo) error { + if len(info.Signature) == 0 { + return &signature.MalformedSignatureError{Msg: "signature not present or is empty"} + } + + if info.SignatureAlgorithm == 0 { + return &signature.MalformedSignatureError{Msg: "SignatureAlgorithm is not present"} + } + + signingTime := info.SignedAttributes.SigningTime + if err := validateSigningTime(signingTime, info.SignedAttributes.Expiry); err != nil { + return err + } + + return validateCertificateChain( + info.CertificateChain, + signingTime, + info.SignatureAlgorithm, + ) +} + +// validateSigningTime checks that signing time is within the valid range of +// time duration. +func validateSigningTime(signingTime, expireTime time.Time) error { + if signingTime.IsZero() { + return &signature.MalformedSignatureError{Msg: "signing-time not present"} + } + + if !expireTime.IsZero() && (expireTime.Before(signingTime) || expireTime.Equal(signingTime)) { + return &signature.MalformedSignatureError{Msg: "expiry cannot be equal or before the signing time"} + } + return nil +} + +// validatePayload performs validation of the payload. +func validatePayload(payload *signature.Payload) error { + switch payload.ContentType { + case signature.MediaTypePayloadV1: + if len(payload.Content) == 0 { + return &signature.MalformedSignatureError{Msg: "content not present"} + } + default: + return &signature.MalformedSignatureError{ + Msg: fmt.Sprintf("payload content type: {%s} not supported", payload.ContentType), + } + } + + return nil +} + +// validateCertificateChain performs the validation of the certificate chain. +func validateCertificateChain(certChain []*x509.Certificate, signTime time.Time, expectedAlg signature.Algorithm) error { + if len(certChain) == 0 { + return &signature.MalformedSignatureError{Msg: "certificate-chain not present or is empty"} + } + + err := nx509.ValidateCodeSigningCertChain(certChain, signTime) + if err != nil { + return &signature.MalformedSignatureError{ + Msg: fmt.Sprintf("certificate-chain is invalid, %s", err), + } + } + + signingAlg, err := getSignatureAlgorithm(certChain[0]) + if err != nil { + return &signature.MalformedSignatureError{Msg: err.Error()} + } + if signingAlg != expectedAlg { + return &signature.MalformedSignatureError{ + Msg: fmt.Sprintf("mismatch between signature algorithm derived from signing certificate (%v) and signing algorithm specified (%vs)", signingAlg, expectedAlg), + } + } + + return nil +} + +// getSignatureAlgorithm picks up a recommended signing algorithm for given +// certificate. +func getSignatureAlgorithm(signingCert *x509.Certificate) (signature.Algorithm, error) { + keySpec, err := signature.ExtractKeySpec(signingCert) + if err != nil { + return 0, err + } + + return keySpec.SignatureAlgorithm(), nil +} diff --git a/signature/internal/base/envelope_test.go b/signature/internal/base/envelope_test.go new file mode 100644 index 00000000..70b3c00b --- /dev/null +++ b/signature/internal/base/envelope_test.go @@ -0,0 +1,784 @@ +package base + +import ( + "crypto/x509" + "errors" + "reflect" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/testhelper" +) + +var ( + errMsg = "error msg" + invalidSigningAgent = "test/1" + validSigningAgent = "test/0" + invalidContentType = "text/plain" + validContentType = "application/vnd.cncf.notary.payload.v1+json" + validContent = "test content" + validBytes = []byte(validContent) + time08_02 time.Time + time08_03 time.Time + timeLayout = "2006-01-02" + validSignerInfo = &signature.SignerInfo{ + Signature: validBytes, + SignatureAlgorithm: signature.AlgorithmPS384, + SignedAttributes: signature.SignedAttributes{ + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, + }, + CertificateChain: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + } + validPayload = &signature.Payload{ + ContentType: validContentType, + Content: validBytes, + } + validReq = &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + Signer: &mockSigner{ + keySpec: signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 3072, + }, + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + }, + SigningAgent: validSigningAgent, + } + signReq1 = &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + Signer: &mockSigner{ + keySpec: signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 3072, + }, + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + }, + SigningAgent: invalidSigningAgent, + } +) + +func init() { + time08_02, _ = time.Parse(timeLayout, "2020-08-02") + time08_03, _ = time.Parse(timeLayout, "2020-08-03") +} + +// Mock an internal envelope that implements signature.Envelope. +type mockEnvelope struct { + payload *signature.Payload + verifiedPayload *signature.Payload + signerInfo *signature.SignerInfo + verifiedSignerInfo *signature.SignerInfo + failVerify bool +} + +// Sign implements Sign of signature.Envelope. +func (e mockEnvelope) Sign(req *signature.SignRequest) ([]byte, error) { + switch req.SigningAgent { + case invalidSigningAgent: + return nil, errors.New(errMsg) + case validSigningAgent: + return validBytes, nil + } + return nil, nil +} + +// Verify implements Verify of signature.Envelope. +func (e mockEnvelope) Verify() (*signature.Payload, *signature.SignerInfo, error) { + if e.failVerify { + return nil, nil, errors.New(errMsg) + } + return e.verifiedPayload, e.verifiedSignerInfo, nil +} + +// Payload implements Payload of signature.Envelope. +func (e mockEnvelope) Payload() (*signature.Payload, error) { + if e.payload == nil { + return nil, errors.New(errMsg) + } + return e.payload, nil +} + +// SignerInfo implements SignerInfo of signature.Envelope. +func (e mockEnvelope) SignerInfo() (*signature.SignerInfo, error) { + if e.signerInfo == nil { + return nil, errors.New(errMsg) + } + return e.signerInfo, nil +} + +// Mock a signer implements signature.Signer. +type mockSigner struct { + certs []*x509.Certificate + keySpec signature.KeySpec +} + +// CertificateChain implements CertificateChain of signature.Signer. +func (s *mockSigner) CertificateChain() ([]*x509.Certificate, error) { + if len(s.certs) == 0 { + return nil, errors.New(errMsg) + } + return s.certs, nil +} + +// Sign implements Sign of signature.Signer. +func (s *mockSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + return nil, nil, nil +} + +// KeySpec implements KeySpec of signature.Signer. +func (s *mockSigner) KeySpec() (signature.KeySpec, error) { + var emptyKeySpec signature.KeySpec + if s.keySpec == emptyKeySpec { + return s.keySpec, errors.New(errMsg) + } + return s.keySpec, nil +} + +func TestSign(t *testing.T) { + tests := []struct { + name string + req *signature.SignRequest + env *Envelope + expect []byte + expectErr bool + }{ + { + name: "invalid request", + req: &signature.SignRequest{ + SigningTime: time08_02, + Expiry: time08_02, + }, + env: &Envelope{ + Raw: nil, + Envelope: mockEnvelope{}, + }, + expect: nil, + expectErr: true, + }, + { + name: "internal envelope fails to sign", + req: signReq1, + env: &Envelope{ + Raw: nil, + Envelope: mockEnvelope{}, + }, + expect: nil, + expectErr: true, + }, + { + name: "internal envelope fails to get signerInfo", + req: validReq, + env: &Envelope{ + Raw: nil, + Envelope: mockEnvelope{}, + }, + expect: nil, + expectErr: true, + }, + { + name: "invalid certificate chain", + req: validReq, + env: &Envelope{ + Raw: nil, + Envelope: mockEnvelope{ + signerInfo: &signature.SignerInfo{}, + }, + }, + expect: nil, + expectErr: true, + }, + { + name: "successfully signed", + req: validReq, + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + signerInfo: validSignerInfo, + }, + }, + expect: validBytes, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sig, err := tt.env.Sign(tt.req) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(sig, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, sig) + } + }) + } +} + +func TestVerify(t *testing.T) { + tests := []struct { + name string + env *Envelope + expectPayload *signature.Payload + expectSignerInfo *signature.SignerInfo + expectErr bool + }{ + { + name: "empty raw", + env: &Envelope{}, + expectPayload: nil, + expectSignerInfo: nil, + expectErr: true, + }, + { + name: "err returned by internal envelope", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + failVerify: true, + payload: validPayload, + }, + }, + expectPayload: nil, + expectSignerInfo: nil, + expectErr: true, + }, + { + name: "payload validation failed after internal envelope verfication", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + failVerify: false, + payload: validPayload, + verifiedPayload: &signature.Payload{}, + }, + }, + expectPayload: nil, + expectSignerInfo: nil, + expectErr: true, + }, + { + name: "signerInfo validation failed after internal envelope verfication", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + failVerify: false, + payload: validPayload, + verifiedPayload: validPayload, + verifiedSignerInfo: &signature.SignerInfo{}, + }, + }, + expectPayload: nil, + expectSignerInfo: nil, + expectErr: true, + }, + { + name: "verify successfully", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + failVerify: false, + payload: validPayload, + verifiedPayload: validPayload, + verifiedSignerInfo: validSignerInfo, + }, + }, + expectPayload: validPayload, + expectSignerInfo: validSignerInfo, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + payload, signerInfo, err := tt.env.Verify() + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(payload, tt.expectPayload) { + t.Errorf("expect payload: %+v, got %+v", tt.expectPayload, payload) + } + if !reflect.DeepEqual(signerInfo, tt.expectSignerInfo) { + t.Errorf("expect signerInfo: %+v, got %+v", tt.expectSignerInfo, signerInfo) + } + }) + } +} + +func TestPayload(t *testing.T) { + tests := []struct { + name string + env *Envelope + expect *signature.Payload + expectErr bool + }{ + { + name: "empty raw", + env: &Envelope{}, + expect: nil, + expectErr: true, + }, + { + name: "err returned by internal envelope", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{}, + }, + expect: nil, + expectErr: true, + }, + { + name: "invalid payload", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + payload: &signature.Payload{ + ContentType: invalidContentType, + }, + }, + }, + expect: nil, + expectErr: true, + }, + { + name: "valid payload", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + payload: validPayload, + }, + }, + expect: validPayload, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + payload, err := tt.env.Payload() + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(payload, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, payload) + } + }) + } +} + +func TestSignerInfo(t *testing.T) { + tests := []struct { + name string + env *Envelope + expect *signature.SignerInfo + expectErr bool + }{ + { + name: "empty raw", + env: &Envelope{}, + expect: nil, + expectErr: true, + }, + { + name: "err returned by internal envelope", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{}, + }, + expect: nil, + expectErr: true, + }, + { + name: "invalid signerInfo", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + signerInfo: &signature.SignerInfo{}, + }, + }, + expect: nil, + expectErr: true, + }, + { + name: "valid signerInfo", + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + signerInfo: validSignerInfo, + }, + }, + expect: validSignerInfo, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signerInfo, err := tt.env.SignerInfo() + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(signerInfo, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, signerInfo) + } + }) + } +} + +func TestEnvelopeValidatePayload(t *testing.T) { + tests := []struct { + name string + env *Envelope + expectErr bool + }{ + { + name: "err returned by internal payload call", + env: &Envelope{ + Envelope: mockEnvelope{}, + }, + expectErr: true, + }, + { + name: "valid payload", + env: &Envelope{ + Envelope: mockEnvelope{ + payload: &signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + }, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.env.validatePayload() + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestValidateSignRequest(t *testing.T) { + tests := []struct { + name string + req *signature.SignRequest + expectErr bool + }{ + { + name: "invalid payload", + req: &signature.SignRequest{}, + expectErr: true, + }, + { + name: "invalid signing time", + req: &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + }, + expectErr: true, + }, + { + name: "signer is nil", + req: &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: time08_02, + Expiry: time08_03, + }, + expectErr: true, + }, + { + name: "empty certificates", + req: &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: time08_02, + Expiry: time08_03, + Signer: &mockSigner{}, + }, + expectErr: true, + }, + { + name: "keySpec is empty", + req: &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: time08_02, + Expiry: time08_03, + Signer: &mockSigner{ + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + keySpec: signature.KeySpec{}, + }, + }, + expectErr: true, + }, + { + name: "valid request", + req: validReq, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSignRequest(tt.req) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestValidateSignerInfo(t *testing.T) { + tests := []struct { + name string + info *signature.SignerInfo + expectErr bool + }{ + { + name: "empty signature", + info: &signature.SignerInfo{}, + expectErr: true, + }, + { + name: "missing signature algorithm", + info: &signature.SignerInfo{ + Signature: validBytes, + }, + expectErr: true, + }, + { + name: "invalid signing time", + info: &signature.SignerInfo{ + Signature: validBytes, + SignatureAlgorithm: signature.AlgorithmPS256, + }, + expectErr: true, + }, + { + name: "valid signerInfo", + info: validSignerInfo, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSignerInfo(tt.info) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestValidateSigningTime(t *testing.T) { + tests := []struct { + name string + signingTime time.Time + expireTime time.Time + expectErr bool + }{ + { + name: "zero signing time", + signingTime: time.Time{}, + expireTime: time.Now(), + expectErr: true, + }, + { + name: "no expire time", + signingTime: time.Now(), + expireTime: time.Time{}, + expectErr: false, + }, + { + name: "expireTime set but invalid", + signingTime: time08_03, + expireTime: time08_02, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSigningTime(tt.signingTime, tt.expireTime) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestValidatePayload(t *testing.T) { + tests := []struct { + name string + payload *signature.Payload + expectErr bool + }{ + { + name: "invalid payload content type", + payload: &signature.Payload{ + ContentType: invalidContentType, + }, + expectErr: true, + }, + { + name: "payload content is empty", + payload: &signature.Payload{ + ContentType: validContentType, + Content: []byte{}, + }, + expectErr: true, + }, + { + name: "valid payload", + payload: validPayload, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePayload(tt.payload) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestValidateCertificateChain(t *testing.T) { + tests := []struct { + name string + certs []*x509.Certificate + signTime time.Time + alg signature.Algorithm + expectErr bool + }{ + { + name: "empty certs", + certs: []*x509.Certificate{}, + signTime: time.Now(), + alg: signature.AlgorithmES256, + expectErr: true, + }, + { + name: "invalid certificates", + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + signTime: time.Now(), + alg: signature.AlgorithmES256, + expectErr: true, + }, + { + name: "unmatched signing algorithm", + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + signTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + alg: signature.AlgorithmPS256, + expectErr: true, + }, + { + name: "valid certificate chain", + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + signTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + alg: signature.AlgorithmPS384, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCertificateChain(tt.certs, tt.signTime, tt.alg) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestGetSignatureAlgorithm(t *testing.T) { + tests := []struct { + name string + cert *x509.Certificate + expect signature.Algorithm + expectErr bool + }{ + { + name: "unsupported cert", + cert: testhelper.GetUnsupportedRSACert().Cert, + expect: 0, + expectErr: true, + }, + { + name: "valid cert", + cert: testhelper.GetRSALeafCertificate().Cert, + expect: signature.AlgorithmPS384, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + alg, err := getSignatureAlgorithm(tt.cert) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(alg, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, alg) + } + }) + } +} diff --git a/signature/signer.go b/signature/signer.go new file mode 100644 index 00000000..b98c3233 --- /dev/null +++ b/signature/signer.go @@ -0,0 +1,134 @@ +package signature + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" +) + +// Signer is used to sign bytes generated after signature envelope created. +type Signer interface { + // Sign signs the payload and returns the raw signature and certificates. + Sign(payload []byte) ([]byte, []*x509.Certificate, error) + + // KeySpec returns the key specification. + KeySpec() (KeySpec, error) +} + +// LocalSigner is used by built-in signers to sign only. +type LocalSigner interface { + Signer + + // CertificateChain returns the certificate chain. + CertificateChain() ([]*x509.Certificate, error) + + // PrivateKey returns the private key. + PrivateKey() crypto.PrivateKey +} + +// localSigner implements LocalSigner interface. +// +// Note that localSigner only holds the signing key, keySpec and certificate +// chain. The underlying signing implementation is provided by the underlying +// crypto library for the specific signature format like go-jwt or go-cose. +type localSigner struct { + keySpec KeySpec + key crypto.PrivateKey + certs []*x509.Certificate +} + +// NewLocalSigner returns a new signer with given certificates and private key. +func NewLocalSigner(certs []*x509.Certificate, key crypto.PrivateKey) (LocalSigner, error) { + if len(certs) == 0 { + return nil, &MalformedArgumentError{ + Param: "certs", + Err: errors.New("empty certs"), + } + } + + keySpec, err := ExtractKeySpec(certs[0]) + if err != nil { + return nil, err + } + + if !isKeyPair(key, certs[0].PublicKey, keySpec) { + return nil, &MalformedArgumentError{ + Param: "key and certs", + Err: errors.New("key not matches certificate"), + } + } + + return &localSigner{ + keySpec: keySpec, + key: key, + certs: certs, + }, nil +} + +// isKeyPair checks if the private key matches the provided public key. +func isKeyPair(priv crypto.PrivateKey, pub crypto.PublicKey, keySpec KeySpec) bool { + switch keySpec.Type { + case KeyTypeRSA: + privateKey, ok := priv.(*rsa.PrivateKey) + if !ok { + return false + } + return privateKey.PublicKey.Equal(pub) + case KeyTypeEC: + privateKey, ok := priv.(*ecdsa.PrivateKey) + if !ok { + return false + } + return privateKey.PublicKey.Equal(pub) + default: + return false + } +} + +// Sign signs the content and returns the raw signature and certificates. +// This implementation should never be used by built-in signers. +func (s *localSigner) Sign(content []byte) ([]byte, []*x509.Certificate, error) { + return nil, nil, fmt.Errorf("local signer doesn't support sign") +} + +// KeySpec returns the key specification. +func (s *localSigner) KeySpec() (KeySpec, error) { + return s.keySpec, nil +} + +// CertificateChain returns the certificate chain. +func (s *localSigner) CertificateChain() ([]*x509.Certificate, error) { + return s.certs, nil +} + +// PrivateKey returns the private key. +func (s *localSigner) PrivateKey() crypto.PrivateKey { + return s.key +} + +// VerifyAuthenticity verifies the certificate chain in the given SignerInfo +// with one of the trusted certificates and returns a certificate that matches +// with one of the certificates in the SignerInfo. +// +// Reference: https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#steps +func VerifyAuthenticity(signerInfo *SignerInfo, trustedCerts []*x509.Certificate) (*x509.Certificate, error) { + if len(trustedCerts) == 0 { + return nil, &MalformedArgumentError{Param: "trustedCerts"} + } + + if signerInfo == nil { + return nil, &MalformedArgumentError{Param: "signerInfo"} + } + + for _, trust := range trustedCerts { + for _, cert := range signerInfo.CertificateChain { + if trust.Equal(cert) { + return trust, nil + } + } + } + return nil, &SignatureAuthenticityError{} +} diff --git a/signature/signer_test.go b/signature/signer_test.go new file mode 100644 index 00000000..b92c0010 --- /dev/null +++ b/signature/signer_test.go @@ -0,0 +1,226 @@ +package signature + +import ( + "crypto" + "crypto/ed25519" + "crypto/x509" + "reflect" + "testing" + + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestNewLocalSigner(t *testing.T) { + tests := []struct { + name string + certs []*x509.Certificate + key crypto.PrivateKey + expect LocalSigner + expectErr bool + }{ + { + name: "empty certs", + certs: []*x509.Certificate{}, + key: nil, + expect: nil, + expectErr: true, + }, + { + name: "unsupported leaf cert", + certs: []*x509.Certificate{ + {PublicKey: ed25519.PublicKey{}}, + }, + key: nil, + expect: nil, + expectErr: true, + }, + { + name: "keys not match", + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + key: testhelper.GetRSARootCertificate().PrivateKey, + expect: nil, + expectErr: true, + }, + { + name: "keys not match", + certs: []*x509.Certificate{ + testhelper.GetRSARootCertificate().Cert, + }, + key: testhelper.GetECLeafCertificate().PrivateKey, + expect: nil, + expectErr: true, + }, + { + name: "RSA keys match", + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + }, + key: testhelper.GetRSALeafCertificate().PrivateKey, + expect: &localSigner{ + keySpec: KeySpec{ + Type: KeyTypeRSA, + Size: 3072, + }, + key: testhelper.GetRSALeafCertificate().PrivateKey, + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + }, + }, + expectErr: false, + }, + { + name: "EC keys match", + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + key: testhelper.GetECLeafCertificate().PrivateKey, + expect: &localSigner{ + keySpec: KeySpec{ + Type: KeyTypeEC, + Size: 384, + }, + key: testhelper.GetECLeafCertificate().PrivateKey, + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signer, err := NewLocalSigner(tt.certs, tt.key) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(signer, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, signer) + } + }) + } +} + +func TestSign(t *testing.T) { + signer := &localSigner{} + + raw, certs, err := signer.Sign([]byte{}) + if err == nil { + t.Errorf("expect error but got nil") + } + if raw != nil { + t.Errorf("expect nil raw signature but got %v", raw) + } + if certs != nil { + t.Errorf("expect nil certs but got %v", certs) + } +} + +func TestKeySpec(t *testing.T) { + expectKeySpec := KeySpec{ + Type: KeyTypeRSA, + Size: 256, + } + signer := &localSigner{keySpec: expectKeySpec} + + keySpec, err := signer.KeySpec() + + if err != nil { + t.Errorf("expect no error but got %v", err) + } + if !reflect.DeepEqual(keySpec, expectKeySpec) { + t.Errorf("expect keySpec %+v, got %+v", expectKeySpec, keySpec) + } +} + +func TestCertificateChain(t *testing.T) { + expectCerts := []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + } + signer := &localSigner{certs: expectCerts} + + certs, err := signer.CertificateChain() + + if err != nil { + t.Errorf("expect no error but got %v", err) + } + if !reflect.DeepEqual(certs, expectCerts) { + t.Errorf("expect certs %+v, got %+v", expectCerts, certs) + } +} + +func TestPrivateKey(t *testing.T) { + expectKey := testhelper.GetRSALeafCertificate().PrivateKey + signer := &localSigner{key: expectKey} + + key := signer.PrivateKey() + + if !reflect.DeepEqual(key, expectKey) { + t.Errorf("expect key %+v, got %+v", expectKey, key) + } +} + +func TestVerifyAuthenticity(t *testing.T) { + tests := []struct { + name string + signerInfo *SignerInfo + certs []*x509.Certificate + expect *x509.Certificate + expectErr bool + }{ + { + name: "empty certs", + signerInfo: nil, + certs: make([]*x509.Certificate, 0), + expect: nil, + expectErr: true, + }, + { + name: "nil signerInfo", + signerInfo: nil, + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + expect: nil, + expectErr: true, + }, + { + name: "no cert matches", + signerInfo: &SignerInfo{}, + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + expect: nil, + expectErr: true, + }, + { + name: "cert matches", + signerInfo: &SignerInfo{ + CertificateChain: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + }, + certs: []*x509.Certificate{ + testhelper.GetECLeafCertificate().Cert, + }, + expect: testhelper.GetECLeafCertificate().Cert, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cert, err := VerifyAuthenticity(tt.signerInfo, tt.certs) + + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(cert, tt.expect) { + t.Errorf("expect cert %+v, got %+v", tt.expect, cert) + } + }) + } +} diff --git a/signature/types.go b/signature/types.go new file mode 100644 index 00000000..27d6e764 --- /dev/null +++ b/signature/types.go @@ -0,0 +1,133 @@ +package signature + +import ( + "crypto/x509" + "errors" + "time" +) + +// MediaTypePayloadV1 is the supported content type for signature's payload. +const MediaTypePayloadV1 = "application/vnd.cncf.notary.payload.v1+json" + +// SigningScheme formalizes the feature set (guarantees) provided by +// the signature. +// Reference: https://github.com/notaryproject/notaryproject/blob/main/signing-scheme.md +type SigningScheme string + +// SigningSchemes supported by notation. +const ( + // notary.x509 signing scheme. + SigningSchemeX509 SigningScheme = "notary.x509" + + // notary.x509.signingAuthority schema. + SigningSchemeX509SigningAuthority SigningScheme = "notary.x509.signingAuthority" +) + +// SignedAttributes represents signed metadata in the signature envelope. +// Reference: https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#signed-attributes +type SignedAttributes struct { + // SigningScheme defines the Notary v2 Signing Scheme used by the signature. + SigningScheme SigningScheme + + // SigningTime indicates the time at which the signature was generated. + SigningTime time.Time + + // Expiry provides a “best by use” time for the artifact. + Expiry time.Time + + // additional signed attributes in the signature envelope. + ExtendedAttributes []Attribute +} + +// UnsignedAttributes represents unsigned metadata in the Signature envelope. +// Reference: https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#unsigned-attributes +type UnsignedAttributes struct { + // TimestampSignature is a counter signature providing authentic timestamp. + TimestampSignature []byte + + // SigningAgent provides the identifier of the software (e.g. Notation) that + // produces the signature on behalf of the user. + SigningAgent string +} + +// Attribute represents metadata in the Signature envelope. +type Attribute struct { + // Key is the key name of the attribute. + Key string + + // Critical marks the attribute that MUST be processed by a verifier. + Critical bool + + // Value is the value of the attribute. + Value interface{} +} + +// SignRequest is used to generate Signature. +type SignRequest struct { + // Payload is the payload to be signed. + Payload Payload + + // Signer is the signer used to sign the digest. + Signer Signer + + // SigningTime is the time at which the signature was generated. + SigningTime time.Time + + // Expiry provides a “best by use” time for the artifact. + Expiry time.Time + + // ExtendedSignedAttributes is additional signed attributes in the + // signature envelope. + ExtendedSignedAttributes []Attribute + + // SigningAgent provides the identifier of the software (e.g. Notation) + // that produced the signature on behalf of the user. + SigningAgent string + + // SigningScheme defines the Notary v2 Signing Scheme used by the signature. + SigningScheme SigningScheme +} + +// SignerInfo represents a parsed signature envelope that is agnostic to +// signature envelope format. +type SignerInfo struct { + // SignedAttributes are additional metadata required to support the + // signature verification process. + SignedAttributes SignedAttributes + + // UnsignedAttributes are considered unsigned with respect to the signing + // key that generates the signature. + UnsignedAttributes UnsignedAttributes + + // SignatureAlgorithm defines the signature algorithm. + SignatureAlgorithm Algorithm + + // CertificateChain is an ordered list of X.509 public certificates + // associated with the signing key used to generate the signature. + // The ordered list starts with the signing certificates, any intermediate + // certificates and ends with the root certificate. + CertificateChain []*x509.Certificate + + // Signature is the bytes generated from the signature. + Signature []byte +} + +// Payload represents payload in bytes and its content type. +type Payload struct { + // ContentType specifies the content type of payload. + ContentType string + + // Content contains the raw bytes of the payload. + Content []byte +} + +// ExtendedAttribute fetches the specified Attribute with provided key from +// signerInfo.SignedAttributes.ExtendedAttributes. +func (signerInfo *SignerInfo) ExtendedAttribute(key string) (Attribute, error) { + for _, attr := range signerInfo.SignedAttributes.ExtendedAttributes { + if attr.Key == key { + return attr, nil + } + } + return Attribute{}, errors.New("key not in ExtendedAttributes") +} diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 6b88449f..bbc6bb7d 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -10,15 +10,17 @@ import ( "crypto/x509" "crypto/x509/pkix" "math/big" + "strconv" "time" ) var ( - rsaRoot RSACertTuple - rsaLeaf RSACertTuple - ecdsaRoot ECCertTuple - ecdsaLeaf ECCertTuple - unsupported RSACertTuple + rsaRoot RSACertTuple + rsaLeaf RSACertTuple + ecdsaRoot ECCertTuple + ecdsaLeaf ECCertTuple + unsupportedECDSARoot ECCertTuple + unsupportedRSARoot RSACertTuple ) type RSACertTuple struct { @@ -56,10 +58,16 @@ func GetECLeafCertificate() ECCertTuple { return ecdsaLeaf } -// GetUnsupportedCertificate returns certificate signed using RSA algorithm with key size of 1024 bits -// which is not supported by notary. -func GetUnsupportedCertificate() RSACertTuple { - return unsupported +// GetUnsupportedRSACert returns certificate signed using RSA algorithm with key +// size of 1024 bits which is not supported by notary. +func GetUnsupportedRSACert() RSACertTuple { + return unsupportedRSARoot +} + +// GetUnsupportedECCert returns certificate signed using EC algorithm with P-224 +// curve which is not supported by notary. +func GetUnsupportedECCert() ECCertTuple { + return unsupportedECDSARoot } func setupCertificates() { @@ -67,11 +75,12 @@ func setupCertificates() { rsaLeaf = getCertTuple("Notation Test Leaf Cert", &rsaRoot) ecdsaRoot = getECCertTuple("Notation Test Root2", nil) ecdsaLeaf = getECCertTuple("Notation Test Leaf Cert", &ecdsaRoot) + unsupportedECDSARoot = getECCertTupleWithCurve("Notation Test Invalid ECDSA Cert", nil, elliptic.P224()) // This will be flagged by the static code analyzer as 'Use of a weak cryptographic key' but its intentional // and is used only for testing. k, _ := rsa.GenerateKey(rand.Reader, 1024) - unsupported = GetRSACertTupleWithPK(k, "Notation Unsupported Root", nil) + unsupportedRSARoot = GetRSACertTupleWithPK(k, "Notation Unsupported Root", nil) } func getCertTuple(cn string, issuer *RSACertTuple) RSACertTuple { @@ -79,6 +88,11 @@ func getCertTuple(cn string, issuer *RSACertTuple) RSACertTuple { return GetRSACertTupleWithPK(pk, cn, issuer) } +func getECCertTupleWithCurve(cn string, issuer *ECCertTuple, curve elliptic.Curve) ECCertTuple { + k, _ := ecdsa.GenerateKey(curve, rand.Reader) + return GetECDSACertTupleWithPK(k, cn, issuer) +} + func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple { k, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) return GetECDSACertTupleWithPK(k, cn, issuer) @@ -147,3 +161,28 @@ func getCertTemplate(isRoot bool, cn string) *x509.Certificate { return template } + +func GetRSACertTuple(size int) RSACertTuple { + rsaRoot := GetRSARootCertificate() + priv, _ := rsa.GenerateKey(rand.Reader, size) + + certTuple := GetRSACertTupleWithPK( + priv, + "Test RSA_"+strconv.Itoa(priv.Size()), + &rsaRoot, + ) + return certTuple +} + +func GetECCertTuple(curve elliptic.Curve) ECCertTuple { + ecdsaRoot := GetECRootCertificate() + priv, _ := ecdsa.GenerateKey(curve, rand.Reader) + bitSize := priv.Params().BitSize + + certTuple := GetECDSACertTupleWithPK( + priv, + "Test EC_"+strconv.Itoa(bitSize), + &ecdsaRoot, + ) + return certTuple +}