Skip to content

Commit

Permalink
doc: add documentation
Browse files Browse the repository at this point in the history
Add missing API documentation, and document profiles
implementation/extension.

Signed-off-by: Sergei Trofimov <[email protected]>
  • Loading branch information
setrofim committed Jul 18, 2024
1 parent 46f5c06 commit 89fde61
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 3 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,13 @@ The package exposes the following functionalities:
# Make targets

* `make test` (or just `make`) to run the unit tests;

# Implementing new profiles

It is possible to support PSA-derived profiles other than profiles 1 and 2
implemented here. To do this you need to provide an implementation of `IClaims`
and an implementation of `IProfile` that associates your `IClaims`
implementation with an `eat.Profile` value, and then register the `IProfile`
implementation using `RegisterProfile()`. The simplest way to implement
`IClaims` is to embed one of the existing implementations. Please refer to [the
example](example_extensions_test.go) for details.
24 changes: 24 additions & 0 deletions claims_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (
SecurityLifecycleDecommissionedMax = 0x60ff
)

// LifeCycleState enumerates the possible states of the PSA security life
// cycle. Each state is represented by a range of values, so different security
// lifecycle claim values can map onto the same state.
type LifeCycleState uint16

const (
Expand Down Expand Up @@ -70,6 +73,8 @@ func (o LifeCycleState) String() string {
}
}

