Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added COSE conformance tests #55

Merged
merged 22 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b6cbbf8
added extended attribute getter for notation-go
Two-Hearts Aug 22, 2022
767ec49
update
Two-Hearts Aug 22, 2022
8e02538
updated COSE envelope unit tests
Two-Hearts Aug 22, 2022
4c58c56
Merge branch 'notaryproject:cose' into cose
patrickzheng200 Aug 22, 2022
ea19e4a
updating certificate chain
Two-Hearts Aug 23, 2022
8b78d04
Merge branch 'cose' of https://github.com/patrickzheng200/notation-co…
Two-Hearts Aug 23, 2022
ee9ed27
updated COSE envelope for the certificate chain changes
Two-Hearts Aug 23, 2022
eeb237b
Merge branch 'notaryproject:cose' into cose
patrickzheng200 Aug 23, 2022
ef91e16
Merge branch 'notaryproject:cose' into cose
patrickzheng200 Aug 23, 2022
0f8ee0a
updated COSE envelope for certificate chain changes
Two-Hearts Aug 24, 2022
b03d483
Merge branch 'notaryproject:cose' into cose
patrickzheng200 Aug 24, 2022
f677400
Merge remote-tracking branch 'upstream/cose' into cose
Two-Hearts Aug 24, 2022
9371835
updated to latest go-cose
Two-Hearts Aug 25, 2022
8cdee34
Merge branch 'cose' of https://github.com/patrickzheng200/notation-co…
Two-Hearts Aug 25, 2022
f402a57
Merge branch 'notaryproject:cose' into cose
patrickzheng200 Aug 25, 2022
12df9fd
Merge remote-tracking branch 'upstream/cose' into cose
Two-Hearts Aug 25, 2022
74be7a4
added COSE conformance tests
Two-Hearts Aug 26, 2022
50e47dc
Merge branch 'cose' of https://github.com/patrickzheng200/notation-co…
Two-Hearts Aug 26, 2022
361cf3e
update
Two-Hearts Aug 26, 2022
1c62efa
Merge remote-tracking branch 'upstream/cose' into cose
Two-Hearts Aug 26, 2022
32173fe
updated comments
Two-Hearts Aug 26, 2022
b480ed3
updated per code review
Two-Hearts Aug 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions signature/cose/conformance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
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 := envelope{}
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
_, _, err = env.Verify()
patrickzheng200 marked this conversation as resolved.
Show resolved Hide resolved
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 := envelope{}
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 := envelope{
base: &msg,
}
patrickzheng200 marked this conversation as resolved.
Show resolved Hide resolved
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
}
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'])"
}
}