Skip to content

Commit 5bdb172

Browse files
committed
feat(profiles/psa): extract and consolidate PSA logic into profile package
Signed-off-by: Abhishek kumar <[email protected]>
1 parent 815040b commit 5bdb172

16 files changed

+1712
-61
lines changed

comid/bytes.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,3 @@ func (o TaggedBytes) Bytes() []byte {
4848

4949
return o
5050
}
51-
52-
// ValidatePSASignerID checks that TaggedBytes conforms to PSA Signer ID requirements.
53-
// Signer IDs must be 32, 48, or 64 bytes (SHA-256, SHA-384, or SHA-512).
54-
func (o TaggedBytes) ValidatePSASignerID() error {
55-
switch len(o) {
56-
case 32, 48, 64:
57-
return nil
58-
default:
59-
return fmt.Errorf("PSA signer ID must be 32, 48, or 64 bytes (got %d)", len(o))
60-
}
61-
}

comid/bytes_test.go

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2026 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package psa
5+
6+
import (
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"github.com/veraison/corim/comid"
15+
"github.com/veraison/corim/corim"
16+
"github.com/veraison/eat"
17+
)
18+
19+
// getTestcasePath returns the absolute path to a testcase file
20+
func getTestcasePath(filename string) string {
21+
_, thisFile, _, ok := runtime.Caller(0)
22+
if !ok {
23+
panic("failed to get current file path")
24+
}
25+
testcasesDir := filepath.Join(filepath.Dir(thisFile), "testcases")
26+
return filepath.Join(testcasesDir, filename)
27+
}
28+
29+
// loadTestcase loads a CBOR testcase file
30+
func loadTestcase(t *testing.T, filename string) []byte {
31+
path := getTestcasePath(filename)
32+
data, err := os.ReadFile(path)
33+
require.NoError(t, err, "failed to load testcase %s", filename)
34+
return data
35+
}
36+
37+
// getComidFromCorim extracts and decodes the first CoMID from a CoRIM
38+
// using the PSA profile extensions
39+
func getComidFromCorim(t *testing.T, corimData []byte) *comid.Comid {
40+
// First, parse the CoRIM to extract the tags
41+
var c corim.UnsignedCorim
42+
err := c.FromCBOR(corimData)
43+
require.NoError(t, err, "failed to parse CoRIM")
44+
require.Greater(t, len(c.Tags), 0, "CoRIM must have at least one tag")
45+
46+
// Get a CoMID with PSA profile extensions registered
47+
profileID, err := eat.NewProfile(ProfileURI)
48+
require.NoError(t, err)
49+
50+
manifest, found := corim.GetProfileManifest(profileID)
51+
require.True(t, found, "PSA profile should be registered")
52+
53+
// Create a Comid with extensions
54+
m := manifest.GetComid()
55+
56+
// Decode the first tag (which should be a CoMID, tag 506)
57+
require.Equal(t, uint64(506), c.Tags[0].Number, "first tag should be a CoMID (506)")
58+
err = m.FromCBOR(c.Tags[0].Content)
59+
require.NoError(t, err, "failed to decode CoMID from tag")
60+
61+
return m
62+
}
63+
64+
// TestCoRIMIntegration_ValidPSA tests that a valid PSA CoRIM:
65+
// - Parses successfully
66+
// - The PSA profile is recognized and loaded
67+
// - CoMID validation passes with PSA constraints
68+
func TestCoRIMIntegration_ValidPSA(t *testing.T) {
69+
data := loadTestcase(t, "psa-valid.cbor")
70+
71+
// Extract and decode CoMID with PSA extensions
72+
m := getComidFromCorim(t, data)
73+
74+
// Validate the CoMID (should pass all PSA constraints)
75+
err := m.Valid()
76+
assert.NoError(t, err, "valid PSA CoMID should pass validation")
77+
}
78+
79+
// TestCoRIMIntegration_InvalidImplementationID tests that a CoRIM with invalid
80+
// Implementation ID (wrong length) is rejected by PSA profile validation.
81+
// The CoRIM structure itself is valid, but fails PSA profile constraints.
82+
func TestCoRIMIntegration_InvalidImplementationID(t *testing.T) {
83+
data := loadTestcase(t, "psa-invalid-impl-id.cbor")
84+
85+
// Extract and decode CoMID with PSA extensions
86+
m := getComidFromCorim(t, data)
87+
88+
// Validation should FAIL due to invalid Implementation ID
89+
err := m.Valid()
90+
require.Error(t, err, "PSA validation should fail for invalid implementation-id")
91+
assert.Contains(t, err.Error(), "implementation-id",
92+
"error should mention implementation-id")
93+
assert.Contains(t, err.Error(), "32 bytes",
94+
"error should mention expected length")
95+
}
96+
97+
// TestCoRIMIntegration_InvalidAttestVerifKey tests that a CoRIM with invalid
98+
// AttestVerifKeys (multiple keys instead of one) is rejected by PSA profile validation.
99+
// The CoRIM structure itself is valid, but fails PSA profile constraints.
100+
func TestCoRIMIntegration_InvalidAttestVerifKey(t *testing.T) {
101+
data := loadTestcase(t, "psa-invalid-attest-key.cbor")
102+
103+
// Extract and decode CoMID with PSA extensions
104+
m := getComidFromCorim(t, data)
105+
106+
// Validation should FAIL due to multiple attestation keys
107+
err := m.Valid()
108+
require.Error(t, err, "PSA validation should fail for multiple attest keys")
109+
assert.Contains(t, err.Error(), "verification-keys",
110+
"error should mention verification-keys")
111+
assert.Contains(t, err.Error(), "exactly one",
112+
"error should mention exactly one key required")
113+
}
114+
115+
// TestCoRIMIntegration_NoProfile tests that a CoRIM without a profile:
116+
// - Parses successfully
117+
// - Validation passes without PSA extensions (base validation only)
118+
// This serves as a control case - the same CoMID that fails with PSA profile
119+
// should pass without it.
120+
func TestCoRIMIntegration_NoProfile(t *testing.T) {
121+
data := loadTestcase(t, "no-profile.cbor")
122+
123+
// Parse the CoRIM
124+
var c corim.UnsignedCorim
125+
err := c.FromCBOR(data)
126+
require.NoError(t, err, "profile-less CoRIM should parse without error")
127+
128+
// Verify no profile is set
129+
assert.Nil(t, c.Profile, "profile should not be present")
130+
131+
// Decode CoMID WITHOUT PSA extensions (plain CoMID)
132+
require.Greater(t, len(c.Tags), 0, "CoRIM must have at least one tag")
133+
m := comid.NewComid()
134+
err = m.FromCBOR(c.Tags[0].Content)
135+
require.NoError(t, err, "failed to decode CoMID")
136+
137+
// Validation should pass (no PSA profile constraints applied)
138+
err = m.Valid()
139+
assert.NoError(t, err, "profile-less CoMID should pass base validation")
140+
}

