monetize_embed/
signing.rs1use ed25519_dalek::{Signature, Verifier, VerifyingKey};
13use monetize_product::EntitlementFact;
14use serde_json::Value;
15
16#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
19pub struct Snapshot {
20 pub issued_unix_ms: u64,
22 pub facts: Vec<EntitlementFact>,
23 pub signature: Vec<u8>,
25}
26
27#[derive(Debug, thiserror::Error, PartialEq, Eq)]
28pub enum SignatureError {
29 #[error("signature is not 64 bytes")]
30 Malformed,
31 #[error("signature does not verify for tenant {0}")]
32 Fact(String),
33 #[error("snapshot envelope signature does not verify")]
34 Envelope,
35 #[error("go-ahead signature does not verify for nonce {0}")]
36 GoAhead(String),
37 #[error("the actor ticket was refused: {0}")]
41 Ticket(String),
42}
43
44pub fn canonical_json(value: &Value, out: &mut String) {
46 match value {
47 Value::Object(map) => {
48 let mut keys: Vec<&String> = map.keys().collect();
49 keys.sort();
50 out.push('{');
51 for (i, k) in keys.iter().enumerate() {
52 if i > 0 {
53 out.push(',');
54 }
55 out.push_str(&serde_json::to_string(k).expect("string"));
56 out.push(':');
57 canonical_json(&map[*k], out);
58 }
59 out.push('}');
60 }
61 Value::Array(items) => {
62 out.push('[');
63 for (i, v) in items.iter().enumerate() {
64 if i > 0 {
65 out.push(',');
66 }
67 canonical_json(v, out);
68 }
69 out.push(']');
70 }
71 other => out.push_str(&other.to_string()),
72 }
73}
74
75pub fn fact_message(fact: &EntitlementFact) -> Vec<u8> {
77 let mut v = serde_json::to_value(fact).expect("fact serializes");
78 v.as_object_mut().expect("fact is an object").remove("signature");
79 let mut s = String::new();
80 canonical_json(&v, &mut s);
81 s.into_bytes()
82}
83
84pub fn snapshot_message(issued_unix_ms: u64, facts: &[EntitlementFact]) -> Vec<u8> {
87 let v = serde_json::json!({ "issued_unix_ms": issued_unix_ms, "facts": facts });
88 let mut s = String::new();
89 canonical_json(&v, &mut s);
90 s.into_bytes()
91}
92
93pub(crate) fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> Result<bool, SignatureError> {
94 let sig = Signature::from_slice(sig).map_err(|_| SignatureError::Malformed)?;
95 Ok(key.verify(msg, &sig).is_ok())
96}
97
98pub fn verify_fact(fact: &EntitlementFact, key: &VerifyingKey) -> Result<(), SignatureError> {
99 if check(key, &fact_message(fact), &fact.signature)? {
100 Ok(())
101 } else {
102 Err(SignatureError::Fact(fact.tenant.0.clone()))
103 }
104}
105
106pub fn verify_snapshot(snap: &Snapshot, key: &VerifyingKey) -> Result<(), SignatureError> {
108 if !check(key, &snapshot_message(snap.issued_unix_ms, &snap.facts), &snap.signature)? {
109 return Err(SignatureError::Envelope);
110 }
111 snap.facts.iter().try_for_each(|f| verify_fact(f, key))
112}
113
114#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
133pub struct GoAhead {
134 pub v: u32,
135 pub signer: String,
136 pub target_sectors: u64,
137 pub nonce: String,
138 pub issued_unix_ms: i64,
139 #[serde(default)]
140 pub signature: String,
141}
142
143pub const GO_AHEAD_SIGNER_MONETIZE: &str = "monetize";
145
146pub fn go_ahead_message(go: &GoAhead) -> Vec<u8> {
149 let mut v = serde_json::to_value(go).expect("go-ahead serializes");
150 v.as_object_mut().expect("go-ahead is an object").remove("signature");
151 let mut s = String::new();
152 canonical_json(&v, &mut s);
153 s.into_bytes()
154}
155
156pub fn verify_go_ahead(bytes: &[u8], key: &VerifyingKey) -> Result<GoAhead, SignatureError> {
159 let go: GoAhead = serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
160 if go.v != 1 || go.signer != GO_AHEAD_SIGNER_MONETIZE || go.nonce.trim().is_empty() {
161 return Err(SignatureError::Malformed);
162 }
163 let sig = base64_decode(&go.signature).ok_or(SignatureError::Malformed)?;
164 if check(key, &go_ahead_message(&go), &sig)? {
165 Ok(go)
166 } else {
167 Err(SignatureError::GoAhead(go.nonce.clone()))
168 }
169}
170
171const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
172
173pub fn base64_encode(bytes: &[u8]) -> String {
177 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
178 for chunk in bytes.chunks(3) {
179 let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
180 let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
181 out.push(B64[(n >> 18) as usize & 63] as char);
182 out.push(B64[(n >> 12) as usize & 63] as char);
183 out.push(if chunk.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
184 out.push(if chunk.len() > 2 { B64[n as usize & 63] as char } else { '=' });
185 }
186 out
187}
188
189pub fn base64_decode(text: &str) -> Option<Vec<u8>> {
191 let text = text.trim();
192 if text.len() % 4 != 0 {
193 return None;
194 }
195 let val = |c: u8| B64.iter().position(|b| *b == c).map(|p| p as u32);
196 let mut out = Vec::with_capacity(text.len() / 4 * 3);
197 for chunk in text.as_bytes().chunks(4) {
198 let pad = chunk.iter().rev().take_while(|c| **c == b'=').count();
199 if pad > 2 || chunk[..4 - pad].iter().any(|c| *c == b'=') {
200 return None;
201 }
202 let mut n = 0u32;
203 for (i, c) in chunk.iter().enumerate() {
204 let v = if i >= 4 - pad { 0 } else { val(*c)? };
205 n = (n << 6) | v;
206 }
207 out.push((n >> 16) as u8);
208 if pad < 2 {
209 out.push((n >> 8) as u8);
210 }
211 if pad < 1 {
212 out.push(n as u8);
213 }
214 }
215 Some(out)
216}
217
218#[cfg(test)]
219mod go_ahead_tests {
220 use super::*;
221
222 #[test]
223 fn base64_round_trips_every_padding_shape_and_refuses_junk() {
224 for n in 0..10 {
225 let bytes: Vec<u8> = (0..n).map(|i| (i * 37 + 11) as u8).collect();
226 let enc = base64_encode(&bytes);
227 assert_eq!(enc.len() % 4, 0);
228 assert_eq!(base64_decode(&enc).unwrap(), bytes, "{enc}");
229 }
230 assert_eq!(base64_encode(b"Man"), "TWFu");
231 assert_eq!(base64_encode(b"Ma"), "TWE=");
232 assert_eq!(base64_encode(b"M"), "TQ==");
233 assert_eq!(base64_decode("TQ="), None);
234 assert_eq!(base64_decode("T@=="), None);
235 assert_eq!(base64_decode("TQ=x"), None);
236 }
237
238 #[test]
239 fn the_go_ahead_message_is_canonical_and_excludes_the_signature() {
240 let go = GoAhead { v: 1, signer: "monetize".into(), target_sectors: 134_217_728, nonce: "n-1".into(), issued_unix_ms: 1_800_000_000_000, signature: "zzz".into() };
241 let msg = String::from_utf8(go_ahead_message(&go)).unwrap();
242 assert_eq!(msg, r#"{"issued_unix_ms":1800000000000,"nonce":"n-1","signer":"monetize","target_sectors":134217728,"v":1}"#);
243 }
244}