diff --git a/cbor.go b/cbor.go new file mode 100644 index 0000000..9983846 --- /dev/null +++ b/cbor.go @@ -0,0 +1,36 @@ +package cmw + +import ( + "reflect" + + "github.com/fxamacker/cbor/v2" +) + +var ( + em, emError = initCBOREncMode() + dm, dmError = initCBORDecMode() +) + +func initCBOREncMode() (en cbor.EncMode, err error) { + o := cbor.CoreDetEncOptions() // use preset options as a starting point + return o.EncMode() +} + +func initCBORDecMode() (en cbor.DecMode, err error) { + tags := cbor.NewTagSet() + tags.Add( + cbor.TagOptions{EncTag: cbor.EncTagNone, DecTag: cbor.DecTagOptional}, + reflect.TypeOf(CMW{}), + 765) + + return cbor.DecOptions{}.DecModeWithTags(tags) +} + +func init() { + if emError != nil { + panic(emError) + } + if dmError != nil { + panic(dmError) + } +} diff --git a/cmw.go b/cmw.go index 593cf37..b8fa5a1 100644 --- a/cmw.go +++ b/cmw.go @@ -1,6 +1,3 @@ -// Copyright 2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 - package cmw import ( @@ -11,213 +8,311 @@ import ( "github.com/fxamacker/cbor/v2" ) -type Serialization uint - -const ( - UnknownSerialization = Serialization(iota) - JSONArray - CBORArray - CBORTag -) - // a CMW object holds the internal representation of a RATS conceptual message // wrapper type CMW struct { - typ Type - val Value - ind Indicator - serialization Serialization + kind Kind + + monad // CMW Record or CBOR tag + collection // CMW Collection + raw // Raw CMW (i.e., whatever we were given) } -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 +type Kind uint - for _, ind := range indicators { - v.Set(ind) +const ( + KindUnknown = Kind(iota) + KindMonad + KindRaw + KindCollection +) + +func (o Kind) String() string { + switch o { + case KindCollection: + return "collection" + case KindMonad: + return "monad" + case KindRaw: + return "raw value" + case KindUnknown: + fallthrough + default: + return "unknown" } +} - o.ind = v +func (o CMW) GetKind() Kind { return o.kind } +func (o CMW) GetFormat() Format { + switch o.kind { + case KindMonad: + return o.monad.format + case KindCollection: + return o.collection.format + case KindRaw: + return FormatUnknown // TODO sniff + default: + return FormatUnknown + } } -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 } +type raw struct { + rawJSON json.RawMessage + rawCBOR cbor.RawMessage -// Deserialize a CMW -func (o *CMW) Deserialize(b []byte) error { - s := sniff(b) + format Format +} - o.serialization = s +type Format uint - switch s { - case JSONArray: - return o.UnmarshalJSON(b) - case CBORArray, CBORTag: - return o.UnmarshalCBOR(b) - } +const ( + FormatUnknown = Format(iota) + // JSON formats + FormatJSONRecord + FormatJSONCollection + // CBOR formats + FormatCBORRecord + FormatCBORCollection + FormatCBORTag + // Tunnel formats + FormatTunnelC2J + FormatTunnelJ2C +) - return errors.New("unknown CMW format") +func (o Format) String() string { + switch o { + case FormatJSONRecord: + return "JSON record" + case FormatJSONCollection: + return "JSON collection" + case FormatCBORRecord: + return "CBOR record" + case FormatCBORCollection: + return "CBOR collection" + case FormatCBORTag: + return "CBOR tag" + case FormatTunnelC2J: + return "CBOR to JSON tunnel" + case FormatTunnelJ2C: + return "JSON to CBOR tunnel" + case FormatUnknown: + fallthrough + default: + return "unknown" + } } -// 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) +func NewMonad(mediaType any, value []byte, indicators ...Indicator) *CMW { + var c CMW + c.val.Set(value) + c.typ.Set(mediaType) + c.setIndicators(indicators...) + c.kind = KindMonad + return &c } -func (o CMW) MarshalJSON() ([]byte, error) { return arrayEncode(json.Marshal, &o) } +func (o CMW) GetMonadType() string { return o.monad.getType() } +func (o CMW) GetMonadValue() []byte { return o.monad.getValue() } +func (o CMW) GetMonadIndicator() Indicator { return o.ind } +func (o *CMW) UseCBORTagFormat() { o.monad.format = FormatCBORTag } + +func NewFromRawJSON(v json.RawMessage) (*CMW, error) { + // TODO validate JSON (TODO add validation API) + var c CMW + c.rawJSON = v + c.kind = KindRaw + return &c, nil +} -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 NewFromRawCBOR(v cbor.RawMessage) (*CMW, error) { + // TODO validate CBOR (TODO add validation API) + var c CMW + c.rawCBOR = v + c.kind = KindRaw + return &c, nil } -func (o CMW) encodeCBORTag() ([]byte, error) { - var ( - tag cbor.RawTag - err error - ) +func NewCollection(cmwct string) *CMW { + var c CMW + c.cmap = make(map[any]CMW) + c.ctyp = cmwct + c.kind = KindCollection + return &c +} - if !o.typ.IsSet() || !o.val.IsSet() { - return nil, fmt.Errorf("type and value MUST be set in CMW") +func (o CMW) GetCollectionType() (string, error) { + if o.kind != KindCollection { + return "", fmt.Errorf("want collection, got %q", o.kind) } + return o.collection.getType(), nil +} - tag.Number, err = o.typ.TagNumber() - if err != nil { - return nil, fmt.Errorf("getting a suitable tag value: %w", err) +func (o *CMW) AddCollectionItem(key any, node *CMW) error { + if o.kind != KindCollection { + return fmt.Errorf("want collection, got %q", o.kind) } + err := o.collection.addItem(key, node) + return err +} - tag.Content, err = cbor.Marshal(o.val) - if err != nil { - return nil, fmt.Errorf("marshaling tag value: %w", err) +func (o CMW) GetCollectionItem(key any) (*CMW, error) { + if o.kind != KindCollection { + return nil, fmt.Errorf("want collection, got %q", o.kind) } + return o.collection.getItem(key) +} - return tag.MarshalCBOR() +func (o CMW) ValidateCollection() error { + if o.kind != KindCollection { + return fmt.Errorf("want collection, got %q", o.kind) + } + return o.collection.validate() } -func (o *CMW) UnmarshalCBOR(b []byte) error { - if arrayDecode[cbor.RawMessage](cbor.Unmarshal, b, o) == nil { - o.serialization = CBORArray - return nil +func (o CMW) Untunnel() (*CMW, error) { + if o.GetKind() != KindMonad || + (o.GetFormat() != FormatTunnelC2J && o.GetFormat() != FormatTunnelJ2C) { + return nil, errors.New("expecting monad with tunnelled CMW") } - if o.decodeCBORTag(b) == nil { - // the serialization attribute is set by decodeCBORTag - return nil + var tun CMW + + if err := tun.Deserialize(o.GetMonadValue()); err != nil { + return nil, err } - return errors.New("invalid CBOR-encoded CMW") + return &tun, nil } -func (o *CMW) UnmarshalJSON(b []byte) error { - err := arrayDecode[json.RawMessage](json.Unmarshal, b, o) - o.serialization = JSONArray - return err +type Meta struct { + Key any + Kind Kind } -func (o *CMW) 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) +// Obtain (sorted) keys and types in a collection +func (o *CMW) GetCollectionMeta() ([]Meta, error) { + if o.kind != KindCollection { + return nil, fmt.Errorf("want collection, got %q", o.kind) } + return o.collection.getMeta(), nil +} - if err = cbor.Unmarshal(v.Content, &m); err != nil { - return fmt.Errorf("unmarshal CMW CBOR Tag bstr-wrapped value: %w", err) - } +func (o *CMW) setIndicators(indicators ...Indicator) { + var v Indicator - _ = o.typ.Set(v.Number) - _ = o.val.Set(m) - o.serialization = CBORTag + for _, ind := range indicators { + v.Set(ind) + } - return nil + o.ind = v } -func sniff(b []byte) Serialization { - if len(b) == 0 { - return UnknownSerialization +func (o CMW) MarshalJSON() ([]byte, error) { + if o.rawJSON != nil { // raw JSON + return o.rawJSON, nil + } else if o.rawCBOR != nil { // tunnel + return json.Marshal([]interface{}{"#cmw-c2j-tunnel", b64uEncode(o.rawCBOR)}) + } else if len(o.cmap) == 0 { // record + return o.monad.MarshalJSON() + } else { // collection + return o.collection.MarshalJSON() } +} - 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 +func (o CMW) MarshalCBOR() ([]byte, error) { + if o.rawCBOR != nil { // raw CBOR + return o.rawCBOR, nil + } else if o.rawJSON != nil { // tunnel + return em.Marshal([]interface{}{"#cmw-j2c-tunnel", o.rawJSON}) + } else if len(o.cmap) == 0 { // record or CBOR tag + return o.monad.MarshalCBOR() + } else { // collection + return o.collection.MarshalCBOR() } - - return UnknownSerialization } -type ( - arrayDecoder func([]byte, any) error - arrayEncoder func(any) ([]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 +func (o *CMW) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return errors.New("empty buffer") } - alen := len(a) + start := b[0] - if alen < 2 || alen > 3 { - return fmt.Errorf("wrong number of entries (%d) in the CMW array", alen) + switch start { + case '[': + if err := o.monad.UnmarshalJSON(b); err != nil { + return err + } + o.kind = KindMonad + case '{': + if err := o.collection.UnmarshalJSON(b); err != nil { + return err + } + o.kind = KindCollection + default: + return fmt.Errorf("want JSON object or JSON array start symbols, got: 0x%02x", start) } - if err := dec(a[0], &o.typ); err != nil { - return fmt.Errorf("unmarshaling type: %w", err) - } + return nil +} - if err := dec(a[1], &o.val); err != nil { - return fmt.Errorf("unmarshaling value: %w", err) +func (o *CMW) UnmarshalCBOR(b []byte) error { + if len(b) == 0 { + return errors.New("empty buffer") } - if alen == 3 { - if err := dec(a[2], &o.ind); err != nil { - return fmt.Errorf("unmarshaling indicator: %w", err) + start := b[0] + + switch { + case startCBORRecord(start) || startCBORTag(start): + if err := o.monad.UnmarshalCBOR(b); err != nil { + return err } + o.kind = KindMonad + case startCBORCollection(start): + if err := o.collection.UnmarshalCBOR(b); err != nil { + return err + } + o.kind = KindCollection + default: + return fmt.Errorf("want CBOR map, CBOR array or CBOR Tag start symbols, got: 0x%02x", start) } 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") +func (o *CMW) Deserialize(b []byte) error { + if len(b) == 0 { + return errors.New("empty buffer") } + s := b[0] + if startCBORCollection(s) || startCBORRecord(s) || startCBORTag(s) { + return o.UnmarshalCBOR(b) + } else if startJSONRecord(s) || startJSONCollection(s) { + return o.UnmarshalJSON(b) + } else { + return fmt.Errorf("unknown start symbol for CMW: %c", b) + } +} - a := []any{o.typ, o.val} +func Sniff(b []byte) Format { + if len(b) == 0 { + return FormatUnknown + } - if !o.ind.Empty() { - a = append(a, o.ind) + start := b[0] + + if startCBORCollection(start) { + return FormatCBORCollection + } else if startCBORRecord(start) { + return FormatCBORRecord + } else if startCBORTag(start) { + return FormatCBORTag + } else if startJSONCollection(start) { + return FormatJSONCollection + } else if startJSONRecord(start) { + return FormatJSONRecord } - return enc(a) + return FormatUnknown } diff --git a/cmw_test.go b/cmw_test.go index 7593fd1..96753f6 100644 --- a/cmw_test.go +++ b/cmw_test.go @@ -13,23 +13,23 @@ func Test_sniff(t *testing.T) { tests := []struct { name string args []byte - want Serialization + want Format }{ { "JSON array with CoAP C-F", []byte(`[30001, "3q2-7w"]`), - JSONArray, + FormatJSONRecord, }, { "JSON array with media type string", []byte(`["application/vnd.intel.sgx", "3q2-7w"]`), - JSONArray, + FormatJSONRecord, }, { "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, + FormatCBORRecord, }, { "CBOR array with media type string", @@ -40,7 +40,7 @@ func Test_sniff(t *testing.T) { 0x6e, 0x74, 0x65, 0x6c, 0x2e, 0x73, 0x67, 0x78, 0x44, 0xde, 0xad, 0xbe, 0xef, }, - CBORArray, + FormatCBORRecord, }, { "CBOR tag", @@ -48,507 +48,109 @@ func Test_sniff(t *testing.T) { []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, + FormatCBORTag, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := sniff(tt.args); got != tt.want { + 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, - }, - }, - } +func Test_NewMonad(t *testing.T) { + typ := "text/plain; charset=utf-8" + val := []byte{0xff} + ind := Indicator(Evidence) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var actual CMW + cmw := NewMonad(typ, val, ind) - err := actual.Deserialize(tt.tv) - assert.NoError(t, err) + assert.Equal(t, cmw.GetKind(), KindMonad) - assert.Equal(t, tt.exp, actual) - }) - } + assert.Equal(t, typ, cmw.GetMonadType()) + assert.Equal(t, val, cmw.GetMonadValue()) + assert.Equal(t, ind, cmw.GetMonadIndicator()) } -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 - } +func Test_Empty(t *testing.T) { + var cmw CMW - 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}, - }, - } + assert.Equal(t, "", cmw.GetMonadType()) + assert.Equal(t, []byte(nil), cmw.GetMonadValue()) + assert.Equal(t, Indicator(0), cmw.GetMonadIndicator()) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var cmw CMW + _, err := cmw.MarshalCBOR() + assert.EqualError(t, err, "type and value MUST be set in CMW") - cmw.SetTagNumber(tt.tv.typ) - cmw.SetValue(tt.tv.val) - cmw.SetSerialization(CBORTag) + _, err = cmw.MarshalJSON() + assert.EqualError(t, err, "type and value MUST be set in CMW") - actual, err := cmw.Serialize() - assert.NoError(t, err) - assert.Equal(t, tt.exp, actual) - }) - } + err = cmw.AddCollectionItem("test", nil) + assert.EqualError(t, err, `want collection, got "unknown"`) } -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()) +func Test_NewCollection(t *testing.T) { + ctyp := "tag:example.com,2024:composite-attester" - cmw.SetContentFormat(0) - assert.Equal(t, "text/plain; charset=utf-8", cmw.GetType()) + cmw := NewCollection(ctyp) - cmw.SetTagNumber(TnMin + 16) - assert.Equal(t, `application/cose; cose-type="cose-encrypt0"`, cmw.GetType()) + assert.Equal(t, cmw.GetKind(), KindCollection) - cmw.SetMediaType("application/eat+cwt") - assert.Equal(t, "application/eat+cwt", cmw.GetType()) + actual, err := cmw.GetCollectionType() + assert.NoError(t, err) + assert.Equal(t, ctyp, actual) - cmw.SetValue([]byte{0xff}) - assert.Equal(t, []byte{0xff}, cmw.GetValue()) + meta, err := cmw.GetCollectionMeta() + assert.NoError(t, err) + assert.Equal(t, meta, []Meta(nil)) } -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_GetCollectionMeta(t *testing.T) { + ctyp := "tag:example.com,2024:composite-attester" -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`, - }, - } + cmw := NewCollection(ctyp) - 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) - }) - } -} + monad := NewMonad("text/plain; charset=utf-8", []byte{0xff}, Indicator(Evidence)) + err := cmw.AddCollectionItem("my-monad", monad) + assert.NoError(t, err) -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`, - }, - } + sub := NewCollection("tag:example.com,2024:nested-composite-attester") + err = cmw.AddCollectionItem("my-collection", sub) + assert.NoError(t, err) - 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) - }) - } + meta, err := cmw.GetCollectionMeta() + assert.NoError(t, err) + assert.Equal(t, meta, []Meta{ + {"my-collection", KindCollection}, + {"my-monad", KindMonad}, + }) } -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}) +func Test_GetCollectionGet(t *testing.T) { + ctyp := "tag:example.com,2024:composite-attester" - 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 := NewCollection(ctyp) - cmw.SetContentFormat(0) + monad := NewMonad("text/plain; charset=utf-8", []byte{0xff}, Indicator(Evidence)) + err := cmw.AddCollectionItem("my-monad", monad) + assert.NoError(t, err) - for _, s := range []Serialization{CBORArray, JSONArray} { - cmw.SetSerialization(s) - _, err := cmw.Serialize() - assert.NoError(t, err) - } -} + sub := NewCollection("tag:example.com,2024:nested-composite-attester") + err = cmw.AddCollectionItem("my-collection", sub) + assert.NoError(t, err) -func Test_Serialize_invalid_serialization(t *testing.T) { - var tv CMW + item1, err := cmw.GetCollectionItem("my-monad") + assert.NoError(t, err) + assert.Equal(t, monad, item1) - tv.SetMediaType("application/vnd.x") - tv.SetValue([]byte{0x00}) + item2, err := cmw.GetCollectionItem("my-collection") + assert.NoError(t, err) + assert.Equal(t, sub, item2) - _, err := tv.Serialize() - assert.Error(t, err, "TPDP") + itemNotFound, err := cmw.GetCollectionItem("uh?") + assert.EqualError(t, err, `item not found for key "uh?"`) + assert.Nil(t, itemNotFound) } diff --git a/collection.go b/collection.go index fbc0ae6..da84505 100644 --- a/collection.go +++ b/collection.go @@ -7,79 +7,143 @@ import ( "encoding/json" "errors" "fmt" + "net/url" + "regexp" + "sort" + "strings" "github.com/fxamacker/cbor/v2" ) -type Collection struct { - m map[any]CMW +const CMWC_T string = "__cmwc_t" + +type collection struct { + cmap map[any]CMW + ctyp string + + format Format } -type CollectionSerialization uint +func (o collection) validate() error { + if len(o.cmap) < 1 { + return errors.New("empty CMW collection") + } -const ( - UnknownCollectionSerialization = CollectionSerialization(iota) - CollectionSerializationJSON - CollectionSerializationCBOR -) + for k, v := range o.cmap { + if err := v.validate(); err != nil { + return fmt.Errorf("invalid collection at key %q: %w", k, err) + } + } -// Deserialize a JSON or CBOR collection -func (o *Collection) Deserialize(b []byte) error { - switch b[0] { - case 0x7b: // '{' - return o.UnmarshalJSON(b) + return nil +} + +func isValidCollectionKey(key any) error { + switch t := key.(type) { + case string: + // make sure it's not reserved and it's not empty/whitespace-only + if t == CMWC_T { + return fmt.Errorf("bad collection key: %s is reserved", CMWC_T) + } + if len(strings.TrimSpace(t)) == 0 { + return errors.New("bad collection key: empty or whitespace only") + } + return nil + case int64, uint64: + return nil default: - return o.UnmarshalCBOR(b) + return fmt.Errorf("unknown collection key type: want string or int, got %T", t) } } -// 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 +var oidRe = regexp.MustCompile(`^([0-2])(([.]0)|([.][1-9][0-9]*))*$`) + +func isValidCollectionType(ctyp string) error { + // "__cmwc_t": ~uri / oid + + if oidRe.MatchString(ctyp) { + return nil } - switch s { - case CollectionSerializationCBOR: - return o.MarshalCBOR() - case CollectionSerializationJSON: - return o.MarshalJSON() - default: - return nil, errors.New("unsupported serialization") + u, uriErr := url.Parse(ctyp) + if uriErr != nil { + return fmt.Errorf("invalid collection type: %q. MUST be URI or OID", ctyp) } + + if !u.IsAbs() { + return fmt.Errorf("invalid collection type: %q. URI is not absolute", ctyp) + } + + return nil } -// GetMap returns a pointer to the internal map -func (o *Collection) GetMap() map[any]CMW { - return o.m +func (o *collection) addItem(key any, node *CMW) error { + if o.cmap == nil { + o.cmap = make(map[any]CMW) + } + + if err := isValidCollectionKey(key); err != nil { + return fmt.Errorf("invalid key: %w", err) + } + + if node == nil { + return errors.New("nil node") + } + + o.cmap[key] = *node + + return nil } // GetItem returns the CMW associated with label k -func (o *Collection) GetItem(k any) (CMW, error) { - v, ok := o.m[k] - if !ok { - return CMW{}, fmt.Errorf("item not found for key %v", k) +func (o collection) getItem(k any) (*CMW, error) { + v, found := o.cmap[k] + if !found { + return nil, fmt.Errorf("item not found for key %q", k) + } + return &v, nil +} + +func (o collection) getType() string { + return o.ctyp +} + +func (o Meta) getKeyForSorting() string { + switch t := o.Key.(type) { + case string: + return t + case uint64: + return fmt.Sprintf("##%d", t) + default: + panic(fmt.Sprintf("key with unknown type %T", t)) } - return v, nil } -// AddItem adds a new item with label k to the collection -func (o *Collection) AddItem(k any, c CMW) { - if o.m == nil { - o.m = make(map[any]CMW) +func (o collection) getMeta() []Meta { + var m []Meta + + for k, v := range o.cmap { + m = append(m, Meta{k, v.kind}) } - o.m[k] = c + + sort.Slice(m, func(i, j int) bool { + return m[i].getKeyForSorting() < m[j].getKeyForSorting() + }) + + return m } // MarshalJSON serializes the collection to JSON -func (o Collection) MarshalJSON() ([]byte, error) { +func (o collection) MarshalJSON() ([]byte, error) { m := make(map[string]json.RawMessage) - for i, v := range o.m { - c, err := v.Serialize() + if o.ctyp != "" { + ct, _ := json.Marshal(o.ctyp) + m[CMWC_T] = json.RawMessage(ct) + } + + for i, v := range o.cmap { + c, err := v.MarshalJSON() if err != nil { return nil, fmt.Errorf("marshaling JSON collection item %v: %w", i, err) } @@ -100,23 +164,23 @@ func (o Collection) MarshalJSON() ([]byte, error) { } // MarshalCBOR serializes the collection to CBOR -func (o Collection) MarshalCBOR() ([]byte, error) { +func (o collection) MarshalCBOR() ([]byte, error) { m := make(map[any]cbor.RawMessage) - for i, v := range o.m { - c, err := v.Serialize() + if o.ctyp != "" { + ct, _ := em.Marshal(o.ctyp) + m[CMWC_T] = cbor.RawMessage(ct) + } + + for i, v := range o.cmap { + c, err := v.MarshalCBOR() if err != nil { return nil, fmt.Errorf("marshaling CBOR collection item %v: %w", i, err) } - switch t := i.(type) { - case string, uint64: - m[t] = c - default: - return nil, fmt.Errorf("CBOR collection, key error: want string or int64, got %T", t) - } + m[i] = c } - b, err := cbor.Marshal(m) + b, err := em.Marshal(m) if err != nil { return nil, fmt.Errorf("marshaling CBOR collection: %w", err) } @@ -125,66 +189,127 @@ 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 *collection) UnmarshalCBOR(b []byte) error { var tmp map[any]cbor.RawMessage - if err := cbor.Unmarshal(b, &tmp); err != nil { + if err := dm.Unmarshal(b, &tmp); err != nil { return fmt.Errorf("unmarshaling CBOR collection: %w", err) } + // extract CMW collection type + cmwc_t, found := tmp[CMWC_T] + if found { + var s string + if err := dm.Unmarshal(cmwc_t, &s); err != nil { + return fmt.Errorf("extracting CBOR collection type: %w", err) + } + if err := isValidCollectionType(s); err != nil { + return fmt.Errorf("checking CBOR collection type: %w", err) + } + o.ctyp = s + delete(tmp, CMWC_T) + } + m := make(map[any]CMW) for k, v := range tmp { var c CMW - if err := c.Deserialize(v); err != nil { - return fmt.Errorf("unmarshaling CBOR collection item %v: %w", k, err) + + start := v[0] + + switch { + case startCBORRecord(start) || startCBORTag(start): + if err := c.monad.UnmarshalCBOR(v); err != nil { + return fmt.Errorf("unmarshaling CBOR record or tag item %v: %w", k, err) + } + c.kind = KindMonad + case startCBORCollection(start): + if err := c.collection.UnmarshalCBOR(v); err != nil { + return fmt.Errorf("unmarshaling CBOR collection item %v: %w", k, err) + } + c.kind = KindCollection + default: + return fmt.Errorf("want CBOR map, CBOR array or CBOR Tag start symbols, got: 0x%02x", start) } + m[k] = c } - o.m = m + o.cmap = m + o.format = FormatCBORCollection return nil } // UnmarshalJSON unmarshals the supplied JSON buffer to a CMW collection -func (o *Collection) UnmarshalJSON(b []byte) error { +func (o *collection) UnmarshalJSON(b []byte) error { var tmp map[string]json.RawMessage if err := json.Unmarshal(b, &tmp); err != nil { return fmt.Errorf("unmarshaling JSON collection: %w", err) } + // extract CMW collection type + cmwc_t, found := tmp[CMWC_T] + if found { + var s string + if err := json.Unmarshal(cmwc_t, &s); err != nil { + return fmt.Errorf("extracting JSON collection type: %w", err) + } + if err := isValidCollectionType(s); err != nil { + return fmt.Errorf("checking CBOR collection type: %w", err) + } + o.ctyp = s + delete(tmp, CMWC_T) + } + m := make(map[any]CMW) for k, v := range tmp { var c CMW - if err := c.Deserialize(v); err != nil { - return fmt.Errorf("unmarshaling JSON collection item %v: %w", k, err) + + start := v[0] + + switch { + case startJSONRecord(start): + if err := c.monad.UnmarshalJSON(v); err != nil { + return fmt.Errorf("unmarshaling JSON record item %v: %w", k, err) + } + c.kind = KindMonad + case startJSONCollection(start): + if err := c.collection.UnmarshalJSON(v); err != nil { + return fmt.Errorf("unmarshaling JSON collection item %v: %w", k, err) + } + c.kind = KindCollection + default: + return fmt.Errorf("want JSON object or JSON array start symbols, got: 0x%02x", start) } + m[k] = c } - o.m = m + o.cmap = m + o.format = FormatJSONCollection return nil } -func (o Collection) detectSerialization() (CollectionSerialization, error) { - rec := make(map[CollectionSerialization]bool) +/* +func (o collection) detectSerialization() (collectionSerialization, error) { + rec := make(map[collectionSerialization]bool) - s := UnknownCollectionSerialization + s := UnknowncollectionSerialization for k, v := range o.m { switch v.serialization { case CBORArray, CBORTag: - s = CollectionSerializationCBOR + s = collectionSerializationCBOR rec[s] = true case JSONArray: - s = CollectionSerializationJSON + s = collectionSerializationJSON rec[s] = true default: - return UnknownCollectionSerialization, + return UnknowncollectionSerialization, fmt.Errorf( "serialization not defined for collection item with k %v", k, ) @@ -192,9 +317,10 @@ func (o Collection) detectSerialization() (CollectionSerialization, error) { } if len(rec) != 1 { - return UnknownCollectionSerialization, + return UnknowncollectionSerialization, errors.New("CMW collection has items with incompatible serializations") } return s, nil } +*/ diff --git a/collection_test.go b/collection_test.go index b68f6b3..92f20a3 100644 --- a/collection_test.go +++ b/collection_test.go @@ -20,111 +20,101 @@ 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 - expectedA.SetMediaType("application/vnd.a") - expectedA.SetValue([]byte{0x61}) - expectedA.SetSerialization(JSONArray) - - var expectedB CMW - expectedB.SetMediaType("application/vnd.b") - expectedB.SetValue([]byte{0x62}) - expectedB.SetSerialization(JSONArray) - - var actual Collection + var actual CMW err := actual.UnmarshalJSON(tv) assert.NoError(t, err) - a, err := actual.GetItem("a") + a, err := actual.GetCollectionItem("a") assert.NoError(t, err) - assert.Equal(t, a, expectedA) + assert.Equal(t, FormatJSONRecord, a.GetFormat()) + assert.Equal(t, KindMonad, a.GetKind()) + assert.Equal(t, "application/vnd.a", a.GetMonadType()) + assert.Equal(t, []byte{0x61}, a.GetMonadValue()) + assert.Equal(t, Indicator(0), a.GetMonadIndicator()) - b, err := actual.GetItem("b") + b, err := actual.GetCollectionItem("b") assert.NoError(t, err) - assert.Equal(t, b, expectedB) + assert.Equal(t, FormatJSONRecord, b.GetFormat()) + assert.Equal(t, KindMonad, b.GetKind()) + assert.Equal(t, "application/vnd.b", b.GetMonadType()) + assert.Equal(t, []byte{0x62}, b.GetMonadValue()) + assert.Equal(t, Indicator(0), b.GetMonadIndicator()) } func Test_Collection_JSON_Serialize_ok(t *testing.T) { expected := mustReadFile(t, "testdata/collection-ok.json") - var tv Collection + tv := NewCollection("") - var a CMW - a.SetMediaType("application/vnd.a") - a.SetValue([]byte{0x61}) - a.SetSerialization(JSONArray) + a := NewMonad("application/vnd.a", []byte{0x61}) - tv.AddItem("a", a) + err := tv.AddCollectionItem("a", a) + require.NoError(t, err) - var b CMW - b.SetMediaType("application/vnd.b") - b.SetValue([]byte{0x62}) - b.SetSerialization(JSONArray) + b := NewMonad("application/vnd.b", []byte{0x62}) - tv.AddItem("b", b) + err = tv.AddCollectionItem("b", b) + require.NoError(t, err) - actual, err := tv.Serialize() + actual, err := tv.MarshalJSON() assert.NoError(t, err) assert.JSONEq(t, string(expected), string(actual)) } -func Test_Collection_JSON_Deserialize_fail_outer(t *testing.T) { - tv := []byte(`;rubbish json;`) - - var actual Collection - err := actual.UnmarshalJSON(tv) - assert.EqualError(t, err, `unmarshaling JSON collection: invalid character ';' looking for beginning of value`) -} - func Test_Collection_JSON_Deserialize_fail_inner(t *testing.T) { - tv := []byte(`{ "a": {} }`) + tv := []byte(`{ "a": { "__cmwc_t": "1.2.3.4" } }`) - var actual Collection + var actual CMW err := actual.UnmarshalJSON(tv) - assert.EqualError(t, err, `unmarshaling JSON collection item a: unknown CMW format`) + require.NoError(t, err) + err = actual.ValidateCollection() + assert.EqualError(t, err, `invalid collection at key "a": empty CMW collection`) } func Test_Collection_CBOR_Deserialize_ok(t *testing.T) { tv := mustReadFile(t, "testdata/collection-cbor-ok.cbor") - var actual Collection + var actual CMW err := actual.UnmarshalCBOR(tv) assert.NoError(t, err) - one, err := actual.GetItem(uint64(1)) + one, err := actual.GetCollectionItem(uint64(1)) assert.NoError(t, err) - 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, KindMonad, one.GetKind()) + assert.Equal(t, FormatCBORRecord, one.GetFormat()) + assert.Equal(t, "application/signed-corim+cbor", one.GetMonadType()) + assert.Equal(t, []byte{0xd2, 0x84, 0x43, 0xa1, 0x1, 0x26, 0xa1}, one.GetMonadValue()) + assert.Equal(t, Indicator(3), one.GetMonadIndicator()) - two, err := actual.GetItem(uint64(2)) + two, err := actual.GetCollectionItem(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, KindMonad, two.GetKind()) + assert.Equal(t, FormatCBORTag, two.GetFormat()) + assert.Equal(t, "29884", two.GetMonadType()) // TN() mapped CoAP C-F + assert.Equal(t, []byte{0x23, 0x47, 0xda, 0x55}, two.GetMonadValue()) + assert.Equal(t, Indicator(0), two.GetMonadIndicator()) - s, err := actual.GetItem("s") + s, err := actual.GetCollectionItem("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, KindMonad, s.GetKind()) + assert.Equal(t, FormatCBORRecord, s.GetFormat()) + assert.Equal(t, "30001", s.GetMonadType()) + assert.Equal(t, []byte{0x23, 0x47, 0xda, 0x55}, s.GetMonadValue()) + assert.Equal(t, Indicator(0), s.GetMonadIndicator()) } func Test_Collection_CBOR_Serialize_ok(t *testing.T) { - var item1 CMW - item1.SetMediaType("application/vnd.1") - item1.SetValue([]byte{0xde, 0xad, 0xbe, 0xef}) - item1.SetSerialization(CBORArray) + expected := mustReadFile(t, "testdata/collection-cbor-ok-2.cbor") - var tv Collection - tv.AddItem(uint64(1), item1) + item1 := NewMonad("application/vnd.1", []byte{0xde, 0xad, 0xbe, 0xef}) - expected := mustReadFile(t, "testdata/collection-cbor-ok-2.cbor") + tv := NewCollection("") - b, err := tv.Serialize() + err := tv.AddCollectionItem(uint64(1), item1) + require.NoError(t, err) + + b, err := tv.MarshalCBOR() assert.NoError(t, err) assert.Equal(t, expected, b) } @@ -132,12 +122,16 @@ 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 CMW err := actual.UnmarshalCBOR(tv) - assert.NoError(t, err) + require.NoError(t, err) + require.Equal(t, KindCollection, actual.GetKind()) + + meta, err := actual.GetCollectionMeta() + require.NoError(t, err) - for k := range actual.GetMap() { - switch v := k.(type) { + for _, m := range meta { + switch v := m.Key.(type) { case string: assert.Equal(t, "string", v) case uint64: @@ -148,40 +142,97 @@ 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 CMW err := c.Deserialize(tv) assert.NoError(t, err) + assert.Equal(t, KindCollection, c.GetKind()) + assert.Equal(t, FormatJSONCollection, c.GetFormat()) } func Test_Collection_Deserialize_CBOR_ok(t *testing.T) { tv := mustReadFile(t, "testdata/collection-cbor-ok.cbor") - var c Collection + var c CMW err := c.Deserialize(tv) assert.NoError(t, err) + assert.Equal(t, KindCollection, c.GetKind()) + assert.Equal(t, FormatCBORCollection, c.GetFormat()) +} + +func Test_Collection_with_tunnel(t *testing.T) { + tv := mustReadFile(t, "testdata/collection-and-tunnel.cbor") + + var c CMW + err := c.Deserialize(tv) + assert.NoError(t, err) + + item, err := c.GetCollectionItem(uint64(2)) + assert.NoError(t, err) + assert.Equal(t, KindMonad, item.GetKind()) + assert.Equal(t, FormatTunnelJ2C, item.GetFormat()) + + d, err := item.Untunnel() + assert.NoError(t, err) + assert.Equal(t, KindMonad, d.GetKind()) + assert.Equal(t, "application/eat+jwt", d.GetMonadType()) + assert.Equal(t, []byte{0x2e, 0x2e, 0x2e}, d.GetMonadValue()) +} + +func Test_isValidCollectionType(t *testing.T) { + type args struct { + ctyp string + } + tests := []struct { + name string + args args + wantErr bool + }{ + // valid + {"tag URI", args{`tag:example.com,2024:composite-attester`}, false}, + {"urn URI", args{`urn:ietf:rfc:rfc9999`}, false}, + {"http URI", args{`http://www.ietf.org/rfc/rfc2396.txt`}, false}, + {"absolute OID", args{`1.2.3.4`}, false}, + // invalid + {"(empty) relative URI", args{``}, true}, + {"relative URI", args{`a/b/c`}, true}, + {"relative OID", args{`.2.3.4`}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := isValidCollectionType(tt.args.ctyp); (err != nil) != tt.wantErr { + t.Errorf("isValidCollectionType() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_isValidCollectionKey(t *testing.T) { + type args struct { + key any + } + tests := []struct { + name string + args args + wantErr bool + }{ + // valid + {"ok-string-label", args{`my-label`}, false}, + {"ok-uint-label", args{uint64(1)}, false}, + {"ok-int-label", args{int64(1)}, false}, + // invalid + {"empty-label", args{``}, true}, + {"whitespace-only-label", args{` `}, true}, + {"bad-type-for-label", args{float32(1.0)}, true}, + {"reserved-for-cmwc_t", args{`__cmwc_t`}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := isValidCollectionKey(tt.args.key); (err != nil) != tt.wantErr { + t.Errorf("isValidCollectionKey() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } } diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..2164580 --- /dev/null +++ b/example_test.go @@ -0,0 +1,199 @@ +package cmw + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/fxamacker/cbor/v2" +) + +func makeCMWCollection() *CMW { + sub := NewCollection(("tag:ietf.org,2024:Y")) + + node := NewMonad("application/eat-ucs+json", []byte(`{"eat_nonce": ...}`), AttestationResults) + _ = sub.AddCollectionItem("polyscopic", node) + + root := NewCollection("tag:ietf.org,2024:X") + + _ = root.AddCollectionItem("murmurless", sub) + + node = NewMonad("application/eat-ucs+cbor", []byte{0xa1, 0x0a}) + _ = root.AddCollectionItem("bretwaldadom", node) + + node = NewMonad("application/eat-ucs+cbor", []byte{0x82, 0x78, 0x18}, ReferenceValues, Endorsements) + _ = root.AddCollectionItem("photoelectrograph", node) + + node, _ = NewFromRawJSON(json.RawMessage(`[ "application/vnd.my.ref-val", "e30K" ]`)) + _ = root.AddCollectionItem("json-raw", node) + + node, _ = NewFromRawCBOR(cbor.RawMessage([]byte{0x82, 0x19, 0x75, 0x31, 0x44, 0x23, 0x47, 0xda, 0x55})) + _ = root.AddCollectionItem("cbor-raw", node) + + // { + // "__cmwc_t": "tag:ietf.org,2024:X", + // "bretwaldadom": [ + // "application/eat-ucs+cbor", + // "oQo=" + // ], + // "cbor-raw": [ + // "#cmw-c2j-tunnel", + // "ghl1MUQjR9pV" + // ], + // "json-raw": [ + // "application/vnd.my.ref-val", + // "e30K" + // ], + // "murmurless": { + // "__cmwc_t": "tag:ietf.org,2024:Y", + // "polyscopic": [ + // "application/eat-ucs+json", + // "eyJlYXRfbm9uY2UiOiAuLi59", + // 8 + // ] + // }, + // "photoelectrograph": [ + // "application/eat-ucs+cbor", + // "gngY", + // 3 + // ] + // } + + return root +} + +func Example_Encode_JSON_collection() { + root := makeCMWCollection() + + b, err := root.MarshalJSON() + if err != nil { + log.Fatalf("marshal to JSON failed: %v", err) + } + + fmt.Println(string(b)) + + // Output: + // {"__cmwc_t":"tag:ietf.org,2024:X","bretwaldadom":["application/eat-ucs+cbor","oQo"],"cbor-raw":["#cmw-c2j-tunnel","ghl1MUQjR9pV"],"json-raw":["application/vnd.my.ref-val","e30K"],"murmurless":{"__cmwc_t":"tag:ietf.org,2024:Y","polyscopic":["application/eat-ucs+json","eyJlYXRfbm9uY2UiOiAuLi59",8]},"photoelectrograph":["application/eat-ucs+cbor","gngY",3]} +} + +func Example_Get_Meta() { + root := makeCMWCollection() + + meta, _ := root.GetCollectionMeta() + + for _, m := range meta { + fmt.Printf("%s: %s\n", m.Key, m.Kind) + } + + // Output: + // bretwaldadom: monad + // cbor-raw: raw value + // json-raw: raw value + // murmurless: collection + // photoelectrograph: monad +} + +func Example_Encode_CBOR_collection() { + root := makeCMWCollection() + + b, err := root.MarshalCBOR() + if err != nil { + log.Fatalf("marshal to CBOR failed: %v", err) + } + + edn, _ := cbor.Diagnose(b) + + fmt.Printf(edn) + + // Output: + // {"__cmwc_t": "tag:ietf.org,2024:X", "cbor-raw": [30001, h'2347da55'], "json-raw": ["#cmw-j2c-tunnel", h'5b20226170706c69636174696f6e2f766e642e6d792e7265662d76616c222c20226533304b22205d'], "murmurless": {"__cmwc_t": "tag:ietf.org,2024:Y", "polyscopic": ["application/eat-ucs+json", h'7b226561745f6e6f6e6365223a202e2e2e7d', 8]}, "bretwaldadom": ["application/eat-ucs+cbor", h'a10a'], "photoelectrograph": ["application/eat-ucs+cbor", h'827818', 3]} +} + +func Example_Decode_JSON_record() { + var o CMW + + err := o.UnmarshalJSON([]byte(`[ "application/vnd.example.rats-conceptual-msg", "I0faVQ", 3 ]`)) + if err != nil { + log.Fatalf("unmarshal JSON record failed: %v", err) + } + + fmt.Printf("CMW format: %s\n", o.GetFormat()) + fmt.Printf("type: %s\n", o.GetMonadType()) + fmt.Printf("value (hex): %x\n", o.GetMonadValue()) + fmt.Printf("indicator: %s\n", o.GetMonadIndicator()) + + // Output: + // CMW format: JSON record + // type: application/vnd.example.rats-conceptual-msg + // value (hex): 2347da55 + // indicator: endorsements, reference values +} + +func Example_Roundtrip_JSON_collection() { + var o CMW + + ex := []byte(`{ + "bretwaldadom": [ + "application/eat-ucs+cbor", + "oQo" + ], + "cbor-raw": [ + "#cmw-c2j-tunnel", + "ghl1MUQjR9pV" + ], + "__cmwc_t": "tag:ietf.org,2024:X", + "json-raw": [ + "application/vnd.my.ref-val", + "e30K" + ], + "murmurless": { + "__cmwc_t": "tag:ietf.org,2024:Y", + "polyscopic": [ + "application/eat-ucs+json", + "eyJlYXRfbm9uY2UiOiAuLi59", + 8 + ] + }, + "photoelectrograph": [ + "application/eat-ucs+cbor", + "gngY", + 3 + ] +}`) + + err := o.Deserialize(ex) + if err != nil { + log.Fatalf("unmarshal JSON collection failed: %v", err) + } + + b, err := o.MarshalJSON() + if err != nil { + log.Fatalf("marshal collection to JSON failed: %v", err) + } + + fmt.Println(string(b)) + + // Output: + // {"__cmwc_t":"tag:ietf.org,2024:X","bretwaldadom":["application/eat-ucs+cbor","oQo"],"cbor-raw":["#cmw-c2j-tunnel","ghl1MUQjR9pV"],"json-raw":["application/vnd.my.ref-val","e30K"],"murmurless":{"__cmwc_t":"tag:ietf.org,2024:Y","polyscopic":["application/eat-ucs+json","eyJlYXRfbm9uY2UiOiAuLi59",8]},"photoelectrograph":["application/eat-ucs+cbor","gngY",3]} +} + +func Example_Decode_CBOR_record() { + var o CMW + + b := mustHexDecode(`83781d6170706c69636174696f6e2f7369676e65642d636f72696d2b63626f724dd901f6d28440a044d901f5a04003`) + + if err := o.UnmarshalCBOR(b); err != nil { + log.Fatalf("unmarshal CBOR record failed: %v", err) + } + + fmt.Printf("CMW format: %s\n", o.GetFormat()) + fmt.Printf("type: %s\n", o.GetMonadType()) + fmt.Printf("value (hex): %x\n", o.GetMonadValue()) + fmt.Printf("indicator: %s\n", o.GetMonadIndicator()) + + // Output: + // CMW format: CBOR record + // type: application/signed-corim+cbor + // value (hex): d901f6d28440a044d901f5a040 + // indicator: endorsements, reference values +} diff --git a/go.mod b/go.mod index d21ab16..a8e1f71 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/veraison/cmw -go 1.19 +go 1.23.0 require ( - github.com/fxamacker/cbor/v2 v2.4.0 - github.com/stretchr/testify v1.8.2 + github.com/fxamacker/cbor/v2 v2.7.0 + github.com/stretchr/testify v1.9.0 ) require ( diff --git a/go.sum b/go.sum index fe66fd4..8d0f7a0 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,14 @@ -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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 index cc9a202..cf4a33f 100644 --- a/indicator.go +++ b/indicator.go @@ -3,7 +3,12 @@ package cmw -// Indicator is the internal representation of the `cm-ind` bit map +import ( + "sort" + "strings" +) + +// Indicator is the internal representation of the `ind` bit map type Indicator uint const ( @@ -16,8 +21,29 @@ const ( const IndicatorNone = 0 +var indMap = map[Indicator]string{ + ReferenceValues: "reference values", + Endorsements: "endorsements", + Evidence: "evidence", + AttestationResults: "attestation results", + TrustAnchors: "trust anchors", +} + 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 } +func (o Indicator) String() string { + var a []string + + for k, v := range indMap { + if o.Has(k) { + a = append(a, v) + } + } + + sort.Sort(sort.StringSlice(a)) + + return strings.Join(a, ", ") +} diff --git a/monad.go b/monad.go new file mode 100644 index 0000000..da4b8ec --- /dev/null +++ b/monad.go @@ -0,0 +1,171 @@ +package cmw + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/fxamacker/cbor/v2" +) + +type monad struct { + typ Type + val Value + ind Indicator + + format Format +} + +func (o monad) getType() string { return o.typ.String() } +func (o monad) getValue() []byte { return o.val } +func (o monad) getIndicator() Indicator { return o.ind } + +func (o monad) MarshalJSON() ([]byte, error) { return recordEncode(json.Marshal, &o) } + +func (o *monad) UnmarshalJSON(b []byte) error { + if err := recordDecode[json.RawMessage](json.Unmarshal, b, o); err != nil { + return err + } + + if o.getType() == "#cmw-c2j-tunnel" { + o.format = FormatTunnelC2J + } else if o.getType() == "#cmw-j2c-tunnel" { + return errors.New("wrong tunnel type (J2C) in JSON formatted monad") + } else { + o.format = FormatJSONRecord + } + + return nil +} + +func (o monad) MarshalCBOR() ([]byte, error) { + s := o.format + switch s { + case FormatCBORRecord, FormatUnknown: // XXX if it is not explicitly set, use the record format + return recordEncode(em.Marshal, &o) + case FormatCBORTag: + return o.encodeCBORTag() + } + // unreachable + panic(fmt.Sprintf("invalid format: want CBOR record or CBOR Tag, got %s", Format(s))) +} + +func (o *monad) UnmarshalCBOR(b []byte) error { + if startCBORRecord(b[0]) { + if err := recordDecode[cbor.RawMessage](cbor.Unmarshal, b, o); err != nil { + return fmt.Errorf("decoding record: %w", err) + } + if o.getType() == "#cmw-j2c-tunnel" { + o.format = FormatTunnelJ2C + } else if o.getType() == "#cmw-c2j-tunnel" { + return errors.New("wrong tunnel type (C2J) in CBOR formatted monad") + } else { + o.format = FormatCBORRecord + } + } else if startCBORTag(b[0]) { + if err := o.decodeCBORTag(b); err != nil { + return fmt.Errorf("decoding tag: %w", err) + } + o.format = FormatCBORTag + } else { + return fmt.Errorf("want CBOR Tag or CBOR array, got 0x%02x", b[0]) + } + + return nil +} + +func (o monad) 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 = em.Marshal(o.val) + if err != nil { + return nil, fmt.Errorf("marshaling tag value: %w", err) + } + + return tag.MarshalCBOR() +} + +func (o *monad) 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 = dm.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.format = FormatCBORTag + + return nil +} + +type ( + recordDecoder func([]byte, any) error + recordEncoder func(any) ([]byte, error) +) + +func recordDecode[V json.RawMessage | cbor.RawMessage]( + dec recordDecoder, b []byte, o *monad, +) 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 record", 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 recordEncode(enc recordEncoder, o *monad) ([]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) +} diff --git a/monad_test.go b/monad_test.go new file mode 100644 index 0000000..bed9c2b --- /dev/null +++ b/monad_test.go @@ -0,0 +1,413 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package cmw + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var testIndicator = Indicator(31) + +func Test_Deserialize_monad_ok(t *testing.T) { + tests := []struct { + name string + tv []byte + exp monad + }{ + { + "JSON array with media type string", + []byte(`["application/vnd.intel.sgx", "3q2-7w"]`), + monad{ + typ: Type{"application/vnd.intel.sgx"}, + val: []byte{0xde, 0xad, 0xbe, 0xef}, + ind: IndicatorNone, + format: FormatJSONRecord, + }, + }, + { + "JSON array with media type string and indicator", + []byte(`["application/vnd.intel.sgx", "3q2-7w", 31]`), + monad{ + Type{"application/vnd.intel.sgx"}, + []byte{0xde, 0xad, 0xbe, 0xef}, + testIndicator, + FormatJSONRecord, + }, + }, + { + "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}, + monad{ + Type{uint16(30001)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + FormatCBORRecord, + }, + }, + { + "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, + }, + monad{ + Type{string("application/vnd.intel.sgx")}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + FormatCBORRecord, + }, + }, + { + "CBOR tag", + // echo "1668576818(h'deadbeef')" | diag2cbor.rb | xxd -p -i + []byte{ + 0xda, 0x63, 0x74, 0x76, 0x32, 0x44, 0xde, 0xad, 0xbe, 0xef, + }, + monad{ + Type{uint64(1668576818)}, + []byte{0xde, 0xad, 0xbe, 0xef}, + IndicatorNone, + FormatCBORTag, + }, + }, + } + + 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, KindMonad, actual.GetKind()) + assert.Equal(t, tt.exp.format, actual.GetFormat()) + assert.Equal(t, tt.exp, actual.monad) + }) + } +} + +func Test_MarshalJSON_record_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) { + cmw := NewMonad(tt.tv.typ, tt.tv.val, tt.tv.ind...) + + actual, err := cmw.MarshalJSON() + assert.NoError(t, err) + assert.JSONEq(t, tt.exp, string(actual)) + }) + } +} + +func Test_MarshalCBOR_record_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) { + cmw := NewMonad(tt.tv.typ, tt.tv.val, tt.tv.ind...) + + actual, err := cmw.MarshalCBOR() + assert.NoError(t, err) + assert.Equal(t, tt.exp, actual) + }) + } +} + +func Test_MarshalCBOR_tag_ok(t *testing.T) { + type args struct { + typ uint16 // C-F + val []byte + } + + tests := []struct { + name string + tv args + exp []byte + }{ + { + "1", + args{ + 50000, + []byte{0xde, 0xad, 0xbe, 0xef}, + }, + // echo "1668597013(h'deadbeef')" | diag2cbor.rb | xxd -i + []byte{0xda, 0x63, 0x74, 0xc5, 0x15, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + { + "2", + args{ + 50001, + []byte{0xde, 0xad, 0xbe, 0xef}, + }, + // echo "1668597014(h'deadbeef')" | diag2cbor.rb | xxd -i + []byte{0xda, 0x63, 0x74, 0xc5, 0x16, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + { + "3", + args{ + 50002, + []byte{0xde, 0xad, 0xbe, 0xef}, + }, + // echo "1668597015(h'deadbeef')" | diag2cbor.rb | xxd + []byte{0xda, 0x63, 0x74, 0xc5, 0x17, 0x44, 0xde, 0xad, 0xbe, 0xef}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmw := NewMonad(tt.tv.typ, tt.tv.val, testIndicator) + + cmw.UseCBORTagFormat() + + actual, err := cmw.MarshalCBOR() + assert.NoError(t, err) + assert.Equal(t, tt.exp, actual) + }) + } +} + +func Test_UnmarshalJSON_record_ko(t *testing.T) { + tests := []struct { + name string + tv []byte + expectedErr string + }{ + { + "empty FormatJSONRecord", + []byte(`[]`), + `wrong number of entries (0) in the CMW record`, + }, + { + "missing mandatory field in FormatJSONRecord (1)", + []byte(`[10000]`), + `wrong number of entries (1) in the CMW record`, + }, + { + "missing mandatory field in FormatJSONRecord (2)", + []byte(`["3q2-7w"]`), + `wrong number of entries (1) in the CMW record`, + }, + { + "too many entries in FormatJSONRecord", + []byte(`[10000, "3q2-7w", 1, "EXTRA"]`), + `wrong number of entries (4) in the CMW record`, + }, + { + "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=="}`), + `want JSON object or JSON array start symbols`, + }, + { + "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.UnmarshalJSON(tt.tv) + assert.ErrorContains(t, err, tt.expectedErr) + }) + } +} + +func Test_UnmarshalCBOR_record_ko(t *testing.T) { + tests := []struct { + name string + tv []byte + expectedErr string + }{ + { + "empty CBOR record", + // echo "[]" | diag2cbor.rb | xxd -i + []byte{0x80}, + `want CBOR map, CBOR array or CBOR Tag start symbols, got: 0x80`, + }, + { + "missing mandatory field in FormatJSONRecord (1)", + // echo "[10000]" | diag2cbor.rb | xxd -i + []byte{0x81, 0x19, 0x27, 0x10}, + `want CBOR map, CBOR array or CBOR Tag start symbols, got: 0x81`, + }, + { + "too many entries in FormatJSONRecord", + // echo "[1000, h'deadbeef', 1, false]" | diag2cbor.rb | xxd -i + []byte{0x84, 0x19, 0x03, 0xe8, 0x44, 0xde, 0xad, 0xbe, 0xef, 0x01, 0xf4}, + `want CBOR map, CBOR array or CBOR Tag start symbols, got: 0x84`, + }, + { + "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, + }, + `decoding record: 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, + }, + `decoding record: 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, + }, + `decoding record: 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.UnmarshalCBOR(tt.tv) + assert.EqualError(t, err, tt.expectedErr) + }) + } +} + +func Test_UnmarshalCBOR_tag_ko(t *testing.T) { + tests := []struct { + name string + tv []byte + expectedErr string + }{ + { + "empty CBOR Tag", + []byte{0xda, 0x63, 0x74, 0x01, 0x01}, + `decoding tag: 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}, + `decoding tag: 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) + }) + } +} diff --git a/testdata/collection-and-tunnel.cbor b/testdata/collection-and-tunnel.cbor new file mode 100644 index 0000000..d43e011 Binary files /dev/null and b/testdata/collection-and-tunnel.cbor differ diff --git a/testdata/collection-and-tunnel.diag b/testdata/collection-and-tunnel.diag new file mode 100644 index 0000000..49cb340 --- /dev/null +++ b/testdata/collection-and-tunnel.diag @@ -0,0 +1,13 @@ +{ + "__cmwc_t": "tag:example.com,2024:composite-attester", + / attester A / 0: [ + 30001, + h'2347da55', + 4 + ], + / attester B / 1: 1668576935(h'2347da55'), + / attester C / 2: [ + "#cmw-j2c-tunnel", + '[ "application/eat+jwt", "Li4u", 8 ]' + ] +} diff --git a/testdata/collection-cbor-mixed-keys.cbor b/testdata/collection-cbor-mixed-keys.cbor index 08be7b6..6176cde 100644 Binary files a/testdata/collection-cbor-mixed-keys.cbor and b/testdata/collection-cbor-mixed-keys.cbor differ diff --git a/testdata/collection-cbor-mixed-keys.diag b/testdata/collection-cbor-mixed-keys.diag index 5449d8a..5fa5117 100644 --- a/testdata/collection-cbor-mixed-keys.diag +++ b/testdata/collection-cbor-mixed-keys.diag @@ -3,5 +3,5 @@ 0, h'ff' ], - 1024: 10000(h'aa') + 1024: 1668546817(h'aa') } diff --git a/type.go b/type.go index 84083fe..78fae28 100644 --- a/type.go +++ b/type.go @@ -7,8 +7,6 @@ import ( "encoding/json" "fmt" "strconv" - - "github.com/fxamacker/cbor/v2" ) type Type struct { @@ -41,10 +39,10 @@ func (o Type) String() string { } 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) MarshalCBOR() ([]byte, error) { return typeEncode(em.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) } +func (o *Type) UnmarshalCBOR(b []byte) error { return typeDecode(dm.Unmarshal, b, o) } type ( typeDecoder func([]byte, any) error diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..3a5801c --- /dev/null +++ b/utils.go @@ -0,0 +1,42 @@ +package cmw + +import ( + "encoding/base64" + "encoding/hex" + "regexp" +) + +func b64uEncode(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} + +func b64uDecode(s string) ([]byte, error) { + return base64.RawURLEncoding.DecodeString(s) +} + +func hexDecode(s string) ([]byte, error) { + // allow a long hex string to be split over multiple lines (with soft or + // hard tab indentation) + m := regexp.MustCompile("[ \t\n]") + s = m.ReplaceAllString(s, "") + + data, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + return data, nil +} + +func mustHexDecode(s string) []byte { + data, err := hexDecode(s) + if err != nil { + panic(err) + } + return data +} + +func startJSONCollection(c byte) bool { return c == 0x7b } +func startJSONRecord(c byte) bool { return c == 0x5b } +func startCBORCollection(c byte) bool { return c >= 0xa0 && c <= 0xbb || c == 0xbf } +func startCBORRecord(c byte) bool { return c == 0x82 || c == 0x83 } +func startCBORTag(c byte) bool { return c >= 0xda } diff --git a/value.go b/value.go index f623f61..5d73c13 100644 --- a/value.go +++ b/value.go @@ -1,14 +1,11 @@ -// Copyright 2023 Contributors to the Veraison project. +// Copyright 2023-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" ) type Value []byte @@ -28,7 +25,7 @@ func (o *Value) UnmarshalJSON(b []byte) error { err error ) - if v, err = base64.RawURLEncoding.DecodeString(string(b[1 : len(b)-1])); err != nil { + if v, err = b64uDecode(string(b[1 : len(b)-1])); err != nil { return fmt.Errorf("cannot base64 url-safe decode: %w", err) } @@ -38,7 +35,7 @@ func (o *Value) UnmarshalJSON(b []byte) error { } func (o Value) MarshalJSON() ([]byte, error) { - s := base64.RawURLEncoding.EncodeToString([]byte(o)) + s := b64uEncode([]byte(o)) return json.Marshal(s) } @@ -48,7 +45,7 @@ func (o *Value) UnmarshalCBOR(b []byte) error { err error ) - if err = cbor.Unmarshal(b, &v); err != nil { + if err = dm.Unmarshal(b, &v); err != nil { return fmt.Errorf("cannot decode value: %w", err) } @@ -58,5 +55,5 @@ func (o *Value) UnmarshalCBOR(b []byte) error { } func (o Value) MarshalCBOR() ([]byte, error) { - return cbor.Marshal([]byte(o)) + return em.Marshal([]byte(o)) }