diff --git a/comid/bytes.go b/comid/bytes.go new file mode 100644 index 00000000..61a66821 --- /dev/null +++ b/comid/bytes.go @@ -0,0 +1,50 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package comid + +import ( + "fmt" +) + +const BytesType = "bytes" + +type TaggedBytes []byte + +func NewBytes(val any) (*TaggedBytes, error) { + var ret TaggedBytes + + if val == nil { + return &ret, nil + } + + switch t := val.(type) { + case string: + b := []byte(t) + ret = TaggedBytes(b) + case []byte: + ret = TaggedBytes(t) + case *[]byte: + ret = TaggedBytes(*t) + default: + return nil, fmt.Errorf("unexpected type for bytes: %T", t) + } + return &ret, nil +} + +func (o TaggedBytes) String() string { + return string(o) +} + +func (o TaggedBytes) Valid() error { + return nil +} + +func (o TaggedBytes) Type() string { + return "bytes" +} + +func (o TaggedBytes) Bytes() []byte { + + return o +} diff --git a/comid/cbor.go b/comid/cbor.go index ab8a9233..f4100783 100644 --- a/comid/cbor.go +++ b/comid/cbor.go @@ -29,7 +29,7 @@ var ( 557: TaggedThumbprint{}, 558: TaggedCOSEKey{}, 559: TaggedCertThumbprint{}, - 560: TaggedRawValueBytes{}, + 560: TaggedBytes{}, 561: TaggedCertPathThumbprint{}, // PSA profile tags 600: TaggedImplID{}, diff --git a/comid/classid.go b/comid/classid.go index acad47d5..706e1f8d 100644 --- a/comid/classid.go +++ b/comid/classid.go @@ -16,7 +16,7 @@ import ( ) // ClassID identifies the environment via a well-known identifier. This can be -// an OID, a UUID, or a profile-defined extension type. +// an OID, a UUID, a variable length opaque bytes or a profile-defined extension type. type ClassID struct { Value IClassIDValue } @@ -85,14 +85,15 @@ func (o *ClassID) UnmarshalCBOR(data []byte) error { // // where must be one of the known IClassIDValue implementation // type names (available in this implementation: "uuid", "oid", -// "psa.impl-id", "int"), and is the JSON encoding of the underlying +// "psa.impl-id", "int", "bytes"), and is the JSON encoding of the underlying // class id value. The exact encoding is dependent. For the base // implementation types it is // -// oid: dot-separated integers, e.g. "1.2.3.4" -// psa.impl-id: base64-encoded bytes, e.g. "YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE=" -// uuid: standard UUID string representation, e.g. "550e8400-e29b-41d4-a716-446655440000" -// int: an integer value, e.g. 7 +// oid: dot-separated integers, e.g. "1.2.3.4" +// psa.impl-id: base64-encoded bytes, e.g. "YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE=" +// uuid: standard UUID string representation, e.g. "550e8400-e29b-41d4-a716-446655440000" +// int: an integer value, e.g. 7 +// bytes: a variable length opaque bytes, example {0x07, 0x12, 0x34} func (o *ClassID) UnmarshalJSON(data []byte) error { var tnv encoding.TypeAndValue @@ -346,6 +347,17 @@ func (o TaggedInt) Bytes() []byte { return ret[:] } +// NewBytesClassID creates a New ClassID of type bytes +// The supplied interface parameter could be +// a byte slice, a pointer to a byte slice or a string +func NewBytesClassID(val any) (*ClassID, error) { + ret, err := NewBytes(val) + if err != nil { + return nil, err + } + return &ClassID{ret}, nil +} + // IClassIDFactory defines the signature for the factory functions that may be // registred using RegisterClassIDType to provide a new implementation of the // corresponding type choice. The factory function should create a new *ClassID @@ -357,10 +369,10 @@ func (o TaggedInt) Bytes() []byte { type IClassIDFactory func(any) (*ClassID, error) var classIDValueRegister = map[string]IClassIDFactory{ - OIDType: NewOIDClassID, - UUIDType: NewUUIDClassID, - IntType: NewIntClassID, - + OIDType: NewOIDClassID, + UUIDType: NewUUIDClassID, + IntType: NewIntClassID, + BytesType: NewBytesClassID, ImplIDType: NewImplIDClassID, } diff --git a/comid/classid_test.go b/comid/classid_test.go index aaeeb393..f09f08a8 100644 --- a/comid/classid_test.go +++ b/comid/classid_test.go @@ -441,3 +441,121 @@ func Test_RegisterClassIDType(t *testing.T) { require.NoError(t, err) assert.Equal(t, classID.Bytes(), out2.Bytes()) } + +func Test_NewBytesClassID_OK(t *testing.T) { + var testBytes = []byte{0x01, 0x02, 0x03, 0x04} + + for _, v := range []any{ + testBytes, + &testBytes, + string(testBytes), + } { + classID, err := NewBytesClassID(v) + require.NoError(t, err) + got := classID.Bytes() + assert.Equal(t, testBytes[:], got) + } +} + +func Test_NewBytesClassID_NOK(t *testing.T) { + for _, tv := range []struct { + Name string + Input any + Err string + }{ + + { + Name: "invalid input integer", + Input: 7, + Err: "unexpected type for bytes: int", + }, + /* + { + Name: "invalid nil input", + Err: "unexpected type for bytes: ", + }, + */ + { + Name: "invalid input fixed array", + Input: [3]byte{0x01, 0x02, 0x03}, + Err: "unexpected type for bytes: [3]uint8", + }, + } { + t.Run(tv.Name, func(t *testing.T) { + _, err := NewBytesClassID(tv.Input) + assert.EqualError(t, err, tv.Err) + }) + } +} + +func TestClassID_MarshalCBOR_Bytes(t *testing.T) { + tv, err := NewBytesClassID(TestBytes) + require.NoError(t, err) + // 560 (h'458999786556') + // tag(560): d9 0230 + expected := MustHexDecode(t, "d90230458999786556") + + actual, err := tv.MarshalCBOR() + fmt.Printf("CBOR: %x\n", actual) + + assert.Nil(t, err) + assert.Equal(t, expected, actual) +} + +func TestClassID_UnmarshalCBOR_Bytes_OK(t *testing.T) { + tv := MustHexDecode(t, "d90230458999786556") + + var actual ClassID + err := actual.UnmarshalCBOR(tv) + + assert.Nil(t, err) + assert.Equal(t, "bytes", actual.Type()) + assert.Equal(t, TestBytes, actual.Bytes()) +} + +func TestClassID_UnmarshalJSON_Bytes_OK(t *testing.T) { + for _, tv := range []struct { + Name string + Input string + }{ + { + Name: "valid input test 1", + Input: `{ "type": "bytes", "value": "MTIzNDU2Nzg5" }`, + }, + { + Name: "valid input test 2", + Input: `{ "type": "bytes", "value": "deadbeef"}`, + }, + } { + t.Run(tv.Name, func(t *testing.T) { + var actual ClassID + err := actual.UnmarshalJSON([]byte(tv.Input)) + require.NoError(t, err) + }) + } +} + +func TestClassID_UnmarshalJSON_Bytes_NOK(t *testing.T) { + for _, tv := range []struct { + Name string + Input string + Err string + }{ + { + Name: "invalid value", + Input: `{ "type": "bytes", "value": "/0" }`, + Err: "cannot unmarshal class id: illegal base64 data at input byte 0", + }, + { + Name: "invalid input", + Input: `{ "type": "bytes", "value": 10 }`, + Err: "cannot unmarshal class id: json: cannot unmarshal number into Go value of type comid.TaggedBytes", + }, + } { + t.Run(tv.Name, func(t *testing.T) { + var actual ClassID + err := actual.UnmarshalJSON([]byte(tv.Input)) + assert.EqualError(t, err, tv.Err) + }) + } +} diff --git a/comid/group.go b/comid/group.go index 789ed197..0678fb5e 100644 --- a/comid/group.go +++ b/comid/group.go @@ -12,7 +12,7 @@ import ( "github.com/veraison/corim/extensions" ) -// Group stores a group identity. The supported format is UUID. +// Group stores a group identity. The supported format is UUID and a variable length opaque bytes. type Group struct { Value IGroupValue } @@ -42,6 +42,23 @@ func (o Group) String() string { return o.Value.String() } +// Type returns the type of the Group +func (o Group) Type() string { + if o.Value == nil { + return "" + } + + return o.Value.Type() +} + +// Bytes returns a []byte containing the raw bytes of the group value +func (o Group) Bytes() []byte { + if o.Value == nil { + return []byte{} + } + return o.Value.Bytes() +} + // MarshalCBOR serializes the target group to CBOR func (o Group) MarshalCBOR() ([]byte, error) { return em.Marshal(o.Value) @@ -53,10 +70,18 @@ func (o *Group) UnmarshalCBOR(data []byte) error { } // UnmarshalJSON deserializes the supplied JSON type/value object into the Group -// target. The only supported format is UUID, e.g.: +// target. The following formats are supported: +// +// (a) UUID, e.g.: +// { +// "type": "uuid", +// "value": "69E027B2-7157-4758-BCB4-D9F167FE49EA" +// } +// +// (b) Tagged bytes, e.g. : // // { -// "type": "uuid", +// "type": "bytes", // "value": "69E027B2-7157-4758-BCB4-D9F167FE49EA" // } func (o *Group) UnmarshalJSON(data []byte) error { @@ -93,6 +118,8 @@ func (o Group) MarshalJSON() ([]byte, error) { type IGroupValue interface { extensions.ITypeChoiceValue + + Bytes() []byte } func NewUUIDGroup(val any) (*Group, error) { @@ -117,8 +144,19 @@ func MustNewUUIDGroup(val any) *Group { return ret } +// NewBytesGroup creates a New Group of type bytes +// The supplied interface parameter could be +// a byte slice, a pointer to a byte slice or a string +func NewBytesGroup(val any) (*Group, error) { + ret, err := NewBytes(val) + if err != nil { + return nil, err + } + return &Group{ret}, nil +} + // IGroupFactory defines the signature for the factory functions that may be -// registred using RegisterGroupType to provide a new implementation of the +// registered using RegisterGroupType to provide a new implementation of the // corresponding type choice. The factory function should create a new *Group // with the underlying value created based on the provided input. The range of // valid inputs is up to the specific type choice implementation, however it @@ -128,7 +166,8 @@ func MustNewUUIDGroup(val any) *Group { type IGroupFactory func(any) (*Group, error) var groupValueRegister = map[string]IGroupFactory{ - UUIDType: NewUUIDGroup, + UUIDType: NewUUIDGroup, + BytesType: NewBytesGroup, } // RegisterGroupType registers a new IGroupValue implementation diff --git a/comid/group_test.go b/comid/group_test.go index 4363f3b2..71ffbfd3 100644 --- a/comid/group_test.go +++ b/comid/group_test.go @@ -1,6 +1,8 @@ package comid import ( + "encoding/binary" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -21,6 +23,11 @@ func (o testGroup) Type() string { func (o testGroup) String() string { return "test" } +func (o testGroup) Bytes() []byte { + var ret [8]byte + binary.BigEndian.PutUint64(ret[:], uint64(o)) + return ret[:] +} func (o testGroup) Valid() error { return nil @@ -60,3 +67,75 @@ func TestGroup_UmarshalJSON(t *testing.T) { err = group.UnmarshalJSON([]byte(`{"type":"uuid","value":"aaaa"}`)) assert.EqualError(t, err, "cannot unmarshal group: bad UUID: invalid UUID length: 4") } + +func TestGroup_MarshalCBOR_Bytes(t *testing.T) { + tv, err := NewBytesGroup(TestBytes) + require.NoError(t, err) + // 560 (h'458999786556') + // tag(560): d9 0230 + expected := MustHexDecode(t, "d90230458999786556") + + actual, err := tv.MarshalCBOR() + fmt.Printf("CBOR: %x\n", actual) + + assert.Nil(t, err) + assert.Equal(t, expected, actual) +} + +func TestGroup_UnmarshalCBOR_Bytes_OK(t *testing.T) { + tv := MustHexDecode(t, "d90230458999786556") + + var actual Group + err := actual.UnmarshalCBOR(tv) + + assert.Nil(t, err) + assert.Equal(t, "bytes", actual.Type()) + assert.Equal(t, TestBytes, actual.Bytes()) +} + +func TestGroup_UnmarshalJSON_Bytes_OK(t *testing.T) { + for _, tv := range []struct { + Name string + Input string + }{ + { + Name: "valid input test 1", + Input: `{ "type": "bytes", "value": "MTIzNDU2Nzg5" }`, + }, + { + Name: "valid input test 2", + Input: `{ "type": "bytes", "value": "deadbeef"}`, + }, + } { + t.Run(tv.Name, func(t *testing.T) { + var actual Group + err := actual.UnmarshalJSON([]byte(tv.Input)) + require.NoError(t, err) + }) + } +} + +func TestGroup_UnmarshalJSON_Bytes_NOK(t *testing.T) { + for _, tv := range []struct { + Name string + Input string + Err string + }{ + { + Name: "invalid value", + Input: `{ "type": "bytes", "value": "/0" }`, + Err: "cannot unmarshal class id: illegal base64 data at input byte 0", + }, + { + Name: "invalid input", + Input: `{ "type": "bytes", "value": 10 }`, + Err: "cannot unmarshal class id: json: cannot unmarshal number into Go value of type comid.TaggedBytes", + }, + } { + t.Run(tv.Name, func(t *testing.T) { + var actual ClassID + err := actual.UnmarshalJSON([]byte(tv.Input)) + assert.EqualError(t, err, tv.Err) + }) + } +} diff --git a/comid/instance.go b/comid/instance.go index 83631b32..39c40b98 100644 --- a/comid/instance.go +++ b/comid/instance.go @@ -8,7 +8,7 @@ import ( "github.com/veraison/corim/extensions" ) -// Instance stores an instance identity. The supported formats are UUID and UEID. +// Instance stores an instance identity. The supported formats are UUID, UEID and variable length opaque bytes. type Instance struct { Value IInstanceValue } @@ -77,6 +77,7 @@ func (o *Instance) UnmarshalCBOR(data []byte) error { // // ueid: base64-encoded bytes, e.g. "YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE=" // uuid: standard UUID string representation, e.g. "550e8400-e29b-41d4-a716-446655440000" +// bytes: a variable length opaque bytes, example {0x07, 0x12, 0x34} func (o *Instance) UnmarshalJSON(data []byte) error { var tnv encoding.TypeAndValue @@ -149,7 +150,6 @@ func MustNewUEIDInstance(val any) *Instance { if err != nil { panic(err) } - return ret } @@ -167,6 +167,17 @@ func NewUUIDInstance(val any) (*Instance, error) { return &Instance{ret}, nil } +// NewBytesInstance creates a new instance of type bytes +// The supplied interface parameter could be +// a byte slice, a pointer to a byte slice or a string +func NewBytesInstance(val any) (*Instance, error) { + ret, err := NewBytes(val) + if err != nil { + return nil, err + } + return &Instance{ret}, nil +} + // MustNewUUIDInstance is like NewUUIDInstance execept it does not return an // error, assuming that the provided value is valid. It panics if that isn't // the case. @@ -180,7 +191,7 @@ func MustNewUUIDInstance(val any) *Instance { } // IInstanceFactory defines the signature for the factory functions that may be -// registred using RegisterInstanceType to provide a new implementation of the +// registered using RegisterInstanceType to provide a new implementation of the // corresponding type choice. The factory function should create a new *Instance // with the underlying value created based on the provided input. The range of // valid inputs is up to the specific type choice implementation, however it @@ -190,8 +201,9 @@ func MustNewUUIDInstance(val any) *Instance { type IInstanceFactory func(any) (*Instance, error) var instanceValueRegister = map[string]IInstanceFactory{ - UEIDType: NewUEIDInstance, - UUIDType: NewUUIDInstance, + UEIDType: NewUEIDInstance, + UUIDType: NewUUIDInstance, + BytesType: NewBytesInstance, } // RegisterInstanceType registers a new IInstanceValue implementation (created diff --git a/comid/instance_test.go b/comid/instance_test.go index 6ea22fd4..8971d870 100644 --- a/comid/instance_test.go +++ b/comid/instance_test.go @@ -2,6 +2,7 @@ package comid import ( "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -67,3 +68,75 @@ func Test_RegisterInstanceType(t *testing.T) { require.NoError(t, err) assert.Equal(t, instance.Bytes(), out2.Bytes()) } + +func TestInstance_MarshalCBOR_Bytes(t *testing.T) { + tv, err := NewBytesInstance(TestBytes) + require.NoError(t, err) + // 560 (h'458999786556') + // tag(560): d9 0230 + expected := MustHexDecode(t, "d90230458999786556") + + actual, err := tv.MarshalCBOR() + fmt.Printf("CBOR: %x\n", actual) + + assert.Nil(t, err) + assert.Equal(t, expected, actual) +} + +func TestInstance_UnmarshalCBOR_Bytes_OK(t *testing.T) { + tv := MustHexDecode(t, "d90230458999786556") + + var actual Instance + err := actual.UnmarshalCBOR(tv) + + assert.Nil(t, err) + assert.Equal(t, "bytes", actual.Type()) + assert.Equal(t, TestBytes, actual.Bytes()) +} + +func TestInstance_UnmarshalJSON_Bytes_OK(t *testing.T) { + for _, tv := range []struct { + Name string + Input string + }{ + { + Name: "valid input test 1", + Input: `{ "type": "bytes", "value": "MTIzNDU2Nzg5" }`, + }, + { + Name: "valid input test 2", + Input: `{ "type": "bytes", "value": "deadbeef"}`, + }, + } { + t.Run(tv.Name, func(t *testing.T) { + var actual Instance + err := actual.UnmarshalJSON([]byte(tv.Input)) + require.NoError(t, err) + }) + } +} + +func TestInstance_UnmarshalJSON_Bytes_NOK(t *testing.T) { + for _, tv := range []struct { + Name string + Input string + Err string + }{ + { + Name: "invalid value", + Input: `{ "type": "bytes", "value": "/0" }`, + Err: "cannot unmarshal instance: illegal base64 data at input byte 0", + }, + { + Name: "invalid input", + Input: `{ "type": "bytes", "value": 10 }`, + Err: "cannot unmarshal instance: json: cannot unmarshal number into Go value of type comid.TaggedBytes", + }, + } { + t.Run(tv.Name, func(t *testing.T) { + var actual Instance + err := actual.UnmarshalJSON([]byte(tv.Input)) + assert.EqualError(t, err, tv.Err) + }) + } +} diff --git a/comid/rawvalue.go b/comid/rawvalue.go index 140b5f8c..5f7554c3 100644 --- a/comid/rawvalue.go +++ b/comid/rawvalue.go @@ -13,16 +13,17 @@ type RawValue struct { val interface{} } -// TaggedRawValueBytes is an alias for []byte to allow its automatic tagging -type TaggedRawValueBytes []byte - func NewRawValue() *RawValue { return &RawValue{} } func (o *RawValue) SetBytes(val []byte) *RawValue { if o != nil { - o.val = TaggedRawValueBytes(val) + v, err := NewBytes(val) + if err != nil { + return nil + } + o.val = *v } return o } @@ -33,7 +34,7 @@ func (o RawValue) GetBytes() ([]byte, error) { } switch t := o.val.(type) { - case TaggedRawValueBytes: + case TaggedBytes: return []byte(t), nil default: return nil, fmt.Errorf("unknown type %T for $raw-value-type-choice", t) @@ -45,7 +46,7 @@ func (o RawValue) MarshalCBOR() ([]byte, error) { } func (o *RawValue) UnmarshalCBOR(data []byte) error { - var rawValue TaggedRawValueBytes + var rawValue TaggedBytes if dm.Unmarshal(data, &rawValue) == nil { o.val = rawValue @@ -56,7 +57,7 @@ func (o *RawValue) UnmarshalCBOR(data []byte) error { } // UnmarshalJSON deserializes the type'n'value JSON object into the target RawValue. -// The only supported type is "bytes" with value +// The only supported type is BytesType with value func (o *RawValue) UnmarshalJSON(data []byte) error { var v tnv @@ -65,7 +66,7 @@ func (o *RawValue) UnmarshalJSON(data []byte) error { } switch v.Type { - case "bytes": + case BytesType: var x []byte if err := json.Unmarshal(v.Value, &x); err != nil { return fmt.Errorf( @@ -73,7 +74,7 @@ func (o *RawValue) UnmarshalJSON(data []byte) error { err, ) } - o.val = TaggedRawValueBytes(x) + o.val = TaggedBytes(x) default: return fmt.Errorf("unknown type %s for $raw-value-type-choice", v.Type) } @@ -89,12 +90,12 @@ func (o RawValue) MarshalJSON() ([]byte, error) { ) switch t := o.val.(type) { - case TaggedRawValueBytes: + case TaggedBytes: b, err = json.Marshal(o.val) if err != nil { return nil, err } - v = tnv{Type: "bytes", Value: b} + v = tnv{Type: BytesType, Value: b} default: return nil, fmt.Errorf("unknown type %T for raw-value-type-choice", t) } diff --git a/comid/rawvalue_test.go b/comid/rawvalue_test.go index d890794f..c5ac0e04 100644 --- a/comid/rawvalue_test.go +++ b/comid/rawvalue_test.go @@ -33,7 +33,7 @@ func TestRawValue_Get_Bytes_nok(t *testing.T) { assert.EqualError(t, err, expectedErr) } -func TestRawValue_Marshal_UnMarshal_ok(t *testing.T) { +func TestRawValue_Marshal_UnMarshal_JSON_ok(t *testing.T) { tv := RawValue{} rv := tv.SetBytes([]byte{0x01, 0x02, 0x03}) bytes, err := rv.MarshalJSON() @@ -43,3 +43,14 @@ func TestRawValue_Marshal_UnMarshal_ok(t *testing.T) { assert.NoError(t, err) assert.Equal(t, *rv, sv) } + +func TestRawValue_Marshal_UnMarshal_CBOR_ok(t *testing.T) { + tv := RawValue{} + rv := tv.SetBytes([]byte{0x01, 0x02, 0x03}) + bytes, err := rv.MarshalCBOR() + assert.NoError(t, err) + sv := RawValue{} + err = sv.UnmarshalCBOR(bytes) + assert.NoError(t, err) + assert.Equal(t, *rv, sv) +} diff --git a/comid/test_vars.go b/comid/test_vars.go index 37b9be1a..ef647dad 100644 --- a/comid/test_vars.go +++ b/comid/test_vars.go @@ -28,6 +28,7 @@ var ( TestRegID = "https://acme.example" TestMACaddr, _ = net.ParseMAC("02:00:5e:10:00:00:00:01") TestIPaddr = net.ParseIP("2001:db8::68") + TestBytes = []byte{0x89, 0x99, 0x78, 0x65, 0x56} TestUEIDString = "02deadbeefdead" TestUEID = eat.UEID(MustHexDecode(nil, TestUEIDString)) TestSignerID = MustHexDecode(nil, "acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b")