1use crate::types::messages::{LdpEnvelope, LdpMessageBody};
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6use subtle::ConstantTimeEq;
7
8type HmacSha256 = Hmac<Sha256>;
9
10pub fn sign_envelope(envelope: &LdpEnvelope, secret: &str) -> String {
15 let mut mac =
16 HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key length");
17
18 mac.update(envelope.from.as_bytes());
20 mac.update(b"|");
21 mac.update(envelope.to.as_bytes());
22 mac.update(b"|");
23 mac.update(envelope.session_id.as_bytes());
24 mac.update(b"|");
25 mac.update(envelope.timestamp.as_bytes());
26 mac.update(b"|");
27 mac.update(envelope.message_id.as_bytes());
28
29 if let Some(ref nonce) = envelope.nonce {
31 mac.update(b"|");
32 mac.update(nonce.as_bytes());
33 }
34
35 mac.update(b"|");
36
37 let body_type = match &envelope.body {
39 LdpMessageBody::Hello { delegate_id, .. } => {
40 mac.update(delegate_id.as_bytes());
41 "HELLO"
42 }
43 LdpMessageBody::CapabilityManifest { .. } => "CAPABILITY_MANIFEST",
44 LdpMessageBody::SessionPropose { .. } => "SESSION_PROPOSE",
45 LdpMessageBody::SessionAccept { session_id, .. } => {
46 mac.update(session_id.as_bytes());
47 "SESSION_ACCEPT"
48 }
49 LdpMessageBody::SessionReject { reason, .. } => {
50 mac.update(reason.as_bytes());
51 "SESSION_REJECT"
52 }
53 LdpMessageBody::TaskSubmit { task_id, skill, .. } => {
54 mac.update(task_id.as_bytes());
55 mac.update(b"|");
56 mac.update(skill.as_bytes());
57 "TASK_SUBMIT"
58 }
59 LdpMessageBody::TaskUpdate { task_id, .. } => {
60 mac.update(task_id.as_bytes());
61 "TASK_UPDATE"
62 }
63 LdpMessageBody::TaskResult { task_id, .. } => {
64 mac.update(task_id.as_bytes());
65 "TASK_RESULT"
66 }
67 LdpMessageBody::TaskFailed { task_id, .. } => {
68 mac.update(task_id.as_bytes());
69 "TASK_FAILED"
70 }
71 LdpMessageBody::TaskCancel { task_id } => {
72 mac.update(task_id.as_bytes());
73 "TASK_CANCEL"
74 }
75 LdpMessageBody::Attestation { .. } => "ATTESTATION",
76 LdpMessageBody::SessionClose { .. } => "SESSION_CLOSE",
77 };
78 mac.update(b"|");
79 mac.update(body_type.as_bytes());
80
81 hex::encode(mac.finalize().into_bytes())
82}
83
84pub fn verify_envelope(envelope: &LdpEnvelope, secret: &str, signature: &str) -> bool {
86 let expected = sign_envelope(envelope, secret);
87 let expected_bytes = expected.as_bytes();
88 let signature_bytes = signature.as_bytes();
89 if expected_bytes.len() != signature_bytes.len() {
90 return false;
91 }
92 expected_bytes.ct_eq(signature_bytes).into()
93}
94
95pub fn apply_signature(envelope: &mut LdpEnvelope, secret: &str) {
97 let sig = sign_envelope(envelope, secret);
98 envelope.signature = Some(sig);
99 envelope.signature_algorithm = Some("hmac-sha256".into());
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::types::payload::PayloadMode;
106
107 fn make_envelope() -> LdpEnvelope {
108 LdpEnvelope::new(
109 "session-1",
110 "from-delegate",
111 "to-delegate",
112 LdpMessageBody::Hello {
113 delegate_id: "test".into(),
114 supported_modes: vec![PayloadMode::Text],
115 },
116 PayloadMode::Text,
117 )
118 }
119
120 #[test]
121 fn sign_and_verify_roundtrip() {
122 let envelope = make_envelope();
123 let sig = sign_envelope(&envelope, "test-secret");
124 assert!(!sig.is_empty());
125 assert!(verify_envelope(&envelope, "test-secret", &sig));
126 }
127
128 #[test]
129 fn tampered_message_fails() {
130 let envelope = make_envelope();
131 let sig = sign_envelope(&envelope, "test-secret");
132 let mut tampered = envelope.clone();
133 tampered.from = "attacker".into();
134 assert!(!verify_envelope(&tampered, "test-secret", &sig));
135 }
136
137 #[test]
138 fn wrong_secret_fails() {
139 let envelope = make_envelope();
140 let sig = sign_envelope(&envelope, "secret-a");
141 assert!(!verify_envelope(&envelope, "secret-b", &sig));
142 }
143
144 #[test]
145 fn apply_signature_sets_fields() {
146 let mut envelope = make_envelope();
147 apply_signature(&mut envelope, "test-secret");
148 assert!(envelope.signature.is_some());
149 assert_eq!(envelope.signature_algorithm.as_deref(), Some("hmac-sha256"));
150 }
151
152 #[test]
153 fn task_submit_signing() {
154 let envelope = LdpEnvelope::new(
155 "s1",
156 "from",
157 "to",
158 LdpMessageBody::TaskSubmit {
159 task_id: "t1".into(),
160 skill: "echo".into(),
161 input: serde_json::json!({"data": 1}),
162 contract: None,
163 },
164 PayloadMode::Text,
165 );
166 let sig = sign_envelope(&envelope, "secret");
167 assert!(verify_envelope(&envelope, "secret", &sig));
168 }
169}