diff --git a/go.mod b/go.mod index 554662e7..7e5829dd 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,12 @@ module github.com/notaryproject/notation-core-go go 1.18 -require github.com/golang-jwt/jwt/v4 v4.4.2 +require ( + github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/veraison/go-cose v1.0.0-rc.1.0.20220824135457-9d2fab636b83 +) + +require ( + github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/x448/float16 v0.8.4 // indirect +) diff --git a/go.sum b/go.sum index f214fedc..92dbfbdb 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,8 @@ +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/veraison/go-cose v1.0.0-rc.1.0.20220824135457-9d2fab636b83 h1:g8vDfnNOPcGzg6mnlBGc0J5t5lAJkaepXqbc9qFRnFs= +github.com/veraison/go-cose v1.0.0-rc.1.0.20220824135457-9d2fab636b83/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= diff --git a/signature/cose/conformance_test.go b/signature/cose/conformance_test.go new file mode 100644 index 00000000..9ca67b5a --- /dev/null +++ b/signature/cose/conformance_test.go @@ -0,0 +1,204 @@ +package cose + +import ( + "bytes" + "crypto/x509" + "encoding/hex" + "encoding/json" + "os" + "reflect" + "sort" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/testhelper" + "github.com/veraison/go-cose" +) + +type sign1 struct { + SigningTime int64 `json:"signingTime"` + Expiry int64 `json:"expiry"` + Payload string `json:"payload"` + ProtectedHeaders *cborStruct `json:"protectedHeaders"` + UnprotectedHeaders *cborStruct `json:"unprotectedHeaders"` + Output cborStruct `json:"expectedOutput"` +} + +type cborStruct struct { + CBORHex string `json:"cborHex"` + CBORDiag string `json:"cborDiag"` +} + +func TestConformance(t *testing.T) { + data, err := os.ReadFile("testdata/conformance.json") + if err != nil { + t.Fatalf("os.ReadFile() failed. Error = %s", err) + } + var sign1 sign1 + err = json.Unmarshal(data, &sign1) + if err != nil { + t.Fatalf("json.Unmarshal() failed. Error = %s", err) + } + testSign(t, &sign1) + testVerify(t, &sign1) +} + +// testSign does conformance check on COSE_Sign1_Tagged +func testSign(t *testing.T, sign1 *sign1) { + signRequest, err := getSignReq(sign1) + if err != nil { + t.Fatalf("getSignReq() failed. Error = %s", err) + } + env := createNewEnv(nil) + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() faild. Error = %s", err) + } + newMsg := generateSign1(env.base) + got, err := newMsg.MarshalCBOR() + if err != nil { + t.Fatalf("MarshalCBOR() faild. Error = %s", err) + } + + // sign1.Output.CBORHex is a manually computed CBOR hex used as ground + // truth in the conformance test. + want := hexToBytes(sign1.Output.CBORHex) + if !bytes.Equal(want, got) { + t.Fatalf("unexpected output:\nwant: %x\n got: %x", want, got) + } + + // Verify using the same envelope struct + // (Verify with UnmarshalCBOR is covered in the testVerify() part) + _, err = env.Verify() + if err != nil { + t.Fatalf("Verify() failed. Error = %s", err) + } +} + +// testVerify does conformance check by decoding COSE_Sign1_Tagged object +// into Sign1Message +func testVerify(t *testing.T, sign1 *sign1) { + signRequest, err := getSignReq(sign1) + if err != nil { + t.Fatalf("getSignReq() failed. Error = %s", err) + } + env := createNewEnv(nil) + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() faild. Error = %s", err) + } + //Verify after UnmarshalCBOR + var msg cose.Sign1Message + // sign1.Output.CBORHex is a manually computed CBOR hex used as ground + // truth in the conformance test. + if err := msg.UnmarshalCBOR(hexToBytes(sign1.Output.CBORHex)); err != nil { + t.Fatalf("msg.UnmarshalCBOR() failed. Error = %s", err) + } + + certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} + certChain := make([]interface{}, len(certs)) + for i, c := range certs { + certChain[i] = c.Raw + } + msg.Headers.Unprotected[cose.HeaderLabelX5Chain] = certChain + msg.Signature = env.base.Signature + + newEnv := createNewEnv(&msg) + content, err := newEnv.Verify() + if err != nil { + t.Fatalf("Verify() failed. Error = %s", err) + } + verifyPayload(&content.Payload, signRequest, t) + verifySignerInfo(&content.SignerInfo, signRequest, t) +} + +func getSignReq(sign1 *sign1) (*signature.SignRequest, error) { + certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} + signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey) + if err != nil { + return &signature.SignRequest{}, err + } + signRequest := &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: "application/vnd.cncf.notary.payload.v1+json", + Content: []byte("hello COSE"), + }, + Signer: signer, + SigningTime: time.Unix(sign1.SigningTime, 0), + Expiry: time.Unix(sign1.Expiry, 0), + ExtendedSignedAttributes: []signature.Attribute{ + {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, + {Key: "signedKey1", Value: "signedValue1", Critical: false}, + }, + SigningAgent: "NotationConformanceTest/1.0.0", + SigningScheme: "notary.x509", + } + return signRequest, nil +} + +func hexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +func verifySignerInfo(signInfo *signature.SignerInfo, request *signature.SignRequest, t *testing.T) { + if request.SigningAgent != signInfo.UnsignedAttributes.SigningAgent { + t.Fatalf("SigningAgent: expected value %q but found %q", request.SigningAgent, signInfo.UnsignedAttributes.SigningAgent) + } + + if request.SigningTime.Format(time.RFC3339) != signInfo.SignedAttributes.SigningTime.Format(time.RFC3339) { + t.Fatalf("SigningTime: expected value %q but found %q", request.SigningTime, signInfo.SignedAttributes.SigningTime) + } + + if request.Expiry.Format(time.RFC3339) != signInfo.SignedAttributes.Expiry.Format(time.RFC3339) { + t.Fatalf("Expiry: expected value %q but found %q", request.SigningTime, signInfo.SignedAttributes.Expiry) + } + + if !areAttrEqual(request.ExtendedSignedAttributes, signInfo.SignedAttributes.ExtendedAttributes) { + if !(len(request.ExtendedSignedAttributes) == 0 && len(signInfo.SignedAttributes.ExtendedAttributes) == 0) { + t.Fatalf("Mistmatch between expected and actual ExtendedAttributes") + } + } + + signer, err := getSigner(request.Signer) + if err != nil { + t.Fatalf("getSigner() failed. Error = %s", err) + } + certs := signer.CertificateChain() + if err != nil || !reflect.DeepEqual(certs, signInfo.CertificateChain) { + t.Fatalf("Mistmatch between expected and actual CertificateChain") + } +} + +func verifyPayload(payload *signature.Payload, request *signature.SignRequest, t *testing.T) { + if request.Payload.ContentType != payload.ContentType { + t.Fatalf("PayloadContentType: expected value %q but found %q", request.Payload.ContentType, payload.ContentType) + } + + if !bytes.Equal(request.Payload.Content, payload.Content) { + t.Fatalf("Payload: expected value %q but found %q", request.Payload.Content, payload.Content) + } +} + +func areAttrEqual(u []signature.Attribute, v []signature.Attribute) bool { + sort.Slice(u, func(p, q int) bool { + return u[p].Key < u[q].Key + }) + sort.Slice(v, func(p, q int) bool { + return v[p].Key < v[q].Key + }) + return reflect.DeepEqual(u, v) +} + +func generateSign1(msg *cose.Sign1Message) *cose.Sign1Message { + newMsg := cose.NewSign1Message() + newMsg.Headers.Protected = msg.Headers.Protected + newMsg.Headers.Unprotected["io.cncf.notary.signingAgent"] = msg.Headers.Unprotected["io.cncf.notary.signingAgent"] + newMsg.Payload = msg.Payload + newMsg.Signature = hexToBytes("31b6cb0cd9c974b39d603465811c2aa3d96a5dff89f80b33cb4e321dc6e68a29b4ba65c00f0f9f22ee4376abfaec2cba6fd21c6881ecaab25775e3fb9226a88cf41660b2d6fd14184540d07ded3744e19ff9dbdd081e15c8f77bb6ca3072ef57141594fad4ea57d206c6b8dd3a6e0a0a7ed764ff08dbcc439bd722e1b3d282921a579a3d860cceea37d633184f9316cb6b4fa4ea550da5ad9e5bf3c2d768a787da76e594290cb10b5b1ead8b7e75967de28e9ff429fe9db814380608a15674f9741563902a620f312213d9dce5c264017cbcb3bb4f8cebee0d5ef32b364f68c11cba5630fac8e3165d06fdebaca095267223c552fe605b4529f25b65f8fa47b010b9096cec275307e82b1062f660a73e07d0b85b978b4a59b5cde51fc9a031b488a3deb38fc312a64ef2ec1250238ae16cfefc00d9aa1ceb938fe6de51f265eebe975c29f4cff8ab0afb40c45e8c985d17347bf20f455851c1a46ab655f51a159cf8910a424c5a8bbdd239e49e43a73c7b5174de29e835063e5e64b459558de5") + return newMsg +} diff --git a/signature/cose/envelope.go b/signature/cose/envelope.go new file mode 100644 index 00000000..58919b07 --- /dev/null +++ b/signature/cose/envelope.go @@ -0,0 +1,575 @@ +package cose + +import ( + "crypto" + "crypto/rand" + "crypto/x509" + "fmt" + "io" + "strconv" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/signature/internal/base" + "github.com/veraison/go-cose" +) + +// MediaTypeEnvelope is the COSE signature envelope blob mediaType. +const MediaTypeEnvelope = "application/cose" + +func init() { + if err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope); err != nil { + panic(err) + } +} + +// Protected Headers +// https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md +const ( + headerLabelExpiry = "io.cncf.notary.expiry" + headerLabelSigningScheme = "io.cncf.notary.signingScheme" + headerLabelSigningTime = "io.cncf.notary.signingTime" + headerLabelAuthenticSigningTime = "io.cncf.notary.authenticSigningTime" +) + +// Unprotected Headers +// https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md +const ( + headerLabelTimeStampSignature = "io.cncf.notary.timestampSignature" + headerLabelSigningAgent = "io.cncf.notary.signingAgent" +) + +// Map of cose.Algorithm to signature.Algorithm +var coseAlgSignatureAlgMap = map[cose.Algorithm]signature.Algorithm{ + cose.AlgorithmPS256: signature.AlgorithmPS256, + cose.AlgorithmPS384: signature.AlgorithmPS384, + cose.AlgorithmPS512: signature.AlgorithmPS512, + cose.AlgorithmES256: signature.AlgorithmES256, + cose.AlgorithmES384: signature.AlgorithmES384, + cose.AlgorithmES512: signature.AlgorithmES512, +} + +// Map of signingScheme to signingTime header label +var signingSchemeTimeLabelMap = map[signature.SigningScheme]string{ + signature.SigningSchemeX509: headerLabelSigningTime, + signature.SigningSchemeX509SigningAuthority: headerLabelAuthenticSigningTime, +} + +// signer interface is a cose.Signer with certificate chain fetcher. +type signer interface { + cose.Signer + CertificateChain() []*x509.Certificate +} + +// remoteSigner implements signer interface. +// It is used in Sign process when base's Sign implementation is desired. +type remoteSigner struct { + base signature.Signer + alg cose.Algorithm + certs []*x509.Certificate +} + +func newRemoteSigner(base signature.Signer) (*remoteSigner, error) { + keySpec, err := base.KeySpec() + if err != nil { + return nil, err + } + alg, err := getSignatureAlgorithmFromKeySpec(keySpec) + if err != nil { + return nil, err + } + return &remoteSigner{ + base: base, + alg: alg, + }, nil +} + +// Algorithm implements cose.Signer interface. +func (signer *remoteSigner) Algorithm() cose.Algorithm { + return signer.alg +} + +// Sign implements cose.Signer interface. +func (signer *remoteSigner) Sign(rand io.Reader, payload []byte) ([]byte, error) { + signature, certs, err := signer.base.Sign(payload) + if err != nil { + return nil, err + } + signer.certs = certs + return signature, nil +} + +// CertificateChain implements signer interface. +func (signer *remoteSigner) CertificateChain() []*x509.Certificate { + return signer.certs +} + +type localSigner struct { + cose.Signer + certs []*x509.Certificate +} + +func newLocalSigner(base signature.LocalSigner) (*localSigner, error) { + key := base.PrivateKey() + if cryptoSigner, ok := key.(crypto.Signer); ok { + certs, err := base.CertificateChain() + if err != nil { + return nil, err + } + keySpec, err := base.KeySpec() + if err != nil { + return nil, err + } + alg, err := getSignatureAlgorithmFromKeySpec(keySpec) + if err != nil { + return nil, err + } + coseSigner, err := cose.NewSigner(alg, cryptoSigner) + if err != nil { + return nil, err + } + return &localSigner{ + Signer: coseSigner, + certs: certs, + }, nil + } + return nil, &signature.UnsupportedSigningKeyError{} +} + +// CertificateChain implements signer interface. +func (signer *localSigner) CertificateChain() []*x509.Certificate { + return signer.certs +} + +type envelope struct { + base *cose.Sign1Message +} + +// NewEnvelope initializes an empty COSE signature envelope. +func NewEnvelope() signature.Envelope { + return &base.Envelope{ + Envelope: &envelope{}, + } +} + +// ParseEnvelope parses envelopeBytes to a COSE signature envelope. +func ParseEnvelope(envelopeBytes []byte) (signature.Envelope, error) { + var msg cose.Sign1Message + if err := msg.UnmarshalCBOR(envelopeBytes); err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + return &base.Envelope{ + Envelope: &envelope{ + base: &msg, + }, + Raw: envelopeBytes, + }, nil +} + +// Sign implements signature.Envelope interface. +// On success, this function returns the COSE signature envelope byte slice. +func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { + // get built-in signer from go-cose or remote signer based on req.Signer + signer, err := getSigner(req.Signer) + if err != nil { + return nil, &signature.InvalidSignRequestError{Msg: err.Error()} + } + + // prepare COSE_Sign1 message + msg := cose.NewSign1Message() + + // generate protected headers of COSE envelope + msg.Headers.Protected.SetAlgorithm(signer.Algorithm()) + if err := generateProtectedHeaders(req, msg.Headers.Protected); err != nil { + return nil, &signature.InvalidSignRequestError{Msg: err.Error()} + } + + // generate payload of COSE envelope + msg.Headers.Protected[cose.HeaderLabelContentType] = req.Payload.ContentType + msg.Payload = req.Payload.Content + + // core sign process, generate signature of COSE envelope + if err := msg.Sign(rand.Reader, nil, signer); err != nil { + return nil, &signature.InvalidSignRequestError{Msg: err.Error()} + } + + // generate unprotected headers of COSE envelope + generateUnprotectedHeaders(req, signer, msg.Headers.Unprotected) + + // TODO: needs to add headerKeyTimeStampSignature. + + // encode Sign1Message into COSE_Sign1_Tagged object + encoded, err := msg.MarshalCBOR() + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + e.base = msg + return encoded, nil +} + +// Verify implements signature.Envelope interface. +// Note: Verfiy only verifies integrity of the given COSE envelope. +func (e *envelope) Verify() (*signature.EnvelopeContent, error) { + // sanity check + if e.base == nil { + return nil, &signature.SignatureEnvelopeNotFoundError{} + } + + certs, ok := e.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]interface{}) + if !ok || len(certs) == 0 { + return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} + } + certRaw, ok := certs[0].([]byte) + if !ok { + return nil, &signature.InvalidSignatureError{Msg: "COSE envelope malformed leaf certificate"} + } + cert, err := x509.ParseCertificate(certRaw) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: "malformed leaf certificate"} + } + + // core verify process, verify integrity of COSE envelope + publicKeyAlg, err := getSignatureAlgorithm(cert) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + verifier, err := cose.NewVerifier(publicKeyAlg, cert.PublicKey) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + err = e.base.Verify(nil, verifier) + if err != nil { + return nil, &signature.SignatureIntegrityError{Err: err} + } + + // extract content + return e.Content() +} + +// Content implements signature.Envelope interface. +func (e *envelope) Content() (*signature.EnvelopeContent, error) { + // sanity check + if e.base == nil { + return nil, &signature.SignatureEnvelopeNotFoundError{} + } + + payload, err := e.payload() + if err != nil { + return nil, err + } + signerInfo, err := e.signerInfo() + if err != nil { + return nil, err + } + return &signature.EnvelopeContent{ + SignerInfo: *signerInfo, + Payload: *payload, + }, nil +} + +// Given a COSE envelope, extracts its signature.Payload. +func (e *envelope) payload() (*signature.Payload, error) { + cty, ok := e.base.Headers.Protected[cose.HeaderLabelContentType] + if !ok { + return nil, &signature.InvalidSignatureError{Msg: "missing content type"} + } + var contentType string + if contentType, ok = cty.(string); !ok { + return nil, &signature.InvalidSignatureError{Msg: "content type should be of 'tstr' type"} + } + return &signature.Payload{ + ContentType: contentType, + Content: e.base.Payload, + }, nil +} + +// Given a COSE envelope, extracts its signature.SignerInfo. +func (e *envelope) signerInfo() (*signature.SignerInfo, error) { + var signerInfo signature.SignerInfo + + // parse signature of COSE envelope, populate signerInfo.Signature + sig := e.base.Signature + if len(sig) == 0 { + return nil, &signature.InvalidSignatureError{Msg: "signature missing in COSE envelope"} + } + signerInfo.Signature = sig + + // parse protected headers of COSE envelope and populate related + // signerInfo fields + err := parseProtectedHeaders(e.base.Headers.Protected, &signerInfo) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + + // parse unprotected headers of COSE envelope + certs, ok := e.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]interface{}) + if !ok || len(certs) == 0 { + return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} + } + var certChain []*x509.Certificate + for _, c := range certs { + certRaw, ok := c.([]byte) + if !ok { + return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} + } + cert, err := x509.ParseCertificate(certRaw) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + certChain = append(certChain, cert) + } + // populate signerInfo.CertificateChain + signerInfo.CertificateChain = certChain + + // populate signerInfo.UnsignedAttributes.SigningAgent + if h, ok := e.base.Headers.Unprotected[headerLabelSigningAgent].(string); ok { + signerInfo.UnsignedAttributes.SigningAgent = h + } + + // TODO: needs to add headerKeyTimeStampSignature. + + return &signerInfo, nil +} + +// getSignatureAlgorithm picks up a recommended signing algorithm for given +// certificate. +func getSignatureAlgorithm(signingCert *x509.Certificate) (cose.Algorithm, error) { + keySpec, err := signature.ExtractKeySpec(signingCert) + if err != nil { + return 0, err + } + return getSignatureAlgorithmFromKeySpec(keySpec) +} + +// getSignatureAlgorithmFromKeySpec ensures the signing algorithm satisfies +// algorithm requirements. +func getSignatureAlgorithmFromKeySpec(keySpec signature.KeySpec) (cose.Algorithm, error) { + switch keySpec.Type { + case signature.KeyTypeRSA: + switch keySpec.Size { + case 2048: + return cose.AlgorithmPS256, nil + case 3072: + return cose.AlgorithmPS384, nil + case 4096: + return cose.AlgorithmPS512, nil + default: + return 0, &signature.UnsupportedSigningKeyError{Msg: fmt.Sprintf("RSA: key size %d not supported", keySpec.Size)} + } + case signature.KeyTypeEC: + switch keySpec.Size { + case 256: + return cose.AlgorithmES256, nil + case 384: + return cose.AlgorithmES384, nil + case 521: + return cose.AlgorithmES512, nil + default: + return 0, &signature.UnsupportedSigningKeyError{Msg: fmt.Sprintf("EC: key size %d not supported", keySpec.Size)} + } + default: + return 0, &signature.UnsupportedSigningKeyError{Msg: "key type not supported"} + } +} + +// getSigner returns the built-in implementation of cose.Signer from go-cose +// or a remote signer implementation of cose.Signer. +func getSigner(signer signature.Signer) (signer, error) { + if localSigner, ok := signer.(signature.LocalSigner); ok { + return newLocalSigner(localSigner) + } + return newRemoteSigner(signer) +} + +// generateProtectedHeaders creates Protected Headers of the COSE envelope +// during Sign process. +func generateProtectedHeaders(req *signature.SignRequest, protected cose.ProtectedHeader) error { + // signingScheme + crit := []interface{}{headerLabelSigningScheme} + protected[headerLabelSigningScheme] = string(req.SigningScheme) + + // signingTime/authenticSigningTime + signingTimeLabel, ok := signingSchemeTimeLabelMap[req.SigningScheme] + if !ok { + return &signature.InvalidSignRequestError{Msg: "signing scheme: require notary.x509 or notary.x509.signingAuthority"} + } + protected[signingTimeLabel] = req.SigningTime.Unix() + if signingTimeLabel == headerLabelAuthenticSigningTime { + crit = append(crit, headerLabelAuthenticSigningTime) + } + + // expiry + if !req.Expiry.IsZero() { + crit = append(crit, headerLabelExpiry) + protected[headerLabelExpiry] = req.Expiry.Unix() + } + + // extended attributes + for _, elm := range req.ExtendedSignedAttributes { + if _, ok := protected[elm.Key]; ok { + return &signature.InvalidSignRequestError{Msg: fmt.Sprintf("%q already exists in the protected header", elm.Key)} + } + if elm.Critical { + crit = append(crit, elm.Key) + } + protected[elm.Key] = elm.Value + } + + // critical headers + protected[cose.HeaderLabelCritical] = crit + + return nil +} + +// generateUnprotectedHeaders creates Unprotected Headers of the COSE envelope +// during Sign process. +func generateUnprotectedHeaders(req *signature.SignRequest, signer signer, unprotected cose.UnprotectedHeader) { + // signing agent + if req.SigningAgent != "" { + unprotected[headerLabelSigningAgent] = req.SigningAgent + } + + // certChain + certs := signer.CertificateChain() + certChain := make([]interface{}, len(certs)) + for i, c := range certs { + certChain[i] = c.Raw + } + unprotected[cose.HeaderLabelX5Chain] = certChain +} + +// parseProtectedHeaders parses COSE envelope's protected headers and +// populates signature.SignerInfo. +func parseProtectedHeaders(protected cose.ProtectedHeader, signerInfo *signature.SignerInfo) error { + // validate critical headers and return extendedAttributeKeys + extendedAttributeKeys, err := validateCritHeaders(protected) + if err != nil { + return err + } + + // populate signerInfo.SignatureAlgorithm + alg, err := protected.Algorithm() + if err != nil { + return err + } + sigAlg, ok := coseAlgSignatureAlgMap[alg] + if !ok { + return &signature.InvalidSignatureError{Msg: "signature algorithm not supported: " + strconv.Itoa(int(alg))} + } + signerInfo.SignatureAlgorithm = sigAlg + + // populate signerInfo.SignedAttributes.SigningScheme + signingSchemeString, ok := protected[headerLabelSigningScheme].(string) + if !ok { + return &signature.InvalidSignatureError{Msg: "invalid signingScheme"} + } + signingScheme := signature.SigningScheme(signingSchemeString) + signerInfo.SignedAttributes.SigningScheme = signingScheme + + // populate signerInfo.SignedAttributes.SigningTime + signingTimeLabel, ok := signingSchemeTimeLabelMap[signingScheme] + if !ok { + return &signature.InvalidSignatureError{Msg: "unsupported signingScheme: " + signingSchemeString} + } + signingTime, ok := protected[signingTimeLabel].(int64) + if !ok { + return &signature.InvalidSignatureError{Msg: "invalid signingTime under signing scheme: " + signingSchemeString} + } + signerInfo.SignedAttributes.SigningTime = time.Unix(signingTime, 0) + + // populate signerInfo.SignedAttributes.Expiry + if exp, ok := protected[headerLabelExpiry]; ok { + expiry, ok := exp.(int64) + if !ok { + return &signature.InvalidSignatureError{Msg: "expiry requires int64 type"} + } + signerInfo.SignedAttributes.Expiry = time.Unix(expiry, 0) + } + + // populate signerInfo.SignedAttributes.ExtendedAttributes + signerInfo.SignedAttributes.ExtendedAttributes, err = generateExtendedAttributes(extendedAttributeKeys, protected) + return err +} + +// validateCritHeaders does a two-way check, namely: +// 1. validate that all critical headers are present in the protected bucket +// 2. validate that all required headers(as per spec) are marked critical +// Returns list of extended attribute keys +func validateCritHeaders(protected cose.ProtectedHeader) ([]string, error) { + // This ensures all critical headers are present in the protected bucket. + labels, err := protected.Critical() + if err != nil { + return nil, err + } + + // set of headers that must be marked as crit + mustMarkedCrit := make(map[interface{}]struct{}) + mustMarkedCrit[headerLabelSigningScheme] = struct{}{} + signingScheme, ok := protected[headerLabelSigningScheme].(string) + if !ok { + return nil, &signature.InvalidSignatureError{Msg: "invalid signingScheme"} + } + if signature.SigningScheme(signingScheme) == signature.SigningSchemeX509SigningAuthority { + mustMarkedCrit[headerLabelAuthenticSigningTime] = struct{}{} + } + if _, ok := protected[headerLabelExpiry]; ok { + mustMarkedCrit[headerLabelExpiry] = struct{}{} + } + + // validate that all required headers(as per spec) are marked as critical + for _, label := range labels { + delete(mustMarkedCrit, label) + } + if len(mustMarkedCrit) != 0 { + headers := make([]interface{}, 0, len(mustMarkedCrit)) + for k := range mustMarkedCrit { + headers = append(headers, k) + } + return nil, &signature.InvalidSignatureError{Msg: fmt.Sprintf("these required headers are not marked as critical: %v", headers)} + } + + // fetch all the extended signed attributes + systemHeaders := []interface{}{cose.HeaderLabelAlgorithm, cose.HeaderLabelCritical, cose.HeaderLabelContentType, + headerLabelExpiry, headerLabelSigningScheme, headerLabelSigningTime, headerLabelAuthenticSigningTime} + var extendedAttributeKeys []string + for label := range protected { + if contains(systemHeaders, label) { + continue + } + label, ok := label.(string) + if !ok { + return nil, &signature.InvalidSignatureError{Msg: "extendedAttributes key requires string type"} + } + extendedAttributeKeys = append(extendedAttributeKeys, label) + } + + return extendedAttributeKeys, nil +} + +// generateExtendedAttributes generates []signature.Attribute during +// SignerInfo process. +func generateExtendedAttributes(extendedAttributeKeys []string, protected cose.ProtectedHeader) ([]signature.Attribute, error) { + criticalHeaders, ok := protected[cose.HeaderLabelCritical].([]interface{}) + if !ok { + return nil, &signature.InvalidSignatureError{Msg: "invalid critical headers"} + } + var extendedAttr []signature.Attribute + for _, key := range extendedAttributeKeys { + extendedAttr = append(extendedAttr, signature.Attribute{ + Key: key, + Critical: contains(criticalHeaders, key), + Value: protected[key], + }) + } + return extendedAttr, nil +} + +// contains checks if e is in s +func contains(s []interface{}, e interface{}) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/signature/cose/envelope_test.go b/signature/cose/envelope_test.go new file mode 100644 index 00000000..9d555468 --- /dev/null +++ b/signature/cose/envelope_test.go @@ -0,0 +1,839 @@ +package cose + +import ( + "crypto" + "crypto/x509" + "errors" + "fmt" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/signature/signaturetest" + "github.com/notaryproject/notation-core-go/testhelper" + "github.com/veraison/go-cose" +) + +const ( + payloadString = "{\"targetArtifact\":{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\",\"size\":16724,\"annotations\":{\"io.wabbit-networks.buildId\":\"123\"}}}" +) + +var ( + signingSchemeString = []string{"notary.x509", "notary.x509.signingAuthority"} +) + +func TestParseEnvelopeError(t *testing.T) { + var emptyCOSE []byte + _, err := ParseEnvelope(emptyCOSE) + if err == nil { + t.Fatalf("ParseEnvelope() expects signature.InvalidSignatureError, but got nil.") + } + + _, err = ParseEnvelope([]byte("invalid")) + if err == nil { + t.Fatalf("ParseEnvelope() expects signature.InvalidSignatureError, but got nil.") + } +} + +func TestSign(t *testing.T) { + env := createNewEnv(nil) + for _, signingScheme := range signingSchemeString { + for _, keyType := range signaturetest.KeyTypes { + for _, size := range signaturetest.GetKeySizes(keyType) { + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when all arguments are present", signingScheme, keyType, size), func(t *testing.T) { + signRequest, err := newSignRequest(signingScheme, keyType, size) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() failed. Error = %s", err) + } + }) + + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when minimal arguments are present", signingScheme, keyType, size), func(t *testing.T) { + signer, err := signaturetest.GetTestLocalSigner(keyType, size) + if err != nil { + t.Fatalf("Sign() failed. Error = %s", err) + } + signRequest := &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: "application/vnd.cncf.notary.payload.v1+json", + Content: []byte(payloadString), + }, + Signer: signer, + SigningTime: time.Now(), + SigningScheme: signature.SigningScheme(signingScheme), + } + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() failed. Error = %s", err) + } + }) + + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when expiry is not present", signingScheme, keyType, size), func(t *testing.T) { + signRequest, err := newSignRequest(signingScheme, keyType, size) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + signRequest.Expiry = time.Time{} + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() failed. Error = %s", err) + } + }) + + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when signingAgent is not present", signingScheme, keyType, size), func(t *testing.T) { + signRequest, err := newSignRequest(signingScheme, keyType, size) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + signRequest.SigningAgent = "" + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() failed. Error = %s", err) + } + }) + + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when extended signed attributes are not present", signingScheme, keyType, size), func(t *testing.T) { + signRequest, err := newSignRequest(signingScheme, keyType, size) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + signRequest.ExtendedSignedAttributes = nil + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() failed. Error = %s", err) + } + }) + } + } + } +} + +func TestSignErrors(t *testing.T) { + env := createNewEnv(nil) + // Testing getSigner() + t.Run("errorLocalSigner: when getSigner has privateKeyError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorLocalSigner{privateKeyError: true} + _, err = env.Sign(signRequest) + expected := errors.New("signing key is not supported") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("errorLocalSigner: when getSigner has keySpecError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorLocalSigner{keySpecError: true} + _, err = env.Sign(signRequest) + expected := errors.New("intended KeySpec() error") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("errorLocalSigner: when getSigner has algError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorLocalSigner{algError: true} + _, err = env.Sign(signRequest) + expected := errors.New("RSA: key size 0 not supported") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("errorRemoteSigner: when getSigner has keySpecError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorRemoteSigner{keySpecError: true} + _, err = env.Sign(signRequest) + expected := errors.New("intended KeySpec() error") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("errorRemoteSigner: when getSigner has algError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorRemoteSigner{algError: true} + _, err = env.Sign(signRequest) + expected := errors.New("RSA: key size 0 not supported") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("errorRemoteSigner: when getSigner has algError and wantEC", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorRemoteSigner{algError: true, wantEC: true} + _, err = env.Sign(signRequest) + expected := errors.New("EC: key size 0 not supported") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("errorRemoteSigner: when getSigner has keyTypeError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorRemoteSigner{keyTypeError: true} + _, err = env.Sign(signRequest) + expected := errors.New("key type not supported") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + // Testing generateProtectedHeaders + t.Run("when signingScheme is absent", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.SigningScheme = "" + _, err = env.Sign(signRequest) + expected := errors.New("signing scheme: require notary.x509 or notary.x509.signingAuthority") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when an extended signed attribute already exists in the protected header", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.ExtendedSignedAttributes = []signature.Attribute{ + {Key: headerLabelSigningScheme, Value: "notary.x509", Critical: true}, + } + _, err = env.Sign(signRequest) + expected := errors.New("\"io.cncf.notary.signingScheme\" already exists in the protected header") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + // Testing core sign process + t.Run("when signer has signError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorRemoteSigner{signError: true} + _, err = env.Sign(signRequest) + expected := errors.New("intended Sign() Error") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when signer returns empty signature", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorRemoteSigner{} + _, err = env.Sign(signRequest) + expected := errors.New("empty signature") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) + + // Testing generateUnprotectedHeaders + t.Run("errorLocalSigner: when signer has certificateChainError", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Signer = &errorLocalSigner{certificateChainError: true} + _, err = env.Sign(signRequest) + expected := errors.New("intended CertificateChain() error") + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + }) +} + +func TestVerifyErrors(t *testing.T) { + t.Run("when signature envelope is not present", func(t *testing.T) { + env := createNewEnv(nil) + _, err := env.Verify() + expected := errors.New("signature envelope is not present") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope has invalid certificate chain", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = []interface{}{} + _, err = env.Verify() + expected := errors.New("certificate chain is not present") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope leaf certificate has wrong type", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = []interface{}{0} + _, err = env.Verify() + expected := errors.New("COSE envelope malformed leaf certificate") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope has malformed leaf certificate", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + certs, ok := env.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]interface{}) + if !ok || len(certs) == 0 { + t.Fatalf("certificate chain is not present") + } + certRaw, ok := certs[0].([]byte) + if !ok { + t.Fatalf("COSE envelope malformed leaf certificate") + } + // Manipulate the leaf certificate + certRaw[0] += 'A' + certs[0] = certRaw + env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = certs + _, err = env.Verify() + expected := errors.New("malformed leaf certificate") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when getSignatureAlgorithm fails due to unsupported public key", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + certs := []*x509.Certificate{testhelper.GetUnsupportedRSACert().Cert} + certChain := make([]interface{}, len(certs)) + for i, c := range certs { + certChain[i] = c.Raw + } + env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = certChain + _, err = env.Verify() + expected := errors.New("rsa key size 1024 bits is not supported") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + // Testing core verify process + t.Run("when tempered signature envelope is provided", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %s", err) + } + env := NewEnvelope() + encoded, err := env.Sign(signRequest) + if err != nil { + t.Fatalf("Sign() failed. Error = %s", err) + } + encoded[len(encoded)-10] += 'A' + newEnv, err := ParseEnvelope(encoded) + if err != nil { + t.Fatalf("ParseEnvelope() failed. Error = %s", err) + } + _, err = newEnv.Verify() + expected := errors.New("signature is invalid. Error: verification error") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when get payload fails", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + delete(env.base.Headers.Protected, cose.HeaderLabelContentType) + _, err = env.Verify() + expected := errors.New("missing content type") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when get signerInfo fails", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[cose.HeaderLabelCritical] = []interface{}{} + _, err = env.Verify() + expected := errors.New("empty crit header") + if !isErrEqual(expected, err) { + t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) + } + }) +} + +func TestPayloadErrors(t *testing.T) { + t.Run("when env.base is nil", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base = nil + _, err = env.Content() + expected := errors.New("signature envelope is not present") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when missing content type", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + delete(env.base.Headers.Protected, cose.HeaderLabelContentType) + _, err = env.Content() + expected := errors.New("missing content type") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when content type has wrong type", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[cose.HeaderLabelContentType] = 0 + _, err = env.Content() + expected := errors.New("content type should be of 'tstr' type") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) +} + +func TestSignerInfoErrors(t *testing.T) { + t.Run("when signature missing in COSE envelope", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Signature = []byte{} + _, err = env.Content() + expected := errors.New("signature missing in COSE envelope") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + // Testing parseProtectedHeaders + t.Run("when COSE envelope protected header has empty crit", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[cose.HeaderLabelCritical] = []interface{}{} + _, err = env.Content() + expected := errors.New("empty crit header") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope protected header signingScheme has wrong type", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[headerLabelSigningScheme] = 0 + _, err = env.Content() + expected := errors.New("invalid signingScheme") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope has required headers that are not marked as critical", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[cose.HeaderLabelCritical] = []interface{}{"io.cncf.notary.expiry"} + _, err = env.Content() + expected := errors.New("these required headers are not marked as critical: [io.cncf.notary.signingScheme]") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope has customized protected header key that is not of string type", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[0] = "unsupported" + _, err = env.Content() + expected := errors.New("extendedAttributes key requires string type") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope protected header missing algorithm", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + delete(env.base.Headers.Protected, cose.HeaderLabelAlgorithm) + _, err = env.Content() + expected := errors.New("algorithm not found") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope protected header has unsupported algorithm", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected.SetAlgorithm(cose.AlgorithmEd25519) + _, err = env.Content() + expected := errors.New("signature algorithm not supported: -8") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope protected header has unsupported signingScheme", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[headerLabelSigningScheme] = "unsupported" + _, err = env.Content() + expected := errors.New("unsupported signingScheme: unsupported") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope protected header has invalid signingTime", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[headerLabelSigningTime] = "invalid" + _, err = env.Content() + expected := errors.New("invalid signingTime under signing scheme: notary.x509") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope protected header has invalid expiry", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Protected[headerLabelExpiry] = "invalid" + _, err = env.Content() + expected := errors.New("expiry requires int64 type") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + // Testing unprotected headers + t.Run("when COSE envelope has invalid certificate chain", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + delete(env.base.Headers.Unprotected, cose.HeaderLabelX5Chain) + _, err = env.Content() + expected := errors.New("certificate chain is not present") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope leaf certificate has wrong type", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = []interface{}{0} + _, err = env.Content() + expected := errors.New("certificate chain is not present") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) + + t.Run("when COSE envelope has malformed leaf certificate", func(t *testing.T) { + env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + certs, ok := env.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]interface{}) + if !ok || len(certs) == 0 { + t.Fatalf("certificate chain is not present") + } + certRaw, ok := certs[0].([]byte) + if !ok { + t.Fatalf("COSE envelope malformed leaf certificate") + } + // Manipulate the leaf certificate + certRaw[0] += 'A' + certs[0] = certRaw + env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = certs + _, err = env.Content() + expected := errors.New("x509: malformed certificate") + if !isErrEqual(expected, err) { + t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) + } + }) +} + +func TestSignAndVerify(t *testing.T) { + env := createNewEnv(nil) + for _, signingScheme := range signingSchemeString { + for _, keyType := range signaturetest.KeyTypes { + for _, size := range signaturetest.GetKeySizes(keyType) { + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize", signingScheme, keyType, size), func(t *testing.T) { + // Sign + signRequest, err := newSignRequest(signingScheme, keyType, size) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + encoded, err := env.Sign(signRequest) + if err != nil || len(encoded) == 0 { + t.Fatalf("Sign() faild. Error = %s", err) + } + + // Verify using the same envelope struct + // (Verify with UnmarshalCBOR is covered in the + // TestSignAndParseVerify() part) + _, err = env.Verify() + if err != nil { + t.Fatalf("Verify() failed. Error = %s", err) + } + }) + } + } + } +} + +func TestSignAndParseVerify(t *testing.T) { + for _, signingScheme := range signingSchemeString { + for _, keyType := range signaturetest.KeyTypes { + for _, size := range signaturetest.GetKeySizes(keyType) { + t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize", signingScheme, keyType, size), func(t *testing.T) { + //Verify after UnmarshalCBOR + env, err := getVerifyCOSE(signingScheme, keyType, size) + if err != nil { + t.Fatalf("getVerifyCOSE() failed. Error = %s", err) + } + _, err = env.Verify() + if err != nil { + t.Fatalf("Verify() failed. Error = %s", err) + } + }) + } + + } + } +} + +func newSignRequest(signingScheme string, keyType signature.KeyType, size int) (*signature.SignRequest, error) { + signer, err := signaturetest.GetTestLocalSigner(keyType, size) + if err != nil { + return nil, err + } + return &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: "application/vnd.cncf.notary.payload.v1+json", + Content: []byte(payloadString), + }, + Signer: signer, + SigningTime: time.Now().Truncate(time.Second), + Expiry: time.Now().AddDate(0, 0, 1).Truncate(time.Second), + ExtendedSignedAttributes: []signature.Attribute{ + {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, + {Key: "signedKey1", Value: "signedValue1", Critical: false}, + }, + SigningAgent: "NotationUnitTest/1.0.0", + SigningScheme: signature.SigningScheme(signingScheme), + }, nil +} + +func getSignRequest() (*signature.SignRequest, error) { + return newSignRequest("notary.x509", signature.KeyTypeRSA, 3072) +} + +func getVerifyCOSE(signingScheme string, keyType signature.KeyType, size int) (envelope, error) { + signRequest, err := newSignRequest(signingScheme, keyType, size) + if err != nil { + return createNewEnv(nil), err + } + env := NewEnvelope() + encoded, err := env.Sign(signRequest) + if err != nil { + return createNewEnv(nil), err + } + var msg cose.Sign1Message + if err := msg.UnmarshalCBOR(encoded); err != nil { + return createNewEnv(nil), err + } + newEnv := createNewEnv(&msg) + return newEnv, nil +} + +// errorLocalSigner implements signature.LocalSigner interface. +type errorLocalSigner struct { + signature.LocalSigner + privateKeyError bool + keySpecError bool + algError bool + certificateChainError bool +} + +// Sign signs the digest and returns the raw signature +func (s *errorLocalSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + return nil, nil, fmt.Errorf("local signer doesn't support Sign with digest") +} + +// CertificateChain returns the certificate chain +func (s *errorLocalSigner) CertificateChain() ([]*x509.Certificate, error) { + if s.certificateChainError { + return nil, fmt.Errorf("intended CertificateChain() error") + } + return []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert}, nil +} + +// KeySpec returns the key specification +func (s *errorLocalSigner) KeySpec() (signature.KeySpec, error) { + if s.keySpecError { + return signature.KeySpec{}, fmt.Errorf("intended KeySpec() error") + } + if s.algError { + return signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 0, + }, nil + } + return signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 3072, + }, nil +} + +// PrivateKey returns the private key +func (s *errorLocalSigner) PrivateKey() crypto.PrivateKey { + if s.privateKeyError { + return fmt.Errorf("intended PrivateKey() Error") + } + return testhelper.GetRSALeafCertificate().PrivateKey +} + +// errorRemoteSigner implements signature.Signer interface. +type errorRemoteSigner struct { + signError bool + keySpecError bool + algError bool + wantEC bool + keyTypeError bool +} + +// Sign signs the digest and returns the raw signature +func (s *errorRemoteSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + if s.signError { + return nil, nil, fmt.Errorf("intended Sign() Error") + } + return nil, nil, nil +} + +// KeySpec returns the key specification +func (s *errorRemoteSigner) KeySpec() (signature.KeySpec, error) { + if s.keySpecError { + return signature.KeySpec{}, fmt.Errorf("intended KeySpec() error") + } + if s.algError { + if s.wantEC { + return signature.KeySpec{ + Type: signature.KeyTypeEC, + Size: 0, + }, nil + } + return signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 0, + }, nil + } + if s.keyTypeError { + return signature.KeySpec{ + Type: 3, + Size: 3072, + }, nil + } + if s.wantEC { + return signature.KeySpec{ + Type: signature.KeyTypeEC, + Size: 384, + }, nil + } + return signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 3072, + }, nil +} + +func isErrEqual(wanted, got error) bool { + if wanted == nil && got == nil { + return true + } + if wanted != nil && got != nil { + return wanted.Error() == got.Error() + } + return false +} + +func createNewEnv(msg *cose.Sign1Message) envelope { + return envelope{ + base: msg, + } +} diff --git a/signature/cose/testdata/conformance.json b/signature/cose/testdata/conformance.json new file mode 100644 index 00000000..254e69d0 --- /dev/null +++ b/signature/cose/testdata/conformance.json @@ -0,0 +1,19 @@ +{ + "title": "Sign1 - RSASSA-PSS w/ SHA-384 (sign)", + "description": "Sign with one signer using RSASSA-PSS w/ SHA-384", + "signingTime": 1661321924, + "expiry": 1661408324, + "payload": "68656C6C6F20434F5345", + "protectedHeaders": { + "cborHex": "A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E6578706972791A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D651A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039", + "cborDiag": "{1: -38, 2: [\"io.cncf.notary.signingScheme\", \"io.cncf.notary.expiry\", \"signedCritKey1\"], 3: \"application/vnd.cncf.notary.payload.v1+json\", \"signedKey1\": \"signedValue1\", \"signedCritKey1\": \"signedCritValue1\", \"io.cncf.notary.expiry\": 1661408324, \"io.cncf.notary.signingTime\": 1661321924, \"io.cncf.notary.signingScheme\": \"notary.x509\"}" + }, + "unprotectedHeaders": { + "cborHex": "A1781B696F2E636E63662E6E6F746172792E7369676E696E674167656E74781D4E6F746174696F6E436F6E666F726D616E6365546573742F312E302E30", + "cborDiag": "{\"io.cncf.notary.signingAgent\":\"NotationConformanceTest/1.0.0\"}" + }, + "expectedOutput": { + "cborHex": "D284590115A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E6578706972791A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D651A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039A1781B696F2E636E63662E6E6F746172792E7369676E696E674167656E74781D4E6F746174696F6E436F6E666F726D616E6365546573742F312E302E304A68656C6C6F20434F534559018031B6CB0CD9C974B39D603465811C2AA3D96A5DFF89F80B33CB4E321DC6E68A29B4BA65C00F0F9F22EE4376ABFAEC2CBA6FD21C6881ECAAB25775E3FB9226A88CF41660B2D6FD14184540D07DED3744E19FF9DBDD081E15C8F77BB6CA3072EF57141594FAD4EA57D206C6B8DD3A6E0A0A7ED764FF08DBCC439BD722E1B3D282921A579A3D860CCEEA37D633184F9316CB6B4FA4EA550DA5AD9E5BF3C2D768A787DA76E594290CB10B5B1EAD8B7E75967DE28E9FF429FE9DB814380608A15674F9741563902A620F312213D9DCE5C264017CBCB3BB4F8CEBEE0D5EF32B364F68C11CBA5630FAC8E3165D06FDEBACA095267223C552FE605B4529F25B65F8FA47B010B9096CEC275307E82B1062F660A73E07D0B85B978B4A59B5CDE51FC9A031B488A3DEB38FC312A64EF2EC1250238AE16CFEFC00D9AA1CEB938FE6DE51F265EEBE975C29F4CFF8AB0AFB40C45E8C985D17347BF20F455851C1A46AB655F51A159CF8910A424C5A8BBDD239E49E43A73C7B5174DE29E835063E5E64B459558DE5", + "cborDiag": "18([h'A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E6578706972791A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D651A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039', {\"io.cncf.notary.signingAgent\":\"NotationConformanceTest/1.0.0\"}, h'68656C6C6F20434F5345', h'31b6cb0cd9c974b39d603465811c2aa3d96a5dff89f80b33cb4e321dc6e68a29b4ba65c00f0f9f22ee4376abfaec2cba6fd21c6881ecaab25775e3fb9226a88cf41660b2d6fd14184540d07ded3744e19ff9dbdd081e15c8f77bb6ca3072ef57141594fad4ea57d206c6b8dd3a6e0a0a7ed764ff08dbcc439bd722e1b3d282921a579a3d860cceea37d633184f9316cb6b4fa4ea550da5ad9e5bf3c2d768a787da76e594290cb10b5b1ead8b7e75967de28e9ff429fe9db814380608a15674f9741563902a620f312213d9dce5c264017cbcb3bb4f8cebee0d5ef32b364f68c11cba5630fac8e3165d06fdebaca095267223c552fe605b4529f25b65f8fa47b010b9096cec275307e82b1062f660a73e07d0b85b978b4a59b5cde51fc9a031b488a3deb38fc312a64ef2ec1250238ae16cfefc00d9aa1ceb938fe6de51f265eebe975c29f4cff8ab0afb40c45e8c985d17347bf20f455851c1a46ab655f51a159cf8910a424c5a8bbdd239e49e43a73c7b5174de29e835063e5e64b459558de5'])" + } +} diff --git a/signature/jws/envelope_test.go b/signature/jws/envelope_test.go index bcd49130..fe68e520 100644 --- a/signature/jws/envelope_test.go +++ b/signature/jws/envelope_test.go @@ -162,19 +162,6 @@ func getSigner(isLocal bool, certs []*x509.Certificate, privateKey *rsa.PrivateK }, nil } -func getEnvelope(signingScheme signature.SigningScheme, isLocal bool, extendedSignedAttribute []signature.Attribute) (*jwsEnvelope, error) { - encoded, err := getEncodedMessage(signingScheme, isLocal, extendedSignedAttribute) - if err != nil { - return nil, err - } - var sigEnv jwsEnvelope - err = json.Unmarshal(encoded, &sigEnv) - if err != nil { - return nil, err - } - return &sigEnv, nil -} - func getEncodedMessage(signingScheme signature.SigningScheme, isLocal bool, extendedSignedAttribute []signature.Attribute) ([]byte, error) { signer, err := getSigner(isLocal, nil, nil) if err != nil { diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 4546690d..6ba3e5a1 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -106,7 +106,7 @@ func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple { return GetECDSACertTupleWithPK(k, cn, issuer) } -func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple { +func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple { // Even though we are creating self-signed root, we are using false for 'isRoot' to not // add root CA's basic constraint, KU and EKU. template := getCertTemplate(false, cn) @@ -119,6 +119,11 @@ func GetRSACertTupleWithPK(privKey *rsa.PrivateKey, cn string, issuer *RSACertTu return getRSACertTupleWithTemplate(template, privKey, issuer) } +func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple { + template := getCertTemplate(false, cn) + return getRSACertTupleWithTemplate(template, privKey, nil) +} + func getRSACertTupleWithTemplate(template *x509.Certificate, privKey *rsa.PrivateKey, issuer *RSACertTuple) RSACertTuple { var certBytes []byte if issuer != nil {