Skip to content

Commit 46b3b25

Browse files
feat: implement signed-cbor-cmw
Implement new `SignCBOR` and `VerifyCBOR` APIs to sign and verify a `signed-cbor-cmw` (see §4.1 of draft-ietf-rats-msg-wrap-17). Fix #14 Signed-off-by: Thomas Fossati <[email protected]>
1 parent 7755718 commit 46b3b25

File tree

5 files changed

+216
-3
lines changed

5 files changed

+216
-3
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ go 1.23.0
44

55
require (
66
github.com/fxamacker/cbor/v2 v2.7.0
7-
github.com/stretchr/testify v1.9.0
7+
github.com/stretchr/testify v1.10.0
8+
github.com/veraison/go-cose v1.3.0
89
)
910

1011
require (

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
44
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
55
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
66
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
8+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9+
github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk=
10+
github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc=
911
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
1012
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
1113
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

signed_cbor.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cmw
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
7+
cose "github.com/veraison/go-cose"
8+
)
9+
10+
// SignCBOR produces a signed-cbor-cmw from the target CMW by signing it with
11+
// the supplied cose.Signer.
12+
func (o CMW) SignCBOR(signer cose.Signer) ([]byte, error) {
13+
msg := cose.NewSignMessage()
14+
15+
msg.Headers.Protected[cose.HeaderLabelAlgorithm] = signer.Algorithm()
16+
msg.Headers.Protected[cose.HeaderLabelContentType] = "application/cmw+cbor"
17+
18+
payload, err := o.MarshalCBOR()
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
return cose.Sign1(rand.Reader, signer, msg.Headers, payload, nil)
24+
}
25+
26+
// VerifyCBOR verifies the signed-cbor-cmw using the supplied cose.Verifier. If
27+
// the signature is succesfully validated and the payload CMW is correctly
28+
// formatted, the CMW target is populated.
29+
func (o *CMW) VerifyCBOR(verifier cose.Verifier, cbor []byte) error {
30+
var msg cose.Sign1Message
31+
if err := msg.UnmarshalCBOR(cbor); err != nil {
32+
return fmt.Errorf("CBOR decoding signed-cbor-cmw: %w", err)
33+
}
34+
35+
if v, ok := msg.Headers.Protected[cose.HeaderLabelContentType]; ok {
36+
if v != "application/cmw+cbor" {
37+
return fmt.Errorf("unexpected content type in signed-cbor-cmw: %v", v)
38+
}
39+
} else {
40+
return fmt.Errorf("missing mandatory cty parameter in signed-cbor-cmw protected headers")
41+
}
42+
43+
if _, ok := msg.Headers.Protected[cose.HeaderLabelAlgorithm]; !ok {
44+
return fmt.Errorf("missing mandatory alg parameter in signed-cbor-cmw protected headers")
45+
}
46+
47+
if err := msg.Verify(nil, verifier); err != nil {
48+
return fmt.Errorf("signed-cbor-cmw signature verification failed: %w", err)
49+
}
50+
51+
if err := o.UnmarshalCBOR(msg.Payload); err != nil {
52+
return fmt.Errorf("CBOR decoding signed-cbor-cmw payload: %w", err)
53+
}
54+
55+
return nil
56+
}

