# Lock chat-identity call verification, version 2

This describes the current one-to-one call implementation, not an independent
audit or a proof of security. It authenticates an existing WebRTC DTLS-SRTP media
connection using automatic chat-key possession proofs. There are no manual
code-comparison prompts. First-use keys are trusted through authenticated server
signalling and pinned locally only after mutual possession proofs succeed.
Future calls authenticate that same key automatically. A changed remembered
key blocks the call and is never silently replaced. This is trust on first use,
not independent verification of the intended person's identity.
The filename is retained for existing links; the production protocol is now version 2.

## Threat model and limits

- DTLS-SRTP protects media between the two WebRTC endpoints; TURN forwards
  encrypted packets. This is not an SFU/group-call protocol or an SFrame layer.
- First-contact identity depends on the signalling service. A compromised service
  can substitute keys before a pin exists. Possession of a self-asserted key alone
  does **not** establish the intended person's identity. Existing pins detect a
  different chat public key on later calls, subject to trusted client storage.
- The service can observe signaling/network metadata, delay or block calls, and
  cause denial of service. It receives no call signing private key or DTLS/SRTP
  private key from this protocol.
- The unlocked clients and their delivered code must be trusted. Malicious web
  code and compromised devices are outside this
  protection. This does not repair chat-key backup/password limitations.
- Pins detect public-key substitution, not theft of the pinned private key.
  Reusing the chat identity inherits its recovery risk: the same account password
  is submitted to authentication and protects the encrypted chat private-key
  backup. A compromised authentication server could capture it and unlock the
  identity. This protocol does not fix that weakness or make chat recipients verified.
- Fresh P-256 signing keys and WebRTC media keys are generated for every call.
  The long-term chat RSA-OAEP key proves identity; it does not encrypt the audio
  stream directly. Browser private keys remain non-extractable. No password or
  additional private-key export/storage is needed. Teardown releases references,
  not a guaranteed memory-zeroization.
- Only remembered public RSA keys are stored in origin/account/teammate-scoped
  browser localStorage or the native app's separate Keychain service. Pins survive
  sign-out, are local to that device/browser, and are not synchronized. Storage
  errors fail closed. A new device establishes its own first-use pin. Pinning does not
  defeat compromised clients, browser storage, or malicious delivered code.
- Browser microphone permission/capture may start during setup. Microphone
  tracks are disabled before being added to WebRTC, and remote playout is held
  until verification. Native iOS also holds WebRTC manual audio disabled.

## Encoding

Strings use UTF-8. The call UUID is lowercase; the conversation is the canonical
`dm_{smallerUserID}_{largerUserID}`. Participant IDs are positive decimal integers
and must be different. Base64 is standard, padded, and canonical, not base64url.

Each client generates an ECDSA P-256 signing key. Public keys are 65-byte
uncompressed X9.63 points. Signatures are SHA-256 ECDSA, 64-byte IEEE P1363
`r || s`, not ASN.1 DER. The authentication object is:

```json
{"version":1,"public_key":"<base64 public key>","signature":"<base64 signature>"}
```

The signed UTF-8 transcript is these fields separated by LF, **including one
final LF**:

```text
purpose
lowercase-call-uuid
canonical-dm-conversation
from-user-id
to-user-id
body
base64-signing-public-key
```

No line-ending normalization is applied to the SDP before hashing. A SHA-256
digest is encoded as lowercase hexadecimal.

## SDP authentication

Offer/answer signals contain `{"type":"offer|answer","sdp":"...","chat_key":"<base64 RSA SPKI>","security":...}`.
The public chat key is canonical DER SubjectPublicKeyInfo for RSA-OAEP SHA-256,
with at least 2048 bits. The purpose is `lock:call-sdp:v2`; the body is the type,
LF, the SHA-256 digest of the **complete original SDP**, LF, and the canonical
base64 public chat key. The receiver verifies the expected call,
conversation, sender/recipient direction, version, key, and signature before
passing SDP to WebRTC. Unsigned/legacy handshakes fail closed.

The current clients accept only audio media sections using DTLS-SRTP
`UDP/TLS/RTP/SAVPF` or `TCP/TLS/RTP/SAVPF`. Plain RTP, SDES `a=crypto:`, missing or
non-SHA-256 fingerprints, and multiple different certificate fingerprints are
rejected. SHA-256 certificate fingerprints are normalized to 64 lowercase hex
characters without colons. WebRTC checks the received DTLS certificate against
the authenticated SDP fingerprint during its handshake.

