ReferenceVINAC-FM Integration Guide

VINAC-FM Integration Guide

VINAC-FM (Verified Inaudible Nonce Acoustic Carrier — Frequency Modulated) provides Layer 4 physical presence verification using ultrasonic signals. This guide covers every step from requesting a challenge to resolving a signed certificate on your server, including replay attack prevention.

Protocol v1.0ES256 SignedReplay-Protected

Ultrasonic

18–22 kHz, inaudible carrier

Signed Cert

ECDSA P-256 / ES256

Single-use

Atomic nonce & cert burn

< 5 sec

Full round-trip latency

How it works

1

Request challenge

Client calls vinacfm-challenge. Server creates a vinac_session with a random nonce embedded in an ultrasonic signal plan.

2

Emit signal

VinacAudioEngine plays the nonce-encoded tone through the device speaker at the challenge frequency (e.g. 18 kHz).

3

Capture via mic

The same device (or a co-located receiver) captures the signal and extracts the nonce from the PCM buffer.

4

Submit proof

Client sends the captured nonce + signal metadata to vinacfm-verify. The server atomically burns the nonce — one-time use, race-free.

5

Receive certificate

On success the server issues an ECDSA-signed vinac_certificate with a 24-hour TTL. The token is single-use.

6

Resolve on backend

Your server calls vinacfm-resolve with the certificate token. The server atomically marks it consumed and returns the action payload.

Prerequisites

SilentAuth project with a public/secret key pair
HTTPS origin (required for Web Audio API microphone access)
Device with a speaker (microphone optional — emit-only mode supported)
Browser: Chrome 90+, Firefox 85+, Safari 14+, Edge 90+
Does not require any native app, plugin, or hardware peripheral
1

Initialize the client

Install the SDK and configure your project credentials. All sensitive keys stay server-side; only the publicKey is exposed to the browser.

bash
import { SilentAuth } from '@silentauth/sdk';
const sa = new SilentAuth({
projectId: process.env.SA_PROJECT_ID,
secretKey: process.env.SA_SECRET_KEY,
});
2

Request a challenge

Call requestChallenge() to obtain a fresh acoustic challenge. The returned jti uniquely identifies this challenge issuance — it is bound to a single vinac_session and cannot be reused.

typescript
// Browser / React — request an acoustic challenge
import { requestChallenge } from '@silentauth/vinacfm';
const challenge = await requestChallenge(
projectId, // your project UUID
publicKey, // your project public key
);
// challenge.vinac_session_id — bind this to your verify session
// challenge.challenge_nonce — encoded into the ultrasonic signal
// challenge.frequency_hz — carrier frequency (e.g. 18000)
// challenge.duration_ms — transmission window (e.g. 3000)
// challenge.expires_at — ISO timestamp, ~30 seconds

Challenges expire in ~30 seconds. Request one only when the user is ready to verify. Each challenge is single-use — once a proof is submitted (success or failure), the nonce is permanently burned.

3

Emit the ultrasonic signal

VinacAudioEngine.runFull() plays the nonce-encoded tone through the Web Audio API and simultaneously records via the microphone. In environments without microphone access, use runEmitOnly() — the server still validates the signal hash.

typescript
import { VinacAudioEngine } from '@silentauth/vinacfm';
const engine = new VinacAudioEngine({
onProgress: (pct) => setProgress(pct),
onState: (s) => setStatus(s),
});
// Emit the nonce and optionally capture it via mic
const result = await engine.runFull(challenge);
// result.capturedNonce — the nonce received back via microphone
// result.signalHash — SHA-256 of raw PCM buffer
// result.dominantFrequency — peak frequency detected
// result.snr — signal-to-noise ratio (dB)
4

Submit the proof

submitProof() sends the captured nonce to the server. The server atomically burns the nonce via UPDATE WHERE nonce_burned_at IS NULL — any concurrent or replayed submission races the same row and loses, receiving NONCE_REPLAYED.

typescript
import { submitProof } from '@silentauth/vinacfm';
const verification = await submitProof(
challenge.vinac_session_id,
result.capturedNonce,
publicKey,
{
signalHash: result.signalHash,
deviceFingerprint: getDeviceFingerprint(),
dominantFrequencyHz: result.dominantFrequency,
snrDb: result.snr,
// Replay prevention — always include
request_timestamp: Date.now(),
},
);
if (verification.verified) {
const cert = verification.certificate;
// cert.token — present this to your backend
// cert.signed — true if ECDSA-signed
// cert.signature — ES256 / P-256 signature
}

Always include request_timestamp: Date.now(). The server rejects requests where the clock skew exceeds 5 minutes, preventing token-lift replay attacks from captured network traffic.

5

Resolve on your server

Pass the certificate token from your client to your backend and call resolveToken(). This is the final authority check — the server atomically marks the certificate consumed (UPDATE WHERE consumed_at IS NULL). Only the first successful resolution proceeds; all subsequent calls for the same token receive CERT_REPLAYED.

