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
88 changes: 88 additions & 0 deletions internal/adapter/anchor/lei/attestation_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// attestation_source.go separates the entity-verification responsibility
// (handled by GLEIFClient) from the attestation-key responsibility
// (the entity's published verification key the agent uses to sign
// registration events).
//
// The GLEIF Public API does not carry an attestation key in the
// Level 1 record. Two paths populate the key:
//
// - vLEI Option A: a self-attestation issued through GLEIF's vLEI
// infrastructure, fetched via that infrastructure's API.
// - vLEI Option B: a custom field at the entity's Local Operating
// Unit (LOU), retrieved through the LOU's API.
//
// Both paths are deployment choices, not protocol-level concerns.
// The Resolver composes a GLEIFClient (entity verification) with an
// AttestationJWKSource (key retrieval); the two surfaces stay
// independent so a deployment can use the basic GLEIF Level 1 API
// for verification and a separate pluggable source for the key.
package lei

import (
"context"
"sync"
)

// AttestationJWKSource returns the entity's published verification
// key as a JWK byte sequence (RFC 7517).
//
// Implementations MUST be safe for concurrent use. The Resolver may
// call Lookup concurrently from multiple verification cycles.
type AttestationJWKSource interface {
// Lookup returns the JWK bytes for the given (canonical, uppercase)
// LEI. Returns nil with no error when the source has no key for
// the LEI; the caller decides whether absence is a failure.
Lookup(ctx context.Context, lei string) ([]byte, error)
}

// StaticAttestationSource is an in-memory map from LEI to JWK bytes.
// Useful for testbeds, integration tests, and deployments that
// preconfigure entity keys out-of-band (e.g., from a sealed config
// file or a secret store).
//
// A vLEI-aware source replaces this in production deployments that
// resolve keys through GLEIF's vLEI infrastructure.
type StaticAttestationSource struct {
mu sync.RWMutex
keys map[string][]byte
}

// NewStaticAttestationSource returns an empty static source.
// Populate it with Set before passing it to Resolver.WithAttestationSource.
func NewStaticAttestationSource() *StaticAttestationSource {
return &StaticAttestationSource{
keys: make(map[string][]byte),
}
}

// Set registers a JWK for the given LEI. The LEI is canonicalized
// to uppercase before storage so callers may pass either form.
func (s *StaticAttestationSource) Set(lei string, jwk []byte) {
canonical, err := Canonicalize(lei)
if err != nil {
// Stored as the input form; Lookup will not match. Caller
// bug surfaces at lookup time rather than here. Keeping Set
// non-erroring lets callers wire up a source from config
// without a separate validation step.
canonical = lei
}
s.mu.Lock()
defer s.mu.Unlock()
cp := make([]byte, len(jwk))
copy(cp, jwk)
s.keys[canonical] = cp
}

// Lookup implements AttestationJWKSource. Returns a defensive copy
// so callers cannot mutate the stored bytes.
func (s *StaticAttestationSource) Lookup(_ context.Context, lei string) ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
stored, ok := s.keys[lei]
if !ok {
return nil, nil
}
cp := make([]byte, len(stored))
copy(cp, stored)
return cp, nil
}
164 changes: 164 additions & 0 deletions internal/adapter/anchor/lei/attestation_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package lei

import (
"context"
"testing"
)

func TestStaticAttestationSource_SetAndLookup(t *testing.T) {
s := NewStaticAttestationSource()
jwk := []byte(`{"kty":"OKP","crv":"Ed25519","x":"abc"}`)
s.Set(validLEI, jwk)

got, err := s.Lookup(context.Background(), validLEI)
if err != nil {
t.Fatalf("Lookup: %v", err)
}
if string(got) != string(jwk) {
t.Errorf("Lookup: got %q, want %q", got, jwk)
}
}

func TestStaticAttestationSource_LookupMissing(t *testing.T) {
s := NewStaticAttestationSource()
got, err := s.Lookup(context.Background(), validLEI)
if err != nil {
t.Fatalf("Lookup on empty: %v", err)
}
if got != nil {
t.Errorf("Lookup on empty source should return nil, got %q", got)
}
}

func TestStaticAttestationSource_SetCanonicalizes(t *testing.T) {
s := NewStaticAttestationSource()
jwk := []byte(`{"kty":"OKP","crv":"Ed25519","x":"abc"}`)
// Set with lowercase form; Lookup with canonical form should hit.
s.Set("529900t8bm49aursdo55", jwk)

got, err := s.Lookup(context.Background(), validLEI)
if err != nil {
t.Fatalf("Lookup: %v", err)
}
if string(got) != string(jwk) {
t.Errorf("canonicalization failed: got %q", got)
}
}

