diff --git a/go.mod b/go.mod index 16e9b3c..7eb559e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/veraison/swid go 1.15 require ( + github.com/fxamacker/cbor v1.5.1 github.com/fxamacker/cbor/v2 v2.2.0 github.com/stretchr/testify v1.6.1 ) diff --git a/go.sum b/go.sum index fc5a34c..92702e8 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= +github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/softwareidentity.go b/softwareidentity.go index b101e25..71055c1 100644 --- a/softwareidentity.go +++ b/softwareidentity.go @@ -6,6 +6,7 @@ package swid import ( "encoding/json" "encoding/xml" + "errors" ) // SoftwareIdentity represents the top-level SWID @@ -189,12 +190,12 @@ func (t *SoftwareIdentity) FromCBOR(data []byte) error { } func (t *SoftwareIdentity) setTagID(v interface{}) error { - tagID, err := checkTagID(v) - if err != nil { - return err + tagID := NewTagID(v) + if tagID == nil { + return errors.New("bad type for TagID: expecting string or [16]byte") } - t.TagID = tagID + t.TagID = *tagID return nil } diff --git a/softwareidentity_test.go b/softwareidentity_test.go index af0b900..94fba5a 100644 --- a/softwareidentity_test.go +++ b/softwareidentity_test.go @@ -22,7 +22,7 @@ func makeACMEEntityWithRoles(t *testing.T, roles ...interface{}) Entity { func TestTag_RoundtripPSABundle(t *testing.T) { tv := SoftwareIdentity{ - TagID: TagID("example.acme.roadrunner-sw-v1-0-0"), + TagID: *NewTagID("example.acme.roadrunner-sw-v1-0-0"), SoftwareName: "Roadrunner software bundle", SoftwareVersion: "1.0.0", Entities: Entities{ @@ -139,7 +139,7 @@ func TestTag_RoundtripPSABundle(t *testing.T) { func TestTag_RoundtripPSAComponent(t *testing.T) { tv := SoftwareIdentity{ - TagID: TagID("example.acme.roadrunner-sw-bl-v1-0-0"), + TagID: *NewTagID("example.acme.roadrunner-sw-bl-v1-0-0"), SoftwareName: "Roadrunner boot loader", SoftwareVersion: "1.0.0", Entities: Entities{ diff --git a/tagid.go b/tagid.go index 6085d6f..e7241c6 100644 --- a/tagid.go +++ b/tagid.go @@ -4,24 +4,125 @@ package swid import ( + "encoding/hex" + "encoding/json" + "encoding/xml" "errors" "fmt" + + "github.com/fxamacker/cbor" ) // TagID is the type of a tag identifier. Allowed formats (enforced via // checkTagID) are string or [16]byte -type TagID interface{} +type TagID struct { + val interface{} +} + +// NewTagID returns a TagID initialized with the supplied value v +// v is either a string or a [16]byte +func NewTagID(v interface{}) *TagID { + if checkTagID(v) != nil { + return nil + } + return &TagID{v} +} + +// String returns the value of the TagID as string. If the TagID has type +// [16]byte the Base 16 encoding is returned +func (t TagID) String() string { + switch v := t.val.(type) { + case string: + return v + case []byte: + return hex.EncodeToString(v) + default: + return "unknown type for tag-id" + } +} -func checkTagID(v interface{}) (TagID, error) { +func checkTagID(v interface{}) error { switch t := v.(type) { case string: case []byte: if len(t) != 16 { - return nil, errors.New("binary tag-id MUST be 16 bytes") + return errors.New("binary tag-id MUST be 16 bytes") } default: - return nil, fmt.Errorf("tag-id MUST be []byte or string; got %T", v) + return fmt.Errorf("tag-id MUST be []byte or string; got %T", v) + } + + return nil +} + +func (t TagID) isString() bool { + switch t.val.(type) { + case string: + return true + } + return false +} + +// MarshalXMLAttr encodes the TagID receiver as XML attribute +func (t TagID) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + if !t.isString() { + return xml.Attr{}, errors.New("only tag-id of type string can be serialized to XML") + } + return xml.Attr{Name: name, Value: t.String()}, nil +} + +// UnmarshalXMLAttr decodes the supplied XML attribute into a TagID +// Note that this can only unmarshal to string. +func (t *TagID) UnmarshalXMLAttr(attr xml.Attr) error { + t.val = attr.Value + return nil +} + +// MarshalJSON encodes the TagID receiver as JSON string +func (t TagID) MarshalJSON() ([]byte, error) { + if !t.isString() { + return nil, errors.New("only tag-id of type string can be serialized to JSON") + } + + return json.Marshal(t.val) +} + +// UnmarshalJSON decodes the supplied JSON data into a TagID +// Note that this can only unmarshal to string. +func (t *TagID) UnmarshalJSON(data []byte) error { + var v interface{} + + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + switch s := v.(type) { + case string: + t.val = s + return nil + default: + return fmt.Errorf("expecting string, found %T instead", s) + } +} + +// MarshalCBOR encodes the TagID receiver to CBOR +func (t TagID) MarshalCBOR() ([]byte, error) { + return em.Marshal(t.val) +} + +// UnmarshalCBOR decodes the supplied data into a TagID +func (t *TagID) UnmarshalCBOR(data []byte) error { + var v interface{} + + if err := cbor.Unmarshal(data, &v); err != nil { + return err } - return TagID(v), nil + if err := checkTagID(v); err != nil { + return err + } + + t.val = v + + return nil } diff --git a/tagid_test.go b/tagid_test.go index f82c5bf..34e1a02 100644 --- a/tagid_test.go +++ b/tagid_test.go @@ -4,6 +4,7 @@ package swid import ( + "encoding/xml" "testing" "github.com/stretchr/testify/assert" @@ -15,12 +16,12 @@ func TestTagID_16Bytes(t *testing.T) { 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, } - expected := tv + expected := "00010001000100010001000100010001" - actual, err := checkTagID(tv) + actual := NewTagID(tv) - assert.Nil(t, err) - assert.Equal(t, expected, actual) + assert.NotNil(t, actual) + assert.Equal(t, expected, actual.String()) } func TestTagID_15Bytes(t *testing.T) { @@ -29,7 +30,7 @@ func TestTagID_15Bytes(t *testing.T) { 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, } - _, err := checkTagID(tv) + err := checkTagID(tv) assert.EqualError(t, err, "binary tag-id MUST be 16 bytes") } @@ -41,7 +42,7 @@ func TestTagID_17Bytes(t *testing.T) { 0x00, } - _, err := checkTagID(tv) + err := checkTagID(tv) assert.EqualError(t, err, "binary tag-id MUST be 16 bytes") } @@ -49,10 +50,10 @@ func TestTagID_17Bytes(t *testing.T) { func TestTagID_String(t *testing.T) { tv := "example.acme.roadrunner-sw-v1-0-0" - actual, err := checkTagID(tv) + actual := NewTagID(tv) - assert.Nil(t, err) - assert.Equal(t, tv, actual) + assert.NotNil(t, actual) + assert.Equal(t, tv, actual.String()) } func TestTagID_UnhandledType(t *testing.T) { @@ -64,7 +65,77 @@ func TestTagID_UnhandledType(t *testing.T) { b: "one", } - _, err := checkTagID(tv) + err := checkTagID(tv) assert.EqualError(t, err, "tag-id MUST be []byte or string; got struct { a int; b string }") } + +func TestTagID_UnmarshalXMLAttrString(t *testing.T) { + v := "example.acme.roadrunner-sw-v1-0-0" + + tv := xml.Attr{ + Name: xml.Name{Local: "tagId"}, + Value: v, + } + + expected := *NewTagID(v) + + var actual TagID + + err := actual.UnmarshalXMLAttr(tv) + + assert.Nil(t, err) + assert.Equal(t, expected, actual) +} + +func TestTagID_MarshalXMLAttrString(t *testing.T) { + v := "example.acme.roadrunner-sw-v1-0-0" + + tv := *NewTagID(v) + + expected := xml.Attr{ + Name: xml.Name{Local: "tagId"}, + Value: v, + } + + actual, err := tv.MarshalXMLAttr(xml.Name{Local: "tagId"}) + + assert.Nil(t, err) + assert.Equal(t, expected, actual) +} + +func TestTagID_MarshalXMLAttrBytes(t *testing.T) { + v := []byte{ + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + } + + tv := *NewTagID(v) + + _, err := tv.MarshalXMLAttr(xml.Name{Local: "tagId"}) + + assert.EqualError(t, err, "only tag-id of type string can be serialized to XML") +} + +func TestTagID_MarshalJSONBytes(t *testing.T) { + v := []byte{ + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + } + + tv := *NewTagID(v) + + _, err := tv.MarshalJSON() + + assert.EqualError(t, err, "only tag-id of type string can be serialized to JSON") +} + +func TestTagID_UnMarshalJSONUnhandled(t *testing.T) { + tv := []byte(`{ "k": "0" }`) + + var actual TagID + + err := actual.UnmarshalJSON(tv) + + assert.EqualError(t, err, "expecting string, found map[string]interface {} instead") +}