diff --git a/cmw.go b/cmw.go index 593cf37..aa77b74 100644 --- a/cmw.go +++ b/cmw.go @@ -1,223 +1,71 @@ -// Copyright 2023 Contributors to the Veraison project. +// Copyright 2024 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 ( UnknownSerialization = Serialization(iota) - JSONArray - CBORArray - CBORTag + JSON + CBOR ) -// a CMW object holds the internal representation of a RATS conceptual message -// wrapper type CMW struct { - typ Type - val Value - ind Indicator - serialization Serialization -} - -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 + val any } -func (o *CMW) SetSerialization(s Serialization) { o.serialization = s } -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 } -func (o CMW) GetSerialization() Serialization { return o.serialization } - -// Deserialize a CMW func (o *CMW) Deserialize(b []byte) error { - s := sniff(b) - - o.serialization = s - - switch s { - case JSONArray: - return o.UnmarshalJSON(b) - case CBORArray, CBORTag: - return o.UnmarshalCBOR(b) - } - - return errors.New("unknown CMW format") -} - -// Serialize a CMW according to its provided Serialization -func (o CMW) Serialize() ([]byte, error) { - s := o.serialization - switch s { - case JSONArray: - return o.MarshalJSON() - case CBORArray, CBORTag: - return o.MarshalCBOR() - } - return nil, fmt.Errorf("invalid serialization format %d", s) -} + typ := sniff(b) -func (o CMW) MarshalJSON() ([]byte, error) { return arrayEncode(json.Marshal, &o) } - -func (o CMW) MarshalCBOR() ([]byte, error) { - s := o.serialization - switch s { - case CBORArray: - return arrayEncode(cbor.Marshal, &o) - case CBORTag: - return o.encodeCBORTag() - } - return nil, fmt.Errorf("invalid serialization format: want CBORArray or CBORTag, got %d", s) -} - -func (o CMW) encodeCBORTag() ([]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) + switch typ { + case collection: + var coll CollectionCMW + coll.Deserialize(b) + o.val = coll + case leaf: + var lf LeafCMW + lf.Deserialize(b) + o.val = lf } - tag.Content, err = cbor.Marshal(o.val) - if err != nil { - return nil, fmt.Errorf("marshaling tag value: %w", err) - } - - return tag.MarshalCBOR() + return errors.New("unknown CMW type") } -func (o *CMW) UnmarshalCBOR(b []byte) error { - if arrayDecode[cbor.RawMessage](cbor.Unmarshal, b, o) == nil { - o.serialization = CBORArray - return nil - } - - if o.decodeCBORTag(b) == nil { - // the serialization attribute is set by decodeCBORTag - return nil +func (o CMW) Serialize(s Serialization) ([]byte, error) { + switch v := o.val.(type) { + case LeafCMW: + return v.Serialize(s) + case CollectionCMW: + return v.Serialize(s) + default: + return nil, fmt.Errorf("unsupported type %T", v) } - - return errors.New("invalid CBOR-encoded CMW") } -func (o *CMW) UnmarshalJSON(b []byte) error { - err := arrayDecode[json.RawMessage](json.Unmarshal, b, o) - o.serialization = JSONArray - return err -} - -func (o *CMW) decodeCBORTag(b []byte) error { - var ( - v cbor.RawTag - m []byte - err error - ) +type cmwNodeType uint - 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) - o.serialization = CBORTag - - return nil -} - -func sniff(b []byte) Serialization { - if len(b) == 0 { - return UnknownSerialization - } - - if b[0] == 0x82 || b[0] == 0x83 { - return CBORArray - } else if b[0] >= 0xc0 && b[0] <= 0xdb { - return CBORTag - } else if b[0] == 0x5b { - return JSONArray - } - - return UnknownSerialization -} - -type ( - arrayDecoder func([]byte, any) error - arrayEncoder func(any) ([]byte, error) +const ( + unknownType = cmwNodeType(iota) + leaf + collection ) -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 - } +func sniff(b []byte) cmwNodeType { + s := sniff_leaf(b) - 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) + if s == UnknownForm { + if sniff_collection(b) { + return collection } + } else { + return leaf } - 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 := []any{o.typ, o.val} - - if !o.ind.Empty() { - a = append(a, o.ind) - } - - return enc(a) + return unknownType } diff --git a/cmw_test.go b/cmw_test.go index 7593fd1..0271d03 100644 --- a/cmw_test.go +++ b/cmw_test.go @@ -1,554 +1,4 @@ -// Copyright 2023 Contributors to the Veraison project. +// Copyright 2024 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, - JSONArray, - }, - }, - { - "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, - JSONArray, - }, - }, - { - "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, - 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}, - CMW{ - Type{uint16(30001)}, - []byte{0xde, 0xad, 0xbe, 0xef}, - IndicatorNone, - 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, - }, - CMW{ - Type{string("application/vnd.intel.sgx")}, - []byte{0xde, 0xad, 0xbe, 0xef}, - IndicatorNone, - CBORArray, - }, - }, - { - "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, - CBORTag, - }, - }, - } - - 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...) - cmw.SetSerialization(JSONArray) - - actual, err := cmw.Serialize() - 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...) - cmw.SetSerialization(CBORArray) - - actual, err := cmw.Serialize() - 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) - cmw.SetSerialization(CBORTag) - - actual, err := cmw.Serialize() - 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, - }, - `invalid CBOR-encoded CMW`, - }, - { - "overflow for type", - // echo "[65536, h'deadbeef']" | diag2cbor.rb | xxd -i - []byte{ - 0x82, 0x1a, 0x00, 0x01, 0x00, 0x00, 0x44, 0xde, 0xad, 0xbe, - 0xef, - }, - `invalid CBOR-encoded CMW`, - }, - { - "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, - }, - `invalid CBOR-encoded CMW`, - }, - } - - 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}, - `invalid CBOR-encoded CMW`, - }, - { - "bad type (uint) for value", - // echo "1668546817(1)" | diag2cbor.rb | xxd -i - []byte{0xda, 0x63, 0x74, 0x01, 0x01, 0x01}, - `invalid CBOR-encoded CMW`, - }, - } - - 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} { - cmw.SetSerialization(s) - _, err := cmw.Serialize() - assert.EqualError(t, err, "type and value MUST be set in CMW") - } - - cmw.SetValue([]byte{0xff}) - - for _, s := range []Serialization{CBORArray, JSONArray} { - cmw.SetSerialization(s) - _, err := cmw.Serialize() - assert.EqualError(t, err, "type and value MUST be set in CMW") - } - - cmw.SetMediaType("") - - for _, s := range []Serialization{CBORArray, JSONArray} { - cmw.SetSerialization(s) - _, err := cmw.Serialize() - assert.EqualError(t, err, "type and value MUST be set in CMW") - } - - cmw.SetContentFormat(0) - - for _, s := range []Serialization{CBORArray, JSONArray} { - cmw.SetSerialization(s) - _, err := cmw.Serialize() - assert.NoError(t, err) - } -} - -func Test_Serialize_invalid_serialization(t *testing.T) { - var tv CMW - - tv.SetMediaType("application/vnd.x") - tv.SetValue([]byte{0x00}) - - _, err := tv.Serialize() - assert.Error(t, err, "TPDP") -} diff --git a/collection.go b/collection.go index fbc0ae6..b8772e3 100644 --- a/collection.go +++ b/collection.go @@ -11,20 +11,12 @@ import ( "github.com/fxamacker/cbor/v2" ) -type Collection struct { +type CollectionCMW struct { m map[any]CMW } -type CollectionSerialization uint - -const ( - UnknownCollectionSerialization = CollectionSerialization(iota) - CollectionSerializationJSON - CollectionSerializationCBOR -) - // Deserialize a JSON or CBOR collection -func (o *Collection) Deserialize(b []byte) error { +func (o *CollectionCMW) Deserialize(b []byte) error { switch b[0] { case 0x7b: // '{' return o.UnmarshalJSON(b) @@ -36,16 +28,11 @@ func (o *Collection) Deserialize(b []byte) error { // Serialize the collection. The type of serialization depends on the // serialization specified for each item. Items must have compatible // serializations: CBORArray/CBORTag or JSON. -func (o *Collection) Serialize() ([]byte, error) { - s, err := o.detectSerialization() - if err != nil { - return nil, err - } - +func (o *CollectionCMW) Serialize(s Serialization) ([]byte, error) { switch s { - case CollectionSerializationCBOR: + case CBOR: return o.MarshalCBOR() - case CollectionSerializationJSON: + case JSON: return o.MarshalJSON() default: return nil, errors.New("unsupported serialization") @@ -53,12 +40,12 @@ func (o *Collection) Serialize() ([]byte, error) { } // GetMap returns a pointer to the internal map -func (o *Collection) GetMap() map[any]CMW { +func (o *CollectionCMW) GetMap() map[any]CMW { return o.m } // GetItem returns the CMW associated with label k -func (o *Collection) GetItem(k any) (CMW, error) { +func (o *CollectionCMW) GetItem(k any) (CMW, error) { v, ok := o.m[k] if !ok { return CMW{}, fmt.Errorf("item not found for key %v", k) @@ -67,7 +54,7 @@ func (o *Collection) GetItem(k any) (CMW, error) { } // AddItem adds a new item with label k to the collection -func (o *Collection) AddItem(k any, c CMW) { +func (o *CollectionCMW) AddItem(k any, c CMW) { if o.m == nil { o.m = make(map[any]CMW) } @@ -75,11 +62,11 @@ func (o *Collection) AddItem(k any, c CMW) { } // MarshalJSON serializes the collection to JSON -func (o Collection) MarshalJSON() ([]byte, error) { +func (o CollectionCMW) MarshalJSON() ([]byte, error) { m := make(map[string]json.RawMessage) for i, v := range o.m { - c, err := v.Serialize() + c, err := v.Serialize(JSON) if err != nil { return nil, fmt.Errorf("marshaling JSON collection item %v: %w", i, err) } @@ -100,11 +87,11 @@ func (o Collection) MarshalJSON() ([]byte, error) { } // MarshalCBOR serializes the collection to CBOR -func (o Collection) MarshalCBOR() ([]byte, error) { +func (o CollectionCMW) MarshalCBOR() ([]byte, error) { m := make(map[any]cbor.RawMessage) for i, v := range o.m { - c, err := v.Serialize() + c, err := v.Serialize(CBOR) if err != nil { return nil, fmt.Errorf("marshaling CBOR collection item %v: %w", i, err) } @@ -125,7 +112,7 @@ func (o Collection) MarshalCBOR() ([]byte, error) { } // UnmarshalCBOR unmarshal the supplied CBOR buffer to a CMW collection -func (o *Collection) UnmarshalCBOR(b []byte) error { +func (o *CollectionCMW) UnmarshalCBOR(b []byte) error { var tmp map[any]cbor.RawMessage if err := cbor.Unmarshal(b, &tmp); err != nil { @@ -148,7 +135,7 @@ func (o *Collection) UnmarshalCBOR(b []byte) error { } // UnmarshalJSON unmarshals the supplied JSON buffer to a CMW collection -func (o *Collection) UnmarshalJSON(b []byte) error { +func (o *CollectionCMW) UnmarshalJSON(b []byte) error { var tmp map[string]json.RawMessage if err := json.Unmarshal(b, &tmp); err != nil { @@ -170,31 +157,16 @@ func (o *Collection) UnmarshalJSON(b []byte) error { return nil } -func (o Collection) detectSerialization() (CollectionSerialization, error) { - rec := make(map[CollectionSerialization]bool) - - s := UnknownCollectionSerialization - - for k, v := range o.m { - switch v.serialization { - case CBORArray, CBORTag: - s = CollectionSerializationCBOR - rec[s] = true - case JSONArray: - s = CollectionSerializationJSON - rec[s] = true - default: - return UnknownCollectionSerialization, - fmt.Errorf( - "serialization not defined for collection item with k %v", k, - ) - } +func sniff_collection(b []byte) bool { + if len(b) == 0 { + return false } - if len(rec) != 1 { - return UnknownCollectionSerialization, - errors.New("CMW collection has items with incompatible serializations") + if b[0] == 0x7b { + return true + } else if (b[0] >= 0xa0 && b[0] <= 0xbb) || b[0] == 0xbf { + return true } - return s, nil + return false } diff --git a/collection_test.go b/collection_test.go index b68f6b3..796f6ab 100644 --- a/collection_test.go +++ b/collection_test.go @@ -20,17 +20,17 @@ func mustReadFile(t *testing.T, fname string) []byte { func Test_Collection_JSON_Deserialize_ok(t *testing.T) { tv := mustReadFile(t, "testdata/collection-ok.json") - var expectedA CMW + var expectedA LeafCMW expectedA.SetMediaType("application/vnd.a") expectedA.SetValue([]byte{0x61}) - expectedA.SetSerialization(JSONArray) + expectedA.SetForm(JSONArray) - var expectedB CMW + var expectedB LeafCMW expectedB.SetMediaType("application/vnd.b") expectedB.SetValue([]byte{0x62}) - expectedB.SetSerialization(JSONArray) + expectedB.SetForm(JSONArray) - var actual Collection + var actual CollectionCMW err := actual.UnmarshalJSON(tv) assert.NoError(t, err) @@ -46,19 +46,19 @@ func Test_Collection_JSON_Deserialize_ok(t *testing.T) { func Test_Collection_JSON_Serialize_ok(t *testing.T) { expected := mustReadFile(t, "testdata/collection-ok.json") - var tv Collection + var tv CollectionCMW - var a CMW + var a LeafCMW a.SetMediaType("application/vnd.a") a.SetValue([]byte{0x61}) - a.SetSerialization(JSONArray) + a.SetForm(JSONArray) tv.AddItem("a", a) - var b CMW + var b LeafCMW b.SetMediaType("application/vnd.b") b.SetValue([]byte{0x62}) - b.SetSerialization(JSONArray) + b.SetForm(JSONArray) tv.AddItem("b", b) @@ -71,7 +71,7 @@ func Test_Collection_JSON_Serialize_ok(t *testing.T) { func Test_Collection_JSON_Deserialize_fail_outer(t *testing.T) { tv := []byte(`;rubbish json;`) - var actual Collection + var actual CollectionCMW err := actual.UnmarshalJSON(tv) assert.EqualError(t, err, `unmarshaling JSON collection: invalid character ';' looking for beginning of value`) } @@ -79,7 +79,7 @@ func Test_Collection_JSON_Deserialize_fail_outer(t *testing.T) { func Test_Collection_JSON_Deserialize_fail_inner(t *testing.T) { tv := []byte(`{ "a": {} }`) - var actual Collection + var actual CollectionCMW err := actual.UnmarshalJSON(tv) assert.EqualError(t, err, `unmarshaling JSON collection item a: unknown CMW format`) } @@ -87,7 +87,7 @@ func Test_Collection_JSON_Deserialize_fail_inner(t *testing.T) { func Test_Collection_CBOR_Deserialize_ok(t *testing.T) { tv := mustReadFile(t, "testdata/collection-cbor-ok.cbor") - var actual Collection + var actual CollectionCMW err := actual.UnmarshalCBOR(tv) assert.NoError(t, err) @@ -96,30 +96,30 @@ func Test_Collection_CBOR_Deserialize_ok(t *testing.T) { assert.Equal(t, "application/signed-corim+cbor", one.GetType()) assert.Equal(t, []byte{0xd2, 0x84, 0x43, 0xa1, 0x1, 0x26, 0xa1}, one.GetValue()) assert.Equal(t, Indicator(3), one.GetIndicator()) - assert.Equal(t, CBORArray, one.GetSerialization()) + assert.Equal(t, CBORArray, one.GetForm()) two, err := actual.GetItem(uint64(2)) assert.NoError(t, err) assert.Equal(t, "29884", two.GetType()) // TN() mapped CoAP C-F assert.Equal(t, []byte{0x23, 0x47, 0xda, 0x55}, two.GetValue()) assert.Equal(t, Indicator(0), two.GetIndicator()) - assert.Equal(t, CBORTag, two.GetSerialization()) + assert.Equal(t, CBORTag, two.GetForm()) s, err := actual.GetItem("s") assert.NoError(t, err) assert.Equal(t, "30001", s.GetType()) assert.Equal(t, []byte{0x23, 0x47, 0xda, 0x55}, s.GetValue()) assert.Equal(t, Indicator(0), s.GetIndicator()) - assert.Equal(t, CBORArray, s.GetSerialization()) + assert.Equal(t, CBORArray, s.GetForm()) } func Test_Collection_CBOR_Serialize_ok(t *testing.T) { - var item1 CMW + var item1 LeafCMW item1.SetMediaType("application/vnd.1") item1.SetValue([]byte{0xde, 0xad, 0xbe, 0xef}) - item1.SetSerialization(CBORArray) + item1.SetForm(CBORArray) - var tv Collection + var tv CollectionCMW tv.AddItem(uint64(1), item1) expected := mustReadFile(t, "testdata/collection-cbor-ok-2.cbor") @@ -132,7 +132,7 @@ func Test_Collection_CBOR_Serialize_ok(t *testing.T) { func Test_Collection_CBOR_Deserialize_and_iterate(t *testing.T) { tv := mustReadFile(t, "testdata/collection-cbor-mixed-keys.cbor") - var actual Collection + var actual CollectionCMW err := actual.UnmarshalCBOR(tv) assert.NoError(t, err) @@ -148,32 +148,10 @@ func Test_Collection_CBOR_Deserialize_and_iterate(t *testing.T) { } } -func Test_Collection_detectSerialization_fail(t *testing.T) { - var tv Collection - - var a CMW - a.SetMediaType("application/vnd.a") - a.SetValue([]byte{0x61}) - a.SetSerialization(JSONArray) - - tv.AddItem("a", a) - - var b CMW - b.SetMediaType("application/vnd.b") - b.SetValue([]byte{0x62}) - b.SetSerialization(CBORArray) - - tv.AddItem("b", b) - - s, err := tv.detectSerialization() - assert.EqualError(t, err, "CMW collection has items with incompatible serializations") - assert.Equal(t, UnknownCollectionSerialization, s) -} - func Test_Collection_Deserialize_JSON_ok(t *testing.T) { tv := mustReadFile(t, "testdata/collection-ok.json") - var c Collection + var c CollectionCMW err := c.Deserialize(tv) assert.NoError(t, err) } @@ -181,7 +159,7 @@ func Test_Collection_Deserialize_JSON_ok(t *testing.T) { func Test_Collection_Deserialize_CBOR_ok(t *testing.T) { tv := mustReadFile(t, "testdata/collection-cbor-ok.cbor") - var c Collection + var c CollectionCMW err := c.Deserialize(tv) assert.NoError(t, err) } diff --git a/leaf.go b/leaf.go new file mode 100644 index 0000000..920030d --- /dev/null +++ b/leaf.go @@ -0,0 +1,284 @@ +// 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 LeafForm uint + +const ( + UnknownForm = LeafForm(iota) + JSONArray + CBORArray + CBORTag + c2jTunnel + j2cTunnel +) + +// a LeafCMW object holds the internal representation of a RATS conceptual message +// wrapper +type LeafCMW struct { + typ Type + val Value + ind Indicator + form LeafForm +} + +func (o *LeafCMW) SetMediaType(v string) { _ = o.typ.Set(v) } +func (o *LeafCMW) SetContentFormat(v uint16) { _ = o.typ.Set(v) } +func (o *LeafCMW) SetTagNumber(v uint64) { _ = o.typ.Set(v) } +func (o *LeafCMW) SetValue(v []byte) { _ = o.val.Set(v) } +func (o *LeafCMW) SetIndicators(indicators ...Indicator) { + var v Indicator + + for _, ind := range indicators { + v.Set(ind) + } + + o.ind = v +} +func (o *LeafCMW) SetForm(s LeafForm) { o.form = s } + +func (o LeafCMW) GetValue() []byte { return o.val } +func (o LeafCMW) GetType() string { return o.typ.String() } +func (o LeafCMW) GetIndicator() Indicator { return o.ind } +func (o LeafCMW) GetForm() LeafForm { return o.form } + +// Deserialize a CMW +func (o *LeafCMW) Deserialize(b []byte) error { + var err error + f := sniff_leaf(b) + + switch f { + case j2cTunnel: + b, err = deserializeJsonToCbor(b) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON-to-CBOR tunnel: %w", err) + } + f = sniff_leaf(b) + case c2jTunnel: + b, err = deserializeCborToJson(b) + if err != nil { + return fmt.Errorf("failed to unmarshal CBOR-to-JSON tunnel: %w", err) + } + f = sniff_leaf(b) + } + + o.form = f + + switch f { + case JSONArray: + return o.UnmarshalJSON(b) + case CBORArray, CBORTag: + return o.UnmarshalCBOR(b) + } + + return errors.New("unknown leaf CMW format") +} + +// Serialize a CMW according to to the provided serialization +func (o LeafCMW) Serialize(s Serialization) ([]byte, error) { + var b []byte + var err error + f := o.form + switch f { + case JSONArray: + b, err = o.MarshalJSON() + case CBORArray, CBORTag: + b, err = o.MarshalCBOR() + default: + return nil, fmt.Errorf("invalid leaf form %d", f) + } + if err != nil { + return nil, fmt.Errorf("failed to marshal leaf CMW: %w", err) + } + + if !o.isCompatibleWithSerialization(s) { + switch s { + case JSON: + return serializeCborToJson(b) + case CBOR: + return serializeJsonToCbor(b) + default: + return nil, errors.New("Unknown serialization format") + } + } + + return b, nil +} + +func (o LeafCMW) MarshalJSON() ([]byte, error) { return arrayEncode(json.Marshal, &o) } + +func (o LeafCMW) MarshalCBOR() ([]byte, error) { + f := o.form + switch f { + case CBORArray: + return arrayEncode(cbor.Marshal, &o) + case CBORTag: + return o.encodeCBORTag() + } + return nil, fmt.Errorf("invalid serialization format: want CBORArray or CBORTag, got %d", f) +} + +func (o LeafCMW) encodeCBORTag() ([]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 *LeafCMW) UnmarshalCBOR(b []byte) error { + if arrayDecode[cbor.RawMessage](cbor.Unmarshal, b, o) == nil { + o.form = CBORArray + return nil + } + + if o.decodeCBORTag(b) == nil { + // the serialization attribute is set by decodeCBORTag + return nil + } + + return errors.New("invalid CBOR-encoded CMW") +} + +func (o *LeafCMW) UnmarshalJSON(b []byte) error { + err := arrayDecode[json.RawMessage](json.Unmarshal, b, o) + o.form = JSONArray + return err +} + +func (o *LeafCMW) decodeCBORTag(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) + o.form = CBORTag + + return nil +} + +func sniff_leaf(b []byte) LeafForm { + if len(b) == 0 { + return UnknownForm + } + + if len(b) > 3 { + // c2j-tunnel starts with `["#` encoded as UTF-8: [0x5b, 0x22, 0x23] + // j2c-tunnel starts with: + // 82 # array(2) + // 6f # text(15) + // 23... # "#..." + if b[0] == 0x5b && b[1] == 0x22 && b[2] == 0x23 { + + } else if b[0] == 0x82 && b[1] == 0x6F && b[2] == 0x23 { + + } + } + + if b[0] == 0x82 || b[0] == 0x83 { + return CBORArray + } else if b[0] >= 0xc0 && b[0] <= 0xdb { + return CBORTag + } else if b[0] == 0x5b { + return JSONArray + } + + return UnknownForm +} + +type ( + arrayDecoder func([]byte, any) error + arrayEncoder func(any) ([]byte, error) +) + +func arrayDecode[V json.RawMessage | cbor.RawMessage]( + dec arrayDecoder, b []byte, o *LeafCMW, +) 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 *LeafCMW) ([]byte, error) { + if !o.typ.IsSet() || !o.val.IsSet() { + return nil, fmt.Errorf("type and value MUST be set in CMW") + } + + a := []any{o.typ, o.val} + + if !o.ind.Empty() { + a = append(a, o.ind) + } + + return enc(a) +} + +func (o LeafCMW) isCompatibleWithSerialization(s Serialization) bool { + switch s { + case JSON: + return o.form == JSONArray + case CBOR: + return o.form == CBORArray || o.form == CBORTag + } + + return false +} diff --git a/leaf_test.go b/leaf_test.go new file mode 100644 index 0000000..6ccf209 --- /dev/null +++ b/leaf_test.go @@ -0,0 +1,554 @@ +// 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 LeafForm + }{ + { + "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_leaf(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 LeafCMW + }{ + { + "JSON array with CoAP C-F", + []byte(`[30001, "3q2-7w"]`), + LeafCMW{ + Type{uint16(30001)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + JSONArray, + }, + }, + { + "JSON array with media type string", + []byte(`["application/vnd.intel.sgx", "3q2-7w"]`), + LeafCMW{ + Type{"application/vnd.intel.sgx"}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + JSONArray, + }, + }, + { + "JSON array with media type string and indicator", + []byte(`["application/vnd.intel.sgx", "3q2-7w", 31]`), + LeafCMW{ + Type{"application/vnd.intel.sgx"}, + []byte{0xde, 0xad, 0xbe, 0xef}, + testIndicator, + 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}, + LeafCMW{ + Type{uint16(30001)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + 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, + }, + LeafCMW{ + Type{string("application/vnd.intel.sgx")}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + CBORArray, + }, + }, + { + "CBOR tag", + // echo "1668576818(h'deadbeef')" | diag2cbor.rb | xxd -p -i + []byte{ + 0xda, 0x63, 0x74, 0x76, 0x32, 0x44, 0xde, 0xad, 0xbe, 0xef, + }, + LeafCMW{ + Type{uint64(1668576818)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + CBORTag, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actual LeafCMW + + 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 LeafCMW + + cmw.SetMediaType(tt.tv.typ) + cmw.SetValue(tt.tv.val) + cmw.SetIndicators(tt.tv.ind...) + cmw.SetForm(JSONArray) + + actual, err := cmw.Serialize() + 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 LeafCMW + + cmw.SetContentFormat(tt.tv.typ) + cmw.SetValue(tt.tv.val) + cmw.SetIndicators(tt.tv.ind...) + cmw.SetForm(CBORArray) + + actual, err := cmw.Serialize() + 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 LeafCMW + + cmw.SetTagNumber(tt.tv.typ) + cmw.SetValue(tt.tv.val) + cmw.SetForm(CBORTag) + + actual, err := cmw.Serialize() + assert.NoError(t, err) + assert.Equal(t, tt.exp, actual) + }) + } +} + +func Test_SettersGetters(t *testing.T) { + var cmw LeafCMW + + 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 LeafCMW + 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, + }, + `invalid CBOR-encoded CMW`, + }, + { + "overflow for type", + // echo "[65536, h'deadbeef']" | diag2cbor.rb | xxd -i + []byte{ + 0x82, 0x1a, 0x00, 0x01, 0x00, 0x00, 0x44, 0xde, 0xad, 0xbe, + 0xef, + }, + `invalid CBOR-encoded CMW`, + }, + { + "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, + }, + `invalid CBOR-encoded CMW`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw LeafCMW + 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}, + `invalid CBOR-encoded CMW`, + }, + { + "bad type (uint) for value", + // echo "1668546817(1)" | diag2cbor.rb | xxd -i + []byte{0xda, 0x63, 0x74, 0x01, 0x01, 0x01}, + `invalid CBOR-encoded CMW`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmw LeafCMW + err := cmw.Deserialize(tt.tv) + assert.EqualError(t, err, tt.expectedErr) + }) + } +} + +func Test_EncodeArray_sanitize_input(t *testing.T) { + var cmw LeafCMW + + for _, s := range []LeafForm{CBORArray, JSONArray} { + cmw.SetForm(s) + _, err := cmw.Serialize() + assert.EqualError(t, err, "type and value MUST be set in CMW") + } + + cmw.SetValue([]byte{0xff}) + + for _, s := range []LeafForm{CBORArray, JSONArray} { + cmw.SetForm(s) + _, err := cmw.Serialize() + assert.EqualError(t, err, "type and value MUST be set in CMW") + } + + cmw.SetMediaType("") + + for _, s := range []LeafForm{CBORArray, JSONArray} { + cmw.SetForm(s) + _, err := cmw.Serialize() + assert.EqualError(t, err, "type and value MUST be set in CMW") + } + + cmw.SetContentFormat(0) + + for _, s := range []LeafForm{CBORArray, JSONArray} { + cmw.SetForm(s) + _, err := cmw.Serialize() + assert.NoError(t, err) + } +} + +func Test_Serialize_invalid_serialization(t *testing.T) { + var tv LeafCMW + + tv.SetMediaType("application/vnd.x") + tv.SetValue([]byte{0x00}) + + _, err := tv.Serialize() + assert.Error(t, err, "TPDP") +} diff --git a/tunnel.go b/tunnel.go new file mode 100644 index 0000000..b07d0e0 --- /dev/null +++ b/tunnel.go @@ -0,0 +1,84 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/fxamacker/cbor/v2" +) + +const ( + jsonToCborSentinel = "#cmw-j2c-tunnel" + cborToJsonSentinel = "#cmw-c2j-tunnel" +) + +func serializeJsonToCbor(b []byte) ([]byte, error) { + a := []any{jsonToCborSentinel, b} + return cbor.Marshal(a) +} + +func serializeCborToJson(b []byte) ([]byte, error) { + a := []any{cborToJsonSentinel, base64.RawURLEncoding.EncodeToString(b)} + return json.Marshal(a) +} + +func deserializeJsonToCbor(b []byte) ([]byte, error) { + var a []json.RawMessage + var sentinel, innerCmw string + + if err := json.Unmarshal(b, &a); err != nil { + return nil, err + } + + alen := len(a) + + if alen != 2 { + return nil, fmt.Errorf("wrong number of entries (%d) in JSON-to-CBOR tunnel", alen) + } + + if err := json.Unmarshal(a[0], sentinel); err != nil { + return nil, fmt.Errorf("unmarshaling sentinel: %w", err) + } + if sentinel != jsonToCborSentinel { + return nil, fmt.Errorf("wrong JSON-to-CBOR sentinel: %s", sentinel) + } + + if err := json.Unmarshal(a[1], innerCmw); err != nil { + return nil, fmt.Errorf("unmarshaling inner CMW: %w", err) + } + + return base64.RawURLEncoding.DecodeString(innerCmw) +} + +func deserializeCborToJson(b []byte) ([]byte, error) { + var a []cbor.RawMessage + var sentinel string + var innerCmw []byte + + if err := cbor.Unmarshal(b, &a); err != nil { + return nil, err + } + + alen := len(a) + + if alen != 2 { + return nil, fmt.Errorf("wrong number of entries (%d) in CBOR-to-JSON tunnel", alen) + } + + if err := cbor.Unmarshal(a[0], sentinel); err != nil { + return nil, fmt.Errorf("unmarshaling sentinel: %w", err) + } + if sentinel != cborToJsonSentinel { + return nil, fmt.Errorf("wrong CBOR-to-JSON sentinel: %s", sentinel) + } + + if err := cbor.Unmarshal(a[1], innerCmw); err != nil { + return nil, fmt.Errorf("unmarshaling inner CMW: %w", err) + } + + return innerCmw, nil +} diff --git a/tunnel_test.go b/tunnel_test.go new file mode 100644 index 0000000..319231e --- /dev/null +++ b/tunnel_test.go @@ -0,0 +1,2 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0