use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use monetize_product::EntitlementFact;
use serde_json::Value;
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Snapshot {
pub issued_unix_ms: u64,
pub facts: Vec<EntitlementFact>,
pub signature: Vec<u8>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SignatureError {
#[error("signature is not 64 bytes")]
Malformed,
#[error("signature does not verify for tenant {0}")]
Fact(String),
#[error("snapshot envelope signature does not verify")]
Envelope,
#[error("go-ahead signature does not verify for nonce {0}")]
GoAhead(String),
#[error("the actor ticket was refused: {0}")]
Ticket(String),
}
pub fn canonical_json(value: &Value, out: &mut String) {
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serde_json::to_string(k).expect("string"));
out.push(':');
canonical_json(&map[*k], out);
}
out.push('}');
}
Value::Array(items) => {
out.push('[');
for (i, v) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
canonical_json(v, out);
}
out.push(']');
}
other => out.push_str(&other.to_string()),
}
}
pub fn fact_message(fact: &EntitlementFact) -> Vec<u8> {
let mut v = serde_json::to_value(fact).expect("fact serializes");
v.as_object_mut().expect("fact is an object").remove("signature");
let mut s = String::new();
canonical_json(&v, &mut s);
s.into_bytes()
}
pub fn snapshot_message(issued_unix_ms: u64, facts: &[EntitlementFact]) -> Vec<u8> {
let v = serde_json::json!({ "issued_unix_ms": issued_unix_ms, "facts": facts });
let mut s = String::new();
canonical_json(&v, &mut s);
s.into_bytes()
}
pub(crate) fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> Result<bool, SignatureError> {
let sig = Signature::from_slice(sig).map_err(|_| SignatureError::Malformed)?;
Ok(key.verify(msg, &sig).is_ok())
}
pub fn verify_fact(fact: &EntitlementFact, key: &VerifyingKey) -> Result<(), SignatureError> {
if check(key, &fact_message(fact), &fact.signature)? {
Ok(())
} else {
Err(SignatureError::Fact(fact.tenant.0.clone()))
}
}
pub fn verify_snapshot(snap: &Snapshot, key: &VerifyingKey) -> Result<(), SignatureError> {
if !check(key, &snapshot_message(snap.issued_unix_ms, &snap.facts), &snap.signature)? {
return Err(SignatureError::Envelope);
}
snap.facts.iter().try_for_each(|f| verify_fact(f, key))
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct GoAhead {
pub v: u32,
pub signer: String,
pub target_sectors: u64,
pub nonce: String,
pub issued_unix_ms: i64,
#[serde(default)]
pub signature: String,
}
pub const GO_AHEAD_SIGNER_MONETIZE: &str = "monetize";
pub fn go_ahead_message(go: &GoAhead) -> Vec<u8> {
let mut v = serde_json::to_value(go).expect("go-ahead serializes");
v.as_object_mut().expect("go-ahead is an object").remove("signature");
let mut s = String::new();
canonical_json(&v, &mut s);
s.into_bytes()
}
pub fn verify_go_ahead(bytes: &[u8], key: &VerifyingKey) -> Result<GoAhead, SignatureError> {
let go: GoAhead = serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
if go.v != 1 || go.signer != GO_AHEAD_SIGNER_MONETIZE || go.nonce.trim().is_empty() {
return Err(SignatureError::Malformed);
}
let sig = base64_decode(&go.signature).ok_or(SignatureError::Malformed)?;
if check(key, &go_ahead_message(&go), &sig)? {
Ok(go)
} else {
Err(SignatureError::GoAhead(go.nonce.clone()))
}
}
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub fn base64_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
out.push(B64[(n >> 18) as usize & 63] as char);
out.push(B64[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
out.push(if chunk.len() > 2 { B64[n as usize & 63] as char } else { '=' });
}
out
}
pub fn base64_decode(text: &str) -> Option<Vec<u8>> {
let text = text.trim();
if text.len() % 4 != 0 {
return None;
}
let val = |c: u8| B64.iter().position(|b| *b == c).map(|p| p as u32);
let mut out = Vec::with_capacity(text.len() / 4 * 3);
for chunk in text.as_bytes().chunks(4) {
let pad = chunk.iter().rev().take_while(|c| **c == b'=').count();
if pad > 2 || chunk[..4 - pad].iter().any(|c| *c == b'=') {
return None;
}
let mut n = 0u32;
for (i, c) in chunk.iter().enumerate() {
let v = if i >= 4 - pad { 0 } else { val(*c)? };
n = (n << 6) | v;
}
out.push((n >> 16) as u8);
if pad < 2 {
out.push((n >> 8) as u8);
}
if pad < 1 {
out.push(n as u8);
}
}
Some(out)
}
#[cfg(test)]
mod go_ahead_tests {
use super::*;
#[test]
fn base64_round_trips_every_padding_shape_and_refuses_junk() {
for n in 0..10 {
let bytes: Vec<u8> = (0..n).map(|i| (i * 37 + 11) as u8).collect();
let enc = base64_encode(&bytes);
assert_eq!(enc.len() % 4, 0);
assert_eq!(base64_decode(&enc).unwrap(), bytes, "{enc}");
}
assert_eq!(base64_encode(b"Man"), "TWFu");
assert_eq!(base64_encode(b"Ma"), "TWE=");
assert_eq!(base64_encode(b"M"), "TQ==");
assert_eq!(base64_decode("TQ="), None);
assert_eq!(base64_decode("T@=="), None);
assert_eq!(base64_decode("TQ=x"), None);
}
#[test]
fn the_go_ahead_message_is_canonical_and_excludes_the_signature() {
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() };
let msg = String::from_utf8(go_ahead_message(&go)).unwrap();
assert_eq!(msg, r#"{"issued_unix_ms":1800000000000,"nonce":"n-1","signer":"monetize","target_sectors":134217728,"v":1}"#);
}
}