use std::time::{Duration, SystemTime};
use ciborium::Value;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use smallvec::SmallVec;
use kryphocron_lexicons::SemVer;
use crate::audit::BatchRejectionReason;
use crate::identity::{
PublicKey, ServiceIdentity, SessionId, SignatureAlgorithm, SubstrateSessionDerivationKey,
};
use crate::proto::{Did, Nsid};
use crate::wire::canonical_cbor;
use crate::wire::signature::ClaimSignature;
pub const HELLO_DOMAIN_TAG: &[u8] = b"kryphocron/v1/sync-handshake/hello/";
pub const ACCEPT_DOMAIN_TAG: &[u8] = b"kryphocron/v1/sync-handshake/accept/";
pub const REJECT_DOMAIN_TAG: &[u8] = b"kryphocron/v1/sync-handshake/reject/";
pub const ESTABLISHED_DOMAIN_TAG: &[u8] =
b"kryphocron/v1/sync-handshake/established/";
pub const DEFAULT_FEDERATION_TIME_WINDOW: Duration =
Duration::from_secs(7 * 86400);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionNonce([u8; 32]);
impl SessionNonce {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
SessionNonce(bytes)
}
#[must_use]
pub fn generate() -> Self {
let mut bytes = [0u8; 32];
getrandom::getrandom(&mut bytes).expect("OS CSPRNG must be available");
SessionNonce(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncDirection {
Receive,
Send,
Bidirectional,
}
impl SyncDirection {
fn wire_name(self) -> &'static str {
match self {
SyncDirection::Receive => "receive",
SyncDirection::Send => "send",
SyncDirection::Bidirectional => "bidirectional",
}
}
fn from_wire(name: &str) -> Option<Self> {
match name {
"receive" => Some(SyncDirection::Receive),
"send" => Some(SyncDirection::Send),
"bidirectional" => Some(SyncDirection::Bidirectional),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyncTimeWindow {
pub start: SystemTime,
pub end: SystemTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncRequestedScope {
pub nsids: SmallVec<[Nsid; 8]>,
pub time_window: Option<SyncTimeWindow>,
pub direction: SyncDirection,
}
#[derive(Debug, Clone)]
pub struct SyncChannelHello {
pub initiator_identity: ServiceIdentity,
pub initiator_lexicon_set_version: SemVer,
pub proposed_session_nonce: SessionNonce,
pub requested_scope: SyncRequestedScope,
pub initiator_signature: ClaimSignature,
pub at: SystemTime,
}
#[derive(Debug, Clone)]
pub struct SyncChannelAccept {
pub responder_identity: ServiceIdentity,
pub responder_lexicon_set_version: SemVer,
pub session_id: SessionId,
pub negotiated_scope: SyncRequestedScope,
pub responder_signature: ClaimSignature,
pub at: SystemTime,
}
#[derive(Debug, Clone)]
pub struct SyncChannelReject {
pub reason: BatchRejectionReason,
pub responder_identity: ServiceIdentity,
pub responder_signature: ClaimSignature,
pub at: SystemTime,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum SyncChannelResponse {
Accept(SyncChannelAccept),
Reject(SyncChannelReject),
}
#[derive(Debug, Clone)]
pub struct SyncChannelEstablished {
pub session_id: SessionId,
pub initiator_signature: ClaimSignature,
pub at: SystemTime,
}
#[must_use]
pub fn derive_session_id(
key: &SubstrateSessionDerivationKey,
proposed_nonce: &SessionNonce,
responder_entropy: &[u8; 32],
) -> SessionId {
let mut input = [0u8; 64];
input[..32].copy_from_slice(proposed_nonce.as_bytes());
input[32..].copy_from_slice(responder_entropy);
let hash = blake3::keyed_hash(key.as_bytes(), &input);
SessionId::from_bytes(*hash.as_bytes())
}
pub const MAX_HANDSHAKE_MESSAGE_SIZE: usize = 8 * 1024;
#[must_use]
pub fn hello_sign_input(
initiator: &ServiceIdentity,
initiator_lexicon_set_version: SemVer,
proposed_session_nonce: &SessionNonce,
requested_scope: &SyncRequestedScope,
at: SystemTime,
) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("initiator_identity".into()),
service_identity_value(initiator),
),
(
Value::Text("initiator_lexicon_set_version".into()),
semver_value(initiator_lexicon_set_version),
),
(
Value::Text("proposed_session_nonce".into()),
Value::Bytes(proposed_session_nonce.as_bytes().to_vec()),
),
(
Value::Text("requested_scope".into()),
sync_requested_scope_value(requested_scope),
),
(Value::Text("at".into()), system_time_value(at)),
]);
let mut out = HELLO_DOMAIN_TAG.to_vec();
out.extend_from_slice(&canonical_cbor::to_canonical_bytes(payload));
out
}
#[must_use]
pub fn accept_sign_input(
responder: &ServiceIdentity,
responder_lexicon_set_version: SemVer,
session_id: &SessionId,
negotiated_scope: &SyncRequestedScope,
at: SystemTime,
) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("responder_identity".into()),
service_identity_value(responder),
),
(
Value::Text("responder_lexicon_set_version".into()),
semver_value(responder_lexicon_set_version),
),
(
Value::Text("session_id".into()),
Value::Bytes(session_id.as_bytes().to_vec()),
),
(
Value::Text("negotiated_scope".into()),
sync_requested_scope_value(negotiated_scope),
),
(Value::Text("at".into()), system_time_value(at)),
]);
let mut out = ACCEPT_DOMAIN_TAG.to_vec();
out.extend_from_slice(&canonical_cbor::to_canonical_bytes(payload));
out
}
#[must_use]
pub fn reject_sign_input(
reason: &BatchRejectionReason,
responder: &ServiceIdentity,
at: SystemTime,
) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("reason".into()),
batch_rejection_reason_value(reason),
),
(
Value::Text("responder_identity".into()),
service_identity_value(responder),
),
(Value::Text("at".into()), system_time_value(at)),
]);
let mut out = REJECT_DOMAIN_TAG.to_vec();
out.extend_from_slice(&canonical_cbor::to_canonical_bytes(payload));
out
}
#[must_use]
pub fn established_sign_input(
session_id: &SessionId,
responder: &ServiceIdentity,
at: SystemTime,
) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("session_id".into()),
Value::Bytes(session_id.as_bytes().to_vec()),
),
(
Value::Text("responder_identity".into()),
service_identity_value(responder),
),
(Value::Text("at".into()), system_time_value(at)),
]);
let mut out = ESTABLISHED_DOMAIN_TAG.to_vec();
out.extend_from_slice(&canonical_cbor::to_canonical_bytes(payload));
out
}
#[must_use]
pub fn sign_handshake_payload(key: &SigningKey, sign_input: &[u8]) -> ClaimSignature {
let sig = key.sign(sign_input);
ClaimSignature {
algorithm: SignatureAlgorithm::Ed25519,
bytes: sig.to_bytes(),
}
}
#[must_use]
pub fn verify_handshake_signature(
public_key: &PublicKey,
sign_input: &[u8],
signature: &ClaimSignature,
) -> bool {
if signature.algorithm != SignatureAlgorithm::Ed25519
|| public_key.algorithm != SignatureAlgorithm::Ed25519
{
return false;
}
let Ok(vk) = VerifyingKey::from_bytes(&public_key.bytes) else {
return false;
};
let sig = Signature::from_bytes(&signature.bytes);
vk.verify(sign_input, &sig).is_ok()
}
fn service_identity_value(s: &ServiceIdentity) -> Value {
Value::Map(vec![
(
Value::Text("did".into()),
Value::Text(s.service_did().as_str().to_string()),
),
(
Value::Text("key_id".into()),
Value::Bytes(s.key_id().as_bytes().to_vec()),
),
(
Value::Text("key_alg".into()),
Value::Text(signature_alg_name(s.key_material().algorithm).into()),
),
(
Value::Text("key_material".into()),
Value::Bytes(s.key_material().bytes.to_vec()),
),
])
}
fn signature_alg_name(a: SignatureAlgorithm) -> &'static str {
match a {
SignatureAlgorithm::Ed25519 => "Ed25519",
SignatureAlgorithm::Es256 => "Es256",
SignatureAlgorithm::Es256K => "Es256K",
}
}
fn semver_value(v: SemVer) -> Value {
Value::Array(vec![
Value::Integer(v.major.into()),
Value::Integer(v.minor.into()),
Value::Integer(v.patch.into()),
])
}
fn sync_requested_scope_value(s: &SyncRequestedScope) -> Value {
let nsids = Value::Array(
s.nsids
.iter()
.map(|n| Value::Text(n.as_str().to_string()))
.collect(),
);
let time_window = match s.time_window {
None => Value::Null,
Some(w) => Value::Map(vec![
(Value::Text("start".into()), system_time_value(w.start)),
(
Value::Text("end".into()),
system_time_value(w.end),
),
]),
};
Value::Map(vec![
(Value::Text("nsids".into()), nsids),
(Value::Text("time_window".into()), time_window),
(
Value::Text("direction".into()),
Value::Text(s.direction.wire_name().to_string()),
),
])
}
fn batch_rejection_reason_value(r: &BatchRejectionReason) -> Value {
match r {
BatchRejectionReason::LexiconSetMajorVersionMismatch { local, peer } => {
Value::Map(vec![
(
Value::Text("kind".into()),
Value::Text("lexicon_set_major_version_mismatch".into()),
),
(Value::Text("local".into()), semver_value(*local)),
(Value::Text("peer".into()), semver_value(*peer)),
])
}
BatchRejectionReason::UnauthorizedPeer => Value::Map(vec![(
Value::Text("kind".into()),
Value::Text("unauthorized_peer".into()),
)]),
BatchRejectionReason::HandshakeSignatureInvalid => Value::Map(vec![(
Value::Text("kind".into()),
Value::Text("handshake_signature_invalid".into()),
)]),
BatchRejectionReason::HandshakeTimeout => Value::Map(vec![(
Value::Text("kind".into()),
Value::Text("handshake_timeout".into()),
)]),
BatchRejectionReason::HandshakeNonceReplay { first_seen_at } => {
Value::Map(vec![
(
Value::Text("kind".into()),
Value::Text("handshake_nonce_replay".into()),
),
(
Value::Text("first_seen_at".into()),
system_time_value(*first_seen_at),
),
])
}
}
}
fn system_time_value(t: SystemTime) -> Value {
let secs = t
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime before UNIX_EPOCH not supported")
.as_secs();
Value::Integer(secs.into())
}
#[allow(clippy::type_complexity)]
pub(crate) fn decode_hello_wire(
bytes: &[u8],
) -> Result<
(
ServiceIdentity, // initiator_identity
SemVer, // initiator_lexicon_set_version
SessionNonce,
SyncRequestedScope,
SystemTime, // at
ClaimSignature,
),
(),
> {
let value = canonical_cbor::from_bytes(bytes)?;
let map = into_map(&value)?;
Ok((
decode_service_identity(map_get(map, "initiator_identity")?)?,
decode_semver(map_get(map, "initiator_lexicon_set_version")?)?,
decode_session_nonce(map_get(map, "proposed_session_nonce")?)?,
decode_sync_requested_scope(map_get(map, "requested_scope")?)?,
decode_system_time(map_get(map, "at")?)?,
decode_claim_signature(map_get(map, "initiator_signature")?)?,
))
}
#[allow(clippy::type_complexity)]
pub(crate) fn decode_accept_wire(
bytes: &[u8],
) -> Result<
(
ServiceIdentity, // responder_identity
SemVer, // responder_lexicon_set_version
SessionId,
SyncRequestedScope,
SystemTime,
ClaimSignature,
),
(),
> {
let value = canonical_cbor::from_bytes(bytes)?;
let map = into_map(&value)?;
Ok((
decode_service_identity(map_get(map, "responder_identity")?)?,
decode_semver(map_get(map, "responder_lexicon_set_version")?)?,
decode_session_id(map_get(map, "session_id")?)?,
decode_sync_requested_scope(map_get(map, "negotiated_scope")?)?,
decode_system_time(map_get(map, "at")?)?,
decode_claim_signature(map_get(map, "responder_signature")?)?,
))
}
pub(crate) fn decode_reject_wire(
bytes: &[u8],
) -> Result<
(
BatchRejectionReason,
ServiceIdentity, // responder_identity
SystemTime,
ClaimSignature,
),
(),
> {
let value = canonical_cbor::from_bytes(bytes)?;
let map = into_map(&value)?;
Ok((
decode_batch_rejection_reason(map_get(map, "reason")?)?,
decode_service_identity(map_get(map, "responder_identity")?)?,
decode_system_time(map_get(map, "at")?)?,
decode_claim_signature(map_get(map, "responder_signature")?)?,
))
}
pub(crate) fn decode_established_wire(
bytes: &[u8],
) -> Result<(SessionId, SystemTime, ClaimSignature), ()> {
let value = canonical_cbor::from_bytes(bytes)?;
let map = into_map(&value)?;
Ok((
decode_session_id(map_get(map, "session_id")?)?,
decode_system_time(map_get(map, "at")?)?,
decode_claim_signature(map_get(map, "initiator_signature")?)?,
))
}
#[must_use]
pub(crate) fn hello_to_wire_bytes(h: &SyncChannelHello) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("initiator_identity".into()),
service_identity_value(&h.initiator_identity),
),
(
Value::Text("initiator_lexicon_set_version".into()),
semver_value(h.initiator_lexicon_set_version),
),
(
Value::Text("proposed_session_nonce".into()),
Value::Bytes(h.proposed_session_nonce.as_bytes().to_vec()),
),
(
Value::Text("requested_scope".into()),
sync_requested_scope_value(&h.requested_scope),
),
(Value::Text("at".into()), system_time_value(h.at)),
(
Value::Text("initiator_signature".into()),
claim_signature_value(&h.initiator_signature),
),
]);
canonical_cbor::to_canonical_bytes(payload)
}
#[must_use]
pub(crate) fn accept_to_wire_bytes(a: &SyncChannelAccept) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("responder_identity".into()),
service_identity_value(&a.responder_identity),
),
(
Value::Text("responder_lexicon_set_version".into()),
semver_value(a.responder_lexicon_set_version),
),
(
Value::Text("session_id".into()),
Value::Bytes(a.session_id.as_bytes().to_vec()),
),
(
Value::Text("negotiated_scope".into()),
sync_requested_scope_value(&a.negotiated_scope),
),
(Value::Text("at".into()), system_time_value(a.at)),
(
Value::Text("responder_signature".into()),
claim_signature_value(&a.responder_signature),
),
]);
canonical_cbor::to_canonical_bytes(payload)
}
#[must_use]
pub(crate) fn reject_to_wire_bytes(r: &SyncChannelReject) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("reason".into()),
batch_rejection_reason_value(&r.reason),
),
(
Value::Text("responder_identity".into()),
service_identity_value(&r.responder_identity),
),
(Value::Text("at".into()), system_time_value(r.at)),
(
Value::Text("responder_signature".into()),
claim_signature_value(&r.responder_signature),
),
]);
canonical_cbor::to_canonical_bytes(payload)
}
#[must_use]
pub(crate) fn established_to_wire_bytes(e: &SyncChannelEstablished) -> Vec<u8> {
let payload = Value::Map(vec![
(
Value::Text("session_id".into()),
Value::Bytes(e.session_id.as_bytes().to_vec()),
),
(Value::Text("at".into()), system_time_value(e.at)),
(
Value::Text("initiator_signature".into()),
claim_signature_value(&e.initiator_signature),
),
]);
canonical_cbor::to_canonical_bytes(payload)
}
fn claim_signature_value(s: &ClaimSignature) -> Value {
Value::Map(vec![
(
Value::Text("alg".into()),
Value::Text(signature_alg_name(s.algorithm).into()),
),
(
Value::Text("bytes".into()),
Value::Bytes(s.bytes.to_vec()),
),
])
}
fn into_map(v: &Value) -> Result<&Vec<(Value, Value)>, ()> {
match v {
Value::Map(m) => Ok(m),
_ => Err(()),
}
}
fn map_get<'a>(map: &'a [(Value, Value)], key: &str) -> Result<&'a Value, ()> {
map.iter()
.find(|(k, _)| matches!(k, Value::Text(s) if s.as_str() == key))
.map(|(_, v)| v)
.ok_or(())
}
fn decode_service_identity(v: &Value) -> Result<ServiceIdentity, ()> {
let m = into_map(v)?;
let did = match map_get(m, "did")? {
Value::Text(s) => Did::new(s).map_err(|_| ())?,
_ => return Err(()),
};
let key_id_bytes: [u8; 32] = match map_get(m, "key_id")? {
Value::Bytes(b) if b.len() == 32 => {
let mut a = [0u8; 32];
a.copy_from_slice(b);
a
}
_ => return Err(()),
};
let alg = match map_get(m, "key_alg")? {
Value::Text(s) => decode_signature_alg(s)?,
_ => return Err(()),
};
let key_material_bytes: [u8; 32] = match map_get(m, "key_material")? {
Value::Bytes(b) if b.len() == 32 => {
let mut a = [0u8; 32];
a.copy_from_slice(b);
a
}
_ => return Err(()),
};
Ok(ServiceIdentity::new_internal(
did,
crate::identity::KeyId::from_bytes(key_id_bytes),
PublicKey {
algorithm: alg,
bytes: key_material_bytes,
},
None,
))
}
fn decode_signature_alg(s: &str) -> Result<SignatureAlgorithm, ()> {
match s {
"Ed25519" => Ok(SignatureAlgorithm::Ed25519),
"Es256" => Ok(SignatureAlgorithm::Es256),
"Es256K" => Ok(SignatureAlgorithm::Es256K),
_ => Err(()),
}
}
fn decode_semver(v: &Value) -> Result<SemVer, ()> {
let arr = match v {
Value::Array(a) if a.len() == 3 => a,
_ => return Err(()),
};
let major = decode_u32(&arr[0])?;
let minor = decode_u32(&arr[1])?;
let patch = decode_u32(&arr[2])?;
Ok(SemVer::new(major, minor, patch))
}
fn decode_u32(v: &Value) -> Result<u32, ()> {
match v {
Value::Integer(i) => {
let n: i128 = (*i).into();
u32::try_from(n).map_err(|_| ())
}
_ => Err(()),
}
}
fn decode_session_nonce(v: &Value) -> Result<SessionNonce, ()> {
match v {
Value::Bytes(b) if b.len() == 32 => {
let mut a = [0u8; 32];
a.copy_from_slice(b);
Ok(SessionNonce::from_bytes(a))
}
_ => Err(()),
}
}
fn decode_session_id(v: &Value) -> Result<SessionId, ()> {
match v {
Value::Bytes(b) if b.len() == 32 => {
let mut a = [0u8; 32];
a.copy_from_slice(b);
Ok(SessionId::from_bytes(a))
}
_ => Err(()),
}
}
fn decode_sync_requested_scope(v: &Value) -> Result<SyncRequestedScope, ()> {
let m = into_map(v)?;
let nsids = match map_get(m, "nsids")? {
Value::Array(items) => {
let mut sv: SmallVec<[Nsid; 8]> = SmallVec::new();
for item in items {
let s = match item {
Value::Text(s) => s,
_ => return Err(()),
};
sv.push(Nsid::new(s).map_err(|_| ())?);
}
sv
}
_ => return Err(()),
};
let time_window = match map_get(m, "time_window")? {
Value::Null => None,
Value::Map(_) => Some(decode_sync_time_window(map_get(m, "time_window")?)?),
_ => return Err(()),
};
let direction = match map_get(m, "direction")? {
Value::Text(s) => SyncDirection::from_wire(s).ok_or(())?,
_ => return Err(()),
};
Ok(SyncRequestedScope {
nsids,
time_window,
direction,
})
}
fn decode_sync_time_window(v: &Value) -> Result<SyncTimeWindow, ()> {
let m = into_map(v)?;
let start = decode_system_time(map_get(m, "start")?)?;
let end = decode_system_time(map_get(m, "end")?)?;
Ok(SyncTimeWindow { start, end })
}
fn decode_system_time(v: &Value) -> Result<SystemTime, ()> {
match v {
Value::Integer(i) => {
let n: i128 = (*i).into();
let secs = u64::try_from(n).map_err(|_| ())?;
Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(secs))
}
_ => Err(()),
}
}
fn decode_claim_signature(v: &Value) -> Result<ClaimSignature, ()> {
let m = into_map(v)?;
let alg = match map_get(m, "alg")? {
Value::Text(s) => decode_signature_alg(s)?,
_ => return Err(()),
};
let bytes: [u8; 64] = match map_get(m, "bytes")? {
Value::Bytes(b) if b.len() == 64 => {
let mut a = [0u8; 64];
a.copy_from_slice(b);
a
}
_ => return Err(()),
};
Ok(ClaimSignature {
algorithm: alg,
bytes,
})
}
fn decode_batch_rejection_reason(v: &Value) -> Result<BatchRejectionReason, ()> {
let m = into_map(v)?;
let kind = match map_get(m, "kind")? {
Value::Text(s) => s.as_str(),
_ => return Err(()),
};
match kind {
"lexicon_set_major_version_mismatch" => {
let local = decode_semver(map_get(m, "local")?)?;
let peer = decode_semver(map_get(m, "peer")?)?;
Ok(BatchRejectionReason::LexiconSetMajorVersionMismatch {
local,
peer,
})
}
"unauthorized_peer" => Ok(BatchRejectionReason::UnauthorizedPeer),
"handshake_signature_invalid" => {
Ok(BatchRejectionReason::HandshakeSignatureInvalid)
}
"handshake_timeout" => Ok(BatchRejectionReason::HandshakeTimeout),
"handshake_nonce_replay" => {
let first_seen_at =
decode_system_time(map_get(m, "first_seen_at")?)?;
Ok(BatchRejectionReason::HandshakeNonceReplay { first_seen_at })
}
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::KeyId;
fn sample_identity(seed: u8) -> ServiceIdentity {
let did = format!("did:plc:{seed:02x}sample0000000000");
ServiceIdentity::new_internal(
Did::new(&did).unwrap(),
KeyId::from_bytes([seed; 32]),
PublicKey {
algorithm: SignatureAlgorithm::Ed25519,
bytes: [seed.wrapping_add(1); 32],
},
None,
)
}
fn signing_pair(seed: u8) -> (SigningKey, PublicKey) {
let sk = SigningKey::from_bytes(&[seed; 32]);
let vk = sk.verifying_key();
let pk = PublicKey {
algorithm: SignatureAlgorithm::Ed25519,
bytes: vk.to_bytes(),
};
(sk, pk)
}
fn empty_scope() -> SyncRequestedScope {
SyncRequestedScope {
nsids: SmallVec::new(),
time_window: None,
direction: SyncDirection::Bidirectional,
}
}
#[test]
fn derive_session_id_is_deterministic_and_input_separated() {
let key = SubstrateSessionDerivationKey::from_bytes([0x55; 32]);
let nonce = SessionNonce::from_bytes([0x11; 32]);
let entropy = [0x22; 32];
let s1 = derive_session_id(&key, &nonce, &entropy);
let s2 = derive_session_id(&key, &nonce, &entropy);
assert_eq!(s1, s2, "deterministic for the same inputs");
let other_entropy = [0x33; 32];
let s3 = derive_session_id(&key, &nonce, &other_entropy);
assert_ne!(s1, s3, "different entropy produces different session id");
let other_nonce = SessionNonce::from_bytes([0x44; 32]);
let s4 = derive_session_id(&key, &other_nonce, &entropy);
assert_ne!(s1, s4, "different nonce produces different session id");
}
#[test]
fn derive_session_id_separates_by_substrate_key() {
let key_a = SubstrateSessionDerivationKey::from_bytes([0xAA; 32]);
let key_b = SubstrateSessionDerivationKey::from_bytes([0xBB; 32]);
let nonce = SessionNonce::from_bytes([0x11; 32]);
let entropy = [0x22; 32];
let s_a = derive_session_id(&key_a, &nonce, &entropy);
let s_b = derive_session_id(&key_b, &nonce, &entropy);
assert_ne!(s_a, s_b);
}
#[test]
fn hello_sign_verify_round_trip() {
let (sk, pk) = signing_pair(0x01);
let mut id = sample_identity(0x01);
id = ServiceIdentity::new_internal(
id.service_did().clone(),
id.key_id(),
pk,
None,
);
let nonce = SessionNonce::from_bytes([0x42; 32]);
let scope = empty_scope();
let at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
let input = hello_sign_input(&id, SemVer::new(1, 0, 0), &nonce, &scope, at);
let sig = sign_handshake_payload(&sk, &input);
assert!(verify_handshake_signature(id.key_material(), &input, &sig));
}
#[test]
fn hello_signature_does_not_verify_as_accept() {
let (sk, pk) = signing_pair(0x02);
let id = ServiceIdentity::new_internal(
Did::new("did:plc:samplesamplesample0000").unwrap(),
KeyId::from_bytes([0x02; 32]),
pk,
None,
);
let nonce = SessionNonce::from_bytes([0x42; 32]);
let scope = empty_scope();
let at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
let hello_input =
hello_sign_input(&id, SemVer::new(1, 0, 0), &nonce, &scope, at);
let sig = sign_handshake_payload(&sk, &hello_input);
let session_id = SessionId::from_bytes([0xFF; 32]);
let accept_input = accept_sign_input(
&id,
SemVer::new(1, 0, 0),
&session_id,
&scope,
at,
);
assert!(!verify_handshake_signature(id.key_material(), &accept_input, &sig));
}
#[test]
fn handshake_domain_tags_are_byte_distinct() {
let tags = [
HELLO_DOMAIN_TAG,
ACCEPT_DOMAIN_TAG,
REJECT_DOMAIN_TAG,
ESTABLISHED_DOMAIN_TAG,
];
for i in 0..tags.len() {
for j in (i + 1)..tags.len() {
assert_ne!(
tags[i], tags[j],
"handshake domain tags must be byte-distinct"
);
}
}
}
#[test]
fn handshake_domain_tags_are_distinct_from_other_section_tags() {
let handshake_tags: [&[u8]; 4] = [
HELLO_DOMAIN_TAG,
ACCEPT_DOMAIN_TAG,
REJECT_DOMAIN_TAG,
ESTABLISHED_DOMAIN_TAG,
];
let other_tags: [&[u8]; 3] = [
crate::wire::CLAIM_DOMAIN_TAG,
crate::trust::TRUST_DECLARATION_DOMAIN_TAG,
b"kryphocron/v1/attribution-receipt/",
];
for h in handshake_tags {
for o in other_tags {
assert_ne!(
h, o,
"handshake tag must be distinct from other §7 tags"
);
}
}
}
#[test]
fn hello_wire_round_trips_canonical() {
let (sk, pk) = signing_pair(0x05);
let id = ServiceIdentity::new_internal(
Did::new("did:plc:samplesamplesample0000").unwrap(),
KeyId::from_bytes([0x05; 32]),
pk,
None,
);
let nonce = SessionNonce::from_bytes([0x42; 32]);
let scope = empty_scope();
let at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
let input = hello_sign_input(&id, SemVer::new(1, 0, 0), &nonce, &scope, at);
let sig = sign_handshake_payload(&sk, &input);
let hello = SyncChannelHello {
initiator_identity: id.clone(),
initiator_lexicon_set_version: SemVer::new(1, 0, 0),
proposed_session_nonce: nonce,
requested_scope: scope.clone(),
initiator_signature: sig,
at,
};
let bytes = hello_to_wire_bytes(&hello);
let (
d_id,
d_ver,
d_nonce,
d_scope,
d_at,
d_sig,
) = decode_hello_wire(&bytes).unwrap();
assert_eq!(d_id, id);
assert_eq!(d_ver, SemVer::new(1, 0, 0));
assert_eq!(d_nonce, nonce);
assert_eq!(d_scope, scope);
assert_eq!(d_at, at);
assert_eq!(d_sig, hello.initiator_signature);
let re_encoded = canonical_cbor::to_canonical_bytes(
canonical_cbor::from_bytes(&bytes).unwrap(),
);
assert_eq!(bytes, re_encoded);
}
#[test]
fn max_handshake_message_size_pinned() {
assert_eq!(MAX_HANDSHAKE_MESSAGE_SIZE, 8 * 1024);
}
#[test]
fn default_federation_time_window_pinned_at_7_days() {
assert_eq!(DEFAULT_FEDERATION_TIME_WINDOW, Duration::from_secs(7 * 86400));
}
}