kv-proto: OPAQUE suite, RFC 9421 signing profile and M1 auth wire types
Adds the OpaqueSuite (Ristretto255 OPRF, TripleDH, SHA-512, identity KSF) shared by client and server, the httpsig signature-base builders, and the setup/credentials/account/device/SSE types for milestone M1. Cargo.lock changes only for kv-proto's new dependencies. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Generated
+56
@@ -395,6 +395,7 @@ dependencies = [
|
|||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
"digest",
|
"digest",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
|
"rand_core",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -427,6 +428,17 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive-where"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -569,8 +581,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -858,8 +872,11 @@ name = "kv-proto"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
|
"opaque-ke",
|
||||||
|
"rand_core",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -955,6 +972,27 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-ke"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb31f7b2c5760d8ffbc39652043f5cff14029b6249a2280b28ae6f0cd61f5098"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"derive-where",
|
||||||
|
"digest",
|
||||||
|
"displaydoc",
|
||||||
|
"elliptic-curve",
|
||||||
|
"generic-array",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"hkdf",
|
||||||
|
"hmac",
|
||||||
|
"rand",
|
||||||
|
"subtle",
|
||||||
|
"voprf",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@@ -1630,6 +1668,24 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "voprf"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28f59c30c76e2fea54cdece6a054e2662feffa7ab19658a7887524265ee39470"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"derive-where",
|
||||||
|
"digest",
|
||||||
|
"displaydoc",
|
||||||
|
"elliptic-curve",
|
||||||
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ publish.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
# OPAQUE cipher suite shared by client and server (docs/02 "Authentication to the server").
|
||||||
|
opaque-ke = { version = "3", default-features = false, features = ["ristretto255-voprf", "std"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
rand_core.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//! The RFC 9421 request-signing profile for every authenticated request (docs/03).
|
||||||
|
//!
|
||||||
|
//! A client sends these headers on each call after login:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! Content-Digest: sha-256=:<base64 SHA-256 of the body, also for an empty body>:
|
||||||
|
//! KV-Session: <session id from login>
|
||||||
|
//! KV-Ts: <unix seconds>
|
||||||
|
//! Signature-Input: kv=<signature_params(...)>
|
||||||
|
//! Signature: kv=:<base64 of the 64-byte P-256 ECDSA signature r||s over signature_base(...)>:
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The server rebuilds the base from the request it received and checks it with the
|
||||||
|
//! device's enrolled P-256 key. It rejects a `KV-Ts` more than [`MAX_SKEW_SECS`] off,
|
||||||
|
//! a reused nonce, and any deviation from this exact profile (label, components, order).
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub const HEADER_SESSION: &str = "kv-session";
|
||||||
|
pub const HEADER_TS: &str = "kv-ts";
|
||||||
|
pub const HEADER_CONTENT_DIGEST: &str = "content-digest";
|
||||||
|
pub const HEADER_SIGNATURE_INPUT: &str = "signature-input";
|
||||||
|
pub const HEADER_SIGNATURE: &str = "signature";
|
||||||
|
|
||||||
|
/// The only accepted signature label.
|
||||||
|
pub const LABEL: &str = "kv";
|
||||||
|
pub const ALG: &str = "ecdsa-p256-sha256";
|
||||||
|
/// Covered components, in this order.
|
||||||
|
pub const COVERED_COMPONENTS: &str = r#"("@method" "@target-uri" "content-digest" "kv-session" "kv-ts")"#;
|
||||||
|
pub const MAX_SKEW_SECS: i64 = 120;
|
||||||
|
/// Nonces are 16–64 characters of base64url (`A-Z a-z 0-9 - _`), e.g. 16 random bytes encoded.
|
||||||
|
pub const NONCE_MIN_LEN: usize = 16;
|
||||||
|
pub const NONCE_MAX_LEN: usize = 64;
|
||||||
|
|
||||||
|
/// `Content-Digest` value (RFC 9530) for a request body.
|
||||||
|
pub fn content_digest(body: &[u8]) -> String {
|
||||||
|
format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `@signature-params` value. `created` must equal the `KV-Ts` header and `keyid` is the device id.
|
||||||
|
pub fn signature_params(created: i64, keyid: Uuid, nonce: &str) -> String {
|
||||||
|
format!(r#"{COVERED_COMPONENTS};created={created};keyid="{keyid}";alg="{ALG}";nonce="{nonce}""#)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The RFC 9421 signature base the device key signs.
|
||||||
|
///
|
||||||
|
/// `target_uri` is the absolute URI the client requested, e.g.
|
||||||
|
/// `https://vault.example.com/v1/vaults/…/changes?since=3`. Header values are passed exactly as sent.
|
||||||
|
pub fn signature_base(
|
||||||
|
method: &str,
|
||||||
|
target_uri: &str,
|
||||||
|
content_digest: &str,
|
||||||
|
session: &str,
|
||||||
|
ts: &str,
|
||||||
|
params: &str,
|
||||||
|
) -> String {
|
||||||
|
format!(
|
||||||
|
"\"@method\": {method}\n\"@target-uri\": {target_uri}\n\"content-digest\": {content_digest}\n\
|
||||||
|
\"kv-session\": {session}\n\"kv-ts\": {ts}\n\"@signature-params\": {params}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Signature-Input` header value.
|
||||||
|
pub fn signature_input_header(params: &str) -> String {
|
||||||
|
format!("{LABEL}={params}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Signature` header value for a raw 64-byte `r||s` signature.
|
||||||
|
pub fn signature_header(sig: &[u8]) -> String {
|
||||||
|
format!("{LABEL}=:{}:", STANDARD.encode(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if `nonce` fits the allowed alphabet and length.
|
||||||
|
pub fn valid_nonce(nonce: &str) -> bool {
|
||||||
|
(NONCE_MIN_LEN..=NONCE_MAX_LEN).contains(&nonce.len())
|
||||||
|
&& nonce.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_body_digest() {
|
||||||
|
assert_eq!(content_digest(b""), "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base_matches_rfc9421_layout() {
|
||||||
|
let kid = Uuid::nil();
|
||||||
|
let params = signature_params(1_700_000_000, kid, "AAAAAAAAAAAAAAAAAAAAAA");
|
||||||
|
assert_eq!(
|
||||||
|
params,
|
||||||
|
r#"("@method" "@target-uri" "content-digest" "kv-session" "kv-ts");created=1700000000;keyid="00000000-0000-0000-0000-000000000000";alg="ecdsa-p256-sha256";nonce="AAAAAAAAAAAAAAAAAAAAAA""#
|
||||||
|
);
|
||||||
|
let base = signature_base("GET", "https://v.example/v1/vaults", "sha-256=:x:", "s", "1700000000", ¶ms);
|
||||||
|
assert_eq!(
|
||||||
|
base,
|
||||||
|
format!(
|
||||||
|
"\"@method\": GET\n\"@target-uri\": https://v.example/v1/vaults\n\"content-digest\": sha-256=:x:\n\
|
||||||
|
\"kv-session\": s\n\"kv-ts\": 1700000000\n\"@signature-params\": {params}"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(signature_input_header(¶ms), format!("kv={params}"));
|
||||||
|
assert_eq!(signature_header(&[0u8; 3]), "kv=:AAAA:");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonce_rules() {
|
||||||
|
assert!(valid_nonce("abcdefghijklmnop"));
|
||||||
|
assert!(!valid_nonce("short"));
|
||||||
|
assert!(!valid_nonce("abcdefghijklmno\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub mod httpsig;
|
||||||
|
pub mod opaque;
|
||||||
|
|
||||||
pub const FORMAT_VERSION: u8 = 1;
|
pub const FORMAT_VERSION: u8 = 1;
|
||||||
pub const API_VERSION: u32 = 1;
|
pub const API_VERSION: u32 = 1;
|
||||||
|
|
||||||
@@ -93,11 +96,34 @@ pub struct DeviceEnroll {
|
|||||||
pub p256_pk: Vec<u8>,
|
pub p256_pk: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `POST /v1/setup/start`: OPAQUE registration, first message. Needs the setup token.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SetupStart {
|
||||||
|
pub setup_token: String,
|
||||||
|
pub account_id: Uuid,
|
||||||
|
/// Serialized opaque-ke `RegistrationRequest`.
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub registration_request: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reply to `POST /v1/setup/start` and `POST /v1/account/credentials/start`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RegistrationStartResponse {
|
||||||
|
/// Serialized opaque-ke `RegistrationResponse`.
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub registration_response: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST /v1/setup/finish`. `account_id` must match the one sent to `/v1/setup/start`.
|
||||||
|
pub type SetupFinish = SetupRequest;
|
||||||
|
|
||||||
|
/// Body of `POST /v1/setup/finish` (alias [`SetupFinish`]).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SetupRequest {
|
pub struct SetupRequest {
|
||||||
pub setup_token: String,
|
pub setup_token: String,
|
||||||
pub account_id: Uuid,
|
pub account_id: Uuid,
|
||||||
pub kdf: KdfParams,
|
pub kdf: KdfParams,
|
||||||
|
/// Serialized opaque-ke `RegistrationUpload`.
|
||||||
#[serde(with = "b64")]
|
#[serde(with = "b64")]
|
||||||
pub opaque_registration_upload: Vec<u8>,
|
pub opaque_registration_upload: Vec<u8>,
|
||||||
#[serde(with = "b64")]
|
#[serde(with = "b64")]
|
||||||
@@ -108,27 +134,35 @@ pub struct SetupRequest {
|
|||||||
pub device: DeviceEnroll,
|
pub device: DeviceEnroll,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `POST /v1/auth/login/start`.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoginStart {
|
pub struct LoginStart {
|
||||||
pub account_id: Uuid,
|
pub account_id: Uuid,
|
||||||
|
/// Serialized opaque-ke `CredentialRequest`.
|
||||||
#[serde(with = "b64")]
|
#[serde(with = "b64")]
|
||||||
pub ke1: Vec<u8>,
|
pub ke1: Vec<u8>,
|
||||||
|
/// The logging-in device. An unknown `device_id` is enrolled as pending after a successful login.
|
||||||
pub device: DeviceEnroll,
|
pub device: DeviceEnroll,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoginStartResponse {
|
pub struct LoginStartResponse {
|
||||||
|
/// Handle for `/v1/auth/login/finish`; expires after 60 s.
|
||||||
pub login_id: String,
|
pub login_id: String,
|
||||||
|
/// Serialized opaque-ke `CredentialResponse`.
|
||||||
#[serde(with = "b64")]
|
#[serde(with = "b64")]
|
||||||
pub ke2: Vec<u8>,
|
pub ke2: Vec<u8>,
|
||||||
pub kdf: KdfParams,
|
pub kdf: KdfParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `POST /v1/auth/login/finish`.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoginFinish {
|
pub struct LoginFinish {
|
||||||
pub login_id: String,
|
pub login_id: String,
|
||||||
|
/// Serialized opaque-ke `CredentialFinalization`.
|
||||||
#[serde(with = "b64")]
|
#[serde(with = "b64")]
|
||||||
pub ke3: Vec<u8>,
|
pub ke3: Vec<u8>,
|
||||||
|
/// Reserved for the TOTP enrollment path; not implemented by the server yet.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub totp: Option<String>,
|
pub totp: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -140,13 +174,80 @@ pub enum LoginStatus {
|
|||||||
PendingApproval,
|
PendingApproval,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `status: ok` carries a session (put it in `KV-Session`); `pending_approval` carries none:
|
||||||
|
/// approve the device from an enrolled one, then log in again.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoginFinishResponse {
|
pub struct LoginFinishResponse {
|
||||||
pub status: LoginStatus,
|
pub status: LoginStatus,
|
||||||
pub session: Option<String>,
|
pub session: Option<String>,
|
||||||
|
/// RFC 3339. Rolling: every authenticated request extends it by 12 h.
|
||||||
pub expires_at: Option<String>,
|
pub expires_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `GET /v1/account`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AccountInfo {
|
||||||
|
pub account_id: Uuid,
|
||||||
|
pub kdf: KdfParams,
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub enc_pk: Vec<u8>,
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub sig_pk: Vec<u8>,
|
||||||
|
pub wrapped_keyset: Envelope,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST /v1/account/credentials/start`: OPAQUE re-registration for a password change.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CredentialsStart {
|
||||||
|
/// Serialized opaque-ke `RegistrationRequest` for the new `AUTH_INPUT`.
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub registration_request: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `PUT /v1/account/credentials`: replaces the OPAQUE record, KDF params and wrapped keyset atomically.
|
||||||
|
/// Sessions of all other devices are ended.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CredentialsChange {
|
||||||
|
pub kdf: KdfParams,
|
||||||
|
/// Serialized opaque-ke `RegistrationUpload`.
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub registration_upload: Vec<u8>,
|
||||||
|
pub wrapped_keyset: Envelope,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An entry of `GET /v1/devices`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
pub device_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub platform: Platform,
|
||||||
|
/// SEC1 uncompressed P-256 public key; show a fingerprint before approving.
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
pub p256_pk: Vec<u8>,
|
||||||
|
pub approved: bool,
|
||||||
|
pub revoked_at: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
/// True for the device that made the request.
|
||||||
|
pub current: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server-Sent Events on `GET /v1/events`. The SSE event name equals `type`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum ServerEvent {
|
||||||
|
VaultChanged { vault_id: Uuid, head: u64 },
|
||||||
|
DevicePending { device_id: Uuid },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerEvent {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ServerEvent::VaultChanged { .. } => "vault_changed",
|
||||||
|
ServerEvent::DevicePending { .. } => "device_pending",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Vault {
|
pub struct Vault {
|
||||||
pub vault_id: Uuid,
|
pub vault_id: Uuid,
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//! The OPAQUE profile used for login, setup and password change (docs/02 "Authentication to the server").
|
||||||
|
//!
|
||||||
|
//! Client and server MUST both use [`OpaqueSuite`] with these conventions, or login fails:
|
||||||
|
//! * password input: the 32-byte `AUTH_INPUT`, which the client has already stretched with Argon2id,
|
||||||
|
//! so the suite's key-stretching function is the identity;
|
||||||
|
//! * credential identifier (server side only): the 16 raw bytes of the account UUID;
|
||||||
|
//! * default parameters everywhere (`ClientRegistrationFinishParameters::default()`,
|
||||||
|
//! `ClientLoginFinishParameters::default()`, `ServerLoginStartParameters::default()`):
|
||||||
|
//! no custom identifiers and no context string.
|
||||||
|
//!
|
||||||
|
//! Messages travel base64url-encoded (no padding) inside the JSON bodies in this crate, using
|
||||||
|
//! opaque-ke's own `serialize()` / `deserialize()`.
|
||||||
|
|
||||||
|
pub use opaque_ke;
|
||||||
|
|
||||||
|
/// Ristretto255 OPRF, Ristretto255 + TripleDH key exchange, SHA-512, identity KSF.
|
||||||
|
pub struct OpaqueSuite;
|
||||||
|
|
||||||
|
impl opaque_ke::CipherSuite for OpaqueSuite {
|
||||||
|
type OprfCs = opaque_ke::Ristretto255;
|
||||||
|
type KeGroup = opaque_ke::Ristretto255;
|
||||||
|
type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh;
|
||||||
|
type Ksf = opaque_ke::ksf::Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use opaque_ke::{
|
||||||
|
ClientLogin, ClientLoginFinishParameters, ClientRegistration, ClientRegistrationFinishParameters,
|
||||||
|
CredentialFinalization, CredentialRequest, CredentialResponse, RegistrationRequest, RegistrationResponse,
|
||||||
|
RegistrationUpload, ServerLogin, ServerLoginStartParameters, ServerRegistration, ServerSetup,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use super::OpaqueSuite;
|
||||||
|
|
||||||
|
/// Registration then login across serialized messages, as the client and server exchange them.
|
||||||
|
#[test]
|
||||||
|
fn registration_and_login_roundtrip() {
|
||||||
|
let mut rng = OsRng;
|
||||||
|
let setup = ServerSetup::<OpaqueSuite>::new(&mut rng);
|
||||||
|
let cred_id = uuid::Uuid::new_v4();
|
||||||
|
let auth_input = [7u8; 32];
|
||||||
|
|
||||||
|
let c = ClientRegistration::<OpaqueSuite>::start(&mut rng, &auth_input).unwrap();
|
||||||
|
let req = RegistrationRequest::deserialize(&c.message.serialize()).unwrap();
|
||||||
|
let s = ServerRegistration::<OpaqueSuite>::start(&setup, req, cred_id.as_bytes()).unwrap();
|
||||||
|
let resp = RegistrationResponse::deserialize(&s.message.serialize()).unwrap();
|
||||||
|
let fin = c.state.finish(&mut rng, &auth_input, resp, ClientRegistrationFinishParameters::default()).unwrap();
|
||||||
|
let upload = RegistrationUpload::<OpaqueSuite>::deserialize(&fin.message.serialize()).unwrap();
|
||||||
|
let record = ServerRegistration::finish(upload).serialize();
|
||||||
|
|
||||||
|
let login = |password: &[u8]| {
|
||||||
|
let c = ClientLogin::<OpaqueSuite>::start(&mut OsRng, password).unwrap();
|
||||||
|
let ke1 = CredentialRequest::deserialize(&c.message.serialize()).unwrap();
|
||||||
|
let record = ServerRegistration::deserialize(&record).unwrap();
|
||||||
|
let s = ServerLogin::start(
|
||||||
|
&mut OsRng,
|
||||||
|
&setup,
|
||||||
|
Some(record),
|
||||||
|
ke1,
|
||||||
|
cred_id.as_bytes(),
|
||||||
|
ServerLoginStartParameters::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let ke2 = CredentialResponse::deserialize(&s.message.serialize()).unwrap();
|
||||||
|
let Ok(done) = c.state.finish(password, ke2, ClientLoginFinishParameters::default()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let ke3 = CredentialFinalization::deserialize(&done.message.serialize()).unwrap();
|
||||||
|
let server = s.state.finish(ke3).unwrap();
|
||||||
|
server.session_key == done.session_key
|
||||||
|
};
|
||||||
|
assert!(login(&auth_input));
|
||||||
|
assert!(!login(&[8u8; 32]));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user