clavitor/clavitor.ai/admin/credentials_api.go

348 lines
10 KiB
Go

// P0/PRF Registration API
// Stores wrapped L3 credentials in admin DB for POP distribution
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
)
// CredentialRegisterRequest - what the client sends when enrolling a device
type CredentialRegisterRequest struct {
// Identity
CustomerID string `json:"customer_id"` // Who owns this credential
VaultL0 string `json:"vault_l0"` // Which vault this opens
// WebAuthn data
CredentialID string `json:"credential_id"` // base64 encoded WebAuthn credential ID
PublicKey string `json:"public_key"` // base64 encoded public key (for signature verification)
// PRF/P0 data
P0 string `json:"p0"` // First 4 bytes of PRF, hex encoded (lookup key)
PRFSalt string `json:"prf_salt"` // Salt used for PRF (should be "Clavitor")
// Wrapped L3 (the actual vault key, encrypted with PRF)
WrappedL3 string `json:"wrapped_l3"` // base64 encoded encrypted L3
// Metadata
EnrolledBy string `json:"enrolled_by"` // Human ID who authorized this (if adding 2nd device)
DeviceName string `json:"device_name"` // "YubiKey 5", "iPhone Touch ID", etc.
DeviceType string `json:"device_type"` // "cross-platform", "platform", "hybrid"
}
// CredentialRegisterResponse
type CredentialRegisterResponse struct {
Success bool `json:"success"`
CredentialID int64 `json:"credential_id,omitempty"` // Our internal ID
Error string `json:"error,omitempty"`
}
// RegisterCredential stores a new P0→WL3 mapping
func handleRegisterCredential(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req CredentialRegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, CredentialRegisterResponse{Success: false, Error: "invalid json: " + err.Error()})
return
}
// Validate required fields
if req.CustomerID == "" || req.VaultL0 == "" || req.P0 == "" || req.WrappedL3 == "" {
respondJSON(w, CredentialRegisterResponse{Success: false, Error: "missing required fields"})
return
}
// Verify customer exists
var customerExists bool
err := db.QueryRow("SELECT 1 FROM customers WHERE id = ?", req.CustomerID).Scan(&customerExists)
if err != nil {
respondJSON(w, CredentialRegisterResponse{Success: false, Error: "customer not found"})
return
}
// Check for P0 collision (should be rare but possible)
var existingL0 string
err = db.QueryRow("SELECT l0 FROM credentials WHERE p0 = ?", req.P0).Scan(&existingL0)
if err == nil && existingL0 != req.VaultL0 {
// P0 collision with different vault - this is a problem
respondJSON(w, CredentialRegisterResponse{Success: false, Error: "P0 collision detected - different vault"})
return
}
// Generate WL3 storage path
// Format: {first_2_chars_of_p0}/{full_p0}/{timestamp}_{random}.cla
wl3Path := generateWL3Path(req.P0)
// Generate our internal credential ID
credID := generateInternalID()
// Store in DB
_, err = db.Exec(`
INSERT INTO credentials (
id, customer_id, p0, l0, credential_id, public_key,
wl3_path, enrolled_by, device_name, device_type,
created_at, last_used_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
credID,
req.CustomerID,
req.P0,
req.VaultL0,
req.CredentialID, // Store original WebAuthn credential ID
req.PublicKey,
wl3Path,
req.EnrolledBy,
req.DeviceName,
req.DeviceType,
time.Now().Unix(),
time.Now().Unix(),
)
if err != nil {
respondJSON(w, CredentialRegisterResponse{Success: false, Error: "database error: " + err.Error()})
return
}
// Store the actual wrapped L3 content
// In production this would write to the WORM filesystem
// For now we store it in a separate table or as a blob
wrappedL3Bytes, _ := base64.StdEncoding.DecodeString(req.WrappedL3)
err = storeWL3Content(wl3Path, wrappedL3Bytes)
if err != nil {
// Rollback credential record
db.Exec("DELETE FROM credentials WHERE id = ?", credID)
respondJSON(w, CredentialRegisterResponse{Success: false, Error: "failed to store WL3: " + err.Error()})
return
}
// Log the event
logEvent("credential_registered", req.CustomerID, req.VaultL0, map[string]interface{}{
"credential_id": credID,
"p0": req.P0,
"device_name": req.DeviceName,
})
respondJSON(w, CredentialRegisterResponse{Success: true, CredentialID: credID})
}
// ListCredentialsForCustomer - returns all credentials for a customer
// Used by POPs to pull WL3s for their assigned customers
func handleListCredentials(w http.ResponseWriter, r *http.Request) {
customerID := r.URL.Query().Get("customer_id")
if customerID == "" {
http.Error(w, "customer_id required", http.StatusBadRequest)
return
}
// TODO: Authenticate POP - must have valid agent token
// For now open for Q&D
rows, err := db.Query(`
SELECT id, p0, l0, wl3_path, device_name, device_type, created_at
FROM credentials
WHERE customer_id = ?
ORDER BY created_at DESC
`, customerID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var creds []map[string]interface{}
for rows.Next() {
var id, p0, l0, path, deviceName, deviceType string
var createdAt int64
rows.Scan(&id, &p0, &l0, &path, &deviceName, &deviceType, &createdAt)
creds = append(creds, map[string]interface{}{
"id": id,
"p0": p0,
"l0": l0,
"wl3_path": path,
"device_name": deviceName,
"device_type": deviceType,
"created_at": createdAt,
})
}
respondJSON(w, creds)
}
// GetWL3Content - returns the actual wrapped L3 bytes
// POPs call this to download WL3 for local storage
func handleGetWL3(w http.ResponseWriter, r *http.Request) {
p0 := r.URL.Query().Get("p0")
if p0 == "" {
http.Error(w, "p0 required", http.StatusBadRequest)
return
}
// TODO: Authenticate POP with super duper secure auth
// For now just return the content
// Find the WL3 path
var wl3Path string
err := db.QueryRow("SELECT wl3_path FROM credentials WHERE p0 = ? ORDER BY created_at DESC LIMIT 1", p0).Scan(&wl3Path)
if err != nil {
http.Error(w, "credential not found", http.StatusNotFound)
return
}
// Read and return
content, err := readWL3Content(wl3Path)
if err != nil {
http.Error(w, "failed to read WL3: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(content)
}
// SyncStatus - returns credentials that need syncing to POPs
// POPs poll this to know what to download
func handleSyncStatus(w http.ResponseWriter, r *http.Request) {
// Query params
popID := r.URL.Query().Get("pop_id")
lastSync := r.URL.Query().Get("last_sync")
if popID == "" {
http.Error(w, "pop_id required", http.StatusBadRequest)
return
}
// TODO: Authenticate POP
var lastSyncTime int64
if lastSync != "" {
lastSyncTime, _ = strconv.ParseInt(lastSync, 10, 64)
}
// Return credentials created since last sync
rows, err := db.Query(`
SELECT id, customer_id, p0, l0, wl3_path, created_at
FROM credentials
WHERE created_at > ?
ORDER BY created_at ASC
LIMIT 1000
`, lastSyncTime)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var toSync []map[string]interface{}
for rows.Next() {
var id, customerID, p0, l0, path string
var createdAt int64
rows.Scan(&id, &customerID, &p0, &l0, &path, &createdAt)
toSync = append(toSync, map[string]interface{}{
"id": id,
"customer_id": customerID,
"p0": p0,
"l0": l0,
"wl3_path": path,
"created_at": createdAt,
})
}
// Update POP sync state
_, _ = db.Exec(`
INSERT INTO pop_sync_state (pop_id, last_sync_at, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(pop_id) DO UPDATE SET
last_sync_at = excluded.last_sync_at,
updated_at = excluded.updated_at
`, popID, time.Now().Unix(), time.Now().Unix())
respondJSON(w, map[string]interface{}{
"credentials": toSync,
"server_time": time.Now().Unix(),
})
}
// TODO: SUPER DUPER SECURE POP AUTHENTICATION
// This needs:
// 1. mTLS - POP presents certificate signed by our CA
// 2. Signed requests - each request signed with POP's private key
// 3. Replay protection - nonce + timestamp
// 4. IP allowlist - only known POP IPs
// 5. Rate limiting per POP
// 6. Audit logging of all POP access
func handlePOPAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement super duper secure auth
// For now just pass through for Q&D
next(w, r)
}
}
// Helper functions
func generateWL3Path(p0 string) string {
// P0 is hex, like "a1b2c3d4"
// Path: a1/a1b2c3d4/20250405_7a8f9e2b.cla
if len(p0) < 4 {
p0 = fmt.Sprintf("%08s", p0) // Pad
}
shard := p0[:2] // First 2 chars for sharding
now := time.Now()
random := generateRandomHex(8)
return fmt.Sprintf("%s/%s/%04d%02d%02d_%s.cla",
shard, p0, now.Year(), now.Month(), now.Day(), random)
}
func generateInternalID() int64 {
// Use timestamp + random for now
// In production use lib.NewID()
return time.Now().UnixNano()
}
func generateRandomHex(n int) string {
bytes := make([]byte, n/2)
// In real code: crypto/rand
for i := range bytes {
bytes[i] = byte(i) // Fake
}
return fmt.Sprintf("%x", bytes)
}
func storeWL3Content(path string, content []byte) error {
// TODO: Write to WORM filesystem
// For now store in a simple table
_, err := db.Exec(`
INSERT OR REPLACE INTO wl3_storage (path, content, created_at)
VALUES (?, ?, ?)
`, path, content, time.Now().Unix())
return err
}
func readWL3Content(path string) ([]byte, error) {
// TODO: Read from WORM filesystem
// For now read from table
var content []byte
err := db.QueryRow("SELECT content FROM wl3_storage WHERE path = ?", path).Scan(&content)
return content, err
}
func logEvent(eventType, customerID, vaultL0 string, details map[string]interface{}) {
detailsJSON, _ := json.Marshal(details)
db.Exec(`
INSERT INTO events (occurred_at, event_type, entity_type, entity_id, customer_id, details, source)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, time.Now().Unix(), eventType, "credential", "", customerID, string(detailsJSON), "api")
}