Files
mpc/pkg/webauthn/liveness_verifier.go
zeekay ec0fb82eaa webauthn: vendor-neutral pluggable liveness Verifier; drop
lux OSS names no PAD-2 / liveness vendor. Decomplect the verifier into a
pluggable extension point so a white-label deployment (e.g. liquidity)
registers its own provider without forking:

- pkg/webauthn: add Verifier interface + Binding (vendor-neutral per-call
  input) + Ed25519EnvelopeVerifier (OSS default, delegates to the audited
  VerifyLiveness) + RegisterVerifier/LookupVerifier registry. Tests cover
  the default verifier, the registry, and a custom non-Ed25519 scheme.
- pkg/api: resolve the active Verifier at boot from MPC_LIVENESS_PROVIDER_ID
  (registered custom wins; else Ed25519 from MPC_LIVENESS_PUBKEY_ED25519;
  else nil → enroll fails closed 503). handleBiometricEnroll calls
  s.livenessVerifier.Verify(...) — no vendor code in lux.
- Rename MPC__* env vars → MPC_LIVENESS_*; drop every 
  identifier/comment across code + LLM.md. Zero /onyxd in lux/mpc.

Also finish the half-done decomplect from a326cc6 'rip zapauth gate' that
left pkg/api/zap_kms_server.go calling a removed checkAuth and missing the
encoding/json import — restore the import, drop the orphaned guard.

(Pre-existing, unrelated: cmd/mpcd still references thresholdd.NewZapServer
from the same in-flight WIP — not touched here.)
2026-06-10 21:42:27 -07:00

102 lines
3.7 KiB
Go

package webauthn
import (
"crypto/ed25519"
"sync"
"time"
)
// Binding is the vendor-neutral, per-call input to liveness verification:
// who the caller is, the score/freshness policy, and the enrollment
// ceremony the attestation must be bound to (R3-8). It deliberately holds
// no provider keys — those belong to the Verifier implementation. This is
// the seam that lets a white-label deployment swap the attestation scheme
// without the MPC call sites changing.
type Binding struct {
UserID string
MinScore float64
MaxAge time.Duration
ExpectedCredentialHash string
ExpectedChallengeID string
Mode BindingMode
Now func() time.Time
WarnFn func(string)
}
// Verifier verifies one provider's liveness attestation against a Binding,
// returning the verified attestation or an error. It is THE extension point
// for identity-verification vendors.
//
// lux OSS ships exactly one implementation — Ed25519EnvelopeVerifier — and
// names no vendor. A white-label deployment (e.g. a regulated broker that
// integrates a specific PAD-2 vendor) either:
//
// - constructs an Ed25519EnvelopeVerifier with its vendor's public key
// (zero code — its attestation is an Ed25519-signed envelope), or
// - implements Verifier for a wholly different scheme (a remote
// attestation service, a different signature suite, …),
//
// and registers it via RegisterVerifier before the MPC server boots. The
// server resolves the verifier by provider id — no fork of this package.
type Verifier interface {
Verify(envelope string, b Binding) (*LivenessAttestation, error)
}
// Ed25519EnvelopeVerifier is the OSS-default Verifier: a JSON attestation
// envelope `{"attestation":{...},"sig":"b64"}` signed with Ed25519 over the
// canonical attestation bytes. It delegates to VerifyLiveness — the same
// audited verification logic — so registering one is the zero-code path for
// any vendor whose attestation is an Ed25519-signed envelope.
type Ed25519EnvelopeVerifier struct {
// PubKey verifies the envelope signature.
PubKey ed25519.PublicKey
// ProviderID, when non-empty, locks attestation.ProviderID to this value.
ProviderID string
}
// Verify implements Verifier.
func (v Ed25519EnvelopeVerifier) Verify(envelope string, b Binding) (*LivenessAttestation, error) {
return VerifyLiveness(envelope, &LivenessOpts{
PubKey: v.PubKey,
ProviderID: v.ProviderID,
UserID: b.UserID,
MinScore: b.MinScore,
MaxAge: b.MaxAge,
ExpectedCredentialHash: b.ExpectedCredentialHash,
ExpectedChallengeID: b.ExpectedChallengeID,
BindingMode: b.Mode,
Now: b.Now,
WarnFn: b.WarnFn,
})
}
// --- registry -------------------------------------------------------------
var (
verifierMu sync.RWMutex
verifiers = map[string]Verifier{}
)
// RegisterVerifier registers a Verifier under a provider id. A white-label
// deployment calls this from an init() or main() before the MPC server
// boots; the server then resolves the verifier for its configured
// MPC_LIVENESS_PROVIDER_ID. Re-registering the same id replaces the prior
// entry (last write wins). Passing a nil Verifier removes the entry.
func RegisterVerifier(providerID string, v Verifier) {
verifierMu.Lock()
defer verifierMu.Unlock()
if v == nil {
delete(verifiers, providerID)
return
}
verifiers[providerID] = v
}
// LookupVerifier returns the Verifier registered under providerID, if any.
func LookupVerifier(providerID string) (Verifier, bool) {
verifierMu.RLock()
defer verifierMu.RUnlock()
v, ok := verifiers[providerID]
return v, ok
}