signed_cbor_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package cmw
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
cose "github.com/veraison/go-cose"
9+
)
10+
11+
func TestCMW_Signed_CBOR_roundtrip_ok(t *testing.T) {
12+
in := makeCMWCollection()
13+
c0, _ := in.MarshalCBOR()
14+
15+
signer, verifier, err := getCOSESignerAndVerifier(t, testES256Key, cose.AlgorithmES256)
16+
require.NoError(t, err)
17+
18+
got, err := in.SignCBOR(signer)
19+
assert.NoError(t, err)
20+
21+
var out CMW
22+
err = out.VerifyCBOR(verifier, got)
23+
assert.NoError(t, err)
24+
25+
c1, _ := out.MarshalCBOR()
26+
assert.Equal(t, c0, c1)
27+
}
28+
29+
func TestCMW_Signed_CBOR_Verify_phdr_failures(t *testing.T) {
30+
tvs := []struct {
31+
v []byte
32+
e string
33+
}{
34+
{
35+
mustHexDecode("d28457a103746170706c69636174696f6e2f636d772b63626f72a058dea4685f5f636d77635f74737461673a696574662e6f72672c323032343a586a6d75726d75726c657373a2685f5f636d77635f74737461673a696574662e6f72672c323032343a596a706f6c7973636f7069638378186170706c69636174696f6e2f6561742d7563732b6a736f6e527b226561745f6e6f6e6365223a202e2e2e7d086c6272657477616c6461646f6d8278186170706c69636174696f6e2f6561742d7563732b63626f7242a10a7170686f746f656c656374726f67726170688378186170706c69636174696f6e2f6561742d7563732b63626f7243827818035840c274b981723930b3cc14912fe385a9dd2b29425a60afb01ebc625444ac05edd74bd7ebc69faa1b961606b58b575bf4ade87e5835d90454c6336a6cead4b3e5c7"),
36+
"missing mandatory alg parameter in signed-cbor-cmw protected headers",
37+
},
38+
{
39+
mustHexDecode("d28443a10126a058dea4685f5f636d77635f74737461673a696574662e6f72672c323032343a586a6d75726d75726c657373a2685f5f636d77635f74737461673a696574662e6f72672c323032343a596a706f6c7973636f7069638378186170706c69636174696f6e2f6561742d7563732b6a736f6e527b226561745f6e6f6e6365223a202e2e2e7d086c6272657477616c6461646f6d8278186170706c69636174696f6e2f6561742d7563732b63626f7242a10a7170686f746f656c656374726f67726170688378186170706c69636174696f6e2f6561742d7563732b63626f7243827818035840c274b981723930b3cc14912fe385a9dd2b29425a60afb01ebc625444ac05edd74bd7ebc69faa1b961606b58b575bf4ade87e5835d90454c6336a6cead4b3e5c7"),
40+
"missing mandatory cty parameter in signed-cbor-cmw protected headers",
41+
},
42+
{
43+
mustHexDecode("d2845820a2012603781a6170706c69636174696f6e2f736f6d657468696e672b656c7365a058dea4685f5f636d77635f74737461673a696574662e6f72672c323032343a586a6d75726d75726c657373a2685f5f636d77635f74737461673a696574662e6f72672c323032343a596a706f6c7973636f7069638378186170706c69636174696f6e2f6561742d7563732b6a736f6e527b226561745f6e6f6e6365223a202e2e2e7d086c6272657477616c6461646f6d8278186170706c69636174696f6e2f6561742d7563732b63626f7242a10a7170686f746f656c656374726f67726170688378186170706c69636174696f6e2f6561742d7563732b63626f7243827818035840c274b981723930b3cc14912fe385a9dd2b29425a60afb01ebc625444ac05edd74bd7ebc69faa1b961606b58b575bf4ade87e5835d90454c6336a6cead4b3e5c7"),
44+
"unexpected content type in signed-cbor-cmw: application/something+else",
45+
},
46+
{
47+
// clobbered signature field
48+
mustHexDecode("d2845819a2012603746170706c69636174696f6e2f636d772b63626f72a058dea4685f5f636d77635f74737461673a696574662e6f72672c323032343a586a6d75726d75726c657373a2685f5f636d77635f74737461673a696574662e6f72672c323032343a596a706f6c7973636f7069638378186170706c69636174696f6e2f6561742d7563732b6a736f6e527b226561745f6e6f6e6365223a202e2e2e7d086c6272657477616c6461646f6d8278186170706c69636174696f6e2f6561742d7563732b63626f7242a10a7170686f746f656c656374726f67726170688378186170706c69636174696f6e2f6561742d7563732b63626f7243827818035840c274b981723930b3cc14912fe385a9dd2b29425a60afb01ebc625444ac05edd74bd7ebc69faa1b961606b58b575bf4ade87e5835d90454c6336a6cead4b3e5ff"),
49+
"signed-cbor-cmw signature verification failed: verification error",
50+
},
51+
{
52+
mustHexDecode("d2845819a20126037461"),
53+
"CBOR decoding signed-cbor-cmw: unexpected EOF",
54+
},
55+
{
56+
// invalid CMW payload 0xff (start of indef-length)
57+
mustHexDecode("d2845819a2012603746170706c69636174696f6e2f636d772b63626f72a041ff5840746bbcd3f317eeed7de98aae40e0940dbae9f08a348bca6015c58e03eba1090d3f5c6bbcf6237fa0b74670c45eda09835d36d43c970c3e9df50811144e42aeff"),
58+
"CBOR decoding signed-cbor-cmw payload: decoding tag",
59+
},
60+
}
61+
62+
_, verifier, err := getCOSESignerAndVerifier(t, testES256Key, cose.AlgorithmES256)
63+
require.NoError(t, err)
64+
65+
for _, tv := range tvs {
66+
var c CMW
67+
err := c.VerifyCBOR(verifier, tv.v)
68+
assert.ErrorContains(t, err, tv.e)
69+
}
70+
}