## Comparison code

Sort the two participants by numeric user ID. Hash the following UTF-8 fields
joined by LF, with a final LF:

```text
lock:call-code:v2
lowercase-call-uuid
canonical-dm-conversation
smaller-user-id
smaller-user-base64-signing-public-key
smaller-user-dtls-fingerprint-hex
smaller-user-base64-rsa-public-chat-key
larger-user-id
larger-user-base64-signing-public-key
larger-user-dtls-fingerprint-hex
larger-user-base64-rsa-public-chat-key
```

The first 32 hexadecimal SHA-256 characters, uppercased, remain the internal
transcript identifier carried in signed challenges and proofs. They are not
displayed for user comparison. A missing pin permits automatic first-use
authentication; an exact existing pin permits later authentication. A mismatched
pin fails closed. No key is persisted from the handshake alone without mutual
private-key possession proofs.
This is a 128-bit code format, not an independently established claim of attack
strength or an audit. Neither the code nor the public signing key is secret.

## Mutual confirmation and media gate

`verify` signals carry a version-2 identity challenge or response:

```json
{"version":2,"code":"<32 uppercase hex>","challenge":"<base64 RSA ciphertext>","security":{}}
{"version":2,"code":"<32 uppercase hex>","proof":"<base64 HMAC>","security":{}}
```

The security object is the P-256 authentication object above, not an empty object.
Exactly one of `challenge` or `proof` is allowed. Each confirming client generates
a fresh 32-byte nonce and encrypts UTF-8 `lock:call-identity:v2\n` followed by that
nonce to the peer's RSA-OAEP SHA-256 public chat key. The P-256 purpose is
`lock:call-challenge:v2`; its body is the code, LF, and base64 ciphertext.

The receiver checks the call, code, direction, and exact remote P-256 key before
decrypting. It requires the purpose prefix and exactly 32 following bytes: raw
chat-message key capsules are not accepted as identity challenges. It responds
with HMAC-SHA-256 keyed by that nonce over the common transcript above, using
purpose `lock:call-rsa-proof:v2`, the unformatted code as body, and an **empty
public-key field**. This transcript has two trailing LFs. The response's P-256
purpose is `lock:call-proof:v2`; its signed body is code, LF, and base64 HMAC.
The challenger verifies both that signature and the HMAC with its saved nonce.
No nonce plaintext, RSA private key, or chat-message decryption result is sent.

Both clients must receive the peer's signed comparison/challenge and verify the
response proving possession of the peer's RSA private key. Receiving just one
side is insufficient. Early packets wait for authenticated SDP. A local signing
operation still in progress cannot enable voice. New pins are saved only after
successful mutual proofs; later calls retain their existing pin. Key changes
produce a blocked-call error; no automatic replacement or manual code override exists.

Voice is enabled only after local confirmation and both identity proof checks.
Unmute cannot bypass the gate. Browser remote streams
are not attached to output before verification; native receivers are disabled,
and iOS manual WebRTC audio is enabled only after verification. Invalid proofs
stop the call and release media before awaiting an End/Decline HTTP response.
Hang-up clears call proof state; public identity pins remain. Each new call uses
fresh nonces, signing keys, and media keys, even when identity checks are automatic.

Both clients and the backend must be updated together. There is deliberately no
unsigned fallback or trust-on-first-use. Earlier unpinned version-1 handshakes
exist only in compatibility test helpers; production clients require the unlocked
chat identity and reject that downgrade.

## Reproduction and validation scope

The actual implementations are `resources/js/call-security.js`,
`resources/js/call-chat-identity.js`, native `Lock/CallSecurity.swift`,
`Lock/CallChatIdentity.swift`, and `Lock/CallTrustStore.swift`. Tests cover signatures, context replay,
tampering, key substitution, mutual confirmation and disabled-media gates.
`Tests/CallSecurityInterop.swift` exchanges signed SDP and confirmations with
`tests/js/call-security.test.js` and `tests/js/call-chat-identity.test.js`, using
CryptoKit/Security and WebCrypto implementations, including real RSA-OAEP/HMAC
proofs, first-use gating, later automatic calls, and changed-key blocking.
The browser verification fixture uses generated audio and real WebRTC-created
SDPs without user microphones or account credentials. These checks are not an
independent cryptographic review or a signed-in two-device audio test.

Before relying on this feature, review the protocol and implementation and test
the updated web/native clients together, including mismatched codes, old
clients, network loss, permission denial, and cancellation during verification.
