mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
* feat: Add Enclave Usage Examples * feat(es/ucan): Add comprehensive integration tests - Create integration.test.ts with full UCAN token lifecycle testing - Cover end-to-end token creation, parsing, and validation - Test capability attenuation and delegation chains - Validate multi-algorithm support and timestamp scenarios - Implement error recovery and performance test scenarios 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * No commit suggestions generated * No commit suggestions generated * chore: Remove migrated components and add migration documentation Removed all code and references for components that have been moved to separate repositories: **Moved to sonr-io/hway:** - bridge/ - HTTP service with OAuth2/OIDC/WebAuthn handlers - cmd/hway/ - Highway service binary - internal/migrations/ - PostgreSQL schema migrations **Moved to sonr-io/motr:** - cmd/motr/ - Motor worker service (WASM vault operations) - cmd/vault/ - Vault CLI tool - crypto/ - Comprehensive cryptographic library - packages/ - TypeScript SDK packages (es, sdk, ui, com, pkl) - web/auth/ - Authentication web application - web/dash/ - Dashboard web application **Updated Configuration:** - Makefile: Removed build/test/release targets for moved components - CLAUDE.md: Simplified to focus on core blockchain components - devbox.json: Removed scripts for moved services - docker-compose.yml: Removed hway, postgres, redis, auth, dash services - .github/scopes.yml: Removed CI scopes for migrated components - .goreleaser.yml: Updated release configuration **Added Migration Documentation:** - MIGRATE_HWAY.md: Comprehensive Highway service architecture and migration guide - MIGRATE_MOTR.md: Comprehensive Motor/Worker/Vault architecture and migration guide These migration documents provide complete context for setting up the new repositories including architecture diagrams, component breakdowns, API documentation, and migration checklists. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * No commit suggestions generated * chore: Remove contracts references and documentation Removed all references to the contracts directory that was migrated to a separate repository. **Changes:** - .gitignore: Removed contract-specific ignore patterns for DAO and wSNR contracts - .gitignore: Removed hway and motr binary references (already migrated) - .rgignore: Removed contracts, chains, and crypto directory references - docs/reference/contracts/: Removed DAO.mdx and wSNR.mdx documentation files This completes the cleanup of migrated components from the repository. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add crypto library migration documentation Added comprehensive migration documentation for the crypto library that was moved to sonr-io/crypto repository. This documentation provides complete context for understanding the cryptographic primitives and protocols used throughout the Sonr ecosystem. ## Key Documentation Added ### MIGRATE_CRYPTO.md Complete documentation of the crypto library covering: **Core Cryptographic Primitives** - Elliptic curve implementations (Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta) - Native curve arithmetic with optimized field operations - Pairing-friendly curves for BLS signatures **Multi-Party Computation (MPC)** - MPC enclave for vault key generation and management - Threshold cryptography (TECDSA, TED25519 with FROST protocol) - Distributed Key Generation (DKG) via Gennaro and FROST protocols - Secret sharing schemes (Shamir, Feldman VSS, Pedersen VSS) **Digital Signature Schemes** - BLS signatures with aggregation support - BBS+ signatures for selective disclosure - Schnorr signatures (standard and Mina/NEM variants) - ECDSA with deterministic nonce generation **Zero-Knowledge Proofs** - Bulletproofs for range proofs - Inner Product Arguments (IPA) - Batch verification support **Advanced Cryptographic Protocols** - Cryptographic accumulators for set membership proofs - Paillier homomorphic encryption - Oblivious Transfer (OT) protocols - Verifiable Random Functions (VRF) **Key Management & Identity** - DID key management with multi-chain support - Multi-algorithm public key handling - Wallet address derivation (Bitcoin, Ethereum, Cosmos, Solana, etc.) **UCAN Integration** - User-Controlled Authorization Networks - Capability delegation and attenuation - JWT-based capability tokens - MPC-enabled UCAN signing **Security Utilities** - AEAD encryption (AES-GCM, AES-SIV) - Argon2 key derivation - ECIES encryption - Secure memory handling ### MIGRATE_MOTR.md Updates Updated Motor migration documentation to clarify that the crypto library is now a separate external dependency at github.com/sonr-io/crypto v1.0.1 ## Repository Context The crypto library has been successfully migrated to its own repository and is published as a Go module. It serves as the foundational cryptographic layer for: - Sonr blockchain (snrd) - DID signatures, vault operations - Highway service (hway) - UCAN token signing, WebAuthn - Motor/Worker (motr) - MPC vault operations, threshold signatures ## Integration Impact All Sonr ecosystem components now depend on the external crypto library: ```go require github.com/sonr-io/crypto v1.0.1 ``` The migration enables independent versioning and maintenance of cryptographic primitives while maintaining security and compatibility across the ecosystem. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * No commit suggestions generated * No commit suggestions generated * No commit suggestions generated --------- Co-authored-by: Claude <noreply@anthropic.com>
1136 lines
32 KiB
Go
1136 lines
32 KiB
Go
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"cosmossdk.io/errors"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/types/query"
|
|
"lukechampine.com/blake3"
|
|
|
|
apiv1 "github.com/sonr-io/sonr/api/did/v1"
|
|
"github.com/sonr-io/common/webauthn"
|
|
"github.com/sonr-io/sonr/x/did/types"
|
|
)
|
|
|
|
var _ types.QueryServer = Querier{}
|
|
|
|
type Querier struct {
|
|
Keeper
|
|
}
|
|
|
|
func NewQuerier(keeper Keeper) Querier {
|
|
return Querier{Keeper: keeper}
|
|
}
|
|
|
|
func (k Querier) Params(
|
|
c context.Context,
|
|
req *types.QueryParamsRequest,
|
|
) (*types.QueryParamsResponse, error) {
|
|
ctx := sdk.UnwrapSDKContext(c)
|
|
|
|
p, err := k.Keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &types.QueryParamsResponse{Params: &p}, nil
|
|
}
|
|
|
|
// ResolveDID implements types.QueryServer.
|
|
func (k Querier) ResolveDID(
|
|
goCtx context.Context,
|
|
req *types.QueryResolveDIDRequest,
|
|
) (*types.QueryResolveDIDResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.Did == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyDID, "DID cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Get DID document from ORM
|
|
ormDoc, err := k.OrmDB.DIDDocumentTable().Get(ctx, req.Did)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", req.Did)
|
|
}
|
|
|
|
// Convert from ORM type
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
|
|
// Get metadata
|
|
ormMetadata, err := k.OrmDB.DIDDocumentMetadataTable().Get(ctx, req.Did)
|
|
if err != nil {
|
|
// Metadata might not exist, which is ok
|
|
ormMetadata = nil
|
|
}
|
|
|
|
var metadata *types.DIDDocumentMetadata
|
|
if ormMetadata != nil {
|
|
metadata = types.DIDDocumentMetadataFromORM(ormMetadata)
|
|
}
|
|
|
|
return &types.QueryResolveDIDResponse{
|
|
DidDocument: didDoc,
|
|
DidDocumentMetadata: metadata,
|
|
}, nil
|
|
}
|
|
|
|
// GetDIDDocument implements types.QueryServer.
|
|
func (k Querier) GetDIDDocument(
|
|
goCtx context.Context,
|
|
req *types.QueryGetDIDDocumentRequest,
|
|
) (*types.QueryGetDIDDocumentResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.Did == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyDID, "DID cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Get DID document from ORM
|
|
ormDoc, err := k.OrmDB.DIDDocumentTable().Get(ctx, req.Did)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", req.Did)
|
|
}
|
|
|
|
// Convert from ORM type
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
|
|
return &types.QueryGetDIDDocumentResponse{
|
|
DidDocument: didDoc,
|
|
}, nil
|
|
}
|
|
|
|
// ListDIDDocuments implements types.QueryServer.
|
|
func (k Querier) ListDIDDocuments(
|
|
goCtx context.Context,
|
|
req *types.QueryListDIDDocumentsRequest,
|
|
) (*types.QueryListDIDDocumentsResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Create pagination query
|
|
var pageReq *query.PageRequest
|
|
if req.Pagination != nil {
|
|
pageReq = req.Pagination
|
|
} else {
|
|
// Default pagination
|
|
pageReq = &query.PageRequest{
|
|
Limit: 100,
|
|
}
|
|
}
|
|
|
|
// List DID documents with pagination
|
|
var documents []*types.DIDDocument
|
|
pageRes := &query.PageResponse{}
|
|
|
|
// Get all documents from the table
|
|
iter, err := k.OrmDB.DIDDocumentTable().List(ctx, apiv1.DIDDocumentPrimaryKey{})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to list DID documents")
|
|
}
|
|
defer iter.Close()
|
|
|
|
// Apply pagination manually
|
|
offset := pageReq.Offset
|
|
limit := pageReq.Limit
|
|
count := uint64(0)
|
|
totalCount := uint64(0)
|
|
|
|
for iter.Next() {
|
|
totalCount++
|
|
|
|
// Skip items before offset
|
|
if count < offset {
|
|
count++
|
|
continue
|
|
}
|
|
|
|
// Stop if we've reached the limit
|
|
if uint64(len(documents)) >= limit && limit > 0 {
|
|
continue
|
|
}
|
|
|
|
ormDoc, err := iter.Value()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get DID document from iterator")
|
|
}
|
|
|
|
// Convert from ORM type
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
documents = append(documents, didDoc)
|
|
count++
|
|
}
|
|
|
|
// Set page response
|
|
pageRes.Total = totalCount
|
|
if uint64(len(documents)) < limit || limit == 0 {
|
|
pageRes.NextKey = nil
|
|
} else {
|
|
pageRes.NextKey = []byte(documents[len(documents)-1].Id)
|
|
}
|
|
|
|
return &types.QueryListDIDDocumentsResponse{
|
|
DidDocuments: documents,
|
|
Pagination: pageRes,
|
|
}, nil
|
|
}
|
|
|
|
// GetDIDDocumentsByController implements types.QueryServer.
|
|
func (k Querier) GetDIDDocumentsByController(
|
|
goCtx context.Context,
|
|
req *types.QueryGetDIDDocumentsByControllerRequest,
|
|
) (*types.QueryGetDIDDocumentsByControllerResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.Controller == "" {
|
|
return nil, errors.Wrap(
|
|
types.ErrInvalidControllerAddress,
|
|
"controller address cannot be empty",
|
|
)
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Get all DID documents by controller using the index
|
|
indexKey := apiv1.DIDDocumentPrimaryControllerIndexKey{}.WithPrimaryController(
|
|
req.Controller,
|
|
)
|
|
iter, err := k.OrmDB.DIDDocumentTable().List(ctx, indexKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to list DID documents by controller")
|
|
}
|
|
defer iter.Close()
|
|
|
|
var documents []*types.DIDDocument
|
|
for iter.Next() {
|
|
ormDoc, err := iter.Value()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get DID document from iterator")
|
|
}
|
|
|
|
// Convert from ORM type
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
documents = append(documents, didDoc)
|
|
}
|
|
|
|
return &types.QueryGetDIDDocumentsByControllerResponse{
|
|
DidDocuments: documents,
|
|
}, nil
|
|
}
|
|
|
|
// GetVerificationMethod implements types.QueryServer.
|
|
func (k Querier) GetVerificationMethod(
|
|
goCtx context.Context,
|
|
req *types.QueryGetVerificationMethodRequest,
|
|
) (*types.QueryGetVerificationMethodResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.Did == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyDID, "DID cannot be empty")
|
|
}
|
|
|
|
if req.MethodId == "" {
|
|
return nil, errors.Wrap(
|
|
types.ErrEmptyVerificationMethodID,
|
|
"verification method ID cannot be empty",
|
|
)
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Get DID document from ORM
|
|
ormDoc, err := k.OrmDB.DIDDocumentTable().Get(ctx, req.Did)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", req.Did)
|
|
}
|
|
|
|
// Convert from ORM type
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
|
|
// Find the verification method
|
|
for _, vm := range didDoc.VerificationMethod {
|
|
if vm.Id == req.MethodId {
|
|
return &types.QueryGetVerificationMethodResponse{
|
|
VerificationMethod: vm,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.Wrapf(types.ErrVerificationMethodNotFound, "%s", req.MethodId)
|
|
}
|
|
|
|
// GetService implements types.QueryServer.
|
|
func (k Querier) GetService(
|
|
goCtx context.Context,
|
|
req *types.QueryGetServiceRequest,
|
|
) (*types.QueryGetServiceResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.Did == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyDID, "DID cannot be empty")
|
|
}
|
|
|
|
if req.ServiceId == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyServiceID, "service ID cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Get DID document from ORM
|
|
ormDoc, err := k.OrmDB.DIDDocumentTable().Get(ctx, req.Did)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrDIDNotFound, "%s", req.Did)
|
|
}
|
|
|
|
// Convert from ORM type
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
|
|
// Find the service
|
|
for _, svc := range didDoc.Service {
|
|
if svc.Id == req.ServiceId {
|
|
return &types.QueryGetServiceResponse{
|
|
Service: svc,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.Wrapf(types.ErrServiceNotFound, "%s", req.ServiceId)
|
|
}
|
|
|
|
// GetVerifiableCredential implements types.QueryServer.
|
|
func (k Querier) GetVerifiableCredential(
|
|
goCtx context.Context,
|
|
req *types.QueryGetVerifiableCredentialRequest,
|
|
) (*types.QueryGetVerifiableCredentialResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.CredentialId == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyCredentialID, "credential ID cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Get credential from ORM
|
|
ormCred, err := k.OrmDB.VerifiableCredentialTable().Get(ctx, req.CredentialId)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrCredentialNotFound, "%s", req.CredentialId)
|
|
}
|
|
|
|
// Convert from ORM type
|
|
credential := types.VerifiableCredentialFromORM(ormCred)
|
|
|
|
return &types.QueryGetVerifiableCredentialResponse{
|
|
Credential: credential,
|
|
}, nil
|
|
}
|
|
|
|
// ListVerifiableCredentials implements types.QueryServer with filtering support.
|
|
func (k Querier) ListVerifiableCredentials(
|
|
goCtx context.Context,
|
|
req *types.QueryListVerifiableCredentialsRequest,
|
|
) (*types.QueryListVerifiableCredentialsResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Create pagination query
|
|
var pageReq *query.PageRequest
|
|
if req.Pagination != nil {
|
|
pageReq = req.Pagination
|
|
} else {
|
|
// Default pagination
|
|
pageReq = &query.PageRequest{
|
|
Limit: 100,
|
|
}
|
|
}
|
|
|
|
var credentials []*types.VerifiableCredential
|
|
pageRes := &query.PageResponse{}
|
|
|
|
// Determine which index to use based on filters
|
|
var iter apiv1.VerifiableCredentialIterator
|
|
var err error
|
|
|
|
if req.Issuer != "" {
|
|
// Use issuer index
|
|
indexKey := apiv1.VerifiableCredentialIssuerIndexKey{}.WithIssuer(req.Issuer)
|
|
iter, err = k.OrmDB.VerifiableCredentialTable().List(ctx, indexKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to list credentials by issuer")
|
|
}
|
|
} else if req.Holder != "" {
|
|
// Use subject/holder index
|
|
indexKey := apiv1.VerifiableCredentialSubjectIndexKey{}.WithSubject(req.Holder)
|
|
iter, err = k.OrmDB.VerifiableCredentialTable().List(ctx, indexKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to list credentials by holder")
|
|
}
|
|
} else {
|
|
// List all credentials
|
|
iter, err = k.OrmDB.VerifiableCredentialTable().
|
|
List(ctx, apiv1.VerifiableCredentialPrimaryKey{})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to list credentials")
|
|
}
|
|
}
|
|
defer iter.Close()
|
|
|
|
// Apply pagination and filters
|
|
offset := pageReq.Offset
|
|
limit := pageReq.Limit
|
|
count := uint64(0)
|
|
totalCount := uint64(0)
|
|
|
|
for iter.Next() {
|
|
ormCred, err := iter.Value()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get credential from iterator")
|
|
}
|
|
|
|
// Convert from ORM type
|
|
credential := types.VerifiableCredentialFromORM(ormCred)
|
|
|
|
// Apply additional filters
|
|
if req.Issuer != "" && credential.Issuer != req.Issuer {
|
|
continue
|
|
}
|
|
if req.Holder != "" && credential.Subject != req.Holder {
|
|
continue
|
|
}
|
|
if !req.IncludeRevoked && credential.Revoked {
|
|
continue
|
|
}
|
|
|
|
totalCount++
|
|
|
|
// Skip items before offset
|
|
if count < offset {
|
|
count++
|
|
continue
|
|
}
|
|
|
|
// Stop if we've reached the limit
|
|
if uint64(len(credentials)) >= limit && limit > 0 {
|
|
continue
|
|
}
|
|
|
|
credentials = append(credentials, credential)
|
|
count++
|
|
}
|
|
|
|
// Set page response
|
|
pageRes.Total = totalCount
|
|
if uint64(len(credentials)) < limit || limit == 0 {
|
|
pageRes.NextKey = nil
|
|
} else if len(credentials) > 0 {
|
|
pageRes.NextKey = []byte(credentials[len(credentials)-1].Id)
|
|
}
|
|
|
|
return &types.QueryListVerifiableCredentialsResponse{
|
|
Credentials: credentials,
|
|
Pagination: pageRes,
|
|
}, nil
|
|
}
|
|
|
|
// getVaultInfoForDID retrieves vault information for a DID
|
|
func (k Querier) getVaultInfoForDID(
|
|
ctx sdk.Context,
|
|
did string,
|
|
) (vaultId string, isEncrypted bool) {
|
|
// Default values
|
|
vaultId = ""
|
|
isEncrypted = false
|
|
|
|
// Check if DWN keeper is available
|
|
if k.dwnKeeper == nil {
|
|
return vaultId, isEncrypted
|
|
}
|
|
|
|
// Query vaults associated with the DID
|
|
vaults, err := k.dwnKeeper.GetVaultsByDID(ctx, did)
|
|
if err != nil || len(vaults) == 0 {
|
|
return vaultId, isEncrypted
|
|
}
|
|
|
|
// Use the first active vault
|
|
for _, vault := range vaults {
|
|
if vault.Status == "active" {
|
|
vaultId = vault.VaultID
|
|
isEncrypted = true // Assume vault storage means encryption
|
|
return vaultId, isEncrypted
|
|
}
|
|
}
|
|
|
|
return vaultId, isEncrypted
|
|
}
|
|
|
|
// GetCredentialsByDID implements types.QueryServer - unified method for all credentials.
|
|
func (k Querier) GetCredentialsByDID(
|
|
goCtx context.Context,
|
|
req *types.QueryGetCredentialsByDIDRequest,
|
|
) (*types.QueryGetCredentialsByDIDResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
if req.Did == "" {
|
|
return nil, errors.Wrap(types.ErrEmptyDID, "DID cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
var credentialInfos []*types.CredentialInfo
|
|
|
|
// 1. Get WebAuthn credentials if requested (default: true)
|
|
if req.IncludeWebauthn || (!req.IncludeVerifiable && !req.IncludeWebauthn) {
|
|
webauthnCreds, err := k.GetWebAuthnCredentialsByDID(ctx, req.Did)
|
|
if err == nil {
|
|
for _, wc := range webauthnCreds {
|
|
info := &types.CredentialInfo{
|
|
Credential: &types.CredentialInfo_WebauthnCredential{
|
|
WebauthnCredential: wc,
|
|
},
|
|
}
|
|
|
|
// Check if stored in vault
|
|
vaultId, isEncrypted := k.getVaultInfoForDID(ctx, req.Did)
|
|
info.VaultId = vaultId
|
|
info.IsEncrypted = isEncrypted
|
|
|
|
credentialInfos = append(credentialInfos, info)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Get Verifiable Credentials if requested (default: true)
|
|
if req.IncludeVerifiable || (!req.IncludeVerifiable && !req.IncludeWebauthn) {
|
|
// Get credentials where DID is issuer
|
|
issuerIndex := apiv1.VerifiableCredentialIssuerIndexKey{}.WithIssuer(req.Did)
|
|
issuerIter, err := k.OrmDB.VerifiableCredentialTable().List(ctx, issuerIndex)
|
|
if err == nil {
|
|
defer issuerIter.Close()
|
|
for issuerIter.Next() {
|
|
ormCred, err := issuerIter.Value()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
credential := types.VerifiableCredentialFromORM(ormCred)
|
|
|
|
// Skip revoked if not requested
|
|
if credential.Revoked && !req.IncludeRevoked {
|
|
continue
|
|
}
|
|
|
|
info := &types.CredentialInfo{
|
|
Credential: &types.CredentialInfo_VerifiableCredential{
|
|
VerifiableCredential: credential,
|
|
},
|
|
}
|
|
|
|
// Check if stored in vault
|
|
vaultId, isEncrypted := k.getVaultInfoForDID(ctx, req.Did)
|
|
info.VaultId = vaultId
|
|
info.IsEncrypted = isEncrypted
|
|
|
|
credentialInfos = append(credentialInfos, info)
|
|
}
|
|
}
|
|
|
|
// Get credentials where DID is holder
|
|
holderIndex := apiv1.VerifiableCredentialSubjectIndexKey{}.WithSubject(req.Did)
|
|
holderIter, err := k.OrmDB.VerifiableCredentialTable().List(ctx, holderIndex)
|
|
if err == nil {
|
|
defer holderIter.Close()
|
|
for holderIter.Next() {
|
|
ormCred, err := holderIter.Value()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
credential := types.VerifiableCredentialFromORM(ormCred)
|
|
|
|
// Skip if already added as issuer
|
|
isDuplicate := false
|
|
for _, existing := range credentialInfos {
|
|
if vc := existing.GetVerifiableCredential(); vc != nil &&
|
|
vc.Id == credential.Id {
|
|
isDuplicate = true
|
|
break
|
|
}
|
|
}
|
|
if isDuplicate {
|
|
continue
|
|
}
|
|
|
|
// Skip revoked if not requested
|
|
if credential.Revoked && !req.IncludeRevoked {
|
|
continue
|
|
}
|
|
|
|
info := &types.CredentialInfo{
|
|
Credential: &types.CredentialInfo_VerifiableCredential{
|
|
VerifiableCredential: credential,
|
|
},
|
|
}
|
|
|
|
// Check if stored in vault
|
|
vaultId, isEncrypted := k.getVaultInfoForDID(ctx, req.Did)
|
|
info.VaultId = vaultId
|
|
info.IsEncrypted = isEncrypted
|
|
|
|
credentialInfos = append(credentialInfos, info)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply pagination
|
|
pageRes := &query.PageResponse{}
|
|
if req.Pagination != nil {
|
|
start := int(req.Pagination.Offset)
|
|
end := start + int(req.Pagination.Limit)
|
|
|
|
if start > len(credentialInfos) {
|
|
credentialInfos = []*types.CredentialInfo{}
|
|
} else if end > len(credentialInfos) {
|
|
credentialInfos = credentialInfos[start:]
|
|
} else {
|
|
credentialInfos = credentialInfos[start:end]
|
|
pageRes.NextKey = []byte("next")
|
|
}
|
|
}
|
|
|
|
pageRes.Total = uint64(len(credentialInfos))
|
|
|
|
return &types.QueryGetCredentialsByDIDResponse{
|
|
Credentials: credentialInfos,
|
|
Pagination: pageRes,
|
|
}, nil
|
|
}
|
|
|
|
// RegisterStart implements types.QueryServer.
|
|
// TODO: Complete implementation per issue #269
|
|
func (k Querier) RegisterStart(
|
|
goCtx context.Context,
|
|
req *types.QueryRegisterStartRequest,
|
|
) (*types.QueryRegisterStartResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
// Validate assertion DID format
|
|
if req.AssertionDid == "" {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "assertion_did cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Check that assertion DID does NOT exist (opposite of LoginStart)
|
|
existingAssertion, err := k.OrmDB.AssertionTable().Get(ctx, req.AssertionDid)
|
|
if err == nil && existingAssertion != nil {
|
|
return nil, errors.Wrapf(types.ErrDIDAlreadyExists, "assertion already exists: %s", req.AssertionDid)
|
|
}
|
|
|
|
// Generate deterministic challenge
|
|
challenge, nonce, err := k.generateWebAuthnChallenge(ctx, req.AssertionDid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to generate challenge")
|
|
}
|
|
|
|
// Get relying party ID from params
|
|
params, err := k.Keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get module params")
|
|
}
|
|
|
|
// Extract user information from assertion DID
|
|
userName, displayName := k.ExtractUserInfoFromAssertionDID(req.AssertionDid)
|
|
user := map[string]string{
|
|
"id": req.AssertionDid, // Full DID as unique identifier
|
|
"name": userName, // Human-readable name (email/phone type)
|
|
"displayName": displayName, // User-friendly display name
|
|
}
|
|
|
|
// Store registration session for later validation
|
|
if err := k.storeRegistrationSession(ctx, req.AssertionDid, challenge, nonce); err != nil {
|
|
return nil, errors.Wrap(err, "failed to store registration session")
|
|
}
|
|
|
|
// Convert challenge string to bytes
|
|
challengeBytes := []byte(challenge)
|
|
|
|
// Get relying party ID from webauthn params
|
|
rpId := ""
|
|
if params.Webauthn != nil {
|
|
rpId = params.Webauthn.DefaultRpId
|
|
}
|
|
|
|
return &types.QueryRegisterStartResponse{
|
|
Challenge: challengeBytes,
|
|
RelyingPartyId: rpId,
|
|
User: user,
|
|
}, nil
|
|
}
|
|
|
|
// LoginStart implements types.QueryServer.
|
|
// TODO: Complete implementation per issue #269
|
|
func (k Querier) LoginStart(
|
|
goCtx context.Context,
|
|
req *types.QueryLoginStartRequest,
|
|
) (*types.QueryLoginStartResponse, error) {
|
|
if req == nil {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "request cannot be nil")
|
|
}
|
|
|
|
// Validate assertion DID format
|
|
if req.AssertionDid == "" {
|
|
return nil, errors.Wrap(types.ErrInvalidRequest, "assertion_did cannot be empty")
|
|
}
|
|
|
|
ctx := sdk.UnwrapSDKContext(goCtx)
|
|
|
|
// Verify assertion DID exists (opposite of RegisterStart)
|
|
assertion, err := k.OrmDB.AssertionTable().Get(ctx, req.AssertionDid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrAssertionNotFound, "assertion DID %s not found", req.AssertionDid)
|
|
}
|
|
|
|
// Log login attempt for security audit trail
|
|
ctx.Logger().Info("WebAuthn login start",
|
|
"assertion_did", req.AssertionDid,
|
|
"controller", assertion.Controller,
|
|
"block_height", ctx.BlockHeight(),
|
|
)
|
|
|
|
// Get controller DID from assertion
|
|
if assertion.Controller == "" {
|
|
return nil, errors.Wrapf(types.ErrInvalidAssertion, "assertion %s has no controller", req.AssertionDid)
|
|
}
|
|
|
|
// Get controller's DID document
|
|
controllerDoc, err := k.OrmDB.DIDDocumentTable().Get(ctx, assertion.Controller)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(types.ErrDIDNotFound, "controller DID %s not found", assertion.Controller)
|
|
}
|
|
|
|
// Check if DID is deactivated
|
|
if controllerDoc.Deactivated {
|
|
return nil, errors.Wrapf(types.ErrDIDDeactivated, "controller DID %s is deactivated", assertion.Controller)
|
|
}
|
|
|
|
// Extract credential IDs from authentication methods (WebAuthn only)
|
|
credentialIds, err := k.ExtractWebAuthnCredentialIDs(ctx, controllerDoc)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to extract WebAuthn credential IDs")
|
|
}
|
|
|
|
if len(credentialIds) == 0 {
|
|
return nil, errors.Wrapf(types.ErrNoCredentials, "no WebAuthn credentials found for controller %s", assertion.Controller)
|
|
}
|
|
|
|
ctx.Logger().Debug("extracted WebAuthn credentials",
|
|
"controller", assertion.Controller,
|
|
"credential_count", len(credentialIds),
|
|
)
|
|
|
|
// Generate deterministic challenge
|
|
challenge, nonce, err := k.generateWebAuthnChallenge(ctx, req.AssertionDid)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to generate challenge")
|
|
}
|
|
|
|
// Get relying party ID from params
|
|
params, err := k.Keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get module params")
|
|
}
|
|
|
|
// Store login session for later validation
|
|
if err := k.storeLoginSession(ctx, req.AssertionDid, challenge, nonce); err != nil {
|
|
return nil, errors.Wrap(err, "failed to store login session")
|
|
}
|
|
|
|
// Convert challenge string to bytes
|
|
challengeBytes := []byte(challenge)
|
|
|
|
// Get relying party ID from webauthn params
|
|
rpId := ""
|
|
if params.Webauthn != nil {
|
|
rpId = params.Webauthn.DefaultRpId
|
|
}
|
|
|
|
return &types.QueryLoginStartResponse{
|
|
CredentialIds: credentialIds,
|
|
Challenge: challengeBytes,
|
|
RelyingPartyId: rpId,
|
|
}, nil
|
|
}
|
|
|
|
// Helper methods for WebAuthn validation
|
|
|
|
// ValidateServiceOrigin validates the service origin for WebAuthn operations.
|
|
// Validation rules:
|
|
// 1. Origin format must be valid (scheme://host[:port])
|
|
// 2. HTTPS is required except for localhost/127.0.0.1
|
|
// 3. Check against x/svc module domain registry (if available)
|
|
// 4. Check against module params allowed origins list
|
|
// 5. Support wildcard subdomain matching (*.example.com)
|
|
func (k Querier) ValidateServiceOrigin(ctx sdk.Context, origin string) error {
|
|
// Validate origin format
|
|
if origin == "" {
|
|
return errors.Wrap(types.ErrInvalidRequest, "origin cannot be empty")
|
|
}
|
|
|
|
// Check for valid scheme
|
|
if !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://") {
|
|
return errors.Wrapf(types.ErrInvalidRequest, "origin must start with http:// or https://, got: %s", origin)
|
|
}
|
|
|
|
// Extract domain from origin
|
|
domain := ExtractDomainFromOrigin(origin)
|
|
if domain == "" {
|
|
return errors.Wrapf(types.ErrInvalidRequest, "invalid origin format: %s", origin)
|
|
}
|
|
|
|
// Allow localhost and 127.0.0.1 with both HTTP and HTTPS
|
|
if k.IsLocalhostOrigin(domain) {
|
|
ctx.Logger().Debug("allowing localhost origin", "origin", origin)
|
|
return nil
|
|
}
|
|
|
|
// Non-localhost origins must use HTTPS
|
|
if strings.HasPrefix(origin, "http://") {
|
|
return errors.Wrapf(types.ErrInvalidRequest, "non-localhost origins must use HTTPS, got: %s", origin)
|
|
}
|
|
|
|
// Try x/svc module verification first (if keeper available)
|
|
if k.serviceKeeper != nil {
|
|
if err := k.serviceKeeper.VerifyOrigin(ctx, origin); err == nil {
|
|
ctx.Logger().Debug("origin verified via x/svc module", "origin", origin)
|
|
return nil
|
|
}
|
|
// Continue to check module params if x/svc verification fails
|
|
ctx.Logger().Debug("origin not found in x/svc registry, checking module params", "origin", origin)
|
|
}
|
|
|
|
// Check module params allowed origins list
|
|
params, err := k.Keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get module params")
|
|
}
|
|
|
|
if params.Webauthn == nil || len(params.Webauthn.AllowedOrigins) == 0 {
|
|
return errors.Wrapf(types.ErrInvalidRequest,
|
|
"origin %s not registered in x/svc and no allowed origins configured in module params", origin)
|
|
}
|
|
|
|
// Check against allowed origins with wildcard support
|
|
for _, allowedOrigin := range params.Webauthn.AllowedOrigins {
|
|
if k.MatchesOrigin(origin, domain, allowedOrigin) {
|
|
ctx.Logger().Debug("origin matched in allowed origins", "origin", origin, "pattern", allowedOrigin)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return errors.Wrapf(types.ErrInvalidRequest,
|
|
"origin %s not registered in x/svc module and not in allowed origins list", origin)
|
|
}
|
|
|
|
// IsLocalhostOrigin checks if the domain is localhost or 127.0.0.1
|
|
func (k Querier) IsLocalhostOrigin(domain string) bool {
|
|
return domain == "localhost" || domain == "127.0.0.1" || domain == "[::1]"
|
|
}
|
|
|
|
// MatchesOrigin checks if an origin matches an allowed origin pattern.
|
|
// Supports wildcard subdomain matching (*.example.com matches app.example.com)
|
|
func (k Querier) MatchesOrigin(fullOrigin, domain, allowedOrigin string) bool {
|
|
// Exact match
|
|
if fullOrigin == allowedOrigin {
|
|
return true
|
|
}
|
|
|
|
// Extract domain from allowed origin for wildcard matching
|
|
allowedDomain := ExtractDomainFromOrigin(allowedOrigin)
|
|
|
|
// Check for wildcard subdomain pattern (*.example.com)
|
|
if strings.HasPrefix(allowedDomain, "*.") {
|
|
baseDomain := strings.TrimPrefix(allowedDomain, "*.")
|
|
// Match if domain ends with .baseDomain
|
|
if strings.HasSuffix(domain, "."+baseDomain) {
|
|
return true
|
|
}
|
|
// Also match the base domain itself (example.com matches *.example.com)
|
|
if domain == baseDomain {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ExtractDomainFromOrigin extracts the domain from an origin URL
|
|
func ExtractDomainFromOrigin(origin string) string {
|
|
// Remove protocol
|
|
domain := strings.TrimPrefix(origin, "https://")
|
|
domain = strings.TrimPrefix(domain, "http://")
|
|
|
|
// Remove path if present (do this before port to handle IPv6)
|
|
if idx := strings.Index(domain, "/"); idx != -1 {
|
|
domain = domain[:idx]
|
|
}
|
|
|
|
// Handle IPv6 addresses (enclosed in brackets)
|
|
if strings.HasPrefix(domain, "[") {
|
|
// Find closing bracket
|
|
if idx := strings.Index(domain, "]"); idx != -1 {
|
|
// Return everything up to and including the closing bracket
|
|
domain = domain[:idx+1]
|
|
}
|
|
} else {
|
|
// For non-IPv6, remove port if present
|
|
if idx := strings.Index(domain, ":"); idx != -1 {
|
|
domain = domain[:idx]
|
|
}
|
|
}
|
|
|
|
return domain
|
|
}
|
|
|
|
// ExtractUserInfoFromAssertionDID extracts human-readable user information from assertion DID.
|
|
// Assertion DID format: did:sonr:email:<blake3_hash> or did:sonr:phone:<blake3_hash>
|
|
// Returns: (name, displayName) suitable for WebAuthn user object
|
|
func (k Querier) ExtractUserInfoFromAssertionDID(assertionDid string) (name string, displayName string) {
|
|
// Remove "did:" prefix
|
|
trimmed := types.TrimDIDMethodPrefix(assertionDid)
|
|
|
|
// Parse format: sonr:email:<hash> or sonr:phone:<hash>
|
|
parts := strings.Split(trimmed, ":")
|
|
if len(parts) < 3 {
|
|
// Fallback if format doesn't match expected
|
|
return assertionDid, assertionDid
|
|
}
|
|
|
|
// parts[0] = "sonr"
|
|
// parts[1] = assertion type (email, phone, etc.)
|
|
// parts[2] = blake3 hash
|
|
assertionType := parts[1]
|
|
hashValue := parts[2]
|
|
|
|
// Create human-readable name based on assertion type
|
|
switch assertionType {
|
|
case "email":
|
|
name = fmt.Sprintf("Email User")
|
|
displayName = fmt.Sprintf("Email (%s...)", hashValue[:8])
|
|
case "phone", "tel":
|
|
name = fmt.Sprintf("Phone User")
|
|
displayName = fmt.Sprintf("Phone (%s...)", hashValue[:8])
|
|
case "github":
|
|
name = fmt.Sprintf("GitHub User")
|
|
displayName = fmt.Sprintf("GitHub (%s...)", hashValue[:8])
|
|
case "google":
|
|
name = fmt.Sprintf("Google User")
|
|
displayName = fmt.Sprintf("Google (%s...)", hashValue[:8])
|
|
default:
|
|
name = fmt.Sprintf("%s User", strings.Title(assertionType))
|
|
displayName = fmt.Sprintf("%s (%s...)", strings.Title(assertionType), hashValue[:8])
|
|
}
|
|
|
|
return name, displayName
|
|
}
|
|
|
|
// ExtractWebAuthnCredentialIDs extracts WebAuthn credential IDs from a DID document's authentication methods.
|
|
// Only returns credentials with WebAuthn type verification methods.
|
|
func (k Querier) ExtractWebAuthnCredentialIDs(ctx sdk.Context, didDoc *apiv1.DIDDocument) ([]string, error) {
|
|
var credentialIds []string
|
|
|
|
// Iterate through authentication verification method references
|
|
for _, authRef := range didDoc.Authentication {
|
|
// Check if it's an embedded verification method
|
|
if authRef.EmbeddedVerificationMethod != nil {
|
|
vm := authRef.EmbeddedVerificationMethod
|
|
|
|
// Check if this is a WebAuthn verification method
|
|
if vm.WebauthnCredential != nil && vm.WebauthnCredential.CredentialId != "" {
|
|
credentialIds = append(credentialIds, vm.WebauthnCredential.CredentialId)
|
|
ctx.Logger().Debug("found WebAuthn credential",
|
|
"credential_id", vm.WebauthnCredential.CredentialId,
|
|
"verification_method_id", vm.Id,
|
|
)
|
|
}
|
|
} else if authRef.VerificationMethodId != "" {
|
|
// It's a reference - need to look up in verificationMethod array
|
|
for _, vm := range didDoc.VerificationMethod {
|
|
if vm.Id == authRef.VerificationMethodId {
|
|
// Check if this is a WebAuthn verification method
|
|
if vm.WebauthnCredential != nil && vm.WebauthnCredential.CredentialId != "" {
|
|
credentialIds = append(credentialIds, vm.WebauthnCredential.CredentialId)
|
|
ctx.Logger().Debug("found WebAuthn credential",
|
|
"credential_id", vm.WebauthnCredential.CredentialId,
|
|
"verification_method_id", vm.Id,
|
|
)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return credentialIds, nil
|
|
}
|
|
|
|
// generateWebAuthnChallenge generates a deterministic cryptographic challenge for WebAuthn ceremonies
|
|
// The challenge is a BLAKE3 hash of: assertion_did || block_height || chain_id || nonce
|
|
// Returns: base64url-encoded challenge (32 bytes), nonce, error
|
|
func (k Querier) generateWebAuthnChallenge(ctx sdk.Context, assertionDid string) (string, string, error) {
|
|
// Generate a unique nonce based on block time and assertion DID
|
|
// This ensures uniqueness even within the same block
|
|
nonce := fmt.Sprintf("%d:%s:%d",
|
|
ctx.BlockHeight(),
|
|
assertionDid,
|
|
ctx.BlockTime().UnixNano())
|
|
|
|
// Construct deterministic input for BLAKE3 hash
|
|
// Format: assertion_did || block_height || chain_id || nonce
|
|
input := fmt.Sprintf("%s||%d||%s||%s",
|
|
assertionDid,
|
|
ctx.BlockHeight(),
|
|
ctx.ChainID(),
|
|
nonce)
|
|
|
|
// Generate BLAKE3 hash (32 bytes)
|
|
hash := blake3.Sum256([]byte(input))
|
|
|
|
// Encode as base64url for WebAuthn (32 bytes meets spec requirement of 16-64 bytes)
|
|
challenge := base64.RawURLEncoding.EncodeToString(hash[:])
|
|
|
|
return challenge, nonce, nil
|
|
}
|
|
|
|
// storeRegistrationSession stores the registration session for later validation
|
|
func (k Querier) storeRegistrationSession(
|
|
ctx sdk.Context,
|
|
assertionDid string,
|
|
challenge string,
|
|
nonce string,
|
|
) error {
|
|
// TODO: Implement session storage in state
|
|
// This will store:
|
|
// - assertion_did
|
|
// - challenge
|
|
// - nonce (for replay prevention)
|
|
// - expiration timestamp (e.g., 5 minutes from now)
|
|
// - session_type: "registration"
|
|
|
|
// For now, just log that we would store this
|
|
ctx.Logger().Debug("storing registration session",
|
|
"assertion_did", assertionDid,
|
|
"challenge_length", len(challenge),
|
|
"nonce", nonce,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// storeLoginSession stores the login session for later validation
|
|
func (k Querier) storeLoginSession(
|
|
ctx sdk.Context,
|
|
assertionDid string,
|
|
challenge string,
|
|
nonce string,
|
|
) error {
|
|
// TODO: Implement session storage in state
|
|
// This will store:
|
|
// - assertion_did
|
|
// - challenge
|
|
// - nonce (for replay prevention)
|
|
// - expiration timestamp (e.g., 5 minutes from now)
|
|
// - session_type: "login"
|
|
|
|
// For now, just log that we would store this
|
|
ctx.Logger().Debug("storing login session",
|
|
"assertion_did", assertionDid,
|
|
"challenge_length", len(challenge),
|
|
"nonce", nonce,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// createWebAuthnOptions creates WebAuthn credential creation options
|
|
func (k Querier) createWebAuthnOptions(challenge, origin, username string) []byte {
|
|
// Extract RP ID from origin
|
|
rpID := ExtractDomainFromOrigin(origin)
|
|
if rpID == "" {
|
|
rpID = "localhost"
|
|
}
|
|
|
|
// Create WebAuthn options
|
|
options := webauthn.PublicKeyCredentialCreationOptions{
|
|
Challenge: webauthn.URLEncodedBase64(challenge),
|
|
RelyingParty: webauthn.RelyingPartyEntity{
|
|
ID: rpID,
|
|
},
|
|
User: webauthn.UserEntity{
|
|
ID: []byte(username),
|
|
DisplayName: username,
|
|
},
|
|
Parameters: []webauthn.CredentialParameter{
|
|
{
|
|
Type: webauthn.PublicKeyCredentialType,
|
|
Algorithm: -7, // ES256
|
|
},
|
|
{
|
|
Type: webauthn.PublicKeyCredentialType,
|
|
Algorithm: -257, // RS256
|
|
},
|
|
},
|
|
Timeout: 60000, // 60 seconds
|
|
AuthenticatorSelection: webauthn.AuthenticatorSelection{
|
|
AuthenticatorAttachment: webauthn.Platform,
|
|
RequireResidentKey: boolPtr(false),
|
|
ResidentKey: webauthn.ResidentKeyRequirementDiscouraged,
|
|
UserVerification: webauthn.VerificationPreferred,
|
|
},
|
|
Attestation: webauthn.PreferNoAttestation,
|
|
}
|
|
|
|
// Marshal to JSON
|
|
optionsJSON, err := json.Marshal(options)
|
|
if err != nil {
|
|
k.logger.Error("Failed to marshal WebAuthn options", "error", err)
|
|
return []byte("{}")
|
|
}
|
|
|
|
return optionsJSON
|
|
}
|
|
|
|
// boolPtr returns a pointer to a bool value
|
|
func boolPtr(b bool) *bool {
|
|
return &b
|
|
}
|