test_common.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cmw
2+
3+
import (
4+
"crypto"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"encoding/base64"
8+
"encoding/json"
9+
"errors"
10+
"math/big"
11+
"testing"
12+
13+
cose "github.com/veraison/go-cose"
14+
)
15+
16+
var testES256Key = []byte(`{
17+
"kty": "EC",
18+
"crv": "P-256",
19+
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
20+
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
21+
"d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
22+
"kid": "1"
23+
}`)
24+
25+
func getCOSESignerAndVerifier(t *testing.T, keyBytes []byte, alg cose.Algorithm) (cose.Signer, cose.Verifier, error) {
26+
var key map[string]string
27+
28+
err := json.Unmarshal(keyBytes, &key)
29+
if err != nil {
30+
return nil, nil, err
31+
}
32+
33+
pkey, err := getKey(key)
34+
if err != nil {
35+
return nil, nil, err
36+
}
37+
38+
signer, err := cose.NewSigner(alg, pkey)
39+
if err != nil {
40+
return nil, nil, err
41+
}
42+
43+
verifier, err := cose.NewVerifier(alg, pkey.Public())
44+
if err != nil {
45+
return nil, nil, err
46+
}
47+
48+
return signer, verifier, nil
49+
}
50+
51+
func getKey(key map[string]string) (crypto.Signer, error) {
52+
switch key["kty"] {
53+
case "EC":
54+
var c elliptic.Curve
55+
switch key["crv"] {
56+
case "P-256":
57+
c = elliptic.P256()
58+
case "P-384":
59+
c = elliptic.P384()
60+
case "P-521":
61+
c = elliptic.P521()
62+
default:
63+
return nil, errors.New("unsupported EC curve: " + key["crv"])
64+
}
65+
pkey := &ecdsa.PrivateKey{
66+
PublicKey: ecdsa.PublicKey{
67+
X: mustBase64ToBigInt(key["x"]),
68+
Y: mustBase64ToBigInt(key["y"]),
69+
Curve: c,
70+
},
71+
D: mustBase64ToBigInt(key["d"]),
72+
}
73+
return pkey, nil
74+
}
75+
return nil, errors.New("unsupported key type: " + key["kty"])
76+
}
77+
78+
func mustBase64ToBigInt(s string) *big.Int {
79+
val, err := base64.RawURLEncoding.DecodeString(s)
80+
if err != nil {
81+
panic(err)
82+
}
83+
return new(big.Int).SetBytes(val)
84+
}

0 commit comments

Comments
 (0)