func TestStaticAttestationSource_DefensiveCopyOnLookup(t *testing.T) {
s := NewStaticAttestationSource()
jwk := []byte(`{"kty":"OKP","crv":"Ed25519","x":"abc"}`)
s.Set(validLEI, jwk)

got, _ := s.Lookup(context.Background(), validLEI)
got[0] = 'X' // mutate the returned slice

// Lookup again — should return unmutated bytes.
again, _ := s.Lookup(context.Background(), validLEI)
if string(again) != string(jwk) {
t.Errorf("stored bytes mutated by caller: got %q", again)
}
}

func TestStaticAttestationSource_DefensiveCopyOnSet(t *testing.T) {
s := NewStaticAttestationSource()
jwk := []byte(`{"kty":"OKP","crv":"Ed25519","x":"abc"}`)
s.Set(validLEI, jwk)

// Mutate the original slice the caller passed to Set.
jwk[0] = 'X'

got, _ := s.Lookup(context.Background(), validLEI)
if got[0] == 'X' {
t.Error("Set should defensively copy; caller's mutation leaked into store")
}
}

func TestStaticAttestationSource_OverwriteSet(t *testing.T) {
s := NewStaticAttestationSource()
first := []byte(`{"kty":"OKP","crv":"Ed25519","x":"first"}`)
second := []byte(`{"kty":"OKP","crv":"Ed25519","x":"second"}`)
s.Set(validLEI, first)
s.Set(validLEI, second)

got, _ := s.Lookup(context.Background(), validLEI)
if string(got) != string(second) {
t.Errorf("overwrite failed: got %q, want %q", got, second)
}
}

// TestResolver_Resolve_AttestationFromSource exercises the new
// composition: GLEIF entity check + AttestationJWKSource for the
// JWK. The Level 1 record has no AttestationJWK; the source supplies
// it; the resolver returns a complete IdentityClaim.
func TestResolver_Resolve_AttestationFromSource(t *testing.T) {
jwk := []byte(`{"kty":"OKP","crv":"Ed25519","x":"abc"}`)

gleif := &fakeGLEIFClient{
record: &GLEIFRecord{
LEI: validLEI,
EntityName: "Test Entity",
EntityStatus: "ACTIVE",
Jurisdiction: "US-DE",
// AttestationJWK intentionally empty; comes from source.
},
}
source := NewStaticAttestationSource()
source.Set(validLEI, jwk)

r := New().WithClient(gleif).WithAttestationSource(source)
claim, err := r.Resolve(context.Background(), validLEI)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if string(claim.PublicKeyJWK) != string(jwk) {
t.Errorf("PublicKeyJWK: got %q, want %q", claim.PublicKeyJWK, jwk)
}
if claim.ResolvedID != validLEI {
t.Errorf("ResolvedID: got %q, want %q", claim.ResolvedID, validLEI)
}
}

// TestResolver_Resolve_AttestationFallsBackToRecord verifies that a
// vLEI-aware GLEIFClient that already populates AttestationJWK takes
// precedence; the source is the fallback path.
func TestResolver_Resolve_AttestationFallsBackToRecord(t *testing.T) {
fromRecord := []byte(`{"kty":"OKP","crv":"Ed25519","x":"from-record"}`)
fromSource := []byte(`{"kty":"OKP","crv":"Ed25519","x":"from-source"}`)

gleif := &fakeGLEIFClient{
record: &GLEIFRecord{
LEI: validLEI,
EntityStatus: "ACTIVE",
AttestationJWK: fromRecord,
},
}
source := NewStaticAttestationSource()
source.Set(validLEI, fromSource)

r := New().WithClient(gleif).WithAttestationSource(source)
claim, err := r.Resolve(context.Background(), validLEI)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if string(claim.PublicKeyJWK) != string(fromRecord) {
t.Errorf("expected record's AttestationJWK to win, got %q", claim.PublicKeyJWK)
}
}

// TestResolver_Resolve_NoSourceNoKey verifies the existing
// LEI_NO_ATTESTATION_KEY error path: no source, no key on the
// record, error.
func TestResolver_Resolve_NoSourceNoKey(t *testing.T) {
gleif := &fakeGLEIFClient{
record: &GLEIFRecord{
LEI: validLEI,
EntityStatus: "ACTIVE",
},
}
r := New().WithClient(gleif)
_, err := r.Resolve(context.Background(), validLEI)
if err == nil {
t.Fatal("expected LEI_NO_ATTESTATION_KEY error")
}
}
Loading