2025-10-03 14:45:52 -04:00
|
|
|
package keeper
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"slices"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"cosmossdk.io/collections"
|
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
2025-10-10 11:47:18 -04:00
|
|
|
webauthn "github.com/sonr-io/common/webauthn"
|
|
|
|
|
"github.com/sonr-io/common/webauthn/webauthncbor"
|
2025-10-03 14:45:52 -04:00
|
|
|
"github.com/sonr-io/sonr/x/did/types"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// WebAuthnRegistrationData represents the data from a WebAuthn registration ceremony
|
|
|
|
|
type WebAuthnRegistrationData struct {
|
|
|
|
|
CredentialID string
|
|
|
|
|
RawID string
|
|
|
|
|
ClientDataJSON string
|
|
|
|
|
AttestationObject string
|
|
|
|
|
Username string
|
|
|
|
|
PublicKey []byte
|
|
|
|
|
Algorithm int32
|
|
|
|
|
Origin string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProcessWebAuthnRegistration processes a WebAuthn credential and creates a DID document
|
|
|
|
|
func (k Keeper) ProcessWebAuthnRegistration(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
regData *WebAuthnRegistrationData,
|
|
|
|
|
) (*types.DIDDocument, error) {
|
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
|
|
|
|
|
|
// Generate a new DID
|
|
|
|
|
did := k.generateDID(regData.Username)
|
|
|
|
|
|
|
|
|
|
// Create WebAuthn credential with full attestation data
|
|
|
|
|
webAuthnCredential := &types.WebAuthnCredential{
|
|
|
|
|
CredentialId: regData.CredentialID,
|
|
|
|
|
RawId: regData.RawID,
|
|
|
|
|
ClientDataJson: regData.ClientDataJSON,
|
|
|
|
|
AttestationObject: regData.AttestationObject,
|
|
|
|
|
PublicKey: regData.PublicKey,
|
|
|
|
|
Algorithm: regData.Algorithm,
|
|
|
|
|
AttestationType: "none", // For most platform authenticators
|
|
|
|
|
Origin: regData.Origin,
|
|
|
|
|
CreatedAt: sdkCtx.BlockTime().Unix(),
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 11:47:18 -04:00
|
|
|
// Validate the WebAuthn credential using local types validation
|
|
|
|
|
if err := types.ValidateStructure(webAuthnCredential); err != nil {
|
2025-10-03 14:45:52 -04:00
|
|
|
return nil, fmt.Errorf("WebAuthn credential validation failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for credential uniqueness to prevent replay attacks
|
|
|
|
|
if k.HasExistingCredential(sdkCtx, regData.CredentialID) {
|
|
|
|
|
return nil, fmt.Errorf("WebAuthn credential already exists: %s", regData.CredentialID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create verification method with WebAuthn credential
|
|
|
|
|
verificationMethod := &types.VerificationMethod{
|
|
|
|
|
Id: fmt.Sprintf("%s#webauthn-1", did),
|
|
|
|
|
Controller: did,
|
|
|
|
|
VerificationMethodKind: "WebAuthnCredential2024",
|
|
|
|
|
WebauthnCredential: webAuthnCredential,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create verification method references
|
|
|
|
|
authRef := &types.VerificationMethodReference{
|
|
|
|
|
VerificationMethodId: verificationMethod.Id,
|
|
|
|
|
}
|
|
|
|
|
assertRef := &types.VerificationMethodReference{
|
|
|
|
|
VerificationMethodId: verificationMethod.Id,
|
|
|
|
|
}
|
|
|
|
|
capInvRef := &types.VerificationMethodReference{
|
|
|
|
|
VerificationMethodId: verificationMethod.Id,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create DID document
|
|
|
|
|
didDoc := &types.DIDDocument{
|
|
|
|
|
Id: did,
|
|
|
|
|
PrimaryController: "", // Will be set to the cosmos address later
|
|
|
|
|
VerificationMethod: []*types.VerificationMethod{
|
|
|
|
|
verificationMethod,
|
|
|
|
|
},
|
|
|
|
|
Authentication: []*types.VerificationMethodReference{
|
|
|
|
|
authRef,
|
|
|
|
|
},
|
|
|
|
|
AssertionMethod: []*types.VerificationMethodReference{
|
|
|
|
|
assertRef,
|
|
|
|
|
},
|
|
|
|
|
KeyAgreement: []*types.VerificationMethodReference{},
|
|
|
|
|
CapabilityInvocation: []*types.VerificationMethodReference{
|
|
|
|
|
capInvRef,
|
|
|
|
|
},
|
|
|
|
|
CapabilityDelegation: []*types.VerificationMethodReference{},
|
|
|
|
|
Service: []*types.Service{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store the DID document
|
|
|
|
|
if err := k.storeDIDDocument(ctx, didDoc); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to store DID document: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return didDoc, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateWebAuthnChallenge creates a challenge for WebAuthn registration
|
|
|
|
|
func (k Keeper) CreateWebAuthnChallenge(ctx context.Context, username string) (string, error) {
|
|
|
|
|
// Generate cryptographically secure challenge
|
|
|
|
|
challengeBytes := make([]byte, 32)
|
|
|
|
|
if _, err := rand.Read(challengeBytes); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to generate random challenge: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
challenge := base64.URLEncoding.EncodeToString(challengeBytes)
|
|
|
|
|
|
|
|
|
|
// Store challenge with expiration (in production, use proper session storage)
|
|
|
|
|
// For now, we'll rely on the server-side session management
|
|
|
|
|
|
|
|
|
|
return challenge, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VerifyWebAuthnRegistration verifies a WebAuthn registration response
|
|
|
|
|
func (k Keeper) VerifyWebAuthnRegistration(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
regData *WebAuthnRegistrationData,
|
|
|
|
|
challenge string,
|
|
|
|
|
) error {
|
|
|
|
|
// Decode and verify client data
|
|
|
|
|
clientDataBytes, err := base64.URLEncoding.DecodeString(regData.ClientDataJSON)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to decode client data JSON: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var clientData struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Challenge string `json:"challenge"`
|
|
|
|
|
Origin string `json:"origin"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := json.Unmarshal(clientDataBytes, &clientData); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to parse client data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify type
|
|
|
|
|
if clientData.Type != "webauthn.create" {
|
|
|
|
|
return fmt.Errorf("invalid client data type: %s", clientData.Type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify challenge
|
|
|
|
|
if clientData.Challenge != challenge {
|
|
|
|
|
return fmt.Errorf("challenge mismatch")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify origin (should be localhost for CLI usage)
|
|
|
|
|
if clientData.Origin != "http://localhost" &&
|
|
|
|
|
!k.isValidLocalhost(clientData.Origin) {
|
|
|
|
|
return fmt.Errorf("invalid origin: %s", clientData.Origin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse attestation object and extract public key using CBOR
|
|
|
|
|
publicKey, algorithm, err := k.extractPublicKeyFromAttestation(regData.AttestationObject)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to extract public key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update registration data with extracted information
|
|
|
|
|
regData.PublicKey = publicKey
|
|
|
|
|
regData.Algorithm = algorithm
|
|
|
|
|
regData.Origin = clientData.Origin
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// generateDID generates a new DID identifier
|
|
|
|
|
func (k Keeper) generateDID(username string) string {
|
|
|
|
|
// For now, generate a simple DID based on username and timestamp
|
|
|
|
|
// In production, this should be more sophisticated
|
|
|
|
|
return fmt.Sprintf("did:sonr:%s-%d", username, time.Now().Unix())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// storeDIDDocument stores a DID document in the state
|
|
|
|
|
func (k Keeper) storeDIDDocument(ctx context.Context, didDoc *types.DIDDocument) error {
|
|
|
|
|
// Convert to ORM format and store
|
|
|
|
|
ormDoc := didDoc.ToORM()
|
|
|
|
|
|
|
|
|
|
// Store in the ORM database
|
|
|
|
|
if err := k.OrmDB.DIDDocumentTable().Insert(ctx, ormDoc); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to insert DID document: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isValidLocalhost checks if the origin is a valid localhost URL
|
|
|
|
|
func (k Keeper) isValidLocalhost(origin string) bool {
|
|
|
|
|
validOrigins := []string{
|
|
|
|
|
"http://localhost:8080",
|
|
|
|
|
"http://localhost:8081",
|
|
|
|
|
"http://localhost:8082",
|
|
|
|
|
"http://localhost:8083",
|
|
|
|
|
"http://localhost:8084",
|
|
|
|
|
"http://localhost:8085",
|
|
|
|
|
"http://localhost:8086",
|
|
|
|
|
"http://localhost:8087",
|
|
|
|
|
"http://localhost:8088",
|
|
|
|
|
"http://localhost:8089",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return slices.Contains(validOrigins, origin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extractPublicKeyFromAttestation extracts the public key from WebAuthn attestation object
|
|
|
|
|
// Now leverages the full WebAuthn protocol implementation for proper CBOR parsing
|
|
|
|
|
func (k Keeper) extractPublicKeyFromAttestation(attestationObject string) ([]byte, int32, error) {
|
2025-10-10 11:47:18 -04:00
|
|
|
// Use the local types validation to extract public key
|
|
|
|
|
if err := types.ValidateAttestationObjectFormat(attestationObject); err != nil {
|
2025-10-03 14:45:52 -04:00
|
|
|
return nil, 0, fmt.Errorf("invalid attestation object format: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode the attestation object using the full WebAuthn protocol
|
|
|
|
|
attestationBytes, err := base64.RawURLEncoding.DecodeString(attestationObject)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("failed to decode attestation object: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the attestation object using CBOR
|
|
|
|
|
var attestationObj webauthn.AttestationObject
|
|
|
|
|
if err := webauthncbor.Unmarshal(attestationBytes, &attestationObj); err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("failed to unmarshal attestation object: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unmarshal the authenticator data
|
|
|
|
|
if err := attestationObj.AuthData.Unmarshal(attestationObj.RawAuthData); err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("failed to unmarshal authenticator data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract the attested credential data
|
|
|
|
|
if !attestationObj.AuthData.Flags.HasAttestedCredentialData() {
|
|
|
|
|
return nil, 0, fmt.Errorf("attestation object missing attested credential data")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
publicKey := attestationObj.AuthData.AttData.CredentialPublicKey
|
|
|
|
|
if len(publicKey) == 0 {
|
|
|
|
|
return nil, 0, fmt.Errorf("no public key found in attested credential data")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For now, assume ES256 algorithm. In the future, this could be extracted
|
|
|
|
|
// from the COSE key format in the public key bytes
|
|
|
|
|
algorithm := int32(-7) // ES256
|
|
|
|
|
|
|
|
|
|
return publicKey, algorithm, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetWebAuthnCredentialsByDID retrieves all WebAuthn credentials for a DID
|
|
|
|
|
func (k Keeper) GetWebAuthnCredentialsByDID(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
did string,
|
|
|
|
|
) ([]*types.WebAuthnCredential, error) {
|
|
|
|
|
// Get DID document
|
|
|
|
|
ormDoc, err := k.OrmDB.DIDDocumentTable().Get(ctx, did)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == collections.ErrNotFound {
|
|
|
|
|
return nil, fmt.Errorf("DID document not found: %s", did)
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("failed to get DID document: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
|
|
|
|
|
|
|
|
var credentials []*types.WebAuthnCredential
|
|
|
|
|
for _, vm := range didDoc.VerificationMethod {
|
|
|
|
|
if vm.WebauthnCredential != nil {
|
|
|
|
|
credentials = append(credentials, vm.WebauthnCredential)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return credentials, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ValidateWebAuthnCredential validates a WebAuthn credential exists and is valid
|
|
|
|
|
func (k Keeper) ValidateWebAuthnCredential(ctx context.Context, did, credentialID string) error {
|
|
|
|
|
credentials, err := k.GetWebAuthnCredentialsByDID(ctx, did)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, cred := range credentials {
|
|
|
|
|
if cred.CredentialId == credentialID {
|
|
|
|
|
// Credential found and valid
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf("WebAuthn credential %s not found for DID %s", credentialID, did)
|
|
|
|
|
}
|