diff --git a/.github/workflows/ci-go-cover.yml b/.github/workflows/ci-go-cover.yml new file mode 100644 index 0000000..1a3198e --- /dev/null +++ b/.github/workflows/ci-go-cover.yml @@ -0,0 +1,17 @@ +name: cover ≥88% +on: [push, pull_request] +jobs: + cover: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: "1.19" + - name: Checkout code + uses: actions/checkout@v2 + - name: Go Coverage + run: | + go version + go test -short -cover | grep -o "coverage:.*of statements$" | python scripts/cov.py + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f62389d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,16 @@ +name: ci +on: [push, pull_request] +jobs: + tests: + name: Test on ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: "1.19" + - name: Checkout code + uses: actions/checkout@v2 + - name: Run tests + run: | + go version + go test -v \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d49627 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# RATS Conceptual Message Wrappers + +[![cover ≥88%](https://github.com/veraison/cmw/actions/workflows/ci-go-cover.yml/badge.svg)](https://github.com/veraison/cmw/actions/workflows/ci-go-cover.yml) + +Package `cmw` is a golang implementation of [draft-ftbs-rats-msg-wrap](https://datatracker.ietf.org/doc/draft-ftbs-rats-msg-wrap/). diff --git a/cfmap.go b/cfmap.go new file mode 100644 index 0000000..e70053b --- /dev/null +++ b/cfmap.go @@ -0,0 +1,128 @@ +// auto-generated (see utils/README.md) +package cmw + +var mt2cf = map[string]uint16{ + `application/cbor`: 11060, + `image/png`: 23, + `application/multipart-core`: 62, + `application/cose-key-set`: 102, + `application/sensml+cbor`: 113, + `application/aif+cbor`: 290, + `application/yang-data+cbor`: 340, + `application/pkcs7-mime; smime-type=certs-only`: 281, + `application/pkcs10`: 286, + `application/td+json`: 432, + `application/json`: 11050, + `application/dots+cbor`: 271, + `application/pkcs7-mime; smime-type=server-generated-key`: 280, + `application/pkcs8`: 284, + `application/pkix-cert`: 287, + `application/link-format`: 40, + `application/aif+json`: 291, + `application/vnd.ocf+cbor`: 10000, + `text/plain; charset=utf-8`: 0, + `application/senml+xml`: 310, + `application/cose; cose-type="cose-sign1"`: 18, + `application/json-patch+json`: 51, + `application/tm+json`: 433, + `application/octet-stream`: 42, + `application/merge-patch+json`: 52, + `application/sensml+xml`: 311, + `application/vnd.oma.lwm2m+cbor`: 11544, + `application/cose; cose-type="cose-encrypt0"`: 16, + `application/exi`: 47, + `application/cose; cose-type="cose-encrypt"`: 96, + `application/coap-group+json`: 256, + `application/cose; cose-type="cose-sign"`: 98, + `application/cose-key`: 101, + `application/senml+cbor`: 112, + `application/senml-etch+json`: 320, + `image/jpeg`: 22, + `application/sensml+json`: 111, + `application/vnd.oma.lwm2m+tlv`: 11542, + `application/cose; cose-type="cose-mac0"`: 17, + `application/ace+cbor`: 19, + `application/senml-exi`: 114, + `application/swid+cbor`: 258, + `application/missing-blocks+cbor-seq`: 272, + `application/senml-etch+cbor`: 322, + `application/cbor-seq`: 63, + `application/senml+json`: 110, + `application/javascript`: 10002, + `image/svg+xml`: 30000, + `text/css`: 20000, + `application/xml`: 41, + `application/cose; cose-type="cose-mac"`: 97, + `application/sensml-exi`: 115, + `application/yang-data+cbor; id=sid`: 140, + `application/csrattrs`: 285, + `application/vnd.oma.lwm2m+json`: 11543, + `image/gif`: 21, + `application/cwt`: 61, + `application/concise-problem-details+cbor`: 257, + `application/yang-data+cbor; id=name`: 341, + `application/oscore`: 10001, +} + +var cf2mt = map[uint16]string{ + 11060: `application/cbor`, + 23: `image/png`, + 62: `application/multipart-core`, + 102: `application/cose-key-set`, + 113: `application/sensml+cbor`, + 290: `application/aif+cbor`, + 340: `application/yang-data+cbor`, + 281: `application/pkcs7-mime; smime-type=certs-only`, + 286: `application/pkcs10`, + 432: `application/td+json`, + 284: `application/pkcs8`, + 287: `application/pkix-cert`, + 11050: `application/json`, + 271: `application/dots+cbor`, + 280: `application/pkcs7-mime; smime-type=server-generated-key`, + 40: `application/link-format`, + 291: `application/aif+json`, + 10000: `application/vnd.ocf+cbor`, + 0: `text/plain; charset=utf-8`, + 310: `application/senml+xml`, + 18: `application/cose; cose-type="cose-sign1"`, + 51: `application/json-patch+json`, + 433: `application/tm+json`, + 11544: `application/vnd.oma.lwm2m+cbor`, + 42: `application/octet-stream`, + 52: `application/merge-patch+json`, + 311: `application/sensml+xml`, + 256: `application/coap-group+json`, + 16: `application/cose; cose-type="cose-encrypt0"`, + 47: `application/exi`, + 96: `application/cose; cose-type="cose-encrypt"`, + 320: `application/senml-etch+json`, + 98: `application/cose; cose-type="cose-sign"`, + 101: `application/cose-key`, + 112: `application/senml+cbor`, + 22: `image/jpeg`, + 111: `application/sensml+json`, + 258: `application/swid+cbor`, + 272: `application/missing-blocks+cbor-seq`, + 322: `application/senml-etch+cbor`, + 11542: `application/vnd.oma.lwm2m+tlv`, + 17: `application/cose; cose-type="cose-mac0"`, + 19: `application/ace+cbor`, + 114: `application/senml-exi`, + 30000: `image/svg+xml`, + 63: `application/cbor-seq`, + 110: `application/senml+json`, + 10002: `application/javascript`, + 140: `application/yang-data+cbor; id=sid`, + 285: `application/csrattrs`, + 11543: `application/vnd.oma.lwm2m+json`, + 20000: `text/css`, + 41: `application/xml`, + 97: `application/cose; cose-type="cose-mac"`, + 115: `application/sensml-exi`, + 341: `application/yang-data+cbor; id=name`, + 10001: `application/oscore`, + 21: `image/gif`, + 61: `application/cwt`, + 257: `application/concise-problem-details+cbor`, +} diff --git a/cmw.go b/cmw.go new file mode 100644 index 0000000..d772c03 --- /dev/null +++ b/cmw.go @@ -0,0 +1,195 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/fxamacker/cbor/v2" +) + +type Serialization uint + +const ( + JSONArray = Serialization(iota) + CBORArray + CBORTag + Unknown +) + +// a CMW object holds the internal representation of a RATS conceptual message +// wrapper +type CMW struct { + typ Type + val Value + ind Indicator +} + +func (o *CMW) SetMediaType(v string) { _ = o.typ.Set(v) } +func (o *CMW) SetContentFormat(v uint16) { _ = o.typ.Set(v) } +func (o *CMW) SetTagNumber(v uint64) { _ = o.typ.Set(v) } +func (o *CMW) SetValue(v []byte) { _ = o.val.Set(v) } +func (o *CMW) SetIndicators(indicators ...Indicator) { + var v Indicator + + for _, ind := range indicators { + v.Set(ind) + } + + o.ind = v +} + +func (o CMW) GetValue() []byte { return o.val } +func (o CMW) GetType() string { return o.typ.String() } +func (o CMW) GetIndicator() Indicator { return o.ind } + +// Deserialize a CMW +func (o *CMW) Deserialize(b []byte) error { + switch sniff(b) { + case JSONArray: + return o.UnmarshalJSON(b) + case CBORArray: + return o.UnmarshalCBOR(b) + case CBORTag: + return o.UnmarshalCBORTag(b) + } + return errors.New("unknown CMW format") +} + +// Serialize a CMW according to the provided Serialization +func (o CMW) Serialize(s Serialization) ([]byte, error) { + switch s { + case JSONArray: + return o.MarshalJSON() + case CBORArray: + return o.MarshalCBOR() + case CBORTag: + return o.MarshalCBORTag() + } + return nil, fmt.Errorf("invalid serialization format %d", s) +} + +func (o CMW) MarshalJSON() ([]byte, error) { return arrayEncode(json.Marshal, &o) } +func (o CMW) MarshalCBOR() ([]byte, error) { return arrayEncode(cbor.Marshal, &o) } + +func (o CMW) MarshalCBORTag() ([]byte, error) { + var ( + tag cbor.RawTag + err error + ) + + if !o.typ.IsSet() || !o.val.IsSet() { + return nil, fmt.Errorf("type and value MUST be set in CMW") + } + + tag.Number, err = o.typ.TagNumber() + if err != nil { + return nil, fmt.Errorf("getting a suitable tag value: %w", err) + } + + tag.Content, err = cbor.Marshal(o.val) + if err != nil { + return nil, fmt.Errorf("marshaling tag value: %w", err) + } + + return tag.MarshalCBOR() +} + +func (o *CMW) UnmarshalCBOR(b []byte) error { + return arrayDecode[cbor.RawMessage](cbor.Unmarshal, b, o) +} + +func (o *CMW) UnmarshalJSON(b []byte) error { + return arrayDecode[json.RawMessage](json.Unmarshal, b, o) +} + +func (o *CMW) UnmarshalCBORTag(b []byte) error { + var ( + v cbor.RawTag + m []byte + err error + ) + + if err = v.UnmarshalCBOR(b); err != nil { + return fmt.Errorf("unmarshal CMW CBOR Tag: %w", err) + } + + if err = cbor.Unmarshal(v.Content, &m); err != nil { + return fmt.Errorf("unmarshal CMW CBOR Tag bstr-wrapped value: %w", err) + } + + _ = o.typ.Set(v.Number) + _ = o.val.Set(m) + + return nil +} + +func sniff(b []byte) Serialization { + if len(b) == 0 { + return Unknown + } + + if b[0] == 0x82 || b[0] == 0x83 { + return CBORArray + } else if b[0] >= 0xc0 && b[0] <= 0xdf { + return CBORTag + } else if b[0] == 0x5b { + return JSONArray + } + + return Unknown +} + +type ( + arrayDecoder func([]byte, interface{}) error + arrayEncoder func(interface{}) ([]byte, error) +) + +func arrayDecode[V json.RawMessage | cbor.RawMessage]( + dec arrayDecoder, b []byte, o *CMW, +) error { + var a []V + + if err := dec(b, &a); err != nil { + return err + } + + alen := len(a) + + if alen < 2 || alen > 3 { + return fmt.Errorf("wrong number of entries (%d) in the CMW array", alen) + } + + if err := dec(a[0], &o.typ); err != nil { + return fmt.Errorf("unmarshaling type: %w", err) + } + + if err := dec(a[1], &o.val); err != nil { + return fmt.Errorf("unmarshaling value: %w", err) + } + + if alen == 3 { + if err := dec(a[2], &o.ind); err != nil { + return fmt.Errorf("unmarshaling indicator: %w", err) + } + } + + return nil +} + +func arrayEncode(enc arrayEncoder, o *CMW) ([]byte, error) { + if !o.typ.IsSet() || !o.val.IsSet() { + return nil, fmt.Errorf("type and value MUST be set in CMW") + } + + a := []interface{}{o.typ, o.val} + + if !o.ind.Empty() { + a = append(a, o.ind) + } + + return enc(a) +} diff --git a/cmw_test.go b/cmw_test.go new file mode 100644 index 0000000..20c56bb --- /dev/null +++ b/cmw_test.go @@ -0,0 +1,531 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_sniff(t *testing.T) { + tests := []struct { + name string + args []byte + want Serialization + }{ + { + "JSON array with CoAP C-F", + []byte(`[30001, "3q2-7w"]`), + JSONArray, + }, + { + "JSON array with media type string", + []byte(`["application/vnd.intel.sgx", "3q2-7w"]`), + JSONArray, + }, + { + "CBOR array with CoAP C-F", + // echo "[30001, h'deadbeef']" | diag2cbor.rb | xxd -p -i + []byte{0x82, 0x19, 0x75, 0x31, 0x44, 0xde, 0xad, 0xbe, 0xef}, + CBORArray, + }, + { + "CBOR array with media type string", + // echo "[\"application/vnd.intel.sgx\", h'deadbeef']" | diag2cbor.rb | xxd -p -i + []byte{ + 0x82, 0x78, 0x19, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x6c, 0x2e, 0x73, 0x67, 0x78, 0x44, 0xde, + 0xad, 0xbe, 0xef, + }, + CBORArray, + }, + { + "CBOR tag", + // echo "1668576818(h'deadbeef')" | diag2cbor.rb | xxd -p -i + []byte{ + 0xda, 0x63, 0x74, 0x76, 0x32, 0x44, 0xde, 0xad, 0xbe, 0xef, + }, + CBORTag, + }, + { + "CBOR Tag Intel", + // echo "60000(h'deadbeef')" | diag2cbor.rb| xxd -i + []byte{0xd9, 0xea, 0x60, 0x44, 0xde, 0xad, 0xbe, 0xef}, + CBORTag, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := sniff(tt.args); got != tt.want { + t.Errorf("[TC: %s] sniff() = %v, want %v", tt.name, got, tt.want) + } + }) + } +} + +var ( + testIndicator = Indicator(31) +) + +func Test_Deserialize_ok(t *testing.T) { + tests := []struct { + name string + tv []byte + exp CMW + }{ + { + "JSON array with CoAP C-F", + []byte(`[30001, "3q2-7w"]`), + CMW{ + Type{uint16(30001)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + }, + }, + { + "JSON array with media type string", + []byte(`["application/vnd.intel.sgx", "3q2-7w"]`), + CMW{ + Type{"application/vnd.intel.sgx"}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + }, + }, + { + "JSON array with media type string and indicator", + []byte(`["application/vnd.intel.sgx", "3q2-7w", 31]`), + CMW{ + Type{"application/vnd.intel.sgx"}, + []byte{0xde, 0xad, 0xbe, 0xef}, + testIndicator, + }, + }, + { + "CBOR array with CoAP C-F", + // echo "[30001, h'deadbeef']" | diag2cbor.rb | xxd -p -i + []byte{0x82, 0x19, 0x75, 0x31, 0x44, 0xde, 0xad, 0xbe, 0xef}, + CMW{ + Type{uint16(30001)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + }, + }, + { + "CBOR array with media type string", + // echo "[\"application/vnd.intel.sgx\", h'deadbeef']" | diag2cbor.rb | xxd -p -i + []byte{ + 0x82, 0x78, 0x19, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x6c, 0x2e, 0x73, 0x67, 0x78, 0x44, 0xde, + 0xad, 0xbe, 0xef, + }, + CMW{ + Type{string("application/vnd.intel.sgx")}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + }, + }, + { + "CBOR tag", + // echo "1668576818(h'deadbeef')" | diag2cbor.rb | xxd -p -i + []byte{ + 0xda, 0x63, 0x74, 0x76, 0x32, 0x44, 0xde, 0xad, 0xbe, 0xef, + }, + CMW{ + Type{uint64(1668576818)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actual CMW + + err := actual.Deserialize(tt.tv) + assert.NoError(t, err) + + assert.Equal(t, tt.exp, actual) + }) + } +} + +func Test_Serialize_JSONArray_ok(t *testing.T) { + type args struct { + typ string + val []byte + ind []Indicator + } + + tests := []struct { + name string + tv args + exp string + }{ + { + "CoRIM w/ rv, endorsements and cots", + args{ + "application/corim+signed", + []byte{0xde, 0xad, 0xbe, 0xef}, + []Indicator{ReferenceValues, Endorsements, TrustAnchors}, + }, + `[ "application/corim+signed", "3q2-7w", 19 ]`, + }, + { + "EAR", + args{ + `application/eat+cwt; eat_profile="tag:github.com,2023:veraison/ear"`, + []byte{0xde, 0xad, 0xbe, 0xef}, + []Indicator{}, + }, + `[ "application/eat+cwt; eat_profile=\"tag:github.com,2023:veraison/ear\"", "3q2-7w" ]`, + }, + { + "EAT-based attestation results", + args{ + `application/eat+cwt`, + []byte{0xde, 0xad, 0xbe, 0xef}, + []Indicator{AttestationResults}, + }, + `[ "application/eat+cwt", "3q2-7w", 8 ]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw CMW + + cmw.SetMediaType(tt.tv.typ) + cmw.SetValue(tt.tv.val) + cmw.SetIndicators(tt.tv.ind...) + + actual, err := cmw.Serialize(JSONArray) + assert.NoError(t, err) + assert.JSONEq(t, tt.exp, string(actual)) + }) + } +} + +func Test_Serialize_CBORArray_ok(t *testing.T) { + type args struct { + typ uint16 + val []byte + ind []Indicator + } + + tests := []struct { + name string + tv args + exp []byte + }{ + { + "CoRIM w/ rv, endorsements and cots", + args{ + 10000, + []byte{0xde, 0xad, 0xbe, 0xef}, + []Indicator{ReferenceValues, Endorsements, TrustAnchors}, + }, + []byte{0x83, 0x19, 0x27, 0x10, 0x44, 0xde, 0xad, 0xbe, 0xef, 0x13}, + }, + { + "EAR", + args{ + 10000, + []byte{0xde, 0xad, 0xbe, 0xef}, + []Indicator{}, + }, + []byte{0x82, 0x19, 0x27, 0x10, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + { + "EAT-based attestation results", + args{ + 10001, + []byte{0xde, 0xad, 0xbe, 0xef}, + []Indicator{AttestationResults}, + }, + []byte{0x83, 0x19, 0x27, 0x11, 0x44, 0xde, 0xad, 0xbe, 0xef, 0x08}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw CMW + + cmw.SetContentFormat(tt.tv.typ) + cmw.SetValue(tt.tv.val) + cmw.SetIndicators(tt.tv.ind...) + + actual, err := cmw.Serialize(CBORArray) + assert.NoError(t, err) + assert.Equal(t, tt.exp, actual) + }) + } +} + +func Test_Serialize_CBORTag_ok(t *testing.T) { + type args struct { + typ uint64 + val []byte + } + + tests := []struct { + name string + tv args + exp []byte + }{ + { + "1", + args{ + 50000, + []byte{0xde, 0xad, 0xbe, 0xef}, + }, + []byte{0xd9, 0xc3, 0x50, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + { + "2", + args{ + 50001, + []byte{0xde, 0xad, 0xbe, 0xef}, + }, + []byte{0xd9, 0xc3, 0x51, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + { + "3", + args{ + 50002, + []byte{0xde, 0xad, 0xbe, 0xef}, + }, + []byte{0xd9, 0xc3, 0x52, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw CMW + + cmw.SetTagNumber(tt.tv.typ) + cmw.SetValue(tt.tv.val) + + actual, err := cmw.Serialize(CBORTag) + assert.NoError(t, err) + assert.Equal(t, tt.exp, actual) + }) + } +} + +func Test_SettersGetters(t *testing.T) { + var cmw CMW + + assert.Nil(t, cmw.GetValue()) + assert.Empty(t, cmw.GetType()) + assert.True(t, cmw.GetIndicator().Empty()) + + cmw.SetContentFormat(0) + assert.Equal(t, "text/plain; charset=utf-8", cmw.GetType()) + + cmw.SetTagNumber(TnMin + 16) + assert.Equal(t, `application/cose; cose-type="cose-encrypt0"`, cmw.GetType()) + + cmw.SetMediaType("application/eat+cwt") + assert.Equal(t, "application/eat+cwt", cmw.GetType()) + + cmw.SetValue([]byte{0xff}) + assert.Equal(t, []byte{0xff}, cmw.GetValue()) +} + +func Test_Deserialize_JSONArray_ko(t *testing.T) { + tests := []struct { + name string + tv []byte + expectedErr string + }{ + { + "empty JSONArray", + []byte(`[]`), + `wrong number of entries (0) in the CMW array`, + }, + { + "missing mandatory field in JSONArray (1)", + []byte(`[10000]`), + `wrong number of entries (1) in the CMW array`, + }, + { + "missing mandatory field in JSONArray (2)", + []byte(`["3q2-7w"]`), + `wrong number of entries (1) in the CMW array`, + }, + { + "too many entries in JSONArray", + []byte(`[10000, "3q2-7w", 1, "EXTRA"]`), + `wrong number of entries (4) in the CMW array`, + }, + { + "bad type (float) for type", + []byte(`[10000.23, "3q2-7w"]`), + `unmarshaling type: cannot unmarshal 10000.230000 into uint16`, + }, + { + "bad type (float) for value", + []byte(`[10000, 1.2]`), + `unmarshaling value: cannot base64 url-safe decode: illegal base64 data at input byte 0`, + }, + { + "invalid padded base64 for value", + []byte(`[10000, "3q2-7w=="]`), + `unmarshaling value: cannot base64 url-safe decode: illegal base64 data at input byte 6`, + }, + { + "invalid container (object) for CMW", + []byte(`{"type": 10000, "value": "3q2-7w=="}`), + `unknown CMW format`, + }, + { + "bad type (object) for type", + []byte(`[ { "type": 10000 }, "3q2-7w" ]`), + `unmarshaling type: expecting string or uint16, got map[string]interface {}`, + }, + { + "bad JSON (missing `]` in array)", + []byte(`[10000, "3q2-7w"`), + `unexpected end of JSON input`, + }, + { + "bad indicator", + []byte(`[10000, "3q2-7w", "Evidence"]`), + `unmarshaling indicator: json: cannot unmarshal string into Go value of type cmw.Indicator`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw CMW + err := cmw.Deserialize(tt.tv) + assert.EqualError(t, err, tt.expectedErr) + }) + } +} + +func Test_Deserialize_CBORArray_ko(t *testing.T) { + tests := []struct { + name string + tv []byte + expectedErr string + }{ + { + "empty JSONArray", + // echo "[]" | diag2cbor.rb | xxd -i + []byte{0x80}, + `unknown CMW format`, + }, + { + "missing mandatory field in JSONArray (1)", + // echo "[10000]" | diag2cbor.rb | xxd -i + []byte{0x81, 0x19, 0x27, 0x10}, + `unknown CMW format`, + }, + { + "too many entries in JSONArray", + // echo "[1000, h'deadbeef', 1, false]" | diag2cbor.rb | xxd -i + []byte{0x84, 0x19, 0x03, 0xe8, 0x44, 0xde, 0xad, 0xbe, 0xef, 0x01, 0xf4}, + `unknown CMW format`, + }, + { + "bad type (float) for type", + // echo "[1000.23, h'deadbeef']" | diag2cbor.rb | xxd -i + []byte{ + 0x82, 0xfb, 0x40, 0x8f, 0x41, 0xd7, 0x0a, 0x3d, 0x70, 0xa4, + 0x44, 0xde, 0xad, 0xbe, 0xef, + }, + `unmarshaling type: cannot unmarshal 1000.230000 into uint16`, + }, + { + "overflow for type", + // echo "[65536, h'deadbeef']" | diag2cbor.rb | xxd -i + []byte{ + 0x82, 0x1a, 0x00, 0x01, 0x00, 0x00, 0x44, 0xde, 0xad, 0xbe, + 0xef, + }, + `unmarshaling type: cannot unmarshal 65536 into uint16`, + }, + { + "bad type (float) for value", + // echo "[65535, 1.2]" | diag2cbor.rb | xxd -i + []byte{ + 0x82, 0x19, 0xff, 0xff, 0xfb, 0x3f, 0xf3, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, + }, + `unmarshaling value: cannot decode value: cbor: cannot unmarshal primitives into Go value of type []uint8`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw CMW + err := cmw.Deserialize(tt.tv) + assert.EqualError(t, err, tt.expectedErr) + }) + } +} + +func Test_Deserialize_CBORTag(t *testing.T) { + tests := []struct { + name string + tv []byte + expectedErr string + }{ + { + "empty CBOR Tag", + []byte{0xda, 0x63, 0x74, 0x01, 0x01}, + `unmarshal CMW CBOR Tag bstr-wrapped value: EOF`, + }, + { + "bad type (uint) for value", + // echo "1668546817(1)" | diag2cbor.rb | xxd -i + []byte{0xda, 0x63, 0x74, 0x01, 0x01, 0x01}, + `unmarshal CMW CBOR Tag bstr-wrapped value: cbor: cannot unmarshal positive integer into Go value of type []uint8`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw CMW + err := cmw.Deserialize(tt.tv) + assert.EqualError(t, err, tt.expectedErr) + }) + } +} + +func Test_EncodeArray_sanitize_input(t *testing.T) { + var cmw CMW + + for _, s := range []Serialization{CBORArray, JSONArray} { + _, err := cmw.Serialize(s) + assert.EqualError(t, err, "type and value MUST be set in CMW") + } + + cmw.SetValue([]byte{0xff}) + + for _, s := range []Serialization{CBORArray, JSONArray} { + _, err := cmw.Serialize(s) + assert.EqualError(t, err, "type and value MUST be set in CMW") + } + + cmw.SetMediaType("") + + for _, s := range []Serialization{CBORArray, JSONArray} { + _, err := cmw.Serialize(s) + assert.EqualError(t, err, "type and value MUST be set in CMW") + } + + cmw.SetContentFormat(0) + + for _, s := range []Serialization{CBORArray, JSONArray} { + _, err := cmw.Serialize(s) + assert.NoError(t, err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d21ab16 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/veraison/cmw + +go 1.19 + +require ( + github.com/fxamacker/cbor/v2 v2.4.0 + github.com/stretchr/testify v1.8.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fe66fd4 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/indicator.go b/indicator.go new file mode 100644 index 0000000..cc9a202 --- /dev/null +++ b/indicator.go @@ -0,0 +1,23 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +// Indicator is the internal representation of the `cm-ind` bit map +type Indicator uint + +const ( + ReferenceValues = 1 << iota + Endorsements + Evidence + AttestationResults + TrustAnchors +) + +const IndicatorNone = 0 + +func (o *Indicator) Set(v Indicator) { *o |= v } +func (o *Indicator) Clear(v Indicator) { *o &= ^v } +func (o *Indicator) Toggle(v Indicator) { *o ^= v } +func (o Indicator) Has(v Indicator) bool { return o&v != 0 } +func (o Indicator) Empty() bool { return o == IndicatorNone } diff --git a/indicator_test.go b/indicator_test.go new file mode 100644 index 0000000..ef1569a --- /dev/null +++ b/indicator_test.go @@ -0,0 +1,47 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Indicator_misc(t *testing.T) { + var i Indicator + + assert.True(t, i.Empty()) + assert.False(t, i.Has(AttestationResults)) + assert.False(t, i.Has(ReferenceValues)) + assert.False(t, i.Has(Endorsements)) + assert.False(t, i.Has(Evidence)) + assert.False(t, i.Has(TrustAnchors)) + + i.Set(AttestationResults) + assert.True(t, i.Has(AttestationResults)) + assert.False(t, i.Has(ReferenceValues)) + assert.False(t, i.Has(Endorsements)) + assert.False(t, i.Has(Evidence)) + assert.False(t, i.Has(TrustAnchors)) + + i.Clear(AttestationResults) + assert.True(t, i.Empty()) + + i.Set(AttestationResults) + assert.False(t, i.Empty()) + i.Toggle(AttestationResults) + assert.True(t, i.Empty()) + + i.Set(AttestationResults) + i.Set(ReferenceValues) + i.Set(Evidence) + i.Set(Endorsements) + i.Set(TrustAnchors) + assert.True(t, i.Has(AttestationResults)) + assert.True(t, i.Has(ReferenceValues)) + assert.True(t, i.Has(Endorsements)) + assert.True(t, i.Has(Evidence)) + assert.True(t, i.Has(TrustAnchors)) +} diff --git a/scripts/cov.py b/scripts/cov.py new file mode 100644 index 0000000..0028264 --- /dev/null +++ b/scripts/cov.py @@ -0,0 +1,22 @@ +# Copyright 2022 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 +import os +import re +import sys + +# read output of (potentially many invocations of) "go test -cover -short", +# e.g., "coverage: 65.7% of statements" +cover_report_lines = sys.stdin.read() + +if len(cover_report_lines) == 0: + sys.exit(2) + +# extract min coverage from GITHUB_WORKFLOW, e.g., "60.4%" +min_cover = float(re.findall(r'\d*\.\d+|\d+', os.environ['GITHUB_WORKFLOW'])[0]) + +for l in cover_report_lines.splitlines(): + cover = float(re.findall(r'\d*\.\d+|\d+', l)[0]) + if cover < min_cover: + sys.exit(1) + +sys.exit(0) diff --git a/tn.go b/tn.go new file mode 100644 index 0000000..8718a8c --- /dev/null +++ b/tn.go @@ -0,0 +1,42 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import "fmt" + +const ( + CfMin = uint16(0) + CfMax = uint16(65024) +) + +const ( + TnMin = uint64(1668546817) + TnMax = uint64(1668612095) +) + +// See https://www.rfc-editor.org/rfc/rfc9277.html#section-4.3 + +// CBOR Tag number from CoAP Content-Format number +func TN(cf uint16) uint64 { + // No tag numbers are assigned for Content-Format numbers in range + // [65025, 65535] + if cf > CfMax { + // 18446744073709551615 is registered as "Invalid Tag", so it's good as + // a "nope" return value + return ^uint64(0) + } + + cf64 := uint64(cf) + + return TnMin + (cf64/255)*256 + cf64%255 +} + +// CoAP Content-Format from number CBOR Tag number +func CF(tn uint64) (uint16, error) { + if tn < TnMin || tn > TnMax { + return 0, fmt.Errorf("TN %d out of range", tn) + } + + return uint16((tn-TnMin)*(256/255) - (tn-TnMin)/256), nil +} diff --git a/tn_test.go b/tn_test.go new file mode 100644 index 0000000..190c378 --- /dev/null +++ b/tn_test.go @@ -0,0 +1,32 @@ +package cmw + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_TN_RoundTrip(t *testing.T) { + for cf := uint16(CfMin); cf < CfMax; cf++ { + actual, err := CF(TN(cf)) + assert.NoError(t, err) + assert.Equal(t, cf, actual) + } +} + +func Test_TN_OutOfRange(t *testing.T) { + assert.Equal(t, TN(65535), uint64(18446744073709551615)) +} + +func Test_CF_OutOfRange(t *testing.T) { + _, err := CF(TnMin - 1) + assert.EqualError(t, err, "TN 1668546816 out of range") + + for tn := TnMin; tn <= TnMax; tn++ { + _, err = CF(tn) + assert.NoError(t, err) + } + + _, err = CF(TnMax + 1) + assert.EqualError(t, err, "TN 1668612096 out of range") +} diff --git a/type.go b/type.go new file mode 100644 index 0000000..4610742 --- /dev/null +++ b/type.go @@ -0,0 +1,138 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/fxamacker/cbor/v2" +) + +type Type struct { + val any +} + +func mtFromCf(cf uint16) string { + mt, ok := cf2mt[cf] + if ok { + return mt + } + return strconv.FormatUint(uint64(cf), 10) +} + +func (o Type) String() string { + switch v := o.val.(type) { + case string: + return v + case uint16: + return mtFromCf(v) + case uint64: + cf, err := CF(v) + if err != nil { + return "" + } + return mtFromCf(cf) + default: + return "" + } +} + +func (o Type) MarshalJSON() ([]byte, error) { return typeEncode(json.Marshal, &o) } +func (o Type) MarshalCBOR() ([]byte, error) { return typeEncode(cbor.Marshal, &o) } + +func (o *Type) UnmarshalJSON(b []byte) error { return typeDecode(json.Unmarshal, b, o) } +func (o *Type) UnmarshalCBOR(b []byte) error { return typeDecode(cbor.Unmarshal, b, o) } + +type ( + typeDecoder func([]byte, interface{}) error + typeEncoder func(interface{}) ([]byte, error) +) + +func typeDecode(dec typeDecoder, b []byte, o *Type) error { + var v any + + if err := dec(b, &v); err != nil { + return fmt.Errorf("cannot unmarshal JSON type: %w", err) + } + + switch t := v.(type) { + case string: + o.val = t + case float64: // JSON + if t == float64(uint16(t)) { + o.val = uint16(t) + } else { + return fmt.Errorf("cannot unmarshal %f into uint16", t) + } + case uint64: // CBOR + if t == uint64(uint16(t)) { + o.val = uint16(t) + } else { + return fmt.Errorf("cannot unmarshal %d into uint16", t) + } + default: + return fmt.Errorf("expecting string or uint16, got %T", t) + } + + return nil +} + +func typeEncode(enc typeEncoder, o *Type) ([]byte, error) { + switch t := o.val.(type) { + case string: + case uint16: + break + default: + return nil, fmt.Errorf("wrong type for Type (%T)", t) + } + + return enc(o.val) +} + +func (o Type) TagNumber() (uint64, error) { + switch v := o.val.(type) { + case string: + cf, ok := mt2cf[v] + if !ok { + return 0, fmt.Errorf("media type %q has no registered CoAP Content-Format", v) + } + return TN(cf), nil + case uint16: + return TN(v), nil + case uint64: + return v, nil + default: + return 0, fmt.Errorf("cannot get tag number for %T", v) + } +} + +func (o Type) IsSet() bool { + if o.val == nil { + return false + } + + switch t := o.val.(type) { + case string: + if t == "" { + return false + } + } + + return true +} + +func (o *Type) Set(v any) error { + switch t := v.(type) { + case string, uint16, uint64: + break + default: + return fmt.Errorf("unsupported type %T for CMW type", t) + } + + o.val = v + + return nil +} diff --git a/utils/Makefile b/utils/Makefile new file mode 100644 index 0000000..3f5e4f7 --- /dev/null +++ b/utils/Makefile @@ -0,0 +1,32 @@ +MAP_FILE := ../cfmap.go + +REGISTRY := core-parameters.xml +REGISTRY_URL := https://www.iana.org/assignments/core-parameters/$(REGISTRY) +REGISTRY_GO := core-parameters.go + +EXE := main +CLEANFILES := + +zek ?= $(shell command -v zek) +ifeq ($(strip $(zek)),) +$(error zek not found. To install zek: 'go install github.com/miku/zek/cmd/zek@latest') +endif + +all: $(MAP_FILE) + +$(MAP_FILE): $(REGISTRY) $(EXE) + ./$(EXE) < $(REGISTRY) | gofmt > $@ + +$(EXE): $(REGISTRY_GO) ; go build + +CLEANFILES += $(EXE) + +$(REGISTRY_GO): $(REGISTRY) ; cat $< | $(zek) -P main > $@ + +CLEANFILES += $(REGISTRY_GO) + +$(REGISTRY): ; curl -sO $(REGISTRY_URL) + +CLEANFILES += $(REGISTRY) + +clean: ; rm -f $(CLEANFILES) \ No newline at end of file diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000..e63a05c --- /dev/null +++ b/utils/README.md @@ -0,0 +1,15 @@ +## Content-Format to Media Types maps + +The go maps used to translate Content-Formats to Media Types (and vice-versa) are automatically generated from the IANA [CoRE Parameters](https://www.iana.org/assignments/core-parameters/core-parameters.xhtml) registry. + +The automatic extraction depends on [`zek(1)`](https://github.com/miku/zek), which can be installed via: + +```shell +go install github.com/miku/zek/cmd/zek@latest +``` + +To rebuild the go maps ([`../cfmap.go`](../cfmap.go)), do: + +``` +make all clean +``` diff --git a/utils/go.mod b/utils/go.mod new file mode 100644 index 0000000..30418d6 --- /dev/null +++ b/utils/go.mod @@ -0,0 +1,7 @@ +module main + +go 1.19 + +require golang.org/x/net v0.9.0 + +require golang.org/x/text v0.9.0 // indirect diff --git a/utils/go.sum b/utils/go.sum new file mode 100644 index 0000000..f0239dd --- /dev/null +++ b/utils/go.sum @@ -0,0 +1,4 @@ +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/utils/main.go b/utils/main.go new file mode 100644 index 0000000..a1d0294 --- /dev/null +++ b/utils/main.go @@ -0,0 +1,62 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/xml" + "fmt" + "log" + "os" + "strconv" + "strings" + + "golang.org/x/net/html/charset" +) + +func main() { + dec := xml.NewDecoder(os.Stdin) + dec.CharsetReader = charset.NewReaderLabel + dec.Strict = false + + var doc Registry + if err := dec.Decode(&doc); err != nil { + log.Fatal(err) + } + + M := make(map[string]int) + + for _, r := range doc.Registry { + if r.ID == "content-formats" { + for _, x := range r.Record { + mt := x.Contenttype + if strings.HasPrefix(mt, "Unassigned") || + strings.HasPrefix(mt, "Reserved") || + strings.Contains(mt, "TEMPORARY") { + continue + } + id, err := strconv.Atoi(x.ID) + if err != nil { + log.Fatal(err) + } + + M[mt] = id + } + } + } + + fmt.Println("// auto-generated (see utils/README.md)") + fmt.Println("package cmw") + fmt.Println("") + fmt.Println("var mt2cf = map[string]uint16{") + for k, v := range M { + fmt.Printf("\t`%s`: %d,\n", k, v) + } + fmt.Println("}") + fmt.Println("") + fmt.Println("var cf2mt = map[uint16]string{") + for k, v := range M { + fmt.Printf("\t%d: `%s`,\n", v, k) + } + fmt.Println("}") +} diff --git a/value.go b/value.go new file mode 100644 index 0000000..f623f61 --- /dev/null +++ b/value.go @@ -0,0 +1,62 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/fxamacker/cbor/v2" +) + +type Value []byte + +func (o *Value) Set(v []byte) error { + *o = v + return nil +} + +func (o Value) IsSet() bool { + return len(o) != 0 +} + +func (o *Value) UnmarshalJSON(b []byte) error { + var ( + v []byte + err error + ) + + if v, err = base64.RawURLEncoding.DecodeString(string(b[1 : len(b)-1])); err != nil { + return fmt.Errorf("cannot base64 url-safe decode: %w", err) + } + + *o = v + + return nil +} + +func (o Value) MarshalJSON() ([]byte, error) { + s := base64.RawURLEncoding.EncodeToString([]byte(o)) + return json.Marshal(s) +} + +func (o *Value) UnmarshalCBOR(b []byte) error { + var ( + v []byte + err error + ) + + if err = cbor.Unmarshal(b, &v); err != nil { + return fmt.Errorf("cannot decode value: %w", err) + } + + *o = v + + return nil +} + +func (o Value) MarshalCBOR() ([]byte, error) { + return cbor.Marshal([]byte(o)) +}