Skip to content

Commit

Permalink
feat: added COSE envelope implementation (#75)
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Zheng <[email protected]>
  • Loading branch information
Two-Hearts authored Sep 29, 2022
1 parent 009c09a commit b5df54c
Show file tree
Hide file tree
Showing 8 changed files with 1,658 additions and 15 deletions.
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
204 changes: 204 additions & 0 deletions signature/cose/conformance_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit b5df54c

Please sign in to comment.