mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,602 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
"github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
|
||||
// BlockchainUCANSigner implements UCAN signing with blockchain keys
|
||||
type BlockchainUCANSigner struct {
|
||||
didKeeper DIDKeeperInterface
|
||||
issuerDID string
|
||||
signingMethod jwt.SigningMethod
|
||||
privateKey any
|
||||
}
|
||||
|
||||
// DIDKeeperInterface defines the minimal interface needed from DID keeper
|
||||
type DIDKeeperInterface interface {
|
||||
GetDIDDocument(ctx context.Context, did string) (*types.DIDDocument, error)
|
||||
GetVerificationMethod(
|
||||
ctx context.Context,
|
||||
did string,
|
||||
methodID string,
|
||||
) (*types.VerificationMethod, error)
|
||||
}
|
||||
|
||||
// NewBlockchainUCANSigner creates a new blockchain-integrated UCAN signer
|
||||
func NewBlockchainUCANSigner(
|
||||
didKeeper DIDKeeperInterface,
|
||||
issuerDID string,
|
||||
) (*BlockchainUCANSigner, error) {
|
||||
return &BlockchainUCANSigner{
|
||||
didKeeper: didKeeper,
|
||||
issuerDID: issuerDID,
|
||||
signingMethod: jwt.SigningMethodES256, // Default to ES256
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sign signs a UCAN token with blockchain keys
|
||||
func (s *BlockchainUCANSigner) Sign(token *ucan.Token) (string, error) {
|
||||
// Build JWT claims from UCAN token
|
||||
claims := s.buildClaims(token)
|
||||
|
||||
// Get signing key
|
||||
signingKey, method, err := s.getSigningKey(context.Background())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get signing key: %w", err)
|
||||
}
|
||||
|
||||
// Create JWT token
|
||||
jwtToken := jwt.NewWithClaims(method, claims)
|
||||
|
||||
// Sign token
|
||||
signedToken, err := jwtToken.SignedString(signingKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
// GetIssuerDID returns the issuer DID
|
||||
func (s *BlockchainUCANSigner) GetIssuerDID() string {
|
||||
if s.issuerDID == "" {
|
||||
return "did:sonr:oauth-provider"
|
||||
}
|
||||
return s.issuerDID
|
||||
}
|
||||
|
||||
// buildClaims builds JWT claims from UCAN token
|
||||
func (s *BlockchainUCANSigner) buildClaims(token *ucan.Token) jwt.MapClaims {
|
||||
claims := jwt.MapClaims{
|
||||
"iss": token.Issuer,
|
||||
"aud": token.Audience,
|
||||
"exp": token.ExpiresAt,
|
||||
"iat": time.Now().Unix(),
|
||||
"nbf": token.NotBefore,
|
||||
}
|
||||
|
||||
// Add attenuations
|
||||
if len(token.Attenuations) > 0 {
|
||||
attClaims := make([]map[string]any, len(token.Attenuations))
|
||||
for i, att := range token.Attenuations {
|
||||
attClaims[i] = s.serializeAttenuation(att)
|
||||
}
|
||||
claims["att"] = attClaims
|
||||
}
|
||||
|
||||
// Add proofs if present
|
||||
if len(token.Proofs) > 0 {
|
||||
proofClaims := make([]string, len(token.Proofs))
|
||||
for i, proof := range token.Proofs {
|
||||
proofClaims[i] = string(proof)
|
||||
}
|
||||
claims["prf"] = proofClaims
|
||||
}
|
||||
|
||||
// Add facts if present
|
||||
if len(token.Facts) > 0 {
|
||||
factClaims := make([]json.RawMessage, len(token.Facts))
|
||||
for i, fact := range token.Facts {
|
||||
factClaims[i] = fact.Data
|
||||
}
|
||||
claims["fct"] = factClaims
|
||||
}
|
||||
|
||||
// Add UCAN version
|
||||
claims["ucv"] = "0.10.0"
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
// serializeAttenuation serializes an attenuation for JWT claims
|
||||
func (s *BlockchainUCANSigner) serializeAttenuation(att ucan.Attenuation) map[string]any {
|
||||
result := map[string]any{
|
||||
"with": att.Resource.GetURI(),
|
||||
}
|
||||
|
||||
// Handle capability serialization
|
||||
actions := att.Capability.GetActions()
|
||||
if len(actions) == 1 {
|
||||
result["can"] = actions[0]
|
||||
} else {
|
||||
result["can"] = actions
|
||||
}
|
||||
|
||||
// Add resource-specific metadata
|
||||
scheme := att.Resource.GetScheme()
|
||||
switch scheme {
|
||||
case "vault":
|
||||
result["type"] = "vault_operation"
|
||||
case "service", "svc":
|
||||
result["type"] = "service_operation"
|
||||
case "did":
|
||||
result["type"] = "identity_operation"
|
||||
case "dwn":
|
||||
result["type"] = "data_operation"
|
||||
case "dex", "pool":
|
||||
result["type"] = "trading_operation"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getSigningKey retrieves the signing key from blockchain
|
||||
func (s *BlockchainUCANSigner) getSigningKey(ctx context.Context) (any, jwt.SigningMethod, error) {
|
||||
// If we have a cached private key, use it
|
||||
if s.privateKey != nil {
|
||||
return s.privateKey, s.signingMethod, nil
|
||||
}
|
||||
|
||||
// For OAuth provider, use a service key
|
||||
if s.issuerDID == "did:sonr:oauth-provider" || s.issuerDID == "" {
|
||||
// Generate or retrieve service key
|
||||
privateKey, publicKey, err := s.generateServiceKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
s.privateKey = privateKey
|
||||
s.signingMethod = jwt.SigningMethodEdDSA // Update to EdDSA for Ed25519 keys
|
||||
_ = publicKey // Store public key if needed
|
||||
|
||||
return privateKey, jwt.SigningMethodEdDSA, nil
|
||||
}
|
||||
|
||||
// For DID-based signing, retrieve from DID document
|
||||
if s.didKeeper != nil {
|
||||
didDoc, err := s.didKeeper.GetDIDDocument(ctx, s.issuerDID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get DID document: %w", err)
|
||||
}
|
||||
|
||||
// Use the first verification method for signing
|
||||
if len(didDoc.VerificationMethod) > 0 {
|
||||
vm := didDoc.VerificationMethod[0]
|
||||
|
||||
// Extract key based on verification method kind
|
||||
switch vm.VerificationMethodKind {
|
||||
case "Ed25519VerificationKey2020":
|
||||
// Extract Ed25519 key
|
||||
privateKey, err := s.extractEd25519Key(vm)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s.privateKey = privateKey
|
||||
s.signingMethod = jwt.SigningMethodEdDSA
|
||||
return privateKey, jwt.SigningMethodEdDSA, nil
|
||||
|
||||
case "EcdsaSecp256k1VerificationKey2019":
|
||||
// Extract Secp256k1 key
|
||||
privateKey, err := s.extractSecp256k1Key(vm)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s.privateKey = privateKey
|
||||
s.signingMethod = jwt.SigningMethodES256
|
||||
return privateKey, jwt.SigningMethodES256, nil
|
||||
|
||||
default:
|
||||
return nil, nil, fmt.Errorf(
|
||||
"unsupported verification method type: %s",
|
||||
vm.VerificationMethodKind,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("no signing key available")
|
||||
}
|
||||
|
||||
// generateServiceKey generates a new service key pair
|
||||
func (s *BlockchainUCANSigner) generateServiceKey() (ed25519.PrivateKey, ed25519.PublicKey, error) {
|
||||
// In production, this should retrieve from secure storage
|
||||
// For now, generate a new key pair
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate key pair: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, publicKey, nil
|
||||
}
|
||||
|
||||
// extractEd25519Key extracts Ed25519 private key from verification method
|
||||
func (s *BlockchainUCANSigner) extractEd25519Key(
|
||||
vm *types.VerificationMethod,
|
||||
) (ed25519.PrivateKey, error) {
|
||||
// In production, this would retrieve the private key from secure storage
|
||||
// based on the public key in the verification method
|
||||
|
||||
// For now, return error as we don't have access to private keys
|
||||
return nil, fmt.Errorf("private key retrieval not implemented")
|
||||
}
|
||||
|
||||
// extractSecp256k1Key extracts Secp256k1 private key from verification method
|
||||
func (s *BlockchainUCANSigner) extractSecp256k1Key(
|
||||
vm *types.VerificationMethod,
|
||||
) (*secp256k1.PrivKey, error) {
|
||||
// In production, this would retrieve the private key from secure storage
|
||||
// based on the public key in the verification method
|
||||
|
||||
// For now, return error as we don't have access to private keys
|
||||
return nil, fmt.Errorf("private key retrieval not implemented")
|
||||
}
|
||||
|
||||
// VerifySignature verifies a UCAN token signature
|
||||
func (s *BlockchainUCANSigner) VerifySignature(tokenString string) (*ucan.Token, error) {
|
||||
// Parse token without verification first to get issuer
|
||||
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid claims format")
|
||||
}
|
||||
|
||||
issuer, ok := claims["iss"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no issuer in token")
|
||||
}
|
||||
|
||||
// Get public key for issuer
|
||||
publicKey, err := s.getPublicKey(context.Background(), issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
|
||||
// Verify token with public key
|
||||
parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
return publicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token verification failed: %w", err)
|
||||
}
|
||||
|
||||
if !parsedToken.Valid {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// Convert JWT claims to UCAN token
|
||||
return s.claimsToUCAN(claims, tokenString)
|
||||
}
|
||||
|
||||
// getPublicKey retrieves public key for a DID
|
||||
func (s *BlockchainUCANSigner) getPublicKey(ctx context.Context, did string) (any, error) {
|
||||
if did == "did:sonr:oauth-provider" {
|
||||
// Return service public key
|
||||
// In production, this would be retrieved from configuration
|
||||
return []byte("oauth-provider-public-key"), nil
|
||||
}
|
||||
|
||||
if s.didKeeper != nil {
|
||||
didDoc, err := s.didKeeper.GetDIDDocument(ctx, did)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DID document: %w", err)
|
||||
}
|
||||
|
||||
if len(didDoc.VerificationMethod) > 0 {
|
||||
vm := didDoc.VerificationMethod[0]
|
||||
|
||||
// Extract public key based on verification method kind
|
||||
switch vm.VerificationMethodKind {
|
||||
case "Ed25519VerificationKey2020":
|
||||
// Decode base64 public key
|
||||
publicKeyBytes, err := base64.StdEncoding.DecodeString(vm.PublicKeyMultibase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ed25519.PublicKey(publicKeyBytes), nil
|
||||
|
||||
case "EcdsaSecp256k1VerificationKey2019":
|
||||
// Decode and return Secp256k1 public key
|
||||
publicKeyBytes, err := base64.StdEncoding.DecodeString(vm.PublicKeyMultibase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return publicKeyBytes, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported verification method type: %s",
|
||||
vm.VerificationMethodKind,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no public key found for DID: %s", did)
|
||||
}
|
||||
|
||||
// claimsToUCAN converts JWT claims to UCAN token
|
||||
func (s *BlockchainUCANSigner) claimsToUCAN(
|
||||
claims jwt.MapClaims,
|
||||
rawToken string,
|
||||
) (*ucan.Token, error) {
|
||||
token := &ucan.Token{
|
||||
Raw: rawToken,
|
||||
}
|
||||
|
||||
// Extract standard claims
|
||||
if iss, ok := claims["iss"].(string); ok {
|
||||
token.Issuer = iss
|
||||
}
|
||||
if aud, ok := claims["aud"].(string); ok {
|
||||
token.Audience = aud
|
||||
}
|
||||
if exp, ok := claims["exp"].(float64); ok {
|
||||
token.ExpiresAt = int64(exp)
|
||||
}
|
||||
if nbf, ok := claims["nbf"].(float64); ok {
|
||||
token.NotBefore = int64(nbf)
|
||||
}
|
||||
|
||||
// Extract attenuations
|
||||
if attClaims, ok := claims["att"].([]any); ok {
|
||||
attenuations := make([]ucan.Attenuation, 0, len(attClaims))
|
||||
for _, attItem := range attClaims {
|
||||
if attMap, ok := attItem.(map[string]any); ok {
|
||||
att, err := s.parseAttenuation(attMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attenuation: %w", err)
|
||||
}
|
||||
attenuations = append(attenuations, att)
|
||||
}
|
||||
}
|
||||
token.Attenuations = attenuations
|
||||
}
|
||||
|
||||
// Extract proofs
|
||||
if proofClaims, ok := claims["prf"].([]any); ok {
|
||||
proofs := make([]ucan.Proof, 0, len(proofClaims))
|
||||
for _, proofItem := range proofClaims {
|
||||
if proofStr, ok := proofItem.(string); ok {
|
||||
proofs = append(proofs, ucan.Proof(proofStr))
|
||||
}
|
||||
}
|
||||
token.Proofs = proofs
|
||||
}
|
||||
|
||||
// Extract facts
|
||||
if factClaims, ok := claims["fct"].([]any); ok {
|
||||
facts := make([]ucan.Fact, 0, len(factClaims))
|
||||
for _, factItem := range factClaims {
|
||||
factData, _ := json.Marshal(factItem)
|
||||
facts = append(facts, ucan.Fact{
|
||||
Data: json.RawMessage(factData),
|
||||
})
|
||||
}
|
||||
token.Facts = facts
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// parseAttenuation parses an attenuation from JWT claims
|
||||
func (s *BlockchainUCANSigner) parseAttenuation(attMap map[string]any) (ucan.Attenuation, error) {
|
||||
// Extract resource URI
|
||||
resourceURI, ok := attMap["with"].(string)
|
||||
if !ok {
|
||||
return ucan.Attenuation{}, fmt.Errorf("missing resource URI")
|
||||
}
|
||||
|
||||
// Parse resource
|
||||
resource := &SimpleResource{
|
||||
Scheme: "generic",
|
||||
Value: resourceURI,
|
||||
}
|
||||
|
||||
// Extract scheme from URI if possible
|
||||
if len(resourceURI) > 0 {
|
||||
for _, scheme := range []string{"vault:", "service:", "did:", "dwn:", "dex:", "pool:"} {
|
||||
if len(resourceURI) >= len(scheme) && resourceURI[:len(scheme)] == scheme {
|
||||
resource.Scheme = scheme[:len(scheme)-1]
|
||||
resource.Value = resourceURI[len(scheme):]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract capability
|
||||
var capability ucan.Capability
|
||||
switch can := attMap["can"].(type) {
|
||||
case string:
|
||||
capability = &ucan.SimpleCapability{Action: can}
|
||||
case []any:
|
||||
actions := make([]string, 0, len(can))
|
||||
for _, action := range can {
|
||||
if actionStr, ok := action.(string); ok {
|
||||
actions = append(actions, actionStr)
|
||||
}
|
||||
}
|
||||
capability = &ucan.MultiCapability{Actions: actions}
|
||||
default:
|
||||
return ucan.Attenuation{}, fmt.Errorf("invalid capability format")
|
||||
}
|
||||
|
||||
return ucan.Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetPrivateKey sets a private key for signing (for testing)
|
||||
func (s *BlockchainUCANSigner) SetPrivateKey(privateKey any, method jwt.SigningMethod) {
|
||||
s.privateKey = privateKey
|
||||
s.signingMethod = method
|
||||
}
|
||||
|
||||
// CreateDelegationToken creates a UCAN token for delegation
|
||||
func (s *BlockchainUCANSigner) CreateDelegationToken(
|
||||
issuer, audience string,
|
||||
attenuations []ucan.Attenuation,
|
||||
proofs []ucan.Proof,
|
||||
expiresIn time.Duration,
|
||||
) (string, error) {
|
||||
token := &ucan.Token{
|
||||
Issuer: issuer,
|
||||
Audience: audience,
|
||||
ExpiresAt: time.Now().Add(expiresIn).Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Attenuations: attenuations,
|
||||
Proofs: proofs,
|
||||
}
|
||||
|
||||
return s.Sign(token)
|
||||
}
|
||||
|
||||
// ValidateDelegationChain validates a chain of UCAN delegations
|
||||
func (s *BlockchainUCANSigner) ValidateDelegationChain(tokens []string) error {
|
||||
if len(tokens) == 0 {
|
||||
return fmt.Errorf("empty delegation chain")
|
||||
}
|
||||
|
||||
var previousToken *ucan.Token
|
||||
for i, tokenString := range tokens {
|
||||
// Verify current token
|
||||
token, err := s.VerifySignature(tokenString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify token %d: %w", i, err)
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().Unix() > token.ExpiresAt {
|
||||
return fmt.Errorf("token %d has expired", i)
|
||||
}
|
||||
|
||||
// Check not before
|
||||
if token.NotBefore > 0 && time.Now().Unix() < token.NotBefore {
|
||||
return fmt.Errorf("token %d not yet valid", i)
|
||||
}
|
||||
|
||||
// Validate delegation chain
|
||||
if previousToken != nil {
|
||||
// Check that previous token's audience matches current issuer
|
||||
if previousToken.Audience != token.Issuer {
|
||||
return fmt.Errorf("broken delegation chain at token %d", i)
|
||||
}
|
||||
|
||||
// Check that capabilities are properly attenuated
|
||||
if !s.isProperlyAttenuated(previousToken.Attenuations, token.Attenuations) {
|
||||
return fmt.Errorf("improper attenuation at token %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
previousToken = token
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isProperlyAttenuated checks if child attenuations are properly attenuated from parent
|
||||
func (s *BlockchainUCANSigner) isProperlyAttenuated(parent, child []ucan.Attenuation) bool {
|
||||
// Child cannot have more permissions than parent
|
||||
for _, childAtt := range child {
|
||||
found := false
|
||||
for _, parentAtt := range parent {
|
||||
// Check if child attenuation is covered by parent
|
||||
if s.attenuationCovers(parentAtt, childAtt) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// attenuationCovers checks if parent attenuation covers child
|
||||
func (s *BlockchainUCANSigner) attenuationCovers(parent, child ucan.Attenuation) bool {
|
||||
// Check resource match
|
||||
if parent.Resource.GetScheme() != child.Resource.GetScheme() {
|
||||
// Wildcard scheme matches all
|
||||
if parent.Resource.GetScheme() != "*" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check capability coverage
|
||||
parentActions := parent.Capability.GetActions()
|
||||
childActions := child.Capability.GetActions()
|
||||
|
||||
// If parent has wildcard, it covers everything
|
||||
for _, action := range parentActions {
|
||||
if action == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check each child action is in parent
|
||||
for _, childAction := range childActions {
|
||||
found := false
|
||||
for _, parentAction := range parentActions {
|
||||
if parentAction == childAction {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RefreshToken creates a new token from an existing one with updated expiration
|
||||
func (s *BlockchainUCANSigner) RefreshToken(
|
||||
tokenString string,
|
||||
newExpiration time.Duration,
|
||||
) (string, error) {
|
||||
// Verify existing token
|
||||
token, err := s.VerifySignature(tokenString)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to verify token: %w", err)
|
||||
}
|
||||
|
||||
// Create new token with same claims but new expiration
|
||||
newToken := &ucan.Token{
|
||||
Issuer: token.Issuer,
|
||||
Audience: token.Audience,
|
||||
ExpiresAt: time.Now().Add(newExpiration).Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Attenuations: token.Attenuations,
|
||||
Proofs: append(token.Proofs, ucan.Proof(tokenString)), // Add original as proof
|
||||
Facts: token.Facts,
|
||||
}
|
||||
|
||||
return s.Sign(newToken)
|
||||
}
|
||||
Reference in New Issue
Block a user