Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add profile claim #22

Merged
merged 15 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions eat.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Eat struct {
SecureBoot *bool `cbor:"15,keyasint,omitempty" json:"secure-boot,omitempty"`
Debug *Debug `cbor:"16,keyasint,omitempty" json:"debug-disable,omitempty"`
Location *Location `cbor:"17,keyasint,omitempty" json:"location,omitempty"`
Profile *Profile `cbor:"18,keyasint,omitempty" json:"eat-profile,omitempty"`
Uptime *uint `cbor:"19,keyasint,omitempty" json:"uptime,omitempty"`
Submods *Submods `cbor:"20,keyasint,omitempty" json:"submods,omitempty"`

Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
255 changes: 255 additions & 0 deletions profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Copyright 2021 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package eat

import (
"encoding/asn1"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
)

const (
// MaxASN1OIDLen is the maximum OID length accepted by the implementation
MaxASN1OIDLen = 255
// MinNumOIDArcs represents the minimum required arcs for a valid OID
MinNumOIDArcs = 3
)

const (
// asn1AbsolueOIDType represents the Type number of Absolute OID ASN Encoding
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
asn1AbsolueOIDType = 0x06
// asn1LongLenMask is used to mask bit 8 of Length Indicator byte
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
asn1LongLenMask = 0x80
// asn1LenBytesMask is used to extract first 7 bits of Length Indicator byte
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
asn1LenBytesMask = 0x7F
)

// Profile is either an absolute URI (RFC3986) or an ASN.1 Object Identifier
type Profile struct {
val interface{}
}

// NewProfile instantiates a Profile object from the given input string
// The string can either be an absolute URI or an ASN.1 Object Identifier
// in dotted-decimal notation. Relative Object Identifiers (e.g., .1.1.29) are
// not accepted.
func NewProfile(urlOrOID string) (*Profile, error) {
p := Profile{}
if err := p.Set(urlOrOID); err != nil {
return nil, err
}
return &p, nil
}

// Set sets the internal value of the Profile object to the given urlOrOID string
func (s *Profile) Set(urlOrOID string) error {
return s.decodeProfileFromString(urlOrOID)
}

// Get returns the profile as string (URI or dotted-decimal OID)
func (s Profile) Get() (string, error) {
switch t := s.val.(type) {
case *url.URL:
return t.String(), nil
case asn1.ObjectIdentifier:
return t.String(), nil
default:
return "", fmt.Errorf("no valid EAT profile")
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
}
}

// IsURI checks whether a stored profile is a URI
func (s Profile) IsURI() bool {
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
_, ok := s.val.(*url.URL)
return ok
}

// IsOID checks whether a stored profile is an OID
func (s Profile) IsOID() bool {
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
_, ok := s.val.(asn1.ObjectIdentifier)
return ok
}

// constructASN1fromVal constructs a TLV ASN.1 byte array from an ASN.1 value
// supplied as an input. The supplied OID byte array must be upto
// MaxASN1OIDLen bytes.
func constructASN1fromVal(val []byte) ([]byte, error) {
const maxTLOffset = 3
var OID [MaxASN1OIDLen + maxTLOffset]byte
asn1OID := OID[:2]
asn1OID[0] = asn1AbsolueOIDType
if len(val) < 127 {
asn1OID[1] = byte(len(val))
} else if len(val) <= MaxASN1OIDLen {
// extra one byte is sufficient
asn1OID[1] = 1 // Set to 1 to indicate one more byte carries the length
asn1OID[1] |= asn1LongLenMask
asn1OID = append(asn1OID, byte(len(val)))
} else {
return nil, fmt.Errorf("OIDs greater than %d bytes are not accepted", MaxASN1OIDLen)
}
asn1OID = append(asn1OID, val...)
return asn1OID, nil
}

// decodeProfile decodes from a received CBOR data the profile
// as either a URL or a Object Identifier
func (s *Profile) decodeProfile(val interface{}) error {
switch t := val.(type) {
case string:
u, err := url.Parse(t)
if err != nil {
return fmt.Errorf("profile URL parsing failed: %w", err)
}
if !u.IsAbs() {
return fmt.Errorf("profile URL not in absolute form: %w", err)
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
}
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
s.val = u
case []byte:
var profileOID asn1.ObjectIdentifier
val, err := constructASN1fromVal(t)
if err != nil {
return fmt.Errorf("could not construct valid ASN.1 buffer from ASN.1 value: %w", err)
}
rest, err := asn1.Unmarshal(val, &profileOID)
if err != nil {
return fmt.Errorf("malformed profile OID")
}
if len(rest) > 0 {
return fmt.Errorf("ASN.1 Unmarshal returned with %d leftover bytes", len(rest))
}
if len(profileOID) < MinNumOIDArcs {
return fmt.Errorf("ASN.1 OID decoding failed: got %d arcs, expecting at least %d", len(profileOID), MinNumOIDArcs)
}
s.val = profileOID
default:
return fmt.Errorf("decoding failed: unexpected type for profile: %T", t)
}
return nil
}

// extractASNValue extracts the value component from the supplied ASN.1 OID
func extractASNValue(asn1OID []byte) ([]byte, error) {
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
if asn1OID[0] != asn1AbsolueOIDType {
return nil, fmt.Errorf("the supplied value is not an ASN.1 OID")
}
// offset to default TL bytes
byteOffset := 2
if asn1OID[1]&asn1LongLenMask != 0 {
byteOffset += int(asn1OID[1] & asn1LenBytesMask)
}
return asn1OID[byteOffset:], nil
}

// MarshalCBOR encodes the Profile object as a CBOR text string (if it is a URL),
// or as CBOR byte string (if it is an ASN.1 OID)
func (s Profile) MarshalCBOR() ([]byte, error) {
switch t := s.val.(type) {
case *url.URL:
return em.Marshal(t.String())

thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
case asn1.ObjectIdentifier:
var asn1OID []byte
asn1OID, err := asn1.Marshal(t)
if err != nil {
return nil, fmt.Errorf("ASN.1 encoding failed for OID: %w", err)
}
asn1OIDval, err := extractASNValue(asn1OID)
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("ASN.1 value extraction failed for OID: %w", err)
}
return em.Marshal(asn1OIDval)
default:
return nil, fmt.Errorf("invalid type for EAT profile")
}
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
}

