use chrono::{DateTime, Utc};
use zeroize::Zeroizing;
use auths_crypto::TypedSeed;
use auths_keri::Capability;
use crate::error::ProtocolError;
use crate::response::PairingResponse;
use crate::sas::{self, TransportKey};
use crate::token::{PairingSession, PairingToken};
pub struct CompletedPairing {
pub shared_secret: Zeroizing<[u8; 32]>,
pub peer_signing_pubkey: Vec<u8>,
pub peer_did: String,
pub response: PairingResponse,
pub sas: [u8; 10],
pub transport_key: TransportKey,
pub initiator_ephemeral_pub: Vec<u8>,
}
impl std::fmt::Debug for CompletedPairing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompletedPairing")
.field("shared_secret", &"[redacted 32 bytes]")
.field(
"peer_signing_pubkey",
&format!("{} bytes", self.peer_signing_pubkey.len()),
)
.field("peer_did", &self.peer_did)
.field("response", &self.response)
.field("sas", &"[redacted 10 bytes]")
.field("transport_key", &"[redacted]")
.field(
"initiator_ephemeral_pub",
&format!("{} bytes", self.initiator_ephemeral_pub.len()),
)
.finish()
}
}
pub struct ResponderResult {
pub response: PairingResponse,
pub shared_secret: Zeroizing<[u8; 32]>,
pub sas: [u8; 10],
pub transport_key: TransportKey,
}
pub struct PairingProtocol {
session: PairingSession,
}
impl PairingProtocol {
pub fn initiate(
now: DateTime<Utc>,
controller_did: String,
endpoint: String,
capabilities: Vec<Capability>,
) -> Result<(Self, PairingToken), ProtocolError> {
let session = PairingToken::generate(now, controller_did, endpoint, capabilities)?;
let token = session.token.clone();
Ok((Self { session }, token))
}
pub fn complete(
mut self,
now: DateTime<Utc>,
response_bytes: &[u8],
) -> Result<CompletedPairing, ProtocolError> {
let response: PairingResponse = serde_json::from_slice(response_bytes)?;
response.verify(now, &self.session.token)?;
self.complete_inner(now, response)
}
pub fn complete_with_response(
mut self,
now: DateTime<Utc>,
response: PairingResponse,
) -> Result<CompletedPairing, ProtocolError> {
response.verify(now, &self.session.token)?;
self.complete_inner(now, response)
}
fn complete_inner(
&mut self,
_now: DateTime<Utc>,
response: PairingResponse,
) -> Result<CompletedPairing, ProtocolError> {
let initiator_ecdh_pub = self.session.ephemeral_pubkey_bytes()?;
let responder_ecdh_pub = response.device_ephemeral_pubkey_bytes()?;
let shared_secret = self.session.complete_exchange(&responder_ecdh_pub)?;
let peer_signing_pubkey = response.device_signing_pubkey_bytes()?;
let peer_did = response.device_did.clone();
let session_id = &self.session.token.session_id;
let short_code = &self.session.token.short_code;
let sas_bytes = sas::derive_sas(
&shared_secret,
&initiator_ecdh_pub,
&responder_ecdh_pub,
session_id,
short_code,
);
let transport_key = sas::derive_transport_key(
&shared_secret,
&initiator_ecdh_pub,
&responder_ecdh_pub,
session_id,
short_code,
);
Ok(CompletedPairing {
shared_secret,
peer_signing_pubkey,
peer_did,
response,
sas: sas_bytes,
transport_key,
initiator_ephemeral_pub: initiator_ecdh_pub,
})
}
pub fn token(&self) -> &PairingToken {
&self.session.token
}
}
use std::marker::PhantomData;
pub struct Init;
pub struct Responded;
pub struct Confirmed;
pub struct Paired;
#[non_exhaustive]
pub struct SasMatch {
_sealed: (),
}
impl SasMatch {
pub fn user_confirmed_visual_match(_ours: &[u8; 10], _theirs: &[u8; 10]) -> Self {
Self { _sealed: () }
}
}
pub struct PairingFlow<S> {
session: PairingSession,
accepted: Option<CompletedPairingUnconfirmed>,
_state: PhantomData<S>,
}
struct CompletedPairingUnconfirmed {
shared_secret: Zeroizing<[u8; 32]>,
peer_signing_pubkey: Vec<u8>,
peer_did: String,
response: PairingResponse,
sas: [u8; 10],
transport_key: TransportKey,
initiator_ephemeral_pub: Vec<u8>,
}
impl PairingFlow<Init> {
pub fn initiate(
now: DateTime<Utc>,
controller_did: String,
endpoint: String,
capabilities: Vec<Capability>,
) -> Result<(Self, PairingToken), ProtocolError> {
let session = PairingToken::generate(now, controller_did, endpoint, capabilities)?;
let token = session.token.clone();
Ok((
Self {
session,
accepted: None,
_state: PhantomData,
},
token,
))
}
pub fn token(&self) -> &PairingToken {
&self.session.token
}
pub fn accept_response(
mut self,
now: DateTime<Utc>,
response: PairingResponse,
) -> Result<PairingFlow<Responded>, ProtocolError> {
response.verify(now, &self.session.token)?;
let initiator_ecdh_pub = self.session.ephemeral_pubkey_bytes()?;
let responder_ecdh_pub = response.device_ephemeral_pubkey_bytes()?;
let shared_secret = self.session.complete_exchange(&responder_ecdh_pub)?;
let peer_signing_pubkey = response.device_signing_pubkey_bytes()?;
let peer_did = response.device_did.clone();
let session_id = &self.session.token.session_id;
let short_code = &self.session.token.short_code;
let sas = sas::derive_sas(
&shared_secret,
&initiator_ecdh_pub,
&responder_ecdh_pub,
session_id,
short_code,
);
let transport_key = sas::derive_transport_key(
&shared_secret,
&initiator_ecdh_pub,
&responder_ecdh_pub,
session_id,
short_code,
);
Ok(PairingFlow {
session: self.session,
accepted: Some(CompletedPairingUnconfirmed {
shared_secret,
peer_signing_pubkey,
peer_did,
response,
sas,
transport_key,
initiator_ephemeral_pub: initiator_ecdh_pub,
}),
_state: PhantomData,
})
}
}
impl PairingFlow<Responded> {
pub fn sas(&self) -> Result<&[u8; 10], ProtocolError> {
self.accepted
.as_ref()
.map(|a| &a.sas)
.ok_or_else(|| ProtocolError::KeyExchangeFailed("flow has no accepted response".into()))
}
pub fn confirm(self, _proof: SasMatch) -> PairingFlow<Confirmed> {
PairingFlow {
session: self.session,
accepted: self.accepted,
_state: PhantomData,
}
}
}
impl PairingFlow<Confirmed> {
pub fn finalize(self) -> Result<(PairingFlow<Paired>, CompletedPairing), ProtocolError> {
let accepted = self.accepted.ok_or_else(|| {
ProtocolError::KeyExchangeFailed(
"flow reached Confirmed without accepted response (impossible)".into(),
)
})?;
let completed = CompletedPairing {
shared_secret: accepted.shared_secret,
peer_signing_pubkey: accepted.peer_signing_pubkey,
peer_did: accepted.peer_did,
response: accepted.response,
sas: accepted.sas,
transport_key: accepted.transport_key,
initiator_ephemeral_pub: accepted.initiator_ephemeral_pub,
};
Ok((
PairingFlow {
session: self.session,
accepted: None,
_state: PhantomData,
},
completed,
))
}
}
pub fn respond_to_pairing(
now: DateTime<Utc>,
token_bytes: &[u8],
device_seed: &TypedSeed,
device_pubkey: &[u8],
device_did: String,
device_name: Option<String>,
) -> Result<ResponderResult, ProtocolError> {
let token: PairingToken = serde_json::from_slice(token_bytes)?;
let (response, shared_secret) = PairingResponse::create(
now,
&token,
device_seed,
device_pubkey,
device_did,
device_name,
)?;
let initiator_ecdh_pub = token.ephemeral_pubkey_bytes()?;
let responder_ecdh_pub = response.device_ephemeral_pubkey_bytes()?;
let session_id = &token.session_id;
let short_code = &token.short_code;
let sas_bytes = sas::derive_sas(
&shared_secret,
&initiator_ecdh_pub,
&responder_ecdh_pub,
session_id,
short_code,
);
let transport_key = sas::derive_transport_key(
&shared_secret,
&initiator_ecdh_pub,
&responder_ecdh_pub,
session_id,
short_code,
);
Ok(ResponderResult {
response,
shared_secret,
sas: sas_bytes,
transport_key,
})
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
fn generate_test_keypair() -> (TypedSeed, Vec<u8>) {
use p256::ecdsa::SigningKey;
use p256::elliptic_curve::rand_core::OsRng as P256Rng;
use p256::pkcs8::EncodePrivateKey;
let sk = SigningKey::random(&mut P256Rng);
let pkcs8 = sk.to_pkcs8_der().unwrap();
let parsed = auths_crypto::parse_key_material(pkcs8.as_bytes()).unwrap();
(parsed.seed, parsed.public_key)
}
#[test]
fn happy_path_initiate_and_complete() {
let now = chrono::Utc::now();
let (protocol, token) = PairingProtocol::initiate(
now,
"did:keri:test".to_string(),
"http://localhost:3000".to_string(),
vec![Capability::sign_commit()],
)
.unwrap();
let (seed, pubkey) = generate_test_keypair();
let token_bytes = serde_json::to_vec(&token).unwrap();
let responder_result = respond_to_pairing(
now,
&token_bytes,
&seed,
&pubkey,
"did:key:zDnaTest".to_string(),
None,
)
.unwrap();
let response_bytes = serde_json::to_vec(&responder_result.response).unwrap();
let completed = protocol.complete(now, &response_bytes).unwrap();
assert_eq!(*completed.shared_secret, *responder_result.shared_secret);
assert_eq!(completed.peer_did, "did:key:zDnaTest");
assert_eq!(completed.sas, responder_result.sas);
}
#[test]
fn expired_token_fails() {
use chrono::Duration;
let now = chrono::Utc::now();
let session = PairingToken::generate_with_expiry(
now,
"did:keri:test".to_string(),
"http://localhost:3000".to_string(),
vec![],
Duration::seconds(-1),
)
.unwrap();
let token = session.token.clone();
let protocol = PairingProtocol { session };
let (seed, pubkey) = generate_test_keypair();
let (response, _) = PairingResponse::create(
now - Duration::seconds(10),
&token,
&seed,
&pubkey,
"did:key:zDnaTest".to_string(),
None,
)
.unwrap();
let response_bytes = serde_json::to_vec(&response).unwrap();
let result = protocol.complete(now, &response_bytes);
assert!(matches!(result, Err(ProtocolError::Expired)));
}
#[test]
fn invalid_response_bytes_fails() {
let now = chrono::Utc::now();
let (protocol, _token) = PairingProtocol::initiate(
now,
"did:keri:test".to_string(),
"http://localhost:3000".to_string(),
vec![],
)
.unwrap();
let result = protocol.complete(now, b"not valid json");
assert!(matches!(result, Err(ProtocolError::Serialization(_))));
}
}