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