// UnmarshalCBOR attempts to initialize the Profile from the presented
// CBOR data. The data must be a CBOR text string, representing a URL
// or a CBOR byte string representing an Object Identifier
func (s *Profile) UnmarshalCBOR(data []byte) error {
var val interface{}
if len(data) == 0 {
return fmt.Errorf("decoding of CBOR data failed: zero length data buffer")
}
if err := dm.Unmarshal(data, &val); err != nil {
return fmt.Errorf("CBOR decoding of profile failed: %w", err)
}
if err := s.decodeProfile(val); err != nil {
return fmt.Errorf("invalid profile data: %w", err)
}
return nil
}

func decodeOIDfromString(val string) (asn1.ObjectIdentifier, error) {
// Attempt to decode OID from received string
var oid asn1.ObjectIdentifier
if val == "" {
return nil, fmt.Errorf("no valid OID")
}

for _, s := range strings.Split(val, ".") {
n, err := strconv.Atoi(s)
if err != nil {
return nil, fmt.Errorf("failed to extract OID from string: %w", err)
}
oid = append(oid, n)
}
if len(oid) < MinNumOIDArcs {
return nil, fmt.Errorf("invalid OID: got %d arcs, expecting at least %d", len(oid), MinNumOIDArcs)
}
return oid, nil
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
}

// decodeProfileFromString attempts to decode the supplied string as a URL,
// if that fails, it then attempts to decode it as an OID.
func (s *Profile) decodeProfileFromString(val string) error {
// First attempt to decode profile as a URL
u, err := url.Parse(val)
if err != nil || !u.IsAbs() {
val, err := decodeOIDfromString(val)
if err != nil {
return fmt.Errorf("profile string must be an absolute URL or an ASN.1 OID: %w", err)
}
s.val = val
thomas-fossati marked this conversation as resolved.
Show resolved Hide resolved
} else {
s.val = u
}
return nil
}

// decodeProfileJSON attempts at extracting an absolute URI or ASN.1 OID
// from the supplied JSON string
func (s *Profile) decodeProfileJSON(val string) error {
return s.decodeProfileFromString(val)
}

// MarshalJSON encodes the receiver Profile into a JSON string
func (s Profile) MarshalJSON() ([]byte, error) {
switch t := s.val.(type) {
case *url.URL:
return json.Marshal(t.String())
case asn1.ObjectIdentifier:
return json.Marshal(t.String())
default:
return nil, fmt.Errorf("invalid profile type: %T", t)
}
}

// UnmarshalJSON attempts at decoding the supplied JSON data into the receiver
// Profile
func (s *Profile) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return err
}

if err := s.decodeProfileJSON(v); err != nil {
return fmt.Errorf("encoding of profile failed: %w", err)
}
return nil
}
Loading