1use crate::encoding::STANDARD;
5use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use sha2::{Digest, Sha256};
9
10use crate::canonicalize;
11
12#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
13pub struct OfflineApprovalPacket {
14 pub packet_version: String,
15 pub request: Value,
16 pub decision: String,
17 pub responder: String,
18 pub responded_at: String,
19 pub transport_hint: String,
20 pub signature: SignatureEnvelope,
21}
22
23#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
24pub struct SignatureEnvelope {
25 pub algorithm: String,
26 pub signer: String,
27 pub signature: String,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
31pub struct OfflineApprovalCeremony {
32 pub ceremony_version: String,
33 pub ceremony_id: String,
34 pub kind: String,
35 pub request_id: String,
36 pub responder: String,
37 pub packet_id: String,
38 pub transport_hint: String,
39 pub signature: String,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
43pub struct ApprovalResponse {
44 pub response_version: String,
45 pub request_id: String,
46 pub decision: String,
47 pub responder: String,
48 pub signed_at: String,
49 pub signature: SignatureEnvelope,
50}
51
52#[derive(Debug)]
53pub struct VerifyOfflineApprovalResult {
54 pub ok: bool,
55 pub reason: Option<String>,
56 pub response: Option<ApprovalResponse>,
57 pub ceremony: Option<OfflineApprovalCeremony>,
58}
59
60pub fn sign_offline_approval_packet(
61 request: Value,
62 decision: &str,
63 responder: &str,
64 private_key: &[u8; 32],
65 transport_hint: &str,
66 responded_at: Option<&str>,
67) -> OfflineApprovalPacket {
68 let responded_at = responded_at.map(str::to_string).unwrap_or_else(now_iso8601);
69 let payload_value = serde_json::json!({
70 "request": request,
71 "decision": decision,
72 "responder": responder,
73 "responded_at": responded_at,
74 });
75 let canonical = canonicalize(&payload_value).unwrap_or_default();
76 let digest: [u8; 32] = Sha256::digest(canonical.as_bytes()).into();
77 let signing = SigningKey::from_bytes(private_key);
78 let sig: Signature = signing.sign(&digest);
79 OfflineApprovalPacket {
80 packet_version: "1".into(),
81 request,
82 decision: decision.into(),
83 responder: responder.into(),
84 responded_at,
85 transport_hint: transport_hint.into(),
86 signature: SignatureEnvelope {
87 algorithm: "ed25519".into(),
88 signer: responder.into(),
89 signature: STANDARD.encode(sig.to_bytes()),
90 },
91 }
92}
93
94pub fn verify_offline_approval_packet(
95 packet: &OfflineApprovalPacket,
96 public_key: &[u8; 32],
97 now: Option<&str>,
98 max_age_seconds: Option<i64>,
99) -> VerifyOfflineApprovalResult {
100 if packet.packet_version != "1" {
101 return rejected(format!(
102 "unsupported packet_version {}",
103 packet.packet_version
104 ));
105 }
106 if packet.signature.signer != packet.responder {
107 return rejected("signature signer does not match responder".into());
108 }
109 if packet.signature.algorithm != "ed25519" {
110 return rejected(format!(
111 "unsupported signature algorithm {}",
112 packet.signature.algorithm
113 ));
114 }
115 let max = max_age_seconds.unwrap_or(86_400);
116 if let (Some(now_str), Ok(then_secs)) = (now, parse_iso8601(&packet.responded_at)) {
117 if let Ok(now_secs) = parse_iso8601(now_str) {
118 let age = now_secs - then_secs;
119 if age > max {
120 return rejected(format!("packet older than {}s", max));
121 }
122 if age < -300 {
123 return rejected("packet timestamp is in the future".into());
124 }
125 }
126 }
127 let payload_value = serde_json::json!({
128 "request": packet.request,
129 "decision": packet.decision,
130 "responder": packet.responder,
131 "responded_at": packet.responded_at,
132 });
133 let canonical = canonicalize(&payload_value).unwrap_or_default();
134 let digest: [u8; 32] = Sha256::digest(canonical.as_bytes()).into();
135 let sig_bytes = match STANDARD.decode(&packet.signature.signature) {
136 Ok(b) => b,
137 Err(e) => return rejected(format!("signature base64 decode: {}", e)),
138 };
139 let sig = match Signature::from_slice(&sig_bytes) {
140 Ok(s) => s,
141 Err(e) => return rejected(format!("signature parse: {}", e)),
142 };
143 let vk = match VerifyingKey::from_bytes(public_key) {
144 Ok(v) => v,
145 Err(e) => return rejected(format!("verifying key: {}", e)),
146 };
147 if vk.verify(&digest, &sig).is_err() {
148 return rejected("signature verification failed".into());
149 }
150 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
151 let packet_id = format!("pkt-{}", &hex[..16]);
152 let request_id = packet
153 .request
154 .get("id")
155 .and_then(|v| v.as_str())
156 .unwrap_or_default()
157 .to_string();
158 let response = ApprovalResponse {
159 response_version: "1".into(),
160 request_id: request_id.clone(),
161 decision: packet.decision.clone(),
162 responder: packet.responder.clone(),
163 signed_at: packet.responded_at.clone(),
164 signature: packet.signature.clone(),
165 };
166 let ceremony = OfflineApprovalCeremony {
167 ceremony_version: "1".into(),
168 ceremony_id: format!("cer-{}", packet_id),
169 kind: "offline-signed-packet".into(),
170 request_id,
171 responder: packet.responder.clone(),
172 packet_id,
173 transport_hint: packet.transport_hint.clone(),
174 signature: packet.signature.signature.clone(),
175 };
176 VerifyOfflineApprovalResult {
177 ok: true,
178 reason: None,
179 response: Some(response),
180 ceremony: Some(ceremony),
181 }
182}
183
184fn rejected(reason: String) -> VerifyOfflineApprovalResult {
185 VerifyOfflineApprovalResult {
186 ok: false,
187 reason: Some(reason),
188 response: None,
189 ceremony: None,
190 }
191}
192
193fn now_iso8601() -> String {
194 let secs = std::time::SystemTime::now()
195 .duration_since(std::time::UNIX_EPOCH)
196 .unwrap_or_default()
197 .as_secs() as i64;
198 let (y, m, d, h, mi, s) = secs_to_ymdhms(secs);
199 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, h, mi, s)
200}
201
202fn parse_iso8601(s: &str) -> Result<i64, ()> {
203 if s.len() < 19 || !s.ends_with('Z') {
204 return Err(());
205 }
206 let year: i64 = s[..4].parse().map_err(|_| ())?;
207 let month: u32 = s[5..7].parse().map_err(|_| ())?;
208 let day: u32 = s[8..10].parse().map_err(|_| ())?;
209 let hour: u32 = s[11..13].parse().map_err(|_| ())?;
210 let minute: u32 = s[14..16].parse().map_err(|_| ())?;
211 let second: u32 = s[17..19].parse().map_err(|_| ())?;
212 Ok(unix_from_civil(year, month, day, hour, minute, second))
213}
214
215fn unix_from_civil(year: i64, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> i64 {
216 let y = if month <= 2 { year - 1 } else { year };
218 let era = if y >= 0 { y } else { y - 399 } / 400;
219 let yoe = (y - era * 400) as u64;
220 let m = if month > 2 { month - 3 } else { month + 9 };
221 let doy = (153 * m as u64 + 2) / 5 + day as u64 - 1;
222 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
223 let days = era * 146_097 + doe as i64 - 719_468;
224 days * 86_400 + (hour as i64) * 3600 + (minute as i64) * 60 + second as i64
225}
226
227fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
228 let days = secs.div_euclid(86_400);
229 let time = secs.rem_euclid(86_400);
230 let hour = (time / 3600) as u32;
231 let minute = ((time % 3600) / 60) as u32;
232 let second = (time % 60) as u32;
233 let z = days + 719_468;
234 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
235 let doe = (z - era * 146_097) as u64;
236 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
237 let y = yoe as i64 + era * 400;
238 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
239 let mp = (5 * doy + 2) / 153;
240 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
241 let m = if mp < 10 {
242 (mp + 3) as u32
243 } else {
244 (mp - 9) as u32
245 };
246 let year = if m <= 2 { y + 1 } else { y };
247 (year as i32, m, d, hour, minute, second)
248}