Skip to content

Commit

Permalink
Added COSE conformance tests (#55)
Browse files Browse the repository at this point in the history
* added extended attribute getter for notation-go

Signed-off-by: Patrick Zheng <[email protected]>

* update

Signed-off-by: Patrick Zheng <[email protected]>

* updated COSE envelope unit tests

Signed-off-by: Patrick Zheng <[email protected]>

* updating certificate chain

Signed-off-by: Patrick Zheng <[email protected]>

* updated COSE envelope for the certificate chain changes

Signed-off-by: Patrick Zheng <[email protected]>

* updated COSE envelope for certificate chain changes

Signed-off-by: Patrick Zheng <[email protected]>

* updated to latest go-cose

Signed-off-by: Patrick Zheng <[email protected]>

* added COSE conformance tests

Signed-off-by: Patrick Zheng <[email protected]>

* update

Signed-off-by: Patrick Zheng <[email protected]>

* updated comments

Signed-off-by: Patrick Zheng <[email protected]>

* updated per code review

Signed-off-by: Patrick Zheng <[email protected]>

Signed-off-by: Patrick Zheng <[email protected]>
  • Loading branch information
patrickzheng200 authored Aug 26, 2022
1 parent 1421844 commit 157fd17
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 12 deletions.
215 changes: 215 additions & 0 deletions signature/cose/conformance_test.go
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 16 additions & 12 deletions signature/cose/envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -846,3 +844,9 @@ func isErrEqual(wanted, got error) bool {
}
return false
}

func createNewEnv(msg *cose.Sign1Message) envelope {
return envelope{
base: msg,
}
}
19 changes: 19 additions & 0 deletions signature/cose/testdata/conformance.json
Original file line number Diff line number Diff line change
@@ -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'])"
}
}

0 comments on commit 157fd17

Please sign in to comment.