typescript
// Server-side (Node.js / any backend)
import { resolveToken } from '@silentauth/vinacfm/server';
const result = await resolveToken(
req.body.certificate_token,
req.body.token_id,
process.env.SA_PUBLIC_KEY,
{ request_timestamp: Date.now() },
);
if (!result.resolved) {
// result.error includes:
// 'Certificate has already been consumed' (CERT_REPLAYED)
// 'Certificate has expired' (CERT_EXPIRED)
// 'Certificate has been revoked'
return res.status(401).json({ error: result.error });
}
// Allow the action described by result.action
console.log(result.action.namespace); // e.g. "finance.transfer"
console.log(result.action.risk_tier); // "high"
console.log(result.action.execution_policy);

Drop-in React component

For React applications, VinacFmEmitter wraps the entire challenge–emit–capture–verify flow in a self-contained component. Pass onVerified to receive the signed certificate.

tsx
import { VinacFmEmitter } from '@silentauth/vinacfm/react';
export function PaymentConfirmation() {
return (
<VinacFmEmitter
projectId={process.env.NEXT_PUBLIC_SA_PROJECT_ID}
publicKey={process.env.NEXT_PUBLIC_SA_PUBLIC_KEY}
onVerified={(cert) => {
// cert.token is single-use — send to your backend immediately
fetch('/api/payment/authorize', {
method: 'POST',
body: JSON.stringify({ vinac_cert: cert.token }),
});
}}
onFailed={(err) => console.error('VINAC failed:', err)}
/>
);
}

Props

projectIdstring

Your SilentAuth project UUID

publicKeystring

Project public key

verifySessionIdstring?

Link to an existing verify session

onVerified(cert: VinacCertificate) => void

Called with the issued certificate

onFailed(err: string) => void

Called on verification failure

demoboolean

Run without backend (demo mode)

Backend handler example

Your API route receives the certificate token from the client and resolves it server-side before authorizing the action.

typescript (Next.js Route Handler)
// POST /api/payment/authorize
export async function POST(req: Request) {
const { vinac_cert, token_id } = await req.json();
const result = await resolveToken(
vinac_cert,
token_id,
process.env.SA_PUBLIC_KEY,
);
if (!result.resolved) {
return Response.json({ error: result.error }, { status: 401 });
}
// result.action.requires_vinac is true — physical presence confirmed
await processPayment(/* ... */);
return Response.json({ ok: true });
}

Replay attack prevention

VINAC-FM v1.0 enforces replay prevention at every layer of the protocol. All defenses are server-enforced — no client co-operation required.

Atomic nonce burn

The verification UPDATE uses WHERE nonce_burned_at IS NULL AND status IN ('pending','active'). Only one concurrent request can win the race; all others receive NONCE_REPLAYED (HTTP 409).

Single-use certificates

resolve() atomically sets consumed_at via UPDATE WHERE consumed_at IS NULL. Any subsequent resolution — including concurrent requests — receives CERT_REPLAYED.

Clock skew enforcement

Requests with request_timestamp more than 5 minutes from server time are rejected with CLOCK_SKEW (HTTP 400). Prevents token-lift replay from captured network traffic.

Session terminal states

Once a session reaches completed, failed, or expired, all further submissions are rejected with SESSION_REPLAY (HTTP 409), even with a valid nonce.

Replay audit log

Every replay attempt is written to vinac_replay_log with the replay_type, project ID, reference ID, IP, and user agent. Review these in the Threat Intelligence dashboard.

Error codes

CodeHTTPDescription
NONCE_REPLAYED409The acoustic challenge nonce was already consumed. Start a new challenge.
SESSION_REPLAY409The VINAC session is in a terminal state (completed/failed/expired).
SESSION_EXPIRED410The session TTL (~5 min) elapsed before proof was submitted.
CLOCK_SKEW400request_timestamp deviates more than 5 minutes from server time.
NONCE_MISMATCH200Acoustic proof received but nonce doesn't match the emitted challenge.
CERT_REPLAYED200Certificate token has already been resolved (single-use).
CERT_EXPIRED200The 24-hour certificate TTL has elapsed.

Live demo

The widget below runs in demo mode — no backend required, no microphone permission needed. It plays the ultrasonic tone and simulates the full verification flow so you can preview the UX before integrating.

Ultrasonic Presence Emitter
18,000 Hz
READY
15 kHz18.0 kHz22 kHz

Requires speaker access · Microphone optional for loop-back verification

What you're seeing

Frequency visualizer15–22 kHz spectrum. The carrier sits at 18 kHz by default — inaudible to most adults.
Ripple animationEach expanding ring represents one acoustic propagation wave emitted from the speaker.
Sine waveReal-time render of the ultrasonic waveform being played through the Web Audio API oscillator.
Certificate badgeOn success, the issued certificate token and ES256 signature are displayed. In production, send the token to your backend immediately.
SIG OK / SIG FAIL badgeThe client verifies the ECDSA signature against the project's public key after each successful verification.

Run with a real backend

Pass your project credentials to enable full verification:

<VinacFmEmitter
  projectId="your-project-id"
  publicKey="your-public-key"
  onVerified={(cert) => console.log(cert)}
/>

Next steps