Skip to content

Vero Comparison and Authentication Decision Specification

Version: 1.0.0 Status: Normative Scope: All platforms (iOS, Android, Web)

This document defines the exact formulas, thresholds, and decision logic used to compare entangled latent identity vectors and determine authentication outcomes. Two independent implementations following this specification MUST produce identical authentication decisions given the same input vectors and session parameters.


1. Overview

Authentication in Vero requires evaluating three independent aspects of the entangled vectors:

  1. Facial Identity Similarity -- Do the presenter and verifier vectors encode the same face?
  2. Sequence Fidelity -- Does the embedded light sequence match the expected session key?
  3. Dimensional Verification -- Did the presenter vector originate from a genuine 3D scan?

These three scores are combined into a final authentication decision.


2. Vector Preparation

Before comparison, both the presenter's vector z and the verifier's vector z' must be de-rotated using the session key (see vector-spec.md Section 6.4) and then decomposed into their subspaces.

2.1 De-rotation

Q = generateRotationMatrix(sessionKey)       // See vector-spec.md Section 6.1
z_raw = Q^T * z                              // Presenter's de-rotated vector
z_raw' = Q^T * z'                            // Verifier's de-rotated vector

2.2 Subspace Extraction

z_face    = z_raw[0:128]                     // Face embedding, 128D
z_depth   = z_raw[128:160]                   // Depth features, 32D
z_light   = z_raw[160:231]                   // Light features, 71D

z_face'   = z_raw'[0:128]
z_depth'  = z_raw'[128:160]
z_light'  = z_raw'[160:231]

3. Cosine Similarity

All similarity comparisons use cosine similarity, defined as:

cosineSimilarity(a, b) = dotProduct(a, b) / (||a|| * ||b||)

Where:

dotProduct(a, b) = sum(a[i] * b[i] for i in 0..N-1)
||a|| = sqrt(sum(a[i]^2 for i in 0..N-1))

Since vectors are L2-normalized (see vector-spec.md Section 7), cosine similarity reduces to the dot product:

cosineSimilarity(a, b) = dotProduct(a, b)    // when ||a|| = ||b|| = 1

Range: [-1.0, 1.0] where 1.0 = identical, 0.0 = orthogonal, -1.0 = opposite.

Implementation note: Even with L2-normalized vectors, implementations MUST use the full cosine similarity formula (with denominator) to account for accumulated floating-point error in the normalization step.


4. Facial Identity Similarity

4.1 Computation