// LifeCycleToState translates PSA life cycle claim value onto corresponding
// life cycle state.
func LifeCycleToState(v uint16) LifeCycleState {
if v >= SecurityLifecycleUnknownMin &&
v <= SecurityLifecycleUnknownMax {
Expand Down Expand Up @@ -109,6 +114,8 @@ func LifeCycleToState(v uint16) LifeCycleState {
return StateInvalid
}

// ValidateSecurityLifeCycle returns an error if the provided value does not
// correspond to a valid PSA security life cycle state.
func ValidateSecurityLifeCycle(v uint16) error {
if !LifeCycleToState(v).IsValid() {
return fmt.Errorf("%w: value %d is invalid", ErrWrongSyntax, v)
Expand All @@ -122,6 +129,8 @@ var (
CertificationReferenceP2RE = regexp.MustCompile(`^\d{13}-\d{5}$`)
)

// ValidateImplID returns an error if the provided value is not a valid PSA
// implementation ID.
func ValidateImplID(v []byte) error {
l := len(v)

Expand All @@ -135,6 +144,9 @@ func ValidateImplID(v []byte) error {
return nil
}

// ValidatePSAHashType returns an error if the provided value is not a valid
// PSA hash. A valid PSA hash must be a sequence of exactly 32, 48, or 64
// bytes.
func ValidatePSAHashType(b []byte) error {
l := len(b)

Expand All @@ -148,6 +160,8 @@ func ValidatePSAHashType(b []byte) error {
return nil
}

// ValidateInstID returns an error if the provided value is not a valid PSA
// instance ID.
func ValidateInstID(v []byte) error {
l := len(v)

Expand All @@ -168,6 +182,8 @@ func ValidateInstID(v []byte) error {
return nil
}

// ValidateVSI returns an error if the provided value is not a valid
// verification service indicator.
func ValidateVSI(v string) error {
// https://github.com/thomas-fossati/draft-psa-token/issues/59
if v == "" {
Expand All @@ -177,6 +193,10 @@ func ValidateVSI(v string) error {
return nil
}

// ValidateHashAlgID returns an error if the provided value is not a IANA Hash
// Function Textual Name for one of the algorithms supported by PSA. Must be
// one of: "md2", "md5", "sha-1", "sha-224", "sha-256", "sha-384", "sha-512",
// "shake128", or "shake256".
func ValidateHashAlgID(v string) error {
if v == "" {
return fmt.Errorf("%w: empty string", ErrWrongSyntax)
Expand All @@ -190,6 +210,8 @@ func ValidateHashAlgID(v string) error {
return fmt.Errorf("%w: wrong syntax", ErrWrongSyntax)
}

// ValidateSwComponents return an error if one or more of the provided
// []ISwComponent is invalid, or if the slice is empty.
func ValidateSwComponents(scs []ISwComponent) error {
if len(scs) == 0 {
return fmt.Errorf("%w: there MUST be at least one entry", ErrWrongSyntax)
Expand All @@ -204,6 +226,8 @@ func ValidateSwComponents(scs []ISwComponent) error {
return nil
}

// ValidateNonce returns an error if the provided value is not a valid PSA
// nonce.
func ValidateNonce(v []byte) error {
return ValidatePSAHashType(v)
}
9 changes: 8 additions & 1 deletion claims_p1.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

const Profile1Name = "PSA_IOT_PROFILE_1"

// Profile1 provides the IClaims implementation associated with PSA_IOT_PROFILE_1.
type Profile1 struct{}

func (o Profile1) GetName() string {
Expand All @@ -21,7 +22,8 @@ func (o Profile1) GetClaims() IClaims {
return newP1Claims(true)
}

// P1Claims are associated with profile "PSA_IOT_PROFILE_1"
// P1Claims defines claims associated with profile "PSA_IOT_PROFILE_1".
// See https://arm-software.github.io/psa-api/attestation/1.0/overview/report.html
type P1Claims struct {
Profile *string `cbor:"-75000,keyasint,omitempty" json:"psa-profile"`
ClientID *int32 `cbor:"-75001,keyasint" json:"psa-client-id"`
Expand All @@ -35,6 +37,11 @@ type P1Claims struct {
InstID *[]byte `cbor:"-75009,keyasint" json:"psa-instance-id"`
VSI *string `cbor:"-75010,keyasint,omitempty" json:"psa-verification-service-indicator,omitempty"`

// CanonicalProfile contains the "correct" profile name associated with
// this IClaims implementation (e.g. "PSA_IOT_PROFILE_1" for P1Claims).
// The reason this is a field rather than a global constant is so that
// derived profiles can embed this struct and rely on its existing
// validation methods.
CanonicalProfile string `cbor:"-" json:"-"`
}

Expand Down
7 changes: 7 additions & 0 deletions claims_p2.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

const Profile2Name = "http://arm.com/psa/2.0.0"

// Profile2 provides the IClaims implementation associated with http://arm.com/psa/2.0.0
type Profile2 struct{}

func (o Profile2) GetName() string {
Expand All @@ -24,6 +25,7 @@ func (o Profile2) GetClaims() IClaims {
}

// P2Claims are associated with profile "http://arm.com/psa/2.0.0"
// See https://datatracker.ietf.org/doc/html/draft-tschofenig-rats-psa-token-13
type P2Claims struct {
Profile *eat.Profile `cbor:"265,keyasint" json:"eat-profile"`
ClientID *int32 `cbor:"2394,keyasint" json:"psa-client-id"`
Expand All @@ -36,6 +38,11 @@ type P2Claims struct {
InstID *eat.UEID `cbor:"256,keyasint" json:"psa-instance-id"`
VSI *string `cbor:"2400,keyasint,omitempty" json:"psa-verification-service-indicator,omitempty"`

// CanonicalProfile contains the "correct" profile name associated with
// this IClaims implementation (e.g. "http://arm.com/psa/2.0.0" for
// P2Claims). The reason this is a field rather than a global constant
// is so that derived profiles can embed this struct and rely on its
// existing validation methods.
CanonicalProfile string `cbor:"-" json:"-"`
}

Expand Down
2 changes: 1 addition & 1 deletion iclaims.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func NewClaims(profile string) (IClaims, error) {
// ValidateClaims returns an error if validation fails for any of the standard
// PSA cliams defined by IClaims. This function may be used by new profiles
// whos implementations do not embed existing IClaims implementations and so
// cannot benefit from their existing Validate() methods.
// cannot rely on their existing Validate() methods.
func ValidateClaims(c IClaims) error {
if err := FilterError(c.GetProfile()); err != nil {
return fmt.Errorf("validating profile: %w", err)
Expand Down
6 changes: 6 additions & 0 deletions iswcomponent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package psatoken

import "fmt"

// ISwComponent defines the interface for the software component composite
// claim.
type ISwComponent interface {
Validate() error

Expand All @@ -21,6 +23,10 @@ type ISwComponent interface {
SetMeasurementDesc(v string) error
}

// ValidateSwComponent returns an error if validation fails for any of the
// fields of a software component claim. This function may be used by new
// ISwComponent implementations that do not embed existing SwComponent, and so
// cannot rely on its Validate() method.
func ValidateSwComponent(c ISwComponent) error {
if err := FilterError(c.GetMeasurementType()); err != nil {
return fmt.Errorf("measurement type: %w", err)
Expand Down
15 changes: 15 additions & 0 deletions iswcomponents.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@

package psatoken

// ISwComponents defines an interface for a container of software components
// (implementing ISwComponent) interface. The existing generic implementation
// of this interface, SwComponents[I ISwComponent], should suffice for the vast
// majority of cases, and implementers of ISwComponent, typically should be
// able to just use that, and not have to implement ISwComponents as well.
type ISwComponents interface {
// Validate returns an error if any of the contained ISwComponent's are
// invalid.
Validate() error
// Values returns a []ISwComponents of the contained values. An error
// may be returned if any of the contained valus are invalid.
Values() ([]ISwComponent, error)
// Add one or move ISwComponent's to the container, appending them to
// the existing contents. An error may be returned if any of the values
// being added are invalid.
Add(vals ...ISwComponent) error
// Replace the existing contents with the provided value. An error may
// be returned if any of the new values are invalid.
Replace(vals []ISwComponent) error
// IsEmpty returns true if the container does not contain any values.
IsEmpty() bool
}
35 changes: 35 additions & 0 deletions profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,21 @@ import (
"github.com/veraison/psatoken/encoding"
)

// IProfile defines a way of obtaining an implementation of IClaims that
// corresponds to a particular profiles of a PSA token. A profile can
// - define the CBOR and JSON keys used for the claims defined by IClaims
// - indicate which claims are optional and which are mandatory
// - define extra claims in addition to the ones defined by IClaims
// - disallow some of the claims defined by IClaims
// Essentially, a Profile fully defines a concrete implementation of IClaims.
// This library provides two "base" PSA profile implementations: Profile1 and
// Profile2, corresponding to PSA_IOT_PROFILE_1 and http://arm.com/psa/2.0.0
// respectively.
type IProfile interface {
// GetName returns the name of this profile.
GetName() string
// GetClaims returns a new instance of the IClaims implementation
// associated with this profile.
GetClaims() IClaims
}

Expand All @@ -29,6 +42,11 @@ func DecodeClaims(buf []byte) (IClaims, error) {
return DecodeClaimsFromCBOR(buf)
}

// DecodeClaimsFromCBOR returns an IClaims implementation instance populated
// from the provided CBOR buf. The implementation used is determined by value
// of the eat_profile (key 265) in the provided CBOR object. In the absence of
// this claim, Profile1 (PSA_IOT_PROFILE_1) is assumed. Claims are validated
// according to the profile as part of decoding.
func DecodeClaimsFromCBOR(buf []byte) (IClaims, error) {
claims, err := DecodeUnvalidatedClaimsFromCBOR(buf)
if err != nil {
Expand All @@ -47,6 +65,12 @@ func DecodeUnvalidatedClaims(buf []byte) (IClaims, error) {
return DecodeUnvalidatedClaimsFromCBOR(buf)
}

// DecodeUnvalidatedClaimsFromCBOR returns an IClaims implementation instance
// populated from the provided CBOR buf. The implementation used is determined
// by value of the eat_profile (key 265) in the provided CBOR object. In the
// absence of this key, Profile1 (PSA_IOT_PROFILE_1) is assumed. No validation
// is performed to confirm that the decoded claims actually conform to the
// stated profile.
func DecodeUnvalidatedClaimsFromCBOR(buf []byte) (IClaims, error) {
selector := struct {
// note: code point 265 is defined as the eat_profile claim in
Expand Down Expand Up @@ -93,6 +117,11 @@ func DecodeJSONClaims(buf []byte) (IClaims, error) {
return DecodeClaimsFromJSON(buf)
}

// DecodeClaimsFromJSON returns an IClaims implementation instance populated
// from the provided JSON buf. The implementation used is determined by value
// of the profile field in the provided JSON object. In the absence of
// this field, Profile1 (PSA_IOT_PROFILE_1) is assumed. Claims are validated
// according to the profile as part of decoding.
func DecodeClaimsFromJSON(buf []byte) (IClaims, error) {
claims, err := DecodeUnvalidatedClaimsFromJSON(buf)
if err != nil {
Expand All @@ -111,6 +140,12 @@ func DecodeUnvalidatedJSONClaims(buf []byte) (IClaims, error) {
return DecodeUnvalidatedClaimsFromJSON(buf)
}

// DecodeUnvalidatedClaimsFromJSON returns an IClaims implementation instance
// populated from the provided JSON buf. The implementation used is determined
// by value of the profile field in the provided JSON object. In the absence
// of this field, Profile1 (PSA_IOT_PROFILE_1) is assumed. No validation is
// performed to confirm that the decoded claims actually conform to the stated
// profile.
func DecodeUnvalidatedClaimsFromJSON(buf []byte) (IClaims, error) {
var decoded map[string]interface{}
if err := json.Unmarshal(buf, &decoded); err != nil {
Expand Down
5 changes: 4 additions & 1 deletion swcomponents.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"reflect"
)

// SwComponents is the generic implementation of ISwComponents interface that
// should suffice for most purposes. This provides a container of concrete
// types for marashaling purposes (we can't unmarshal a slice of interfaces,
// such as []ISwComponent, with specifying a concrete type to use).
type SwComponents[I ISwComponent] struct {
values []I // nolint:structcheck
}
Expand Down Expand Up @@ -98,4 +102,3 @@ func validateAndConvert[I ISwComponent](vals []ISwComponent) ([]I, error) {

return ret, nil
}

0 comments on commit 89fde61

Please sign in to comment.