diff --git a/signature/cose/conformance_test.go b/signature/cose/conformance_test.go new file mode 100644 index 00000000..0bfe3216 --- /dev/null +++ b/signature/cose/conformance_test.go @@ -0,0 +1,215 @@ +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) + } + + // Not doing conformance check on CertChain and signature fields, + // becase every time we run this test, a new certChain and signer would be + // generated, and hence, a new signature would be generated after Sign(). + // Instead, CertChain is verified in verifySignerInfo(), and signature is + // verified by go-cose's Verify() later on. + 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) + payload, signerInfo, err := newEnv.Verify() + if err != nil { + t.Fatalf("Verify() failed. Error = %s", err) + } + verifyPayload(payload, signRequest, t) + verifySignerInfo(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: signature.MediaTypePayloadV1, + Content: []byte("hello COSE"), + }, + Signer: signer, + SigningTime: time.Unix(sign1.SigningTime, 0).Truncate(time.Second), + Expiry: time.Unix(sign1.Expiry, 0).Truncate(time.Second), + 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 { + // Not doing conformance check on CertChain and signature fields, + // becase every time we run this test, a new certChain and signer would be + // generated, and hence, a new signature would be generated after Sign(). + // Instead, CertChain is verified in verifySignerInfo, and signature is + // verified by go-cose's Verify(). + 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 + // An arbitrary signature + newMsg.Signature = hexToBytes("31b6cb0cd9c974b39d603465811c2aa3d96a5dff89f80b33cb4e321dc6e68a29b4ba65c00f0f9f22ee4376abfaec2cba6fd21c6881ecaab25775e3fb9226a88cf41660b2d6fd14184540d07ded3744e19ff9dbdd081e15c8f77bb6ca3072ef57141594fad4ea57d206c6b8dd3a6e0a0a7ed764ff08dbcc439bd722e1b3d282921a579a3d860cceea37d633184f9316cb6b4fa4ea550da5ad9e5bf3c2d768a787da76e594290cb10b5b1ead8b7e75967de28e9ff429fe9db814380608a15674f9741563902a620f312213d9dce5c264017cbcb3bb4f8cebee0d5ef32b364f68c11cba5630fac8e3165d06fdebaca095267223c552fe605b4529f25b65f8fa47b010b9096cec275307e82b1062f660a73e07d0b85b978b4a59b5cde51fc9a031b488a3deb38fc312a64ef2ec1250238ae16cfefc00d9aa1ceb938fe6de51f265eebe975c29f4cff8ab0afb40c45e8c985d17347bf20f455851c1a46ab655f51a159cf8910a424c5a8bbdd239e49e43a73c7b5174de29e835063e5e64b459558de5") + return newMsg +} diff --git a/signature/cose/envelope_test.go b/signature/cose/envelope_test.go index 28fb0cc6..712abe0c 100644 --- a/signature/cose/envelope_test.go +++ b/signature/cose/envelope_test.go @@ -36,7 +36,7 @@ func TestParseEnvelopeError(t *testing.T) { } func TestSign(t *testing.T) { - env := envelope{} + env := createNewEnv(nil) for _, signingScheme := range signingSchemeString { for _, keyType := range signaturetest.KeyTypes { for _, size := range signaturetest.GetKeySizes(keyType) { @@ -112,7 +112,7 @@ func TestSign(t *testing.T) { } func TestSignErrors(t *testing.T) { - env := envelope{} + env := createNewEnv(nil) // Testing getSigner() t.Run("errorLocalSigner: when getSigner has privateKeyError", func(t *testing.T) { signRequest, err := getSignRequest() @@ -278,9 +278,7 @@ func TestSignErrors(t *testing.T) { func TestVerifyErrors(t *testing.T) { t.Run("when missing COSE signature envelope", func(t *testing.T) { - env := envelope{ - base: nil, - } + env := createNewEnv(nil) _, _, err := env.Verify() expected := errors.New("missing COSE signature envelope") if !isErrEqual(expected, err) { @@ -645,7 +643,7 @@ func TestSignerInfoErrors(t *testing.T) { } func TestSignAndVerify(t *testing.T) { - env := envelope{} + env := createNewEnv(nil) for _, signingScheme := range signingSchemeString { for _, keyType := range signaturetest.KeyTypes { for _, size := range signaturetest.GetKeySizes(keyType) { @@ -661,6 +659,8 @@ func TestSignAndVerify(t *testing.T) { } // 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) @@ -721,20 +721,18 @@ func getSignRequest() (*signature.SignRequest, error) { func getVerifyCOSE(signingScheme string, keyType signature.KeyType, size int) (envelope, error) { signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { - return envelope{}, err + return createNewEnv(nil), err } env := NewEnvelope() encoded, err := env.Sign(signRequest) if err != nil { - return envelope{}, err + return createNewEnv(nil), err } var msg cose.Sign1Message if err := msg.UnmarshalCBOR(encoded); err != nil { - return envelope{}, err - } - newEnv := envelope{ - base: &msg, + return createNewEnv(nil), err } + newEnv := createNewEnv(&msg) return newEnv, nil } @@ -846,3 +844,9 @@ func isErrEqual(wanted, got error) bool { } 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'])" + } +}