Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions eat.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package eat

import (
"encoding/base64"
"encoding/json"
"fmt"
)

// Eat is the internal representation of a EAT token
Expand All @@ -14,7 +16,7 @@ type Eat struct {
// TODO: support SUEIDs
// TODO: support oemid-pem = int type
OemID *[]byte `cbor:"258,keyasint,omitempty" json:"oemid,omitempty"`
HardwareModel *[]byte `cbor:"259,keyasint,omitempty" json:"hwmodel,omitempty"`
HardwareModel *B64Url `cbor:"259,keyasint,omitempty" json:"hwmodel,omitempty"`
HardwareVersion *Version `cbor:"260,keyasint,omitempty" json:"hwversion,omitempty"`
Uptime *uint `cbor:"261,keyasint,omitempty" json:"uptime,omitempty"`
OemBoot *bool `cbor:"262,keyasint,omitempty" json:"oemboot,omitempty"`
Expand All @@ -23,7 +25,7 @@ type Eat struct {
Profile *Profile `cbor:"265,keyasint,omitempty" json:"eat-profile,omitempty"`
Submods *Submods `cbor:"266,keyasint,omitempty" json:"submods,omitempty"`
BootCount *uint `cbor:"267,keyasint,omitempty" json:"bootcount,omitempty"`
BootSeed *[]byte `cbor:"268,keyasint,omitempty" json:"bootseed,omitempty"`
BootSeed *B64Url `cbor:"268,keyasint,omitempty" json:"bootseed,omitempty"`
// TODO: DLOAs
SoftwareName *StringOrURI `cbor:"270,keyasint,omitempty" json:"swname,omitempty"`
SoftwareVersion *Version `cbor:"271,keyasint,omitempty" json:"swversion,omitempty"`
Expand Down Expand Up @@ -57,3 +59,37 @@ func (e *Eat) FromJSON(data []byte) error {
func (e Eat) ToJSON() ([]byte, error) {
return json.Marshal(e)
}

// B64Url is base64url (§5 of RFC4648) without padding.
// bstr MUST be base64url encoded as per EAT §7.2.2 "JSON Interoperability".
type B64Url []byte

func (o B64Url) MarshalJSON() ([]byte, error) {
return json.Marshal(
base64.RawURLEncoding.EncodeToString(o),
)
}

func (b *B64Url) UnmarshalJSON(data []byte) error {
// get string body
var encoded string
if err := json.Unmarshal(data, &encoded); err != nil {
return err
}

// while base64.RawURLEncoding.DecodeString("") returns
// no err, we need to return err because the CDDL definition is here,
// base64-url-text = tstr .regexp "[A-Za-z0-9_-]+"
if encoded == "" {
return fmt.Errorf("base64url must be a non-empty string")
}

// decode base64url-encoded string
decoded, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
return fmt.Errorf("base64url decode error: %w", err)
}

*b = decoded
return nil
}
2 changes: 1 addition & 1 deletion eat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestEat_Full_RoundtripJSON(t *testing.T) {
"lat": 12.34,
"long": 56.78
},
"ueid": "Ad6tvu/erb7v3q2+796tvu8=",
"ueid": "Ad6tvu_erb7v3q2-796tvu8",
"uptime": 60,
"iss": "Acme Inc.",
"sub": "rr-trap",
Expand Down
83 changes: 77 additions & 6 deletions measured_component.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,90 @@
package eat

import (
"github.com/veraison/swid"
"encoding/json"
"fmt"
)

type MeasuredComponent struct {
Id ComponentID `cbor:"1,keyasint" json:"id"`
Measurement *swid.HashEntry `cbor:"2,keyasint,omitempty" json:"measurement,omitempty"`
Signers *[][]byte `cbor:"3,keyasint,omitempty" json:"signers,omitempty"`
Flags *[]byte `cbor:"4,keyasint,omitempty" json:"flags,omitempty"`
RawMeasurement *[]byte `cbor:"5,keyasint,omitempty" json:"raw-measurement,omitempty"`
Id ComponentID `cbor:"1,keyasint" json:"id"`
Measurement *Digest `cbor:"2,keyasint,omitempty" json:"measurement,omitempty"`
Signers *[]B64Url `cbor:"3,keyasint,omitempty" json:"signers,omitempty"`
Flags *B64Url `cbor:"4,keyasint,omitempty" json:"flags,omitempty"`
RawMeasurement *B64Url `cbor:"5,keyasint,omitempty" json:"raw-measurement,omitempty"`
}

type ComponentID struct {
_ struct{} `cbor:",toarray"`
Name string `cbor:"0,keyasint"`
Version *Version `cbor:"1,keyasint,omitempty"`
}

func (c *ComponentID) UnmarshalJSON(data []byte) error {
var tmp []json.RawMessage
if err := json.Unmarshal(data, &tmp); err != nil {
return fmt.Errorf("expected JSON array: %w", err)
}

if len(tmp) < 1 || 2 < len(tmp) {
return fmt.Errorf("not component-id value: %#v", tmp)
}
if err := json.Unmarshal(tmp[0], &c.Name); err != nil {
return fmt.Errorf("invalid name: %w", err)
}

if len(tmp) == 2 {
if err := json.Unmarshal(tmp[1], &c.Version); err != nil {
return fmt.Errorf("invalid version: %w", err)
}
}

return nil
}

func (c ComponentID) MarshalJSON() ([]byte, error) {
if c.Version == nil {
return json.Marshal([]string{c.Name})
}
return json.Marshal([2]interface{}{c.Name, c.Version})
}

// Based on https://github.com/veraison/swid
// Digest does not support stringify() generating "sha-256;ABC..."
// and encodes/decodes the digest value with base64-url for JSON.
type Digest struct {
_ struct{} `cbor:",toarray"`

// The number used as a value for hash-alg-id is an integer-based
// hash algorithm identifier who's value MUST refer to an ID in the
// IANA "Named Information Hash Algorithm Registry" [IANA.named-information]
// with a Status of "current" (at the time the generator software was built
// or later); other hash algorithms MUST NOT be used. If the hash-alg-id is
// not known, then the integer value "0" MUST be used. This allows for
// conversion from ISO SWID tags [SWID], which do not allow an algorithm to
// be identified for this field.
Alg int `cbor:"0,keyasint"`

// The digest value will be base64-url encoded for JSON.
Val B64Url `cbor:"1,keyasint"`
}

func (d *Digest) UnmarshalJSON(data []byte) error {
var tmp [2]json.RawMessage
if err := json.Unmarshal(data, &tmp); err != nil {
return fmt.Errorf("expected JSON array of length 2: %w", err)
}

if err := json.Unmarshal(tmp[0], &d.Alg); err != nil {
return fmt.Errorf("invalid alg-id: %w", err)
}

if err := json.Unmarshal(tmp[1], &d.Val); err != nil {
return fmt.Errorf("invalid base64url hash value: %w", err)
}

return nil
}

func (d Digest) MarshalJSON() ([]byte, error) {
return json.Marshal([2]interface{}{d.Alg, d.Val})
}
74 changes: 70 additions & 4 deletions measured_component_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,70 @@
package eat

import (
"encoding/json"
"testing"

cbor "github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
)

func TestMeasuredComponent(t *testing.T) {
var (
expectedHashValue = B64Url{
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
}
)

func TestDigest_MarshalJSON_OK(t *testing.T) {
data := []byte(`[1,"3q2-796tvu_erb7v3q2-796tvu_erb7v3q2-796tvu8"]`)

var digest Digest
assert.Nil(t, json.Unmarshal(data, &digest))
assert.Equal(t, 1, digest.Alg)
assert.Equal(t, expectedHashValue, digest.Val)

encoded, err := json.Marshal(digest)
assert.Nil(t, err)
assert.Equal(t, data, encoded)
}

func TestDigest_MarshalCBOR_OK(t *testing.T) {
data := []byte{
0x82, // array(2)
0x01, // unsigned(1)
0x58, 0x20, // bytes(32)
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
}

var digest Digest
assert.Nil(t, dm.Unmarshal(data, &digest))
assert.Equal(t, 1, digest.Alg)
assert.Equal(t, expectedHashValue, digest.Val)

encoded, err := cbor.Marshal(digest)
assert.Nil(t, err)
assert.Equal(t, data, encoded)
}

func TestMeasuredComponent_MarshalJSON_OK(t *testing.T) {
data := []byte(`{"id":["Foo",["1.3.4","multipartnumeric"]],"measurement":[1,"3q2-796tvu_erb7v3q2-796tvu_erb7v3q2-796tvu8"]}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One quick check: AFAIR, there is no special encoding for coswid.$version-scheme in the JSON serialisation. I.e., instead of ”multipartnumeric" it should be 1, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly.
RFC 9393 states that CoSWID does not define any specific encoding or decoding rules for JSON.
https://datatracker.ietf.org/doc/html/rfc9393#name-the-concise-swid-tag-map

The string "multipartnumeric" comes from the veraison/swid implementation specification:
https://github.com/veraison/swid/blob/main/versionscheme.go#L40-L56

Copy link
Contributor

@thomas-fossati thomas-fossati Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for double checking. We will need to fix it in veraison/swid then.

(EDIT: raised veraison/swid#47)


var mc MeasuredComponent
assert.Nil(t, json.Unmarshal(data, &mc))
assert.Equal(t, "Foo", mc.Id.Name)
assert.Equal(t, "1.3.4", mc.Id.Version.Version)
assert.Equal(t, 1, mc.Measurement.Alg)
assert.Equal(t, expectedHashValue, mc.Measurement.Val)

encoded, err := json.Marshal(mc)
assert.Nil(t, err)
assert.Equal(t, data, encoded)
}

func TestMeasuredComponent_MarshalCBOR_OK(t *testing.T) {
data := []byte{
0xA2, // map(2)
0x01, // unsigned(1)
Expand All @@ -21,11 +79,19 @@ func TestMeasuredComponent(t *testing.T) {
0x82, // array(2)
0x01, // unsigned(1)
0x58, 0x20, // bytes(32)
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
}

var mc MeasuredComponent
assert.Nil(t, dm.Unmarshal(data, &mc))
assert.Equal(t, mc.Id.Name, "Foo")
assert.Equal(t, mc.Id.Version.Version, "1.3.4")
assert.Equal(t, "Foo", mc.Id.Name)
assert.Equal(t, "1.3.4", mc.Id.Version.Version)
assert.Equal(t, 1, mc.Measurement.Alg)
assert.Equal(t, expectedHashValue, mc.Measurement.Val)

encoded, err := cbor.Marshal(mc)
assert.Nil(t, err)
assert.Equal(t, data, encoded)
}
2 changes: 1 addition & 1 deletion measurement.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ package eat
type Measurement struct {
_ struct{} `cbor:",toarray"` // TODO: implement Unmarshal.JSON
Type int // coap-content-format, see https://www.iana.org/assignments/core-parameters/core-parameters.xhtml
Format []byte // bstr wrapped untagged-coswid, measured-component, ...
Format B64Url // bstr wrapped untagged-coswid, measured-component, ...
}
2 changes: 1 addition & 1 deletion measurement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

var (
measurementType = 258
measurementFormat = []byte{
measurementFormat = B64Url{
0xa4, 0x00, 0x63, 0x66, 0x6f, 0x6f, 0x0c, 0x01, 0x01, 0x63, 0x62, 0x61,
0x72, 0x02, 0xa2, 0x18, 0x1f, 0x63, 0x62, 0x61, 0x7a, 0x18, 0x21, 0x82,
0x01, 0x02,
Expand Down
5 changes: 5 additions & 0 deletions nonce.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ func (ns *Nonce) UnmarshalCBOR(data []byte) error {
// MarshalJSON encodes the receiver Nonce as either a JSON string containing
// the base64 encoding of the binary nonce (if the array comprises only one
// element) or as an array of base64-encoded JSON strings.
//
// NOTE: While RFC 9711 (EAT) does not restrict the nonce format to base64-encoded
// JSON strings, VERAISON/eat imposes a narrower interpretation for
// JSON <-> CBOR conversion.
// See discussion: https://github.com/ietf-rats-wg/eat/pull/421
func (ns Nonce) MarshalJSON() ([]byte, error) {
if err := ns.Validate(); err != nil {
return nil, fmt.Errorf("JSON encoding failed: %w", err)
Expand Down
28 changes: 27 additions & 1 deletion ueid.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

package eat

import "fmt"
import (
"encoding/base64"
"encoding/json"
"fmt"
)

const (
UEIDTypeInvalid = iota
Expand Down Expand Up @@ -81,3 +85,25 @@ func validateIMEI(value []byte) error {
}
return nil
}

// MarshalJSON encodes the UEID as a base64url string
func (u UEID) MarshalJSON() ([]byte, error) {
return json.Marshal(
base64.RawURLEncoding.EncodeToString(u),
)
}

// UnmarshalJSON decodes the supplied base64url string to an UEID
func (u *UEID) UnmarshalJSON(data []byte) error {
var s string

if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("JSON decoding failed for UEID: %w", err)
}
value, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("%s", err.Error())
}
*u = value
return nil
}
Loading