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:
- Facial Identity Similarity -- Do the presenter and verifier vectors encode the same face?
- Sequence Fidelity -- Does the embedded light sequence match the expected session key?
- 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 fromD(z_light). TheD(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.
5.1 Blink Sequence Reconstruction¶
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.
- De-rotate both vectors using the session key
- Extract subspaces (face, depth, light)
- Compute facial similarity via cosine similarity on face subspaces
- Check face hard-reject (< 0.40 -> REJECT immediately)
- Decode blink sequence from presenter's light subspace
- Compute sequence fidelity against expected session key binary
- Check sequence hard-reject (< 60% -> REJECT immediately)
- Extract 3D confidence from depth subspace
- Check depth hard-gate (when
DEPTH_GATE_ENABLED:confidence3D < DEPTH_REJECT-> REJECT"depth_flat") — 3D is a gate, never a weighted term - 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" - Apply decision thresholds to produce final result
- 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¶
- Cosine similarity MUST be computed using the full formula with denominator
- Match percentage MUST use right-aligned (from-end) comparison
- The two-consecutive-80% rule MUST evaluate sequential terminator-delimited attempts
- Hard-reject thresholds MUST be applied before weighted scoring
- Weights MUST sum to exactly 1.0
- The decision result MUST include all component scores for auditability
- 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 |