Files
breakpilot-compliance/pca-platform/heuristic-service/internal/stepup/webauthn.go
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

173 lines
4.6 KiB
Go

package stepup
import (
"crypto/rand"
"encoding/base64"
"sync"
"time"
"github.com/breakpilot/pca-platform/heuristic-service/internal/config"
)
// WebAuthnService handles WebAuthn challenges and verification
type WebAuthnService struct {
config *config.WebAuthnConfig
challenges map[string]*Challenge
mu sync.RWMutex
}
// Challenge represents a WebAuthn challenge
type Challenge struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
Challenge string `json:"challenge"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
Verified bool `json:"verified"`
}
// ChallengeRequest is the client-side challenge request format
type ChallengeRequest struct {
SessionID string `json:"session_id"`
}
// ChallengeResponse is the WebAuthn public key request options
type ChallengeResponse struct {
PublicKey PublicKeyCredentialRequestOptions `json:"publicKey"`
}
// PublicKeyCredentialRequestOptions mirrors the WebAuthn API structure
type PublicKeyCredentialRequestOptions struct {
Challenge string `json:"challenge"`
Timeout int `json:"timeout"`
RpID string `json:"rpId,omitempty"`
UserVerification string `json:"userVerification"`
AllowCredentials []PublicKeyCredentialDescriptor `json:"allowCredentials,omitempty"`
}
// PublicKeyCredentialDescriptor for allowed credentials
type PublicKeyCredentialDescriptor struct {
Type string `json:"type"`
ID string `json:"id"`
Transports []string `json:"transports,omitempty"`
}
// VerifyRequest for client verification response
type VerifyRequest struct {
SessionID string `json:"session_id"`
ChallengeID string `json:"challenge_id"`
Credential map[string]interface{} `json:"credential"`
}
// NewWebAuthnService creates a new WebAuthn service
func NewWebAuthnService(cfg *config.WebAuthnConfig) *WebAuthnService {
return &WebAuthnService{
config: cfg,
challenges: make(map[string]*Challenge),
}
}
// CreateChallenge generates a new WebAuthn challenge for a session
func (s *WebAuthnService) CreateChallenge(sessionID string) (*ChallengeResponse, error) {
// Generate random challenge bytes
challengeBytes := make([]byte, 32)
if _, err := rand.Read(challengeBytes); err != nil {
return nil, err
}
challengeStr := base64.RawURLEncoding.EncodeToString(challengeBytes)
// Generate challenge ID
idBytes := make([]byte, 16)
rand.Read(idBytes)
challengeID := base64.RawURLEncoding.EncodeToString(idBytes)
// Create challenge
challenge := &Challenge{
ID: challengeID,
SessionID: sessionID,
Challenge: challengeStr,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.config.TimeoutMs) * time.Millisecond),
Verified: false,
}
// Store challenge
s.mu.Lock()
s.challenges[challengeID] = challenge
s.mu.Unlock()
// Build response
response := &ChallengeResponse{
PublicKey: PublicKeyCredentialRequestOptions{
Challenge: challengeStr,
Timeout: s.config.TimeoutMs,
UserVerification: s.config.UserVerification,
// In production, you'd include allowed credentials from user registration
AllowCredentials: []PublicKeyCredentialDescriptor{},
},
}
return response, nil
}
// VerifyChallenge verifies a WebAuthn assertion response
func (s *WebAuthnService) VerifyChallenge(req *VerifyRequest) (bool, error) {
s.mu.RLock()
challenge, exists := s.challenges[req.ChallengeID]
s.mu.RUnlock()
if !exists {
return false, nil
}
// Check expiration
if time.Now().After(challenge.ExpiresAt) {
s.mu.Lock()
delete(s.challenges, req.ChallengeID)
s.mu.Unlock()
return false, nil
}
// Check session match
if challenge.SessionID != req.SessionID {
return false, nil
}
// In production, you would:
// 1. Parse the credential response
// 2. Verify the signature against stored public key
// 3. Verify the challenge matches
// 4. Check the origin
// For MVP, we accept any valid-looking response
// Verify credential structure exists
if req.Credential == nil {
return false, nil
}
// Mark as verified
s.mu.Lock()
challenge.Verified = true
s.mu.Unlock()
return true, nil
}
// CleanupExpiredChallenges removes expired challenges
func (s *WebAuthnService) CleanupExpiredChallenges() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for id, challenge := range s.challenges {
if now.After(challenge.ExpiresAt) {
delete(s.challenges, id)
}
}
}
// IsEnabled returns whether WebAuthn is enabled
func (s *WebAuthnService) IsEnabled() bool {
return s.config.Enabled
}