profiles/psa/implid.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2021-2024 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package psa
5+
6+
import (
7+
"encoding/base64"
8+
"encoding/json"
9+
"fmt"
10+
11+
"github.com/veraison/corim/comid"
12+
)
13+
14+
const ImplIDType = "psa.impl-id"
15+
16+
// TaggedImplID is the PSA Implementation ID type (32 bytes) as a ClassID value.
17+
// It implements IClassIDValue interface with Type() returning "psa.impl-id".
18+
// See Section 3.2.2 of draft-tschofenig-rats-psa-token
19+
type TaggedImplID [32]byte
20+
21+
// String returns the base64-encoded string representation of the ImplID
22+
func (o TaggedImplID) String() string {
23+
return base64.StdEncoding.EncodeToString(o[:])
24+
}
25+
26+
// Valid validates the ImplID (always returns nil as any 32-byte value is valid)
27+
func (o TaggedImplID) Valid() error {
28+
return nil
29+
}
30+
31+
// Type returns the type identifier for PSA Implementation ID
32+
func (o TaggedImplID) Type() string {
33+
return ImplIDType
34+
}
35+
36+
// Bytes returns the raw bytes of the ImplID
37+
func (o TaggedImplID) Bytes() []byte {
38+
return o[:]
39+
}
40+
41+
// MarshalJSON serializes the TaggedImplID to JSON
42+
func (o TaggedImplID) MarshalJSON() ([]byte, error) {
43+
return json.Marshal(o[:])
44+
}
45+
46+
// UnmarshalJSON deserializes JSON into TaggedImplID
47+
func (o *TaggedImplID) UnmarshalJSON(data []byte) error {
48+
var b []byte
49+
if err := json.Unmarshal(data, &b); err != nil {
50+
return err
51+
}
52+
if len(b) != 32 {
53+
return fmt.Errorf("bad psa.impl-id: got %d bytes, want 32", len(b))
54+
}
55+
copy(o[:], b)
56+
return nil
57+
}
58+
59+
// ImplID is an alias for backward compatibility
60+
type ImplID = TaggedImplID
61+
62+
// NewImplIDClassID creates a new ClassID of type psa.impl-id
63+
func NewImplIDClassID(val any) (*comid.ClassID, error) {
64+
var implID TaggedImplID
65+
66+
if val == nil {
67+
return &comid.ClassID{
68+
Value: &implID,
69+
}, nil
70+
}
71+
72+
switch t := val.(type) {
73+
case []byte:
74+
if nb := len(t); nb != 32 {
75+
return nil, fmt.Errorf("bad psa.impl-id: got %d bytes, want 32", nb)
76+
}
77+
78+
copy(implID[:], t)
79+
case string:
80+
v, err := base64.StdEncoding.DecodeString(t)
81+
if err != nil {
82+
return nil, fmt.Errorf("bad psa.impl-id: %w", err)
83+
}
84+
85+
if nb := len(v); nb != 32 {
86+
return nil, fmt.Errorf("bad psa.impl-id: decoded %d bytes, want 32", nb)
87+
}
88+
89+
copy(implID[:], v)
90+
case TaggedImplID:
91+
copy(implID[:], t[:])
92+
case *TaggedImplID:
93+
copy(implID[:], (*t)[:])
94+
default:
95+
return nil, fmt.Errorf("unexpected type for psa.impl-id: %T", t)
96+
}
97+
98+
return &comid.ClassID{
99+
Value: &implID,
100+
}, nil
101+
}
102+
103+
// MustNewImplIDClassID is like NewImplIDClassID except it panics on error
104+
func MustNewImplIDClassID(val any) *comid.ClassID {
105+
ret, err := NewImplIDClassID(val)
106+
if err != nil {
107+
panic(err)
108+
}
109+
110+
return ret
111+
}
112+
113+
// NewClassImplID instantiates a new Class object with the specified PSA Implementation ID
114+
// This is a convenience function for use in PSA profiles only
115+
func NewClassImplID(implID TaggedImplID) *comid.Class {
116+
classID, err := NewImplIDClassID(implID)
117+
if err != nil {
118+
return nil
119+
}
120+
121+
return &comid.Class{ClassID: classID}
122+
}

0 commit comments

Comments
 (0)