mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-12 05:29:57 +02:00
feat(core): save devices through mutual consent and grants
Establish PendingOutgoing/PendingIncoming relationships over a token-bound pairing protocol, exchange directional grants with challenge-response proofs and a final ack before Saved, and merge simultaneous initiations without a second prompt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
134
crates/vnidrop/src/device_relationship/crypto.rs
Normal file
134
crates/vnidrop/src/device_relationship/crypto.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! Relationship-grant possession proofs (design §7).
|
||||
|
||||
use crate::{
|
||||
error::VnidropError,
|
||||
grant::{Challenge, GrantId, GrantProof, GrantSecret},
|
||||
secure_secret::SecretMaterial,
|
||||
};
|
||||
|
||||
const RELATIONSHIP_GRANT_CONTEXT: &[u8] = b"vnidrop-relationship-grant-v1";
|
||||
|
||||
pub(super) fn encode_relationship_grant_secret(
|
||||
secret: &GrantSecret,
|
||||
) -> Result<SecretMaterial, VnidropError> {
|
||||
// Custody stores only the 32-byte secret; issuer/holder/generation/protocol
|
||||
// bindings live in the relationship row and are enforced at prove/verify time.
|
||||
SecretMaterial::new(secret.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub(super) fn secret_from_material(material: &SecretMaterial) -> Result<GrantSecret, VnidropError> {
|
||||
let bytes: [u8; 32] =
|
||||
material
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| VnidropError::SecureStorageCorrupted {
|
||||
reason: "relationship grant secret has invalid length".to_string(),
|
||||
})?;
|
||||
Ok(GrantSecret::from_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(super) fn prove_relationship_grant(
|
||||
grant_id: GrantId,
|
||||
secret: &GrantSecret,
|
||||
challenge: &Challenge,
|
||||
issuer: &str,
|
||||
holder: &str,
|
||||
generation: u64,
|
||||
protocol_version: u16,
|
||||
) -> GrantProof {
|
||||
let mac = relationship_mac(
|
||||
secret,
|
||||
challenge,
|
||||
issuer,
|
||||
holder,
|
||||
generation,
|
||||
protocol_version,
|
||||
);
|
||||
GrantProof::from_parts(grant_id, mac)
|
||||
}
|
||||
|
||||
pub(super) fn verify_relationship_grant(
|
||||
secret: &GrantSecret,
|
||||
proof: &GrantProof,
|
||||
challenge: &Challenge,
|
||||
issuer: &str,
|
||||
holder: &str,
|
||||
generation: u64,
|
||||
protocol_version: u16,
|
||||
) -> Result<(), &'static str> {
|
||||
let expected = relationship_mac(
|
||||
secret,
|
||||
challenge,
|
||||
issuer,
|
||||
holder,
|
||||
generation,
|
||||
protocol_version,
|
||||
);
|
||||
if blake3::Hash::from_bytes(expected) != blake3::Hash::from_bytes(*proof.mac()) {
|
||||
return Err("bad relationship grant proof");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn relationship_mac(
|
||||
secret: &GrantSecret,
|
||||
challenge: &Challenge,
|
||||
issuer: &str,
|
||||
holder: &str,
|
||||
generation: u64,
|
||||
protocol_version: u16,
|
||||
) -> [u8; 32] {
|
||||
let mut hasher = blake3::Hasher::new_keyed(secret.as_bytes());
|
||||
hasher.update(RELATIONSHIP_GRANT_CONTEXT);
|
||||
hasher.update(challenge.as_bytes());
|
||||
hasher.update(&(issuer.len() as u64).to_le_bytes());
|
||||
hasher.update(issuer.as_bytes());
|
||||
hasher.update(&(holder.len() as u64).to_le_bytes());
|
||||
hasher.update(holder.as_bytes());
|
||||
hasher.update(&generation.to_le_bytes());
|
||||
hasher.update(&protocol_version.to_le_bytes());
|
||||
*hasher.finalize().as_bytes()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod grant_vectors {
|
||||
use super::*;
|
||||
use crate::api::experimental_saved_device_capabilities;
|
||||
use data_encoding::HEXLOWER;
|
||||
|
||||
#[test]
|
||||
fn relationship_grant_proof_vectors_are_stable() {
|
||||
let secret =
|
||||
GrantSecret::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
.unwrap();
|
||||
let grant_id = GrantId::decode("0123456789abcdef0123456789abcdef").unwrap();
|
||||
let challenge = Challenge::from_bytes([9u8; 32]);
|
||||
let protocol = experimental_saved_device_capabilities().relationship_protocol_version;
|
||||
let proof = prove_relationship_grant(
|
||||
grant_id, &secret, &challenge, "issuer", "holder", 1, protocol,
|
||||
);
|
||||
// Binding and replay resistance: wrong holder or challenge must fail.
|
||||
verify_relationship_grant(&secret, &proof, &challenge, "issuer", "holder", 1, protocol)
|
||||
.unwrap();
|
||||
let mac_hex = HEXLOWER.encode(proof.mac());
|
||||
assert_eq!(
|
||||
mac_hex,
|
||||
"e6cc2641183b84fae9e3805761961d69e09d25a1f8ceeaeede952774ddd95d6b"
|
||||
);
|
||||
assert!(verify_relationship_grant(
|
||||
&secret, &proof, &challenge, "issuer", "other", 1, protocol,
|
||||
)
|
||||
.is_err());
|
||||
let other_challenge = Challenge::from_bytes([8u8; 32]);
|
||||
assert!(verify_relationship_grant(
|
||||
&secret,
|
||||
&proof,
|
||||
&other_challenge,
|
||||
"issuer",
|
||||
"holder",
|
||||
1,
|
||||
protocol,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
1306
crates/vnidrop/src/device_relationship/mod.rs
Normal file
1306
crates/vnidrop/src/device_relationship/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user