Files
sonr/bridge/handlers/ucan_signer.go
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

* fix: Commenht
2025-10-03 14:45:52 -04:00

603 lines
16 KiB
Go

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)
}