auths_pairing_protocol/lib.rs
1//! Transport-agnostic pairing protocol for the auths identity system.
2//!
3//! This crate implements the cryptographic pairing protocol that allows
4//! cross-device identity linking. It is intentionally free of transport
5//! dependencies (no axum, tower-http, mdns-sd, reqwest) so that mobile
6//! apps can use it with their own transport layer.
7//!
8//! ## Curve choice
9//!
10//! The device's long-term **signing** curve (Ed25519 or P-256, carried via
11//! the `curve` wire field on [`PairingResponse`]) is independent of the
12//! **ephemeral ECDH** curve used for key agreement.
13//!
14//! We use **P-256 ECDH unconditionally**, regardless of signing curve.
15//! Ephemeral keys are generated fresh per session (`p256::ecdh::EphemeralSecret::random`),
16//! never reused, and have zero cryptographic relationship to the device's
17//! long-term signing seed. There is no "signing-curve-to-ECDH-curve mapper"
18//! and no need for one.
19//!
20//! **Why P-256 for ECDH:**
21//! - P-256 is the workspace default curve, already a dependency for signing.
22//! - iOS Secure Enclave is P-256 exclusively; Android StrongBox supports
23//! P-256 only for EC.
24//! - Removes the X25519 dependency and the 32-byte pubkey ambiguity
25//! (X25519 and Ed25519 both produce 32-byte keys).
26//! - Constant-time P-256 ECDH via the `p256` crate (RustCrypto, audited).
27//!
28//! See `docs/architecture/cryptography.md` → Wire-format Curve Tagging for
29//! the workspace-wide curve-agnosticism rule.
30
31// fn-129.T1: statically forbid `unsafe` in this crate. The pairing protocol
32// has no `unsafe` today; this attribute freezes that invariant so future
33// code cannot accidentally introduce a soundness hole around the ephemeral
34// ECDH secret or the transport key.
35#![forbid(unsafe_code)]
36
37pub mod channel_binding;
38pub mod domain_separation;
39pub mod envelope;
40mod error;
41pub mod lookup_auth;
42#[cfg(feature = "pq-hybrid")]
43pub mod pq_hybrid;
44mod protocol;
45mod response;
46pub mod sas;
47mod token;
48pub mod types;
49
50pub use channel_binding::{
51 CHANNEL_BINDING_INFO, ChannelBinding, ChannelBindingError, ChannelBindingProvider,
52 TLS_EXPORTER_LABEL, TLS_EXPORTER_LEN,
53};
54pub use envelope::{
55 Envelope, EnvelopeError, EnvelopeSession, MAX_MESSAGES_PER_SESSION, Open, Sealed,
56};
57pub use error::ProtocolError;
58pub use protocol::{
59 CompletedPairing, Confirmed, Init, Paired, PairingFlow, PairingProtocol, Responded,
60 ResponderResult, SasMatch, respond_to_pairing,
61};
62pub use response::{CurveTag, PairingResponse};
63// The `SubkeyChain` wire type is always compiled in so every builder
64// that constructs a `SubmitResponseRequest` can spell the field — a
65// feature-gated field would require `#[cfg]` at every call site. The
66// verifier logic (`verify_subkey_chain`, `build_binding_message_v1`,
67// and the domain separator) is gated behind `subkey-chain-v1`. A
68// daemon compiled without the feature that receives a request with
69// `subkey_chain.is_some()` must reject with an explicit unsupported
70// error — silent ignore is a security regression.
71pub mod subkey_chain;
72pub use sas::{
73 TransportKey, decrypt_from_transport, derive_sas, derive_transport_key, format_sas_emoji,
74 format_sas_numeric,
75};
76pub use subkey_chain::SubkeyChain;
77#[cfg(feature = "subkey-chain-v1")]
78pub use subkey_chain::{
79 SUBKEY_CHAIN_V1_DOMAIN, SubkeyChainError, build_binding_message_v1, verify_subkey_chain,
80};
81pub use token::{KemSlot, PairingSession, PairingToken, normalize_short_code};
82pub use types::*;