mirror of
https://github.com/sonr-io/crypto.git
synced 2026-08-02 15:31:38 +00:00
No commit suggestions generated
This commit is contained in:
@@ -0,0 +1,984 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/sonr-io/sonr/crypto/keys"
|
||||
)
|
||||
|
||||
// Verifier provides UCAN token verification and validation functionality
|
||||
type Verifier struct {
|
||||
didResolver DIDResolver
|
||||
}
|
||||
|
||||
// DIDResolver resolves DID keys to public keys for signature verification
|
||||
type DIDResolver interface {
|
||||
ResolveDIDKey(ctx context.Context, did string) (keys.DID, error)
|
||||
}
|
||||
|
||||
// NewVerifier creates a new UCAN token verifier
|
||||
func NewVerifier(didResolver DIDResolver) *Verifier {
|
||||
return &Verifier{
|
||||
didResolver: didResolver,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyToken parses and verifies a UCAN JWT token
|
||||
func (v *Verifier) VerifyToken(ctx context.Context, tokenString string) (*Token, error) {
|
||||
if tokenString == "" {
|
||||
return nil, fmt.Errorf("token string cannot be empty")
|
||||
}
|
||||
|
||||
// Parse the JWT token
|
||||
token, err := jwt.Parse(tokenString, v.keyFunc(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT token: %w", err)
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims type")
|
||||
}
|
||||
|
||||
// Parse UCAN-specific fields
|
||||
ucanToken, err := v.parseUCANClaims(claims, tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UCAN claims: %w", err)
|
||||
}
|
||||
|
||||
// Validate token structure
|
||||
if err := v.validateToken(ctx, ucanToken); err != nil {
|
||||
return nil, fmt.Errorf("token validation failed: %w", err)
|
||||
}
|
||||
|
||||
return ucanToken, nil
|
||||
}
|
||||
|
||||
// VerifyCapability validates that a UCAN token grants specific capabilities
|
||||
func (v *Verifier) VerifyCapability(
|
||||
ctx context.Context,
|
||||
tokenString string,
|
||||
resource string,
|
||||
abilities []string,
|
||||
) (*Token, error) {
|
||||
token, err := v.VerifyToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Check if token grants required capabilities
|
||||
if err := v.checkCapabilities(token, resource, abilities); err != nil {
|
||||
return nil, fmt.Errorf("capability check failed: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// VerifyDelegationChain validates the complete delegation chain of a UCAN token
|
||||
func (v *Verifier) VerifyDelegationChain(ctx context.Context, tokenString string) error {
|
||||
token, err := v.VerifyToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify root token: %w", err)
|
||||
}
|
||||
|
||||
// Verify each proof in the delegation chain
|
||||
for i, proof := range token.Proofs {
|
||||
proofToken, err := v.VerifyToken(ctx, string(proof))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify proof[%d] in delegation chain: %w", i, err)
|
||||
}
|
||||
|
||||
// Validate delegation relationship
|
||||
if err := v.validateDelegation(token, proofToken); err != nil {
|
||||
return fmt.Errorf("invalid delegation at proof[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// keyFunc returns a function that resolves the signing key for JWT verification
|
||||
func (v *Verifier) keyFunc(ctx context.Context) jwt.Keyfunc {
|
||||
return func(token *jwt.Token) (any, error) {
|
||||
// Extract issuer from claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid claims type")
|
||||
}
|
||||
|
||||
issuer, ok := claims["iss"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid issuer claim")
|
||||
}
|
||||
|
||||
// Resolve the issuer's DID to get public key
|
||||
did, err := v.didResolver.ResolveDIDKey(ctx, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve issuer DID: %w", err)
|
||||
}
|
||||
|
||||
// Get verification key based on signing method
|
||||
switch token.Method {
|
||||
case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512:
|
||||
return v.getRSAPublicKey(did)
|
||||
case jwt.SigningMethodEdDSA:
|
||||
return v.getEd25519PublicKey(did)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported signing method: %v", token.Method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseUCANClaims extracts UCAN-specific fields from JWT claims
|
||||
func (v *Verifier) parseUCANClaims(claims jwt.MapClaims, raw string) (*Token, error) {
|
||||
issuer, audience := extractStandardClaims(claims)
|
||||
expiresAt, notBefore := extractTimeClaims(claims)
|
||||
|
||||
attenuations, err := v.parseAttenuationsClaims(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proofs := parseProofsClaims(claims)
|
||||
facts := parseFactsClaims(claims)
|
||||
|
||||
return &Token{
|
||||
Raw: raw,
|
||||
Issuer: issuer,
|
||||
Audience: audience,
|
||||
ExpiresAt: expiresAt,
|
||||
NotBefore: notBefore,
|
||||
Attenuations: attenuations,
|
||||
Proofs: proofs,
|
||||
Facts: facts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractStandardClaims extracts standard JWT claims (issuer and audience)
|
||||
func extractStandardClaims(claims jwt.MapClaims) (string, string) {
|
||||
issuer, _ := claims["iss"].(string)
|
||||
audience, _ := claims["aud"].(string)
|
||||
return issuer, audience
|
||||
}
|
||||
|
||||
// extractTimeClaims extracts time-related claims (exp and nbf)
|
||||
func extractTimeClaims(claims jwt.MapClaims) (int64, int64) {
|
||||
var expiresAt, notBefore int64
|
||||
|
||||
if exp, ok := claims["exp"]; ok {
|
||||
if expFloat, ok := exp.(float64); ok {
|
||||
expiresAt = int64(expFloat)
|
||||
}
|
||||
}
|
||||
|
||||
if nbf, ok := claims["nbf"]; ok {
|
||||
if nbfFloat, ok := nbf.(float64); ok {
|
||||
notBefore = int64(nbfFloat)
|
||||
}
|
||||
}
|
||||
|
||||
return expiresAt, notBefore
|
||||
}
|
||||
|
||||
// parseAttenuationsClaims parses the attenuations from claims
|
||||
func (v *Verifier) parseAttenuationsClaims(claims jwt.MapClaims) ([]Attenuation, error) {
|
||||
attClaims, ok := claims["att"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attSlice, ok := attClaims.([]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Pre-allocate slice with known capacity
|
||||
attenuations := make([]Attenuation, 0, len(attSlice))
|
||||
|
||||
for _, attItem := range attSlice {
|
||||
attMap, ok := attItem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
att, err := v.parseAttenuation(attMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attenuation: %w", err)
|
||||
}
|
||||
attenuations = append(attenuations, att)
|
||||
}
|
||||
|
||||
return attenuations, nil
|
||||
}
|
||||
|
||||
// parseProofsClaims parses the proofs from claims
|
||||
func parseProofsClaims(claims jwt.MapClaims) []Proof {
|
||||
var proofs []Proof
|
||||
|
||||
prfClaims, ok := claims["prf"]
|
||||
if !ok {
|
||||
return proofs
|
||||
}
|
||||
|
||||
prfSlice, ok := prfClaims.([]any)
|
||||
if !ok {
|
||||
return proofs
|
||||
}
|
||||
|
||||
for _, prfItem := range prfSlice {
|
||||
if prfStr, ok := prfItem.(string); ok {
|
||||
proofs = append(proofs, Proof(prfStr))
|
||||
}
|
||||
}
|
||||
|
||||
return proofs
|
||||
}
|
||||
|
||||
// parseFactsClaims parses the facts from claims
|
||||
func parseFactsClaims(claims jwt.MapClaims) []Fact {
|
||||
fctClaims, ok := claims["fct"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
fctSlice, ok := fctClaims.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pre-allocate slice with known capacity
|
||||
facts := make([]Fact, 0, len(fctSlice))
|
||||
|
||||
for _, fctItem := range fctSlice {
|
||||
factData, _ := json.Marshal(fctItem)
|
||||
facts = append(facts, Fact{Data: factData})
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
// parseAttenuation converts a map to an Attenuation struct with enhanced module-specific support
|
||||
func (v *Verifier) parseAttenuation(attMap map[string]any) (Attenuation, error) {
|
||||
// Extract capability
|
||||
canValue, ok := attMap["can"]
|
||||
if !ok {
|
||||
return Attenuation{}, fmt.Errorf("missing 'can' field in attenuation")
|
||||
}
|
||||
|
||||
// Extract resource
|
||||
withValue, ok := attMap["with"]
|
||||
if !ok {
|
||||
return Attenuation{}, fmt.Errorf("missing 'with' field in attenuation")
|
||||
}
|
||||
|
||||
withStr, ok := withValue.(string)
|
||||
if !ok {
|
||||
return Attenuation{}, fmt.Errorf("'with' field must be a string")
|
||||
}
|
||||
|
||||
// Parse resource first to determine module type
|
||||
resource, err := v.parseResource(withStr)
|
||||
if err != nil {
|
||||
return Attenuation{}, fmt.Errorf("failed to parse resource: %w", err)
|
||||
}
|
||||
|
||||
// Create module-specific capability based on resource scheme
|
||||
cap, err := v.createModuleSpecificCapability(resource.GetScheme(), canValue, attMap)
|
||||
if err != nil {
|
||||
return Attenuation{}, fmt.Errorf("failed to create capability: %w", err)
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: cap,
|
||||
Resource: resource,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createModuleSpecificCapability creates appropriate capability type based on module
|
||||
func (v *Verifier) createModuleSpecificCapability(scheme string, canValue any, attMap map[string]any) (Capability, error) {
|
||||
// Extract common fields
|
||||
caveats := extractStringSliceFromMap(attMap, "caveats")
|
||||
metadata := extractStringMapFromMap(attMap, "metadata")
|
||||
|
||||
switch scheme {
|
||||
case "did":
|
||||
return v.createDIDCapability(canValue, caveats, metadata)
|
||||
case "dwn":
|
||||
return v.createDWNCapability(canValue, caveats, metadata)
|
||||
case "service", "svc":
|
||||
return v.createServiceCapability(canValue, caveats, metadata)
|
||||
case "dex", "pool":
|
||||
return v.createDEXCapability(canValue, caveats, metadata, attMap)
|
||||
case "ipfs", "vault":
|
||||
// Handle existing vault capabilities
|
||||
return v.createVaultCapabilityFromMap(canValue, attMap)
|
||||
default:
|
||||
// Fallback to simple/multi capability for unknown schemes
|
||||
return v.createGenericCapability(canValue)
|
||||
}
|
||||
}
|
||||
|
||||
// createDIDCapability creates a DID-specific capability
|
||||
func (v *Verifier) createDIDCapability(canValue any, caveats []string, metadata map[string]string) (Capability, error) {
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &DIDCapability{
|
||||
Action: canVal,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &DIDCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DID capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createDWNCapability creates a DWN-specific capability
|
||||
func (v *Verifier) createDWNCapability(canValue any, caveats []string, metadata map[string]string) (Capability, error) {
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &DWNCapability{
|
||||
Action: canVal,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &DWNCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DWN capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createServiceCapability creates a Service-specific capability
|
||||
func (v *Verifier) createServiceCapability(canValue any, caveats []string, metadata map[string]string) (Capability, error) {
|
||||
// Service capabilities can still use MultiCapability for now
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &MultiCapability{Actions: []string{canVal}}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &MultiCapability{Actions: actions}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Service capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createDEXCapability creates a DEX-specific capability
|
||||
func (v *Verifier) createDEXCapability(canValue any, caveats []string, metadata map[string]string, attMap map[string]any) (Capability, error) {
|
||||
maxAmount, _ := attMap["max_amount"].(string)
|
||||
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &DEXCapability{
|
||||
Action: canVal,
|
||||
Caveats: caveats,
|
||||
MaxAmount: maxAmount,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &DEXCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
MaxAmount: maxAmount,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DEX capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createVaultCapabilityFromMap creates vault capability from existing logic
|
||||
func (v *Verifier) createVaultCapabilityFromMap(canValue any, attMap map[string]any) (Capability, error) {
|
||||
// Use existing vault capability creation logic
|
||||
vaultAddress, _ := attMap["vault"].(string)
|
||||
caveats := extractStringSliceFromMap(attMap, "caveats")
|
||||
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &VaultCapability{
|
||||
Action: canVal,
|
||||
VaultAddress: vaultAddress,
|
||||
Caveats: caveats,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &VaultCapability{
|
||||
Actions: actions,
|
||||
VaultAddress: vaultAddress,
|
||||
Caveats: caveats,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported vault capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createGenericCapability creates fallback capability for unknown schemes
|
||||
func (v *Verifier) createGenericCapability(canValue any) (Capability, error) {
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &SimpleCapability{Action: canVal}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &MultiCapability{Actions: actions}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for extracting data from maps
|
||||
func extractStringSliceFromMap(m map[string]any, key string) []string {
|
||||
if value, exists := m[key]; exists {
|
||||
return extractStringSlice(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractStringMapFromMap(m map[string]any, key string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if value, exists := m[key]; exists {
|
||||
if mapValue, ok := value.(map[string]any); ok {
|
||||
for k, v := range mapValue {
|
||||
if strValue, ok := v.(string); ok {
|
||||
result[k] = strValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseResource creates a Resource from a URI string
|
||||
func (v *Verifier) parseResource(uri string) (Resource, error) {
|
||||
if uri == "" {
|
||||
return nil, fmt.Errorf("resource URI cannot be empty")
|
||||
}
|
||||
|
||||
// Parse URI scheme and value - support both "scheme://value" and "scheme:value" formats
|
||||
var scheme, value string
|
||||
if strings.Contains(uri, "://") {
|
||||
parts := strings.SplitN(uri, "://", 2)
|
||||
if len(parts) == 2 {
|
||||
scheme = parts[0]
|
||||
value = parts[1]
|
||||
}
|
||||
} else if strings.Contains(uri, ":") {
|
||||
parts := strings.SplitN(uri, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
scheme = parts[0]
|
||||
value = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if scheme == "" || value == "" {
|
||||
return nil, fmt.Errorf("invalid resource URI format: %s", uri)
|
||||
}
|
||||
|
||||
return &SimpleResource{
|
||||
Scheme: scheme,
|
||||
Value: value,
|
||||
URI: uri,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateToken performs structural and temporal validation
|
||||
func (v *Verifier) validateToken(_ context.Context, token *Token) error {
|
||||
// Check required fields
|
||||
if token.Issuer == "" {
|
||||
return fmt.Errorf("issuer is required")
|
||||
}
|
||||
if token.Audience == "" {
|
||||
return fmt.Errorf("audience is required")
|
||||
}
|
||||
if len(token.Attenuations) == 0 {
|
||||
return fmt.Errorf("at least one attenuation is required")
|
||||
}
|
||||
|
||||
// Check temporal validity
|
||||
now := time.Now().Unix()
|
||||
|
||||
if token.NotBefore > 0 && now < token.NotBefore {
|
||||
return fmt.Errorf("token is not yet valid (nbf: %d, now: %d)", token.NotBefore, now)
|
||||
}
|
||||
|
||||
if token.ExpiresAt > 0 && now >= token.ExpiresAt {
|
||||
return fmt.Errorf("token has expired (exp: %d, now: %d)", token.ExpiresAt, now)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkCapabilities verifies that the token grants the required capabilities with enhanced module-specific validation
|
||||
func (v *Verifier) checkCapabilities(token *Token, resource string, abilities []string) error {
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource.GetURI() == resource {
|
||||
if att.Capability.Grants(abilities) {
|
||||
// Validate caveats for module-specific capabilities
|
||||
if err := v.validateCaveats(att.Capability, att.Resource); err != nil {
|
||||
return fmt.Errorf("caveat validation failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("required capabilities not granted for resource %s", resource)
|
||||
}
|
||||
|
||||
// validateCaveats validates constraints (caveats) for module-specific capabilities
|
||||
func (v *Verifier) validateCaveats(cap Capability, resource Resource) error {
|
||||
scheme := resource.GetScheme()
|
||||
|
||||
switch scheme {
|
||||
case "did":
|
||||
return v.validateDIDCaveats(cap, resource)
|
||||
case "dwn":
|
||||
return v.validateDWNCaveats(cap, resource)
|
||||
case "dex", "pool":
|
||||
return v.validateDEXCaveats(cap, resource)
|
||||
case "service", "svc":
|
||||
return v.validateServiceCaveats(cap, resource)
|
||||
case "vault", "ipfs":
|
||||
return v.validateVaultCaveats(cap, resource)
|
||||
default:
|
||||
return nil // No caveat validation for unknown schemes
|
||||
}
|
||||
}
|
||||
|
||||
// validateDIDCaveats validates DID-specific constraints
|
||||
func (v *Verifier) validateDIDCaveats(cap Capability, resource Resource) error {
|
||||
didCap, ok := cap.(*DIDCapability)
|
||||
if !ok {
|
||||
return nil // Not a DID capability
|
||||
}
|
||||
|
||||
for _, caveat := range didCap.Caveats {
|
||||
switch caveat {
|
||||
case "owner":
|
||||
// Validate that the capability is for the owner's DID
|
||||
if err := v.validateOwnerCaveat(resource); err != nil {
|
||||
return fmt.Errorf("owner caveat validation failed: %w", err)
|
||||
}
|
||||
case "controller":
|
||||
// Validate controller permissions
|
||||
if err := v.validateControllerCaveat(resource); err != nil {
|
||||
return fmt.Errorf("controller caveat validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDWNCaveats validates DWN-specific constraints
|
||||
func (v *Verifier) validateDWNCaveats(cap Capability, resource Resource) error {
|
||||
dwnCap, ok := cap.(*DWNCapability)
|
||||
if !ok {
|
||||
return nil // Not a DWN capability
|
||||
}
|
||||
|
||||
for _, caveat := range dwnCap.Caveats {
|
||||
switch caveat {
|
||||
case "owner":
|
||||
// Validate record ownership
|
||||
if err := v.validateRecordOwnership(resource); err != nil {
|
||||
return fmt.Errorf("record ownership validation failed: %w", err)
|
||||
}
|
||||
case "protocol":
|
||||
// Validate protocol compliance
|
||||
if err := v.validateProtocolCaveat(resource); err != nil {
|
||||
return fmt.Errorf("protocol caveat validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDEXCaveats validates DEX-specific constraints
|
||||
func (v *Verifier) validateDEXCaveats(cap Capability, resource Resource) error {
|
||||
dexCap, ok := cap.(*DEXCapability)
|
||||
if !ok {
|
||||
return nil // Not a DEX capability
|
||||
}
|
||||
|
||||
for _, caveat := range dexCap.Caveats {
|
||||
switch caveat {
|
||||
case "max-amount":
|
||||
// Validate maximum swap amount
|
||||
if dexCap.MaxAmount != "" {
|
||||
if err := v.validateMaxAmountCaveat(dexCap.MaxAmount); err != nil {
|
||||
return fmt.Errorf("max amount caveat validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
case "pool-member":
|
||||
// Validate pool membership
|
||||
if err := v.validatePoolMembershipCaveat(resource); err != nil {
|
||||
return fmt.Errorf("pool membership validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateServiceCaveats validates Service-specific constraints
|
||||
func (v *Verifier) validateServiceCaveats(cap Capability, resource Resource) error {
|
||||
// Service capabilities use MultiCapability for now
|
||||
// Add service-specific caveat validation if needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVaultCaveats validates Vault-specific constraints
|
||||
func (v *Verifier) validateVaultCaveats(cap Capability, resource Resource) error {
|
||||
vaultCap, ok := cap.(*VaultCapability)
|
||||
if !ok {
|
||||
return nil // Not a vault capability
|
||||
}
|
||||
|
||||
for _, caveat := range vaultCap.Caveats {
|
||||
switch caveat {
|
||||
case "vault-owner":
|
||||
// Validate vault ownership
|
||||
if err := v.validateVaultOwnership(vaultCap.VaultAddress); err != nil {
|
||||
return fmt.Errorf("vault ownership validation failed: %w", err)
|
||||
}
|
||||
case "enclave-integrity":
|
||||
// Validate enclave data integrity
|
||||
if err := v.validateEnclaveIntegrity(vaultCap.EnclaveDataCID); err != nil {
|
||||
return fmt.Errorf("enclave integrity validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Caveat validation helper methods (placeholders for actual implementation)
|
||||
|
||||
// validateOwnerCaveat validates DID ownership constraint
|
||||
func (v *Verifier) validateOwnerCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual DID ownership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateControllerCaveat validates DID controller constraint
|
||||
func (v *Verifier) validateControllerCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual controller validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRecordOwnership validates DWN record ownership
|
||||
func (v *Verifier) validateRecordOwnership(resource Resource) error {
|
||||
// Placeholder: Implement actual record ownership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateProtocolCaveat validates DWN protocol constraint
|
||||
func (v *Verifier) validateProtocolCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual protocol validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMaxAmountCaveat validates DEX maximum amount constraint
|
||||
func (v *Verifier) validateMaxAmountCaveat(maxAmount string) error {
|
||||
// Placeholder: Implement actual amount validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePoolMembershipCaveat validates DEX pool membership
|
||||
func (v *Verifier) validatePoolMembershipCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual pool membership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVaultOwnership validates vault ownership
|
||||
func (v *Verifier) validateVaultOwnership(vaultAddress string) error {
|
||||
// Placeholder: Implement actual vault ownership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateEnclaveIntegrity validates enclave data integrity
|
||||
func (v *Verifier) validateEnclaveIntegrity(enclaveDataCID string) error {
|
||||
// Placeholder: Implement actual enclave integrity validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDelegation checks that child token is properly attenuated from parent with enhanced module-specific validation
|
||||
func (v *Verifier) validateDelegation(child, parent *Token) error {
|
||||
// Child's issuer must be parent's audience
|
||||
if child.Issuer != parent.Audience {
|
||||
return fmt.Errorf("delegation chain broken: child issuer must be parent audience")
|
||||
}
|
||||
|
||||
// Child capabilities must be subset of parent with module-specific validation
|
||||
for _, childAtt := range child.Attenuations {
|
||||
if !v.isModuleCapabilitySubset(childAtt, parent.Attenuations) {
|
||||
return fmt.Errorf("child capability exceeds parent capabilities")
|
||||
}
|
||||
}
|
||||
|
||||
// Child expiration must not exceed parent
|
||||
if parent.ExpiresAt > 0 && (child.ExpiresAt == 0 || child.ExpiresAt > parent.ExpiresAt) {
|
||||
return fmt.Errorf("child token expires after parent token")
|
||||
}
|
||||
|
||||
// Validate cross-module delegation constraints
|
||||
if err := v.validateCrossModuleDelegation(child, parent); err != nil {
|
||||
return fmt.Errorf("cross-module delegation validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isModuleCapabilitySubset checks if a capability is a subset with module-specific logic
|
||||
func (v *Verifier) isModuleCapabilitySubset(childAtt Attenuation, parentAtts []Attenuation) bool {
|
||||
for _, parentAtt := range parentAtts {
|
||||
if childAtt.Resource.GetURI() == parentAtt.Resource.GetURI() {
|
||||
if v.isModuleCapabilityContained(childAtt.Capability, parentAtt.Capability, childAtt.Resource.GetScheme()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isModuleCapabilityContained checks containment with module-specific logic
|
||||
func (v *Verifier) isModuleCapabilityContained(child, parent Capability, scheme string) bool {
|
||||
// First check basic containment
|
||||
if parent.Contains(child) {
|
||||
// Additional module-specific containment validation
|
||||
switch scheme {
|
||||
case "did":
|
||||
return v.validateDIDContainment(child, parent)
|
||||
case "dwn":
|
||||
return v.validateDWNContainment(child, parent)
|
||||
case "dex", "pool":
|
||||
return v.validateDEXContainment(child, parent)
|
||||
case "vault", "ipfs":
|
||||
return v.validateVaultContainment(child, parent)
|
||||
default:
|
||||
return true // Basic containment is sufficient for unknown schemes
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateCrossModuleDelegation validates constraints across different modules
|
||||
func (v *Verifier) validateCrossModuleDelegation(child, parent *Token) error {
|
||||
childModules := v.extractModulesFromToken(child)
|
||||
parentModules := v.extractModulesFromToken(parent)
|
||||
|
||||
// Check if child uses modules not present in parent
|
||||
for module := range childModules {
|
||||
if _, exists := parentModules[module]; !exists {
|
||||
return fmt.Errorf("child token uses module '%s' not delegated by parent", module)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate specific cross-module constraints
|
||||
return v.validateSpecificCrossModuleConstraints(child, parent)
|
||||
}
|
||||
|
||||
// extractModulesFromToken extracts the modules used by a token
|
||||
func (v *Verifier) extractModulesFromToken(token *Token) map[string]bool {
|
||||
modules := make(map[string]bool)
|
||||
for _, att := range token.Attenuations {
|
||||
scheme := att.Resource.GetScheme()
|
||||
modules[scheme] = true
|
||||
}
|
||||
return modules
|
||||
}
|
||||
|
||||
// validateSpecificCrossModuleConstraints validates specific cross-module business logic
|
||||
func (v *Verifier) validateSpecificCrossModuleConstraints(child, parent *Token) error {
|
||||
// Example: If DID operations require vault access, ensure both are present
|
||||
childHasDID := v.tokenHasModule(child, "did")
|
||||
childHasVault := v.tokenHasModule(child, "vault") || v.tokenHasModule(child, "ipfs")
|
||||
|
||||
if childHasDID && !childHasVault {
|
||||
// Check if parent has vault capability that can be inherited
|
||||
parentHasVault := v.tokenHasModule(parent, "vault") || v.tokenHasModule(parent, "ipfs")
|
||||
if !parentHasVault {
|
||||
return fmt.Errorf("DID operations require vault access which is not available in delegation chain")
|
||||
}
|
||||
}
|
||||
|
||||
// Add more cross-module constraints as needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenHasModule checks if a token has capabilities for a specific module
|
||||
func (v *Verifier) tokenHasModule(token *Token, module string) bool {
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource.GetScheme() == module {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Module-specific containment validation methods
|
||||
|
||||
// validateDIDContainment validates DID capability containment
|
||||
func (v *Verifier) validateDIDContainment(child, parent Capability) bool {
|
||||
childDID, childOk := child.(*DIDCapability)
|
||||
parentDID, parentOk := parent.(*DIDCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both DID capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childDID.Caveats, parentDID.Caveats)
|
||||
}
|
||||
|
||||
// validateDWNContainment validates DWN capability containment
|
||||
func (v *Verifier) validateDWNContainment(child, parent Capability) bool {
|
||||
childDWN, childOk := child.(*DWNCapability)
|
||||
parentDWN, parentOk := parent.(*DWNCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both DWN capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childDWN.Caveats, parentDWN.Caveats)
|
||||
}
|
||||
|
||||
// validateDEXContainment validates DEX capability containment
|
||||
func (v *Verifier) validateDEXContainment(child, parent Capability) bool {
|
||||
childDEX, childOk := child.(*DEXCapability)
|
||||
parentDEX, parentOk := parent.(*DEXCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both DEX capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Validate max amount restriction
|
||||
if parentDEX.MaxAmount != "" && childDEX.MaxAmount != "" {
|
||||
// Child max amount should be less than or equal to parent
|
||||
if !v.isAmountLessOrEqual(childDEX.MaxAmount, parentDEX.MaxAmount) {
|
||||
return false
|
||||
}
|
||||
} else if parentDEX.MaxAmount != "" && childDEX.MaxAmount == "" {
|
||||
// Child must have max amount if parent does
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childDEX.Caveats, parentDEX.Caveats)
|
||||
}
|
||||
|
||||
// validateVaultContainment validates Vault capability containment
|
||||
func (v *Verifier) validateVaultContainment(child, parent Capability) bool {
|
||||
childVault, childOk := child.(*VaultCapability)
|
||||
parentVault, parentOk := parent.(*VaultCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both Vault capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Vault address must match
|
||||
if childVault.VaultAddress != parentVault.VaultAddress {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childVault.Caveats, parentVault.Caveats)
|
||||
}
|
||||
|
||||
// Helper methods for containment validation
|
||||
|
||||
// areCaveatsMoreRestrictive checks if child caveats are more restrictive than parent
|
||||
func (v *Verifier) areCaveatsMoreRestrictive(childCaveats, parentCaveats []string) bool {
|
||||
parentCaveatSet := make(map[string]bool)
|
||||
for _, caveat := range parentCaveats {
|
||||
parentCaveatSet[caveat] = true
|
||||
}
|
||||
|
||||
// All child caveats must be present in parent caveats (or child can have additional restrictions)
|
||||
for _, childCaveat := range childCaveats {
|
||||
if !parentCaveatSet[childCaveat] {
|
||||
// Child has additional restrictions, which is allowed
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isAmountLessOrEqual compares two amount strings (placeholder implementation)
|
||||
func (v *Verifier) isAmountLessOrEqual(childAmount, parentAmount string) bool {
|
||||
// Placeholder: Implement actual amount comparison
|
||||
// This would parse the amounts and compare them numerically
|
||||
return true
|
||||
}
|
||||
|
||||
// isCapabilitySubset checks if a capability is a subset of any parent capabilities
|
||||
func (v *Verifier) isCapabilitySubset(childAtt Attenuation, parentAtts []Attenuation) bool {
|
||||
for _, parentAtt := range parentAtts {
|
||||
if childAtt.Resource.GetURI() == parentAtt.Resource.GetURI() {
|
||||
if parentAtt.Capability.Contains(childAtt.Capability) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getRSAPublicKey extracts RSA public key from DID
|
||||
func (v *Verifier) getRSAPublicKey(did keys.DID) (*rsa.PublicKey, error) {
|
||||
verifyKey, err := did.VerifyKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get verify key: %w", err)
|
||||
}
|
||||
|
||||
rsaKey, ok := verifyKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DID does not contain RSA public key")
|
||||
}
|
||||
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
// getEd25519PublicKey extracts Ed25519 public key from DID
|
||||
func (v *Verifier) getEd25519PublicKey(did keys.DID) (ed25519.PublicKey, error) {
|
||||
pubKey := did.PublicKey()
|
||||
rawBytes, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get raw public key: %w", err)
|
||||
}
|
||||
|
||||
if pubKey.Type() != crypto.Ed25519 {
|
||||
return nil, fmt.Errorf("DID does not contain Ed25519 public key")
|
||||
}
|
||||
|
||||
return ed25519.PublicKey(rawBytes), nil
|
||||
}
|
||||
|
||||
// StringDIDResolver implements DIDResolver for did:key strings
|
||||
type StringDIDResolver struct{}
|
||||
|
||||
// ResolveDIDKey extracts a public key from a did:key string
|
||||
func (StringDIDResolver) ResolveDIDKey(ctx context.Context, didStr string) (keys.DID, error) {
|
||||
return keys.Parse(didStr)
|
||||
}
|
||||
Reference in New Issue
Block a user