qualia_client_core/
handshake.rs1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
19
20#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
26pub struct Challenge {
27 pub nonce: String,
28 pub from_did: String,
29}
30
31#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
36pub struct ChallengeResponse {
37 pub nonce: String,
38 pub responder_did: String,
39 pub responder_pubkey_hex: String,
40 pub signature_hex: String,
41}
42
43pub fn make_challenge(from_did: &str, nonce: &str) -> Challenge {
48 Challenge {
49 nonce: nonce.to_string(),
50 from_did: from_did.to_string(),
51 }
52}
53
54pub fn signed_message(nonce: &str, responder_did: &str) -> Vec<u8> {
61 format!("qhs1|{nonce}|{responder_did}").into_bytes()
62}
63
64pub fn answer_challenge(
70 challenge: &Challenge,
71 responder_did: &str,
72 key: &SigningKey,
73) -> ChallengeResponse {
74 let msg = signed_message(&challenge.nonce, responder_did);
75 let sig: Signature = key.sign(&msg);
76 let vk = VerifyingKey::from(key);
77 ChallengeResponse {
78 nonce: challenge.nonce.clone(),
79 responder_did: responder_did.to_string(),
80 responder_pubkey_hex: hex::encode(vk.to_bytes()),
81 signature_hex: hex::encode(sig.to_bytes()),
82 }
83}
84
85pub fn verify_response(challenge: &Challenge, resp: &ChallengeResponse) -> Result<(), String> {
92 if resp.nonce != challenge.nonce {
93 return Err(format!(
94 "nonce mismatch: expected {:?}, got {:?}",
95 challenge.nonce, resp.nonce
96 ));
97 }
98
99 let pk_bytes =
100 hex::decode(&resp.responder_pubkey_hex).map_err(|e| format!("invalid pubkey hex: {e}"))?;
101 let pk_arr: [u8; 32] = pk_bytes
102 .as_slice()
103 .try_into()
104 .map_err(|_| format!("pubkey must be 32 bytes, got {}", pk_bytes.len()))?;
105 let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|e| format!("invalid pubkey: {e}"))?;
106
107 let sig_bytes =
108 hex::decode(&resp.signature_hex).map_err(|e| format!("invalid signature hex: {e}"))?;
109 let sig_arr: [u8; 64] = sig_bytes
110 .as_slice()
111 .try_into()
112 .map_err(|_| format!("signature must be 64 bytes, got {}", sig_bytes.len()))?;
113 let sig = Signature::from_bytes(&sig_arr);
114
115 let msg = signed_message(&resp.nonce, &resp.responder_did);
116 vk.verify(&msg, &sig)
117 .map_err(|e| format!("signature verification failed: {e}"))
118}
119
120pub fn responder_is(resp: &ChallengeResponse, expected_did: &str) -> bool {
126 resp.responder_did == expected_did
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn test_key() -> SigningKey {
134 SigningKey::from_bytes(&[7u8; 32])
135 }
136
137 const BOB_DID: &str = "did:qhs:bob";
138 const ALICE_DID: &str = "did:qhs:alice";
139
140 #[test]
141 fn valid_handshake_verifies() {
142 let key = test_key();
143 let challenge = make_challenge(ALICE_DID, "nonce-abc-123");
144 let resp = answer_challenge(&challenge, BOB_DID, &key);
145 assert_eq!(verify_response(&challenge, &resp), Ok(()));
146 }
147
148 #[test]
149 fn wrong_nonce_errs() {
150 let key = test_key();
151 let challenge = make_challenge(ALICE_DID, "nonce-abc-123");
152 let mut resp = answer_challenge(&challenge, BOB_DID, &key);
153 resp.nonce = "different-nonce".to_string();
154 assert!(verify_response(&challenge, &resp).is_err());
155 }
156
157 #[test]
158 fn tampered_responder_did_errs() {
159 let key = test_key();
160 let challenge = make_challenge(ALICE_DID, "nonce-abc-123");
161 let mut resp = answer_challenge(&challenge, BOB_DID, &key);
162 resp.responder_did = "did:qhs:mallory".to_string();
165 assert!(verify_response(&challenge, &resp).is_err());
166 }
167
168 #[test]
169 fn responder_is_matches_real_did_only() {
170 let key = test_key();
171 let challenge = make_challenge(ALICE_DID, "nonce-abc-123");
172 let resp = answer_challenge(&challenge, BOB_DID, &key);
173 assert!(responder_is(&resp, BOB_DID));
174 assert!(!responder_is(&resp, "did:qhs:mallory"));
175 }
176}