faceSimilarity = cosineSimilarity(z_face, z_face')

4.2 Thresholds

Threshold Name Value Description
FACE_MATCH 0.60 Minimum similarity for a positive identity match
FACE_HIGH 0.75 High-confidence identity match
FACE_REJECT 0.40 Below this, definitively different people

4.3 Interpretation

Range Classification
faceSimilarity >= FACE_HIGH High-confidence match
FACE_MATCH <= faceSimilarity < FACE_HIGH Acceptable match
FACE_REJECT <= faceSimilarity < FACE_MATCH Inconclusive
faceSimilarity < FACE_REJECT Definite mismatch

Rationale: The 3D presenter vector z_face and 2D verifier vector z_face' encode the same face at different fidelities (3D scan vs. 2D video observation). The lower threshold of 0.60 accounts for this cross-modal gap. The CNN is specifically trained to align 3D and 2D embeddings in the shared face subspace, but some gap is expected.


5. Sequence Fidelity Scoring

Supersession note (2026-08 — read before implementing this section). The shipping web verifier does NOT gate on §5.3's "two consecutive attempts ≥ 80%" rule, and does NOT reconstruct the key from the vector via the §5.1 auxiliary decoder D(z_light). Both are retained here for historical/spec-completeness only: - Acceptance is governed by ordered acceptance (every data slot observed with non-dark chroma evidence in ≥ 2 loops, every zero positively observed, terminator anchored in ≥ 2 loops, starvation-proof argmax, controller refusal of sub-100% detector completions), because the two-consecutive-80% rule was exploitable by unbalanced streams (an all-ones stream right-align-matches any heavily-one key). This is now defended in depth: the degenerate-key control (crypto-spec.md §4.3.2) removes such keys at generation, and ordered acceptance refuses such streams at decode. - Fidelity comes from the verifier's live optical decode (detector / matched-filter correlator / blind decoder), not from D(z_light). The D(z_light) path is specified-but-not-exercised (a claim-literal profile); the presenter light subspace is scan-phase environmental and does not carry a per-pulse decodable key.

A future revision will replace §5.1/§5.3 with the ordered-acceptance contract as the normative text. Until then, do not build a conforming verifier against §5.3 alone.

Sequence fidelity measures how well the embedded light features in the vector match the expected session key.

The auxiliary decoder D reconstructs the blink sequence from the light subspace:

reconstructedSequence = D(z_light)           // Produces a binary string
expectedBinary = hexToBinary(sessionKeyHex)  // From signal-protocol.md

5.2 Match Percentage Calculation

Match percentage is calculated by comparing the detected/reconstructed binary string against the expected binary string, aligned from the end (right-aligned):

function calculateMatchPercentageFromEnd(detected: string, expected: string) -> number:
    if expected is empty or detected is empty:
        return 0

    expectedLen = length(expected)
    detectedLen = length(detected)
    matchCount = 0
    lenToCompare = min(expectedLen, detectedLen)

    for i in 1..lenToCompare:
        if expected[expectedLen - i] == detected[detectedLen - i]:
            matchCount += 1

    return round((matchCount / expectedLen) * 100)

Key behavior: Comparison proceeds from the END of both strings toward the beginning. This is because the most recently detected bits are the most reliable (closest to the evaluation point in time).

5.3 Real-Time Sequence Detection Fidelity

In addition to CNN-decoded fidelity, the verifier's camera-based detection system independently detects the blink sequence in real time (see signal-protocol.md Section 7). This produces a stream of detection attempts evaluated on each terminator.

Detection attempt evaluation:

function evaluateSequenceCompletion(
    detectedSequence: string,
    expectedBinary: string,
    previousMatchPercentage: number | null
) -> { matchPercentage: number, status: string }:

    matchPercentage = calculateMatchPercentageFromEnd(detectedSequence, expectedBinary)

    // Rule 1: Perfect match
    if matchPercentage >= 100:
        return { matchPercentage, status: "success-100" }

    // Rule 2: Two consecutive attempts at >= 80%
    if previousMatchPercentage != null
       AND previousMatchPercentage >= 80
       AND matchPercentage >= 80:
        return { matchPercentage, status: "success-(2)80" }

    // No success condition met
    return { matchPercentage, status: "idle" }

5.4 Sequence Fidelity Thresholds

Threshold Name Value Description
SEQ_PERFECT 100 Exact match (all bits correct)
SEQ_ACCEPT_SINGLE 100 Minimum for single-attempt acceptance
SEQ_ACCEPT_REPEATED 80 Minimum for two-consecutive-attempt acceptance
SEQ_MINIMUM 60 Below this, sequence is considered undetected

5.5 Fidelity Score Normalization

For the final authentication decision, the sequence fidelity is normalized to [0.0, 1.0]:

sequenceFidelity = matchPercentage / 100.0

6. Dimensional Verification (3D Confidence)

6.1 3D Confidence Score

The depth subspace dimension 159 (z_depth[31]) carries a 3D confidence score, recovered per vector-spec.md §7.1 (revision 2026-08):

confidence3D = derotated[159] / l2Norm(derotated[0:128])   // Range [0.0, 1.0]
// fallback: divide by l2Norm(derotated[160:231]) if the face subspace is
// degenerate (norm < 1e-3); 0.0 if both are degenerate

The raw derotated[159] readout is NOT the score — it is scaled by the fusion-level normalization. Implementations MUST apply the recovery above before comparing against the §6.2 thresholds.

Trust scope: the score is presenter-attested — it defends against an honest client pointed at a flat spoof (photo, screen), not against a malicious client that fabricates depth. It is therefore used as a hard gate and diagnostic, not folded into the weighted score where a spoofer could offset it.

6.2 Thresholds

Threshold Name Value Description
3D_HIGH 0.70 High confidence of genuine 3D scan
3D_LOW 0.30 Below this, likely a 2D source

6.3 Interpretation

Range Classification
confidence3D >= 3D_HIGH Genuine 3D scan (high confidence)
3D_LOW <= confidence3D < 3D_HIGH Inconclusive dimensional source
confidence3D < 3D_LOW Likely 2D source (potential replay)

7. Authentication Decision Logic

7.1 Decision Matrix

The final authentication decision combines all three scores:

function makeAuthenticationDecision(
    faceSimilarity: float,          // [0.0, 1.0] from Section 4
    sequenceFidelity: float,        // [0.0, 1.0] from Section 5
    confidence3D: float,            // [0.0, 1.0] from Section 6 (GATE only, see 7.2)
    hasFaceEmbedding: bool          // false when no ArcFace embedding was available
) -> AuthenticationResult:

    // Hard failures (any one of these rejects immediately)
    if hasFaceEmbedding and faceSimilarity < FACE_REJECT (0.40):
        return REJECT("face_mismatch")

    if sequenceFidelity < SEQ_MINIMUM / 100 (0.60):
        return REJECT("sequence_mismatch")

    // 3D is a GATE, not a weighted term (§7.2). When the depth gate is enabled,
    // a scene that reads flat rejects HERE; confidence3D never enters the score,
    // so a spoofer who mints it freely gains nothing.
    if DEPTH_GATE_ENABLED and confidence3D < DEPTH_REJECT:
        return REJECT("depth_flat")

    // Weighted score over the two VERIFIED axes only (3D excluded). The two
    // weights are renormalized to sum to 1 (divide by W_FACE + W_SEQ).
    faceVerified = hasFaceEmbedding
    score = faceVerified
          ? (W_FACE * faceSimilarity + W_SEQ * sequenceFidelity) / (W_FACE + W_SEQ)
          : sequenceFidelity

    // Decision thresholds
    decision = (score >= AUTH_ACCEPT (0.75)) ? ACCEPT
             : (score >= AUTH_REVIEW (0.60)) ? REVIEW
             : REJECT

    // Fail-safe: never ACCEPT without a face embedding backing the score —
    // there is zero identity binding otherwise. Cap at REVIEW.
    if not faceVerified and decision == ACCEPT:
        return REVIEW(score, "face_unverified")

    return decision(score)

7.2 Weight Constants and the 3D Gate

The weighted score uses the two verified axes only. W_FACE and W_SEQ are renormalized to sum to 1.0 in the score (/ (W_FACE + W_SEQ)), giving effective weights of 0.4706 and 0.5294.

Weight Value Rationale
W_FACE 0.40 Facial identity is the primary biometric signal
W_SEQ 0.45 Sequence fidelity proves session binding and liveness

3D confidence is a GATE, not a weighted term (decided gate-only, 2026-08). It is presenter-attested and a malicious client can mint it freely, so folding it into the score would buy nothing a spoofer cannot offset. Its value is (a) a hard reject against an honest phone presenting a flat subject, and (b) a reported diagnostic. The specific attack it addresses — a printed photo — would in practice also fail because the verifier is watching live; it is the weakest axis and is retained as defense-in-depth.

Gate Value Outcome
DEPTH_REJECT 0.50 When DEPTH_GATE_ENABLED, confidence3D < DEPTH_REJECT → REJECT "depth_flat". 0.50 is the current operational value; the final threshold is set from the genuine-vs-spoof DET (checklist-F: T25a–T27).

7.3 Decision Thresholds

Threshold Value Outcome
AUTH_ACCEPT 0.75 Authentication succeeds
AUTH_REVIEW 0.60 Inconclusive, manual review
Below 0.60 -- Authentication fails

7.4 Result Structure

AuthenticationResult {
    decision:           "ACCEPT" | "REVIEW" | "REJECT"
    score:              float          // [0.0, 1.0] weighted composite score
    faceSimilarity:     float          // [0.0, 1.0]
    sequenceFidelity:   float          // [0.0, 1.0]
    confidence3D:       float          // [0.0, 1.0]
    rejectReason:       string | null  // "face_mismatch" | "sequence_mismatch" | "depth_flat" | "face_unverified" | "score_below_threshold" | null
    timestamp:          uint64         // Unix epoch seconds
    sessionId:          string         // Session identifier
}

8. Verification Steps (Ordered)

The verifier performs these checks in order. Early termination on failure is permitted.

  1. De-rotate both vectors using the session key
  2. Extract subspaces (face, depth, light)
  3. Compute facial similarity via cosine similarity on face subspaces
  4. Check face hard-reject (< 0.40 -> REJECT immediately)
  5. Decode blink sequence from presenter's light subspace
  6. Compute sequence fidelity against expected session key binary
  7. Check sequence hard-reject (< 60% -> REJECT immediately)
  8. Extract 3D confidence from depth subspace
  9. Check depth hard-gate (when DEPTH_GATE_ENABLED: confidence3D < DEPTH_REJECT -> REJECT "depth_flat") — 3D is a gate, never a weighted term
  10. Compute weighted score using W_FACE, W_SEQ only (renormalized); if no face embedding, score = sequenceFidelity and any ACCEPT is capped to REVIEW "face_unverified"
  11. Apply decision thresholds to produce final result
  12. Log result and clean up (zeroize vectors from memory)

9. Cross-Modal Comparison Notes

9.1 3D vs 2D Gap

The presenter's vector is generated from a high-fidelity 3D scan (close-range, front camera). The verifier's vector is generated from a lower-fidelity 2D observation (video feed, potentially at distance). The CNN is trained with cross-modal alignment loss to minimize this gap, but implementations should expect:

  • Face similarity: typically 0.65-0.85 for genuine matches (cross-modal)
  • Face similarity: typically 0.90+ for same-modal comparisons (both 3D or both 2D)
  • Sequence fidelity: similar across modalities (light is captured in both)

9.2 Environmental Factors

Detection accuracy varies with: - Ambient lighting (outdoor sunlight reduces signal-to-noise ratio) - Distance from screen to face - Screen brightness settings - Camera quality and frame rate

The thresholds in this specification are calibrated for typical indoor conditions with a modern smartphone.


10. Conformance Requirements

  1. Cosine similarity MUST be computed using the full formula with denominator
  2. Match percentage MUST use right-aligned (from-end) comparison
  3. The two-consecutive-80% rule MUST evaluate sequential terminator-delimited attempts
  4. Hard-reject thresholds MUST be applied before weighted scoring
  5. Weights MUST sum to exactly 1.0
  6. The decision result MUST include all component scores for auditability
  7. All vectors MUST be zeroized from memory after the authentication decision is made

11. Test Vectors

11.1 Cosine Similarity

Vector A Vector B Expected Similarity
[1, 0, 0] [1, 0, 0] 1.000
[1, 0, 0] [0, 1, 0] 0.000
[1, 0, 0] [-1, 0, 0] -1.000
[1, 1, 0] [1, 0, 0] 0.707 (1/sqrt(2))
[3, 4, 0] [4, 3, 0] 0.960

11.2 Match Percentage (From End)

Detected Expected Match %
"10100011" "10100011" 100
"10100010" "10100011" 88
"00000000" "10100011" 50
"0011" "10100011" 50
"" "10100011" 0
"10100011" "" 0

11.3 Authentication Decision

faceSim seqFidelity conf3D Score Decision
0.85 1.00 0.90 0.400.85 + 0.451.00 + 0.15*0.90 = 0.925 ACCEPT
0.65 0.85 0.75 0.400.65 + 0.450.85 + 0.15*0.75 = 0.755 ACCEPT
0.55 0.90 0.80 0.400.55 + 0.450.90 + 0.15*0.80 = 0.745 REVIEW
0.35 1.00 0.90 -- REJECT (face_mismatch)
0.70 0.55 0.80 -- REJECT (sequence_mismatch)
0.50 0.70 0.20 0.400.50 + 0.450.70 + 0.15*0.20 = 0.545 REJECT