Files
sonr/x/did/keeper/msg_server.go
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

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

1839 lines
54 KiB
Go

package keeper
import (
"context"
"encoding/base64"
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"cosmossdk.io/errors"
apiv1 "github.com/sonr-io/sonr/api/did/v1"
"github.com/sonr-io/sonr/x/did/types"
)
type msgServer struct {
k Keeper
}
var _ types.MsgServer = msgServer{}
// NewMsgServerImpl returns an implementation of the module MsgServer interface.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{k: keeper}
}
// UCAN validation helper functions
// extractUCANToken extracts UCAN token from transaction context
func (ms msgServer) extractUCANToken(ctx context.Context) (string, bool) {
// In production, UCAN tokens would be extracted from:
// 1. Transaction metadata set by ante handlers
// 2. Message extension fields
// 3. Transaction memo field
// For now, we return false to proceed with normal validation
return "", false
}
// validateUCANPermission validates UCAN authorization for a DID operation
func (ms msgServer) validateUCANPermission(
ctx context.Context,
did string,
controller string,
operation types.DIDOperation,
) error {
// Try to extract UCAN token
tokenString, hasToken := ms.extractUCANToken(ctx)
if !hasToken {
// No UCAN token present, proceed with normal validation
return nil
}
// Validate UCAN token for the specific operation
validator := ms.k.GetPermissionValidator()
if validator == nil {
return fmt.Errorf("UCAN permission validator not initialized")
}
// Use controller-specific validation if controller is provided
if controller != "" {
return validator.ValidateControllerPermission(ctx, tokenString, did, controller, operation)
}
// Otherwise use general permission validation
return validator.ValidatePermission(ctx, tokenString, did, operation)
}
// validateWebAuthnUCANPermission validates UCAN authorization for WebAuthn operations
func (ms msgServer) validateWebAuthnUCANPermission(
ctx context.Context,
did string,
credentialID string,
operation types.DIDOperation,
) error {
// Try to extract UCAN token
tokenString, hasToken := ms.extractUCANToken(ctx)
if !hasToken {
// No UCAN token present, proceed with normal validation
return nil
}
// Validate WebAuthn delegation
validator := ms.k.GetPermissionValidator()
if validator == nil {
return fmt.Errorf("UCAN permission validator not initialized")
}
return validator.ValidateWebAuthnDelegation(ctx, tokenString, did, credentialID, operation)
}
// checkGaslessSupport checks if the operation can be executed gaslessly via UCAN
func (ms msgServer) checkGaslessSupport(
ctx context.Context,
did string,
operation types.DIDOperation,
) (bool, uint64) {
// Try to extract UCAN token
tokenString, hasToken := ms.extractUCANToken(ctx)
if !hasToken {
return false, 0
}
// Check gasless support
validator := ms.k.GetPermissionValidator()
if validator == nil {
return false, 0
}
supportsGasless, gasLimit, err := validator.SupportsGaslessTransaction(
ctx,
tokenString,
did,
operation,
)
if err != nil {
ms.k.Logger().Debug("Failed to check gasless support", "error", err)
return false, 0
}
return supportsGasless, gasLimit
}
func (ms msgServer) UpdateParams(
ctx context.Context,
msg *types.MsgUpdateParams,
) (*types.MsgUpdateParamsResponse, error) {
if ms.k.authority != msg.Authority {
return nil, errors.Wrapf(
govtypes.ErrInvalidSigner,
"invalid authority; expected %s, got %s",
ms.k.authority,
msg.Authority,
)
}
return nil, ms.k.Params.Set(ctx, msg.Params)
}
// CreateDID implements types.MsgServer.
func (ms msgServer) CreateDID(
ctx context.Context,
msg *types.MsgCreateDID,
) (*types.MsgCreateDIDResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.DidDocument.Id, msg.Controller, types.DIDOpCreate); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Check for gasless execution support
supportsGasless, gasLimit := ms.checkGaslessSupport(ctx, msg.DidDocument.Id, types.DIDOpCreate)
if supportsGasless {
// Log gasless execution (in production, this might set transaction fees to zero)
ms.k.Logger().Info("Executing DID creation with gasless transaction",
"did", msg.DidDocument.Id,
"gas_limit", gasLimit)
}
// Check if DID already exists
exists, err := ms.k.OrmDB.DIDDocumentTable().Has(ctx, msg.DidDocument.Id)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToCheckDIDExists, "%s: %v", msg.DidDocument.Id, err)
}
if exists {
return nil, errors.Wrapf(types.ErrDIDAlreadyExists, "%s", msg.DidDocument.Id)
}
// Validate DID document
if err := ms.validateDIDDocument(&msg.DidDocument); err != nil {
return nil, err
}
// Set creation metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
blockTime := sdkCtx.BlockTime().Unix()
didDocument := msg.DidDocument
didDocument.CreatedAt = blockHeight
didDocument.UpdatedAt = blockHeight
didDocument.Deactivated = false
didDocument.Version = 1
// Convert to ORM type and insert
ormDoc := didDocument.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Insert(ctx, ormDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToStoreDIDDocument, "%v", err)
}
// Create DID document metadata
metadata := &types.DIDDocumentMetadata{
Did: didDocument.Id,
Created: blockTime,
Updated: blockTime,
Deactivated: 0,
VersionId: "1",
NextUpdate: 0,
NextVersionId: "",
EquivalentId: []string{},
CanonicalId: didDocument.Id,
}
// Convert metadata to ORM type and insert
ormMetadata := metadata.ToORM()
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Insert(ctx, ormMetadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToStoreDIDMetadata, "%v", err)
}
// Auto-create vault for the new DID
vaultID := fmt.Sprintf("%s-vault", didDocument.Id)
keyID := fmt.Sprintf("%s-key-1", didDocument.Id)
vaultResp, vaultErr := ms.k.CreateVaultForDID(
ctx,
didDocument.Id,
msg.Controller,
vaultID,
keyID,
)
if vaultErr != nil {
// Log warning but don't fail DID creation
ms.k.Logger().With(
"did", didDocument.Id,
"vault_id", vaultID,
"error", vaultErr,
).Warn("Failed to auto-create vault for DID")
}
// Emit typed event
event := &types.EventDIDCreated{
Did: didDocument.Id,
Creator: msg.Controller,
PublicKeys: extractPublicKeys(&didDocument),
Services: extractServiceIds(&didDocument),
CreatedAt: sdkCtx.BlockTime(),
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventDIDCreated")
}
// Build response with vault information if creation succeeded
response := &types.MsgCreateDIDResponse{
Did: didDocument.Id,
}
if vaultResp != nil {
response.VaultId = vaultResp.VaultID
// Convert string public key to bytes
if vaultResp.VaultPublicKey != "" {
pubKeyBytes, err := base64.StdEncoding.DecodeString(vaultResp.VaultPublicKey)
if err == nil {
response.VaultPublicKey = pubKeyBytes
}
}
response.EnclaveId = vaultResp.EnclaveID
}
return response, nil
}
// UpdateDID implements types.MsgServer.
func (ms msgServer) UpdateDID(
ctx context.Context,
msg *types.MsgUpdateDID,
) (*types.MsgUpdateDIDResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.Did, msg.Controller, types.DIDOpUpdate); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
existingDoc := types.DIDDocumentFromORM(ormDoc)
// Store copy of old document for comparison
oldDoc := *existingDoc
// Check if DID is deactivated
if existingDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(existingDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized for DID %s",
msg.Controller,
msg.Did,
)
}
// Validate updated DID document
if err := ms.validateDIDDocument(&msg.DidDocument); err != nil {
return nil, err
}
// Ensure DID ID matches
if msg.DidDocument.Id != msg.Did {
return nil, errors.Wrapf(
types.ErrDIDMismatch,
"document ID %s does not match message DID %s",
msg.DidDocument.Id,
msg.Did,
)
}
// Update metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
blockTime := sdkCtx.BlockTime().Unix()
updatedDoc := msg.DidDocument
updatedDoc.CreatedAt = existingDoc.CreatedAt // Preserve creation time
updatedDoc.UpdatedAt = blockHeight
updatedDoc.Version = existingDoc.Version + 1
updatedDoc.Deactivated = false
// Convert to ORM type and update
ormUpdatedDoc := updatedDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = blockTime
metadata.VersionId = fmt.Sprintf("%d", updatedDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventDIDUpdated{
Did: msg.Did,
Updater: msg.Controller,
FieldsUpdated: extractFieldsUpdated(&oldDoc, &updatedDoc),
UpdatedAt: sdkCtx.BlockTime(),
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventDIDUpdated")
}
return &types.MsgUpdateDIDResponse{}, nil
}
// DeactivateDID implements types.MsgServer.
func (ms msgServer) DeactivateDID(
ctx context.Context,
msg *types.MsgDeactivateDID,
) (*types.MsgDeactivateDIDResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.Did, msg.Controller, types.DIDOpDeactivate); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
existingDoc := types.DIDDocumentFromORM(ormDoc)
// Check if DID is already deactivated
if existingDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDAlreadyDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(existingDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized to deactivate DID %s",
msg.Controller,
msg.Did,
)
}
// Update DID document
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
blockTime := sdkCtx.BlockTime().Unix()
existingDoc.Deactivated = true
existingDoc.UpdatedAt = blockHeight
existingDoc.Version = existingDoc.Version + 1
// Convert to ORM type and update
ormUpdatedDoc := existingDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToDeactivateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = blockTime
metadata.Deactivated = blockTime
metadata.VersionId = fmt.Sprintf("%d", existingDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventDIDDeactivated{
Did: msg.Did,
Deactivator: msg.Controller,
DeactivatedAt: sdkCtx.BlockTime(),
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventDIDDeactivated")
}
return &types.MsgDeactivateDIDResponse{}, nil
}
// AddVerificationMethod implements types.MsgServer.
func (ms msgServer) AddVerificationMethod(
ctx context.Context,
msg *types.MsgAddVerificationMethod,
) (*types.MsgAddVerificationMethodResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.Did, msg.Controller, types.DIDOpAddVerificationMethod); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
didDoc := types.DIDDocumentFromORM(ormDoc)
// Check if DID is deactivated
if didDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(didDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized for DID %s",
msg.Controller,
msg.Did,
)
}
// Validate the new verification method
if err := ms.validateVerificationMethod(&msg.VerificationMethod); err != nil {
return nil, err
}
// Check if verification method ID already exists
for _, vm := range didDoc.VerificationMethod {
if vm.Id == msg.VerificationMethod.Id {
return nil, errors.Wrapf(types.ErrVerificationMethodAlreadyExists, "%s", vm.Id)
}
}
// Add the verification method
didDoc.VerificationMethod = append(didDoc.VerificationMethod, &msg.VerificationMethod)
// Add to specified relationships
for _, relationship := range msg.Relationships {
switch relationship {
case "authentication":
didDoc.Authentication = append(
didDoc.Authentication,
&types.VerificationMethodReference{
VerificationMethodId: msg.VerificationMethod.Id,
},
)
case "assertionMethod":
didDoc.AssertionMethod = append(
didDoc.AssertionMethod,
&types.VerificationMethodReference{
VerificationMethodId: msg.VerificationMethod.Id,
},
)
case "keyAgreement":
didDoc.KeyAgreement = append(didDoc.KeyAgreement, &types.VerificationMethodReference{
VerificationMethodId: msg.VerificationMethod.Id,
})
case "capabilityInvocation":
didDoc.CapabilityInvocation = append(
didDoc.CapabilityInvocation,
&types.VerificationMethodReference{
VerificationMethodId: msg.VerificationMethod.Id,
},
)
case "capabilityDelegation":
didDoc.CapabilityDelegation = append(
didDoc.CapabilityDelegation,
&types.VerificationMethodReference{
VerificationMethodId: msg.VerificationMethod.Id,
},
)
}
}
// Update metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
didDoc.UpdatedAt = blockHeight
didDoc.Version = didDoc.Version + 1
// Convert to ORM type and update
ormUpdatedDoc := didDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = sdkCtx.BlockTime().Unix()
metadata.VersionId = fmt.Sprintf("%d", didDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventVerificationMethodAdded{
Did: msg.Did,
MethodId: msg.VerificationMethod.Id,
KeyType: msg.VerificationMethod.VerificationMethodKind,
PublicKey: msg.VerificationMethod.PublicKeyMultibase,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventVerificationMethodAdded")
}
return &types.MsgAddVerificationMethodResponse{}, nil
}
// RemoveVerificationMethod implements types.MsgServer.
func (ms msgServer) RemoveVerificationMethod(
ctx context.Context,
msg *types.MsgRemoveVerificationMethod,
) (*types.MsgRemoveVerificationMethodResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.Did, msg.Controller, types.DIDOpRemoveVerificationMethod); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
didDoc := types.DIDDocumentFromORM(ormDoc)
// Check if DID is deactivated
if didDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(didDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized for DID %s",
msg.Controller,
msg.Did,
)
}
// Find and remove the verification method
found := false
var newVerificationMethods []*types.VerificationMethod
for _, vm := range didDoc.VerificationMethod {
if vm.Id != msg.VerificationMethodId {
newVerificationMethods = append(newVerificationMethods, vm)
} else {
found = true
}
}
if !found {
return nil, errors.Wrapf(
types.ErrVerificationMethodNotFound,
"%s",
msg.VerificationMethodId,
)
}
didDoc.VerificationMethod = newVerificationMethods
// Remove from all relationships
didDoc.Authentication = ms.removeVerificationMethodReference(
didDoc.Authentication,
msg.VerificationMethodId,
)
didDoc.AssertionMethod = ms.removeVerificationMethodReference(
didDoc.AssertionMethod,
msg.VerificationMethodId,
)
didDoc.KeyAgreement = ms.removeVerificationMethodReference(
didDoc.KeyAgreement,
msg.VerificationMethodId,
)
didDoc.CapabilityInvocation = ms.removeVerificationMethodReference(
didDoc.CapabilityInvocation,
msg.VerificationMethodId,
)
didDoc.CapabilityDelegation = ms.removeVerificationMethodReference(
didDoc.CapabilityDelegation,
msg.VerificationMethodId,
)
// Update metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
didDoc.UpdatedAt = blockHeight
didDoc.Version = didDoc.Version + 1
// Convert to ORM type and update
ormUpdatedDoc := didDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = sdkCtx.BlockTime().Unix()
metadata.VersionId = fmt.Sprintf("%d", didDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventVerificationMethodRemoved{
Did: msg.Did,
MethodId: msg.VerificationMethodId,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventVerificationMethodRemoved")
}
return &types.MsgRemoveVerificationMethodResponse{}, nil
}
// AddService implements types.MsgServer.
func (ms msgServer) AddService(
ctx context.Context,
msg *types.MsgAddService,
) (*types.MsgAddServiceResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.Did, msg.Controller, types.DIDOpAddService); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
didDoc := types.DIDDocumentFromORM(ormDoc)
// Check if DID is deactivated
if didDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(didDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized for DID %s",
msg.Controller,
msg.Did,
)
}
// Validate the new service
if err := ms.validateService(&msg.Service); err != nil {
return nil, err
}
// Check if service ID already exists
for _, svc := range didDoc.Service {
if svc.Id == msg.Service.Id {
return nil, errors.Wrapf(types.ErrServiceAlreadyExists, "%s", svc.Id)
}
}
// Add the service
didDoc.Service = append(didDoc.Service, &msg.Service)
// Update metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
didDoc.UpdatedAt = blockHeight
didDoc.Version = didDoc.Version + 1
// Convert to ORM type and update
ormUpdatedDoc := didDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = sdkCtx.BlockTime().Unix()
metadata.VersionId = fmt.Sprintf("%d", didDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventServiceAdded{
Did: msg.Did,
ServiceId: msg.Service.Id,
Type: msg.Service.ServiceKind,
Endpoint: msg.Service.SingleEndpoint,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventServiceAdded")
}
return &types.MsgAddServiceResponse{}, nil
}
// RemoveService implements types.MsgServer.
func (ms msgServer) RemoveService(
ctx context.Context,
msg *types.MsgRemoveService,
) (*types.MsgRemoveServiceResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation
if err := ms.validateUCANPermission(ctx, msg.Did, msg.Controller, types.DIDOpRemoveService); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
didDoc := types.DIDDocumentFromORM(ormDoc)
// Check if DID is deactivated
if didDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(didDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized for DID %s",
msg.Controller,
msg.Did,
)
}
// Find and remove the service
found := false
var newServices []*types.Service
for _, svc := range didDoc.Service {
if svc.Id != msg.ServiceId {
newServices = append(newServices, svc)
} else {
found = true
}
}
if !found {
return nil, errors.Wrapf(types.ErrServiceNotFound, "%s", msg.ServiceId)
}
didDoc.Service = newServices
// Update metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
didDoc.UpdatedAt = blockHeight
didDoc.Version = didDoc.Version + 1
// Convert to ORM type and update
ormUpdatedDoc := didDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = sdkCtx.BlockTime().Unix()
metadata.VersionId = fmt.Sprintf("%d", didDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventServiceRemoved{
Did: msg.Did,
ServiceId: msg.ServiceId,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventServiceRemoved")
}
return &types.MsgRemoveServiceResponse{}, nil
}
// IssueVerifiableCredential implements types.MsgServer.
func (ms msgServer) IssueVerifiableCredential(
ctx context.Context,
msg *types.MsgIssueVerifiableCredential,
) (*types.MsgIssueVerifiableCredentialResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// UCAN authorization validation for credential issuing
if err := ms.validateUCANPermission(ctx, msg.Credential.Issuer, msg.Issuer, types.DIDOpIssueCredential); err != nil {
return nil, errors.Wrapf(types.ErrUCANValidationFailed, "UCAN validation failed: %v", err)
}
// Validate issuer DID exists
ormIssuerDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Credential.Issuer)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "issuer %s", msg.Credential.Issuer)
}
// Convert from ORM type
issuerDoc := types.DIDDocumentFromORM(ormIssuerDoc)
// Check if issuer DID is deactivated
if issuerDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "issuer %s", msg.Credential.Issuer)
}
// Validate issuer authorization
if !ms.isAuthorizedController(issuerDoc, msg.Issuer) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"address %s not authorized for issuer DID %s",
msg.Issuer,
msg.Credential.Issuer,
)
}
// Check if credential ID already exists
exists, err := ms.k.OrmDB.VerifiableCredentialTable().Has(ctx, msg.Credential.Id)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToCheckCredentialExists, "%v", err)
}
if exists {
return nil, errors.Wrapf(types.ErrCredentialAlreadyExists, "%s", msg.Credential.Id)
}
// Set credential metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
credential := msg.Credential
credential.IssuedAt = blockHeight
credential.Revoked = false
// Parse expiration date if provided
if credential.ExpirationDate != "" {
// For now, we'll set a default expiration block height
// In a full implementation, this would parse the ISO date
credential.ExpiresAt = blockHeight + 365*24*60*60/6 // ~1 year in blocks (6s blocks)
} else {
credential.ExpiresAt = 0 // No expiration
}
// Convert to ORM type and store the credential
ormCredential := credential.ToORM()
if err := ms.k.OrmDB.VerifiableCredentialTable().Insert(ctx, ormCredential); err != nil {
return nil, errors.Wrapf(types.ErrFailedToStoreCredential, "%v", err)
}
// Emit typed event
credentialType := ""
if len(credential.CredentialKinds) > 0 {
credentialType = credential.CredentialKinds[0]
}
event := &types.EventCredentialIssued{
CredentialId: credential.Id,
Issuer: credential.Issuer,
Subject: credential.Subject,
Type: credentialType,
IssuedAt: sdkCtx.BlockTime(),
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventCredentialIssued")
}
return &types.MsgIssueVerifiableCredentialResponse{
CredentialId: credential.Id,
}, nil
}
// RevokeVerifiableCredential implements types.MsgServer.
func (ms msgServer) RevokeVerifiableCredential(
ctx context.Context,
msg *types.MsgRevokeVerifiableCredential,
) (*types.MsgRevokeVerifiableCredentialResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// Get the credential
ormCredential, err := ms.k.OrmDB.VerifiableCredentialTable().Get(ctx, msg.CredentialId)
if err != nil {
return nil, errors.Wrapf(types.ErrCredentialNotFound, "%s", msg.CredentialId)
}
// Convert from ORM type
credential := types.VerifiableCredentialFromORM(ormCredential)
// Check if already revoked
if credential.Revoked {
return nil, errors.Wrapf(types.ErrCredentialAlreadyRevoked, "%s", msg.CredentialId)
}
// Validate that the revoker is the issuer
ormIssuerDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, credential.Issuer)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "issuer %s", credential.Issuer)
}
// Convert from ORM type
issuerDoc := types.DIDDocumentFromORM(ormIssuerDoc)
// Validate issuer authorization
if !ms.isAuthorizedController(issuerDoc, msg.Issuer) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"address %s not authorized for issuer DID %s",
msg.Issuer,
credential.Issuer,
)
}
// Update credential status
credential.Revoked = true
// Convert to ORM type and update the credential
ormUpdatedCredential := credential.ToORM()
if err := ms.k.OrmDB.VerifiableCredentialTable().Update(ctx, ormUpdatedCredential); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateCredential, "%v", err)
}
// Emit typed event
sdkCtx := sdk.UnwrapSDKContext(ctx)
event := &types.EventCredentialRevoked{
CredentialId: msg.CredentialId,
Revoker: msg.Issuer,
Reason: msg.RevocationReason,
RevokedAt: sdkCtx.BlockTime(),
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventCredentialRevoked")
}
return &types.MsgRevokeVerifiableCredentialResponse{}, nil
}
// LinkExternalWallet implements types.MsgServer.
func (ms msgServer) LinkExternalWallet(
ctx context.Context,
msg *types.MsgLinkExternalWallet,
) (*types.MsgLinkExternalWalletResponse, error) {
// Validate basic message
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// Get existing DID document
ormDoc, err := ms.k.OrmDB.DIDDocumentTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", msg.Did)
}
// Convert from ORM type
didDoc := types.DIDDocumentFromORM(ormDoc)
// Check if DID is deactivated
if didDoc.Deactivated {
return nil, errors.Wrapf(types.ErrDIDDeactivated, "%s", msg.Did)
}
// Validate controller authorization
if !ms.isAuthorizedController(didDoc, msg.Controller) {
return nil, errors.Wrapf(
types.ErrUnauthorized,
"controller %s not authorized for DID %s",
msg.Controller,
msg.Did,
)
}
// Validate wallet type
walletType := types.WalletType(msg.WalletType)
if err := walletType.Validate(); err != nil {
return nil, err
}
// Validate DWN vault controller requirement
if err := ms.k.ValidateDWNVaultController(ctx, msg.Did); err != nil {
return nil, errors.Wrap(types.ErrDWNVaultControllerRequired, err.Error())
}
// Check if wallet is already linked to any DID
if err := ms.k.CheckWalletNotAlreadyLinked(ctx, msg.WalletAddress, msg.WalletChainId, walletType); err != nil {
return nil, errors.Wrap(types.ErrWalletAlreadyLinked, err.Error())
}
// Verify wallet ownership through signature
verifyErr := ms.k.VerifyWalletOwnership(
ctx,
msg.WalletAddress,
msg.WalletChainId,
walletType,
msg.Challenge,
msg.OwnershipProof,
)
if verifyErr != nil {
return nil, verifyErr
}
// Check if verification method ID already exists
for _, vm := range didDoc.VerificationMethod {
if vm.Id == msg.VerificationMethodId {
return nil, errors.Wrapf(types.ErrVerificationMethodAlreadyExists, "%s", vm.Id)
}
}
// Create the verification method for the external wallet
verificationMethod, err := ms.k.CreateVerificationMethodFromWallet(
msg.VerificationMethodId,
msg.Did,
msg.WalletAddress,
msg.WalletChainId,
walletType,
)
if err != nil {
return nil, errors.Wrapf(
types.ErrInvalidVerificationMethod,
"failed to create verification method: %v",
err,
)
}
// Add the verification method to the DID document
didDoc.VerificationMethod = append(didDoc.VerificationMethod, verificationMethod)
// Add to assertion method relationship (required for external wallets)
didDoc.AssertionMethod = append(didDoc.AssertionMethod, &types.VerificationMethodReference{
VerificationMethodId: msg.VerificationMethodId,
})
// Update metadata
sdkCtx := sdk.UnwrapSDKContext(ctx)
blockHeight := sdkCtx.BlockHeight()
didDoc.UpdatedAt = blockHeight
didDoc.Version = didDoc.Version + 1
// Convert to ORM type and update
ormUpdatedDoc := didDoc.ToORM()
if err := ms.k.OrmDB.DIDDocumentTable().Update(ctx, ormUpdatedDoc); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDDocument, "%v", err)
}
// Update metadata
metadata, err := ms.k.OrmDB.DIDDocumentMetadataTable().Get(ctx, msg.Did)
if err != nil {
return nil, errors.Wrapf(types.ErrFailedToGetDIDMetadata, "%v", err)
}
metadata.Updated = sdkCtx.BlockTime().Unix()
metadata.VersionId = fmt.Sprintf("%d", didDoc.Version)
if err := ms.k.OrmDB.DIDDocumentMetadataTable().Update(ctx, metadata); err != nil {
return nil, errors.Wrapf(types.ErrFailedToUpdateDIDMetadata, "%v", err)
}
// Emit typed event
event := &types.EventExternalWalletLinked{
Did: msg.Did,
WalletType: msg.WalletType,
WalletAddress: msg.WalletAddress,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventExternalWalletLinked")
}
return &types.MsgLinkExternalWalletResponse{
VerificationMethodId: msg.VerificationMethodId,
}, nil
}
// Helper functions for DID operations
// validateDIDDocument validates a W3C DID document structure
func (ms msgServer) validateDIDDocument(doc *types.DIDDocument) error {
// Validate required fields
if doc.Id == "" {
return types.ErrMissingDIDDocumentID
}
// Validate DID syntax (basic check)
if !ms.isValidDIDSyntax(doc.Id) {
return types.ErrInvalidDIDSyntax
}
// Validate verification methods
for _, vm := range doc.VerificationMethod {
if err := ms.validateVerificationMethod(vm); err != nil {
return errors.Wrapf(types.ErrInvalidVerificationMethod, "%s: %v", vm.Id, err)
}
}
// Validate services
for _, service := range doc.Service {
if err := ms.validateService(service); err != nil {
return errors.Wrapf(types.ErrInvalidService, "%s: %v", service.Id, err)
}
}
return nil
}
// validateVerificationMethod validates a verification method structure
func (ms msgServer) validateVerificationMethod(vm *types.VerificationMethod) error {
if vm.Id == "" {
return types.ErrMissingVerificationMethodID
}
if vm.VerificationMethodKind == "" {
return types.ErrMissingVerificationMethodKind
}
if vm.Controller == "" {
return types.ErrMissingVerificationMethodController
}
// Check that at least one public key material is provided
hasStandardKey := vm.PublicKeyJwk != "" || vm.PublicKeyMultibase != "" ||
vm.PublicKeyBase58 != "" ||
vm.PublicKeyBase64 != "" ||
vm.PublicKeyPem != "" ||
vm.PublicKeyHex != ""
// Check for WebAuthn credential
hasWebAuthnKey := vm.WebauthnCredential != nil && vm.WebauthnCredential.CredentialId != ""
if !hasStandardKey && !hasWebAuthnKey {
return types.ErrMissingVerificationMethodKey
}
// If WebAuthn credential is present, validate it
if hasWebAuthnKey {
if err := ms.validateWebAuthnCredentialId(vm.WebauthnCredential.CredentialId); err != nil {
return err
}
}
return nil
}
// validateWebAuthnCredentialId validates a WebAuthn credential ID
func (ms msgServer) validateWebAuthnCredentialId(credentialId string) error {
if credentialId == "" {
return errors.Wrap(types.ErrInvalidVerificationMethod, "WebAuthn credential ID is required")
}
// WebAuthn credential validation is now handled by types/webauthn package
// Additional validation should use types/webauthn validation functions
return nil
}
// validateService validates a service endpoint structure
func (ms msgServer) validateService(service *types.Service) error {
if service.Id == "" {
return types.ErrMissingServiceID
}
if service.ServiceKind == "" {
return types.ErrMissingServiceKind
}
// Check that at least one endpoint is provided
if service.SingleEndpoint == "" && service.MultipleEndpoints == nil &&
service.ComplexEndpoint == nil {
return types.ErrMissingServiceEndpoint
}
return nil
}
// isValidDIDSyntax validates DID syntax according to W3C DID Core specification
// ABNF: did = "did:" method-name ":" method-specific-id
func (ms msgServer) isValidDIDSyntax(did string) bool {
// Minimum length check: "did:x:y" = 7 characters
if len(did) < 7 {
return false
}
// Must start with "did:"
if !strings.HasPrefix(did, "did:") {
return false
}
// Split into components
parts := strings.SplitN(did[4:], ":", 2)
if len(parts) != 2 {
return false // Missing method-name or method-specific-id
}
methodName := parts[0]
methodSpecificID := parts[1]
// Validate method-name: 1*method-char
// method-char = %x61-7A / DIGIT (lowercase letters a-z or digits 0-9)
if !ms.isValidMethodName(methodName) {
return false
}
// Validate method-specific-id: *( *idchar ":" ) 1*idchar
if !ms.isValidMethodSpecificID(methodSpecificID) {
return false
}
return true
}
// isValidMethodName validates DID method name according to W3C spec
// ABNF: method-name = 1*method-char
// method-char = %x61-7A / DIGIT
func (ms msgServer) isValidMethodName(methodName string) bool {
if len(methodName) == 0 {
return false
}
for _, ch := range methodName {
// Must be lowercase letter (a-z) or digit (0-9)
if !((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
return false
}
}
return true
}
// isValidMethodSpecificID validates method-specific-id according to W3C spec
// ABNF: method-specific-id = *( *idchar ":" ) 1*idchar
// idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded
func (ms msgServer) isValidMethodSpecificID(id string) bool {
if len(id) == 0 {
return false
}
// Split by ':' to validate each segment
segments := strings.Split(id, ":")
for _, segment := range segments {
// Each segment must contain at least one idchar
if len(segment) == 0 {
// Empty segment is allowed except for the last one
continue
}
if !ms.isValidIDSegment(segment) {
return false
}
}
// Ensure the last segment is not empty
if len(segments) > 0 && len(segments[len(segments)-1]) == 0 {
return false
}
return true
}
// isValidIDSegment validates a segment of method-specific-id
// idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded
func (ms msgServer) isValidIDSegment(segment string) bool {
i := 0
for i < len(segment) {
ch := segment[i]
// Check for percent-encoded characters (%HEXDIG HEXDIG)
if ch == '%' {
if i+2 >= len(segment) {
return false // Not enough characters for percent-encoding
}
// Validate next two characters are hex digits
if !isHexDigit(segment[i+1]) || !isHexDigit(segment[i+2]) {
return false
}
i += 3
continue
}
// Check for valid idchar: ALPHA / DIGIT / "." / "-" / "_"
if !isValidIDChar(ch) {
return false
}
i++
}
return true
}
// isValidIDChar checks if a character is valid for idchar (excluding percent-encoding)
func isValidIDChar(ch byte) bool {
return (ch >= 'A' && ch <= 'Z') || // Uppercase letters
(ch >= 'a' && ch <= 'z') || // Lowercase letters
(ch >= '0' && ch <= '9') || // Digits
ch == '.' || ch == '-' || ch == '_' // Special characters
}
// isHexDigit checks if a character is a valid hexadecimal digit
func isHexDigit(ch byte) bool {
return (ch >= '0' && ch <= '9') ||
(ch >= 'A' && ch <= 'F') ||
(ch >= 'a' && ch <= 'f')
}
// isAuthorizedController validates controller authorization through verification methods
func (ms msgServer) isAuthorizedController(doc *types.DIDDocument, controller string) bool {
// Check if controller is the primary controller
if doc.PrimaryController == controller {
return true
}
// Check if the controller owns any verification method with capability rights
for _, vm := range doc.VerificationMethod {
// Check if this verification method is controlled by the requesting controller
if vm.Controller == controller {
// Check if this verification method is referenced in capability relationships
if ms.hasCapabilityRights(doc, vm.Id) {
return true
}
}
}
// Check for delegation through controller DIDs
if ms.hasControllerDelegation(doc, controller) {
return true
}
return false
}
// hasCapabilityRights checks if a verification method has capability rights
func (ms msgServer) hasCapabilityRights(doc *types.DIDDocument, vmID string) bool {
// Check capabilityInvocation - allows invoking capabilities on behalf of the DID
for _, ref := range doc.CapabilityInvocation {
if ref.VerificationMethodId == vmID {
return true
}
}
// Check capabilityDelegation - allows delegating capabilities to others
for _, ref := range doc.CapabilityDelegation {
if ref.VerificationMethodId == vmID {
return true
}
}
return false
}
// hasControllerDelegation checks if a controller has delegation through controller relationships
func (ms msgServer) hasControllerDelegation(doc *types.DIDDocument, controller string) bool {
// Check if controller is referenced in verification methods as a delegated controller
for _, vm := range doc.VerificationMethod {
// A verification method can delegate control if:
// 1. Its controller field references the requesting controller
// 2. It has capability delegation rights
if vm.Controller == controller {
// Check if any capability delegation references point to this VM
for _, ref := range doc.CapabilityDelegation {
// Find the verification method that has delegation rights
delegatingVM := ms.findVerificationMethod(doc, ref.VerificationMethodId)
if delegatingVM != nil && delegatingVM.Controller == doc.Id {
// The DID document owner has delegated capabilities
return true
}
}
}
}
return false
}
// findVerificationMethod finds a verification method by ID in the document
func (ms msgServer) findVerificationMethod(
doc *types.DIDDocument,
vmID string,
) *types.VerificationMethod {
for _, vm := range doc.VerificationMethod {
if vm.Id == vmID {
return vm
}
}
return nil
}
// removeVerificationMethodReference removes a verification method reference from a list
func (ms msgServer) removeVerificationMethodReference(
refs []*types.VerificationMethodReference,
vmId string,
) []*types.VerificationMethodReference {
var newRefs []*types.VerificationMethodReference
for _, ref := range refs {
if ref.VerificationMethodId != vmId {
newRefs = append(newRefs, ref)
}
}
return newRefs
}
// RegisterWebAuthnCredential implements types.MsgServer.
func (ms msgServer) RegisterWebAuthnCredential(
ctx context.Context,
msg *types.MsgRegisterWebAuthnCredential,
) (*types.MsgRegisterWebAuthnCredentialResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
// Validate the controller address
controllerAddr, err := sdk.AccAddressFromBech32(msg.Controller)
if err != nil {
return nil, types.ErrInvalidControllerAddress.Wrapf("invalid controller address: %s", err)
}
// Extract assertion information from the message
// The username should contain the assertion value (email/tel)
var assertionType, assertionValue string
if strings.Contains(msg.Username, "@") {
// Email assertion
assertionType = "email"
assertionValue = msg.Username
} else if strings.HasPrefix(msg.Username, "+") || containsOnlyDigits(msg.Username) {
// Phone assertion
assertionType = "tel"
assertionValue = msg.Username
} else {
// Default to username-based assertion
assertionType = "sonr"
assertionValue = msg.Username
}
// Hash the assertion value and create DID
hashedValue := types.HashAssertionValue(assertionValue)
assertionDID := fmt.Sprintf("did:%s:%s", assertionType, hashedValue)
// Check if assertion already exists
existingAssertion, _ := ms.k.OrmDB.AssertionTable().Get(ctx, assertionDID)
if existingAssertion != nil {
return nil, types.ErrDIDAlreadyExists.Wrapf("assertion already registered: %s", assertionDID)
}
// UCAN authorization validation for WebAuthn registration
// For new registrations, we check if a UCAN token grants registration permission
if msg.Username != "" {
// Create a temporary DID pattern for authorization check
didPattern := fmt.Sprintf("sonr:%s", msg.Username)
if err := ms.validateUCANPermission(ctx, didPattern, msg.Controller, types.DIDOpRegisterWebAuthn); err != nil {
// WebAuthn registration can proceed without UCAN for gasless onboarding
ms.k.Logger().Debug("UCAN validation for WebAuthn registration", "error", err)
}
}
// Check if we've exceeded the maximum number of credentials per DID
params, err := ms.k.Params.Get(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get params: %w", err)
}
if params.Webauthn != nil && params.Webauthn.MaxCredentialsPerDid > 0 {
// For new registration, we can check existing credentials if a DID already exists
// For now, we'll proceed with registration and handle limits in future iterations
}
// Create WebAuthn registration data from the message
regData := &WebAuthnRegistrationData{
CredentialID: msg.WebauthnCredential.CredentialId,
RawID: msg.WebauthnCredential.RawId,
ClientDataJSON: msg.WebauthnCredential.ClientDataJson,
AttestationObject: msg.WebauthnCredential.AttestationObject,
Username: msg.Username,
PublicKey: msg.WebauthnCredential.PublicKey,
Algorithm: msg.WebauthnCredential.Algorithm,
Origin: msg.WebauthnCredential.Origin,
}
// Process the WebAuthn registration using existing keeper logic
didDoc, err := ms.k.ProcessWebAuthnRegistration(ctx, regData)
if err != nil {
return nil, err
}
// Update the DID document to set the controller
didDoc.PrimaryController = controllerAddr.String()
// Add assertion methods to the DID document
// 1. Email/Tel assertion
assertionMethod := &types.VerificationMethod{
Id: fmt.Sprintf("%s#assertion-%s", didDoc.Id, assertionType),
Controller: didDoc.Id,
VerificationMethodKind: "AssertionMethod2024",
BlockchainAccountId: assertionDID,
}
didDoc.VerificationMethod = append(didDoc.VerificationMethod, assertionMethod)
didDoc.AssertionMethod = append(didDoc.AssertionMethod, &types.VerificationMethodReference{
VerificationMethodId: assertionMethod.Id,
})
// 2. Sonr account assertion (controller address)
sonrAssertion := &types.VerificationMethod{
Id: fmt.Sprintf("%s#sonr-account", didDoc.Id),
Controller: didDoc.Id,
VerificationMethodKind: "BlockchainAccountId2024",
BlockchainAccountId: fmt.Sprintf("sonr:%s", controllerAddr.String()),
}
didDoc.VerificationMethod = append(didDoc.VerificationMethod, sonrAssertion)
didDoc.AssertionMethod = append(didDoc.AssertionMethod, &types.VerificationMethodReference{
VerificationMethodId: sonrAssertion.Id,
})
// Store the updated DID document
if err := ms.k.storeDIDDocument(ctx, didDoc); err != nil {
return nil, types.ErrFailedToStoreDIDDocument.Wrapf(
"failed to update DID controller: %s",
err,
)
}
// Create Controller entry in ORM
controller := &apiv1.Controller{
Did: didDoc.Id,
Address: controllerAddr.String(),
Subject: assertionValue,
PublicKeyBase64: base64.StdEncoding.EncodeToString(msg.WebauthnCredential.PublicKey),
DidKind: "webauthn",
CreationBlock: sdkCtx.BlockHeight(),
}
if err := ms.k.OrmDB.ControllerTable().Insert(ctx, controller); err != nil {
ms.k.Logger().Error("Failed to store controller", "error", err)
}
// Create Assertion entry in ORM
assertion := &apiv1.Assertion{
Did: assertionDID,
Controller: controllerAddr.String(),
Subject: assertionValue,
PublicKeyBase64: base64.StdEncoding.EncodeToString(msg.WebauthnCredential.PublicKey),
DidKind: assertionType,
CreationBlock: sdkCtx.BlockHeight(),
}
if err := ms.k.OrmDB.AssertionTable().Insert(ctx, assertion); err != nil {
ms.k.Logger().Error("Failed to store assertion", "error", err)
}
// Create Authentication entry in ORM
authentication := &apiv1.Authentication{
Did: didDoc.Id,
Controller: controllerAddr.String(),
Subject: msg.WebauthnCredential.CredentialId,
PublicKeyBase64: base64.StdEncoding.EncodeToString(msg.WebauthnCredential.PublicKey),
DidKind: "webauthn",
CreationBlock: sdkCtx.BlockHeight(),
}
if err := ms.k.OrmDB.AuthenticationTable().Insert(ctx, authentication); err != nil {
ms.k.Logger().Error("Failed to store authentication", "error", err)
}
// Initialize UCAN delegation chain
ucanChain, err := ms.k.InitializeUCANDelegationChain(
ctx,
didDoc.Id,
controllerAddr.String(),
msg.WebauthnCredential.CredentialId,
)
if err != nil {
// Log error but don't fail the registration
// UCAN can be initialized later if needed
ms.k.Logger().Error(
"Failed to initialize UCAN delegation chain",
"did", didDoc.Id,
"error", err,
)
}
// Prepare response
response := &types.MsgRegisterWebAuthnCredentialResponse{
Did: didDoc.Id,
VerificationMethodId: msg.VerificationMethodId,
}
// Add UCAN tokens to response if successfully initialized
if ucanChain != nil {
// Store tokens in response metadata (we may need to add these fields to the response proto)
// For now, emit them as events
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
"ucan_delegation_initialized",
sdk.NewAttribute("did", didDoc.Id),
sdk.NewAttribute("validator_issuer", ucanChain.ValidatorIssuer),
sdk.NewAttribute("expires_at", fmt.Sprintf("%d", ucanChain.ExpiresAt)),
),
)
}
// Auto-create vault if requested
if msg.AutoCreateVault {
// Generate vault and key IDs if not provided
vaultID := fmt.Sprintf("vault-%s", didDoc.Id)
keyID := fmt.Sprintf("key-%s", didDoc.Id)
// Create vault using the DWN keeper integration
vaultResp, err := ms.k.CreateVaultForDID(
ctx,
didDoc.Id,
controllerAddr.String(),
vaultID,
keyID,
)
if err != nil {
// Log error but don't fail the registration
// Vault can be created later if needed
ms.k.Logger().Error(
"Failed to create vault for DID",
"did", didDoc.Id,
"error", err,
)
} else if vaultResp != nil {
// Update response with vault information
response.VaultId = vaultResp.VaultID
// Convert string public key to bytes
if vaultResp.VaultPublicKey != "" {
pubKeyBytes, err := base64.StdEncoding.DecodeString(vaultResp.VaultPublicKey)
if err == nil {
response.VaultPublicKey = pubKeyBytes
}
}
response.EnclaveId = vaultResp.EnclaveID
// Emit vault creation event
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
"vault_created",
sdk.NewAttribute("did", didDoc.Id),
sdk.NewAttribute("vault_id", vaultResp.VaultID),
sdk.NewAttribute("enclave_id", vaultResp.EnclaveID),
),
)
}
}
// Emit typed event
event := &types.EventWebAuthnRegistered{
Did: didDoc.Id,
CredentialId: msg.WebauthnCredential.CredentialId,
AttestationType: msg.WebauthnCredential.AttestationType,
BlockHeight: uint64(sdkCtx.BlockHeight()),
}
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
ms.k.Logger().With("error", err).Error("Failed to emit EventWebAuthnRegistered")
}
return response, nil
}
// Helper functions for extracting event data
// extractPublicKeys extracts public key IDs from DID document
func extractPublicKeys(doc *types.DIDDocument) []string {
keys := make([]string, 0, len(doc.VerificationMethod))
for _, vm := range doc.VerificationMethod {
keys = append(keys, vm.Id)
}
return keys
}
// extractServiceIds extracts service IDs from DID document
func extractServiceIds(doc *types.DIDDocument) []string {
services := make([]string, 0, len(doc.Service))
for _, svc := range doc.Service {
services = append(services, svc.Id)
}
return services
}
// containsOnlyDigits checks if a string contains only digits
func containsOnlyDigits(s string) bool {
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return len(s) > 0
}
// extractFieldsUpdated compares two DID documents and returns updated fields
func extractFieldsUpdated(oldDoc, newDoc *types.DIDDocument) []string {
var fields []string
// Check verification methods
if len(oldDoc.VerificationMethod) != len(newDoc.VerificationMethod) {
fields = append(fields, "verificationMethod")
}
// Check services
if len(oldDoc.Service) != len(newDoc.Service) {
fields = append(fields, "service")
}
// Check authentication
if len(oldDoc.Authentication) != len(newDoc.Authentication) {
fields = append(fields, "authentication")
}
// Check assertion method
if len(oldDoc.AssertionMethod) != len(newDoc.AssertionMethod) {
fields = append(fields, "assertionMethod")
}
// Check key agreement
if len(oldDoc.KeyAgreement) != len(newDoc.KeyAgreement) {
fields = append(fields, "keyAgreement")
}
// Check capability invocation
if len(oldDoc.CapabilityInvocation) != len(newDoc.CapabilityInvocation) {
fields = append(fields, "capabilityInvocation")
}
// Check capability delegation
if len(oldDoc.CapabilityDelegation) != len(newDoc.CapabilityDelegation) {
fields = append(fields, "capabilityDelegation")
}
return fields
}