mirror of
https://github.com/sonr-io/crypto.git
synced 2026-01-11 20:08:57 +00:00
486 lines
13 KiB
Go
486 lines
13 KiB
Go
// 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 (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
z "github.com/Oudwins/zog"
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/multiformats/go-multihash"
|
|
)
|
|
|
|
// Constants for vault capability actions
|
|
const (
|
|
VaultAdminAction = "vault/admin"
|
|
)
|
|
|
|
// VaultCapabilitySchema defines validation specifically for vault capabilities
|
|
var VaultCapabilitySchema = z.Struct(z.Shape{
|
|
"can": z.String().Required().OneOf(
|
|
[]string{
|
|
VaultAdminAction,
|
|
"vault/read",
|
|
"vault/write",
|
|
"vault/sign",
|
|
"vault/export",
|
|
"vault/import",
|
|
"vault/delete",
|
|
},
|
|
z.Message("Invalid vault capability"),
|
|
),
|
|
"with": z.String().
|
|
Required().
|
|
TestFunc(ValidateIPFSCID, z.Message("Vault resource must be IPFS CID in format 'ipfs://CID'")),
|
|
"actions": z.Slice(z.String().OneOf(
|
|
[]string{"read", "write", "sign", "export", "import", "delete"},
|
|
z.Message("Invalid vault action"),
|
|
)).Optional(),
|
|
"vault": z.String().Required().Min(1, z.Message("Vault address cannot be empty")),
|
|
"cavs": z.Slice(z.String()).Optional(), // Caveats as string array for vault capabilities
|
|
})
|
|
|
|
// VaultCapability implements Capability for vault-specific operations
|
|
// with support for admin permissions, actions, and enclave data management.
|
|
type VaultCapability struct {
|
|
Action string `json:"can"`
|
|
Actions []string `json:"actions,omitempty"`
|
|
VaultAddress string `json:"vault,omitempty"`
|
|
Caveats []string `json:"cavs,omitempty"`
|
|
EnclaveDataCID string `json:"enclave_data_cid,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// GetActions returns the actions this vault capability grants
|
|
func (c *VaultCapability) GetActions() []string {
|
|
if c.Action == VaultAdminAction {
|
|
// Admin capability grants all vault actions
|
|
return []string{"read", "write", "sign", "export", "import", "delete", VaultAdminAction}
|
|
}
|
|
|
|
if len(c.Actions) > 0 {
|
|
return c.Actions
|
|
}
|
|
|
|
// Extract action from the main capability string
|
|
if strings.HasPrefix(c.Action, "vault/") {
|
|
return []string{c.Action[6:]} // Remove "vault/" prefix
|
|
}
|
|
|
|
return []string{c.Action}
|
|
}
|
|
|
|
// Grants checks if this capability grants the required abilities
|
|
func (c *VaultCapability) Grants(abilities []string) bool {
|
|
if c.Action == VaultAdminAction {
|
|
// Admin capability grants everything
|
|
return true
|
|
}
|
|
|
|
grantedActions := make(map[string]bool)
|
|
for _, action := range c.GetActions() {
|
|
grantedActions[action] = true
|
|
grantedActions["vault/"+action] = true // Support both formats
|
|
}
|
|
|
|
// Check each required ability
|
|
for _, ability := range abilities {
|
|
if !grantedActions[ability] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Contains checks if this capability contains another capability
|
|
func (c *VaultCapability) Contains(other Capability) bool {
|
|
if c.Action == VaultAdminAction {
|
|
// Admin contains all vault capabilities
|
|
if otherVault, ok := other.(*VaultCapability); ok {
|
|
return strings.HasPrefix(otherVault.Action, "vault/")
|
|
}
|
|
// Admin contains any action that starts with vault-related actions
|
|
for _, action := range other.GetActions() {
|
|
if strings.HasPrefix(action, "vault/") ||
|
|
action == "read" || action == "write" || action == "sign" ||
|
|
action == "export" || action == "import" || action == "delete" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check if our actions contain all of the other capability's actions
|
|
ourActions := make(map[string]bool)
|
|
for _, action := range c.GetActions() {
|
|
ourActions[action] = true
|
|
ourActions["vault/"+action] = true
|
|
}
|
|
|
|
for _, otherAction := range other.GetActions() {
|
|
if !ourActions[otherAction] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// String returns string representation
|
|
func (c *VaultCapability) String() string {
|
|
return c.Action
|
|
}
|
|
|
|
// VaultResourceExt represents an extended IPFS-based vault resource (to avoid redeclaration)
|
|
type VaultResourceExt struct {
|
|
SimpleResource
|
|
VaultAddress string `json:"vault_address"`
|
|
EnclaveDataCID string `json:"enclave_data_cid"`
|
|
}
|
|
|
|
// ValidateIPFSCID validates IPFS CID format for vault resources
|
|
func ValidateIPFSCID(value *string, ctx z.Ctx) bool {
|
|
if !strings.HasPrefix(*value, "ipfs://") {
|
|
return false
|
|
}
|
|
cidStr := (*value)[7:] // Remove "ipfs://" prefix
|
|
|
|
// Enhanced CID validation
|
|
return validateCIDFormat(cidStr)
|
|
}
|
|
|
|
// validateCIDFormat performs comprehensive IPFS CID format validation
|
|
func validateCIDFormat(cidStr string) bool {
|
|
if len(cidStr) == 0 {
|
|
return false
|
|
}
|
|
|
|
// CIDv0: Base58-encoded SHA-256 multihash (starts with 'Qm' and is 46 characters)
|
|
if strings.HasPrefix(cidStr, "Qm") && len(cidStr) == 46 {
|
|
return isValidBase58(cidStr)
|
|
}
|
|
|
|
// CIDv1: Base32 or Base58 encoded (starts with 'b' for base32 or other prefixes)
|
|
if len(cidStr) >= 59 {
|
|
// CIDv1 in base32 typically starts with 'b' and is longer
|
|
if strings.HasPrefix(cidStr, "b") {
|
|
return isValidBase32(cidStr[1:]) // Remove 'b' prefix
|
|
}
|
|
// CIDv1 in base58 or other encodings
|
|
return isValidBase58(cidStr)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isValidBase58 checks if string contains valid base58 characters
|
|
func isValidBase58(s string) bool {
|
|
base58Chars := "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
for _, char := range s {
|
|
if !strings.Contains(base58Chars, string(char)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isValidBase32 checks if string contains valid base32 characters
|
|
func isValidBase32(s string) bool {
|
|
base32Chars := "abcdefghijklmnopqrstuvwxyz234567"
|
|
for _, char := range s {
|
|
if !strings.Contains(base32Chars, string(char)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ValidateEnclaveDataCIDIntegrity validates enclave data against expected CID
|
|
func ValidateEnclaveDataCIDIntegrity(enclaveDataCID string, enclaveData []byte) error {
|
|
if enclaveDataCID == "" {
|
|
return fmt.Errorf("enclave data CID cannot be empty")
|
|
}
|
|
|
|
if len(enclaveData) == 0 {
|
|
return fmt.Errorf("enclave data cannot be empty")
|
|
}
|
|
|
|
// Validate CID format first
|
|
if !validateCIDFormat(enclaveDataCID) {
|
|
return fmt.Errorf("invalid IPFS CID format: %s", enclaveDataCID)
|
|
}
|
|
|
|
// Implement actual CID verification by hashing enclave data
|
|
// 1. Hash the enclave data using SHA-256
|
|
hasher := sha256.New()
|
|
hasher.Write(enclaveData)
|
|
digest := hasher.Sum(nil)
|
|
|
|
// 2. Create multihash with SHA-256 prefix
|
|
mhash, err := multihash.EncodeName(digest, "sha2-256")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create multihash: %w", err)
|
|
}
|
|
|
|
// 3. Create CID and compare with expected
|
|
expectedCID, err := cid.Parse(enclaveDataCID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse expected CID: %w", err)
|
|
}
|
|
|
|
// Create CID v1 with dag-pb codec (IPFS default)
|
|
calculatedCID := cid.NewCidV1(cid.DagProtobuf, mhash)
|
|
|
|
// Compare CIDs
|
|
if !expectedCID.Equals(calculatedCID) {
|
|
return fmt.Errorf(
|
|
"CID verification failed: expected %s, calculated %s",
|
|
expectedCID.String(),
|
|
calculatedCID.String(),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateVaultCapability validates vault-specific capabilities
|
|
func ValidateVaultCapability(att map[string]any) error {
|
|
var validated struct {
|
|
Can string `json:"can"`
|
|
With string `json:"with"`
|
|
Actions []string `json:"actions,omitempty"`
|
|
Vault string `json:"vault"`
|
|
Cavs []string `json:"cavs,omitempty"`
|
|
}
|
|
|
|
errs := VaultCapabilitySchema.Parse(att, &validated)
|
|
if errs != nil {
|
|
return fmt.Errorf("vault capability validation failed: %v", errs)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VaultAttenuationConstructor creates vault-specific attenuations with enhanced validation
|
|
func VaultAttenuationConstructor(m map[string]any) (Attenuation, error) {
|
|
// First validate using vault-specific schema
|
|
if err := ValidateVaultCapability(m); err != nil {
|
|
return Attenuation{}, fmt.Errorf("vault attenuation validation failed: %w", err)
|
|
}
|
|
|
|
capStr, withStr, err := extractRequiredFields(m)
|
|
if err != nil {
|
|
return Attenuation{}, err
|
|
}
|
|
|
|
vaultCap := createVaultCapability(capStr, m)
|
|
resource := createVaultResource(withStr, vaultCap.VaultAddress)
|
|
|
|
// Set enclave data CID if using IPFS resource
|
|
if vaultRes, ok := resource.(*VaultResource); ok {
|
|
vaultCap.EnclaveDataCID = vaultRes.EnclaveDataCID
|
|
}
|
|
|
|
return Attenuation{
|
|
Capability: vaultCap,
|
|
Resource: resource,
|
|
}, nil
|
|
}
|
|
|
|
// extractRequiredFields extracts and validates required 'can' and 'with' fields
|
|
func extractRequiredFields(m map[string]any) (string, string, error) {
|
|
capValue, exists := m["can"]
|
|
if !exists {
|
|
return "", "", fmt.Errorf("missing 'can' field in attenuation")
|
|
}
|
|
capStr, ok := capValue.(string)
|
|
if !ok {
|
|
return "", "", fmt.Errorf("'can' field must be a string")
|
|
}
|
|
|
|
withValue, exists := m["with"]
|
|
if !exists {
|
|
return "", "", fmt.Errorf("missing 'with' field in attenuation")
|
|
}
|
|
withStr, ok := withValue.(string)
|
|
if !ok {
|
|
return "", "", fmt.Errorf("'with' field must be a string")
|
|
}
|
|
|
|
return capStr, withStr, nil
|
|
}
|
|
|
|
// createVaultCapability creates and populates a VaultCapability from the input map
|
|
func createVaultCapability(action string, m map[string]any) *VaultCapability {
|
|
vaultCap := &VaultCapability{Action: action}
|
|
|
|
if actions, exists := m["actions"]; exists {
|
|
vaultCap.Actions = extractStringSlice(actions)
|
|
}
|
|
|
|
if vault, exists := m["vault"]; exists {
|
|
if vaultStr, ok := vault.(string); ok {
|
|
vaultCap.VaultAddress = vaultStr
|
|
}
|
|
}
|
|
|
|
if cavs, exists := m["cavs"]; exists {
|
|
vaultCap.Caveats = extractStringSlice(cavs)
|
|
}
|
|
|
|
return vaultCap
|
|
}
|
|
|
|
// extractStringSlice safely extracts a string slice from an any
|
|
func extractStringSlice(value any) []string {
|
|
if slice, ok := value.([]any); ok {
|
|
result := make([]string, 0, len(slice))
|
|
for _, item := range slice {
|
|
if str, ok := item.(string); ok {
|
|
result = append(result, str)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createVaultResource creates appropriate Resource based on the URI scheme
|
|
func createVaultResource(withStr, vaultAddress string) Resource {
|
|
parts := strings.SplitN(withStr, "://", 2)
|
|
if len(parts) == 2 && parts[0] == "ipfs" {
|
|
return &VaultResource{
|
|
SimpleResource: SimpleResource{
|
|
Scheme: "ipfs",
|
|
Value: parts[1],
|
|
URI: withStr,
|
|
},
|
|
VaultAddress: vaultAddress,
|
|
EnclaveDataCID: parts[1],
|
|
}
|
|
}
|
|
|
|
return &SimpleResource{
|
|
Scheme: "ipfs",
|
|
Value: withStr,
|
|
URI: withStr,
|
|
}
|
|
}
|
|
|
|
// NewVaultAdminToken creates a new UCAN token with vault admin capabilities
|
|
func NewVaultAdminToken(
|
|
builder TokenBuilderInterface,
|
|
vaultOwnerDID string,
|
|
vaultAddress string,
|
|
enclaveDataCID string,
|
|
exp time.Time,
|
|
) (*Token, error) {
|
|
// Validate input parameters
|
|
if !isValidDID(vaultOwnerDID) {
|
|
return nil, fmt.Errorf("invalid vault owner DID: %s", vaultOwnerDID)
|
|
}
|
|
|
|
// Create vault admin attenuation with full permissions
|
|
vaultResource := &VaultResource{
|
|
SimpleResource: SimpleResource{
|
|
Scheme: "ipfs",
|
|
Value: enclaveDataCID,
|
|
URI: fmt.Sprintf("ipfs://%s", enclaveDataCID),
|
|
},
|
|
VaultAddress: vaultAddress,
|
|
EnclaveDataCID: enclaveDataCID,
|
|
}
|
|
|
|
vaultCap := &VaultCapability{
|
|
Action: VaultAdminAction,
|
|
Actions: []string{"read", "write", "sign", "export", "import", "delete"},
|
|
VaultAddress: vaultAddress,
|
|
EnclaveDataCID: enclaveDataCID,
|
|
}
|
|
|
|
// Validate the vault capability using vault-specific schema
|
|
capMap := map[string]any{
|
|
"can": vaultCap.Action,
|
|
"with": vaultResource.URI,
|
|
"actions": vaultCap.Actions,
|
|
"vault": vaultCap.VaultAddress,
|
|
}
|
|
if err := ValidateVaultCapability(capMap); err != nil {
|
|
return nil, fmt.Errorf("invalid vault capability: %w", err)
|
|
}
|
|
|
|
attenuation := Attenuation{
|
|
Capability: vaultCap,
|
|
Resource: vaultResource,
|
|
}
|
|
|
|
// Create token with vault admin capabilities
|
|
return builder.CreateOriginToken(
|
|
vaultOwnerDID,
|
|
[]Attenuation{attenuation},
|
|
nil,
|
|
time.Now(),
|
|
exp,
|
|
)
|
|
}
|
|
|
|
// ValidateVaultTokenCapability validates a UCAN token for vault operations
|
|
func ValidateVaultTokenCapability(token *Token, enclaveDataCID, requiredAction string) error {
|
|
expectedResource := fmt.Sprintf("ipfs://%s", enclaveDataCID)
|
|
|
|
// Validate the required action parameter
|
|
validActions := []string{"read", "write", "sign", "export", "import", "delete"}
|
|
actionValid := slices.Contains(validActions, requiredAction)
|
|
if !actionValid {
|
|
return fmt.Errorf("invalid required action: %s", requiredAction)
|
|
}
|
|
|
|
// Check if token contains the required vault capability
|
|
for _, att := range token.Attenuations {
|
|
if att.Resource.GetURI() == expectedResource {
|
|
// Check if this is a vault capability
|
|
if vaultCap, ok := att.Capability.(*VaultCapability); ok {
|
|
// Validate using vault-specific schema
|
|
validationMap := map[string]any{
|
|
"can": vaultCap.Action,
|
|
"with": att.Resource.GetURI(),
|
|
"actions": vaultCap.Actions,
|
|
"vault": vaultCap.VaultAddress,
|
|
}
|
|
|
|
if err := ValidateVaultCapability(validationMap); err != nil {
|
|
continue // Skip invalid capabilities
|
|
}
|
|
|
|
// Check if capability grants the required action
|
|
if vaultCap.Grants([]string{requiredAction}) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf(
|
|
"insufficient vault capability: required action '%s' for enclave '%s'",
|
|
requiredAction,
|
|
enclaveDataCID,
|
|
)
|
|
}
|
|
|
|
// GetEnclaveDataCID extracts the enclave data CID from vault capabilities
|
|
func GetEnclaveDataCID(token *Token) (string, error) {
|
|
for _, att := range token.Attenuations {
|
|
resource := att.Resource.GetURI()
|
|
if strings.HasPrefix(resource, "ipfs://") {
|
|
return resource[7:], nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no enclave data CID found in token")
|
|
}
|