116 lines
3.9 KiB
Rust
116 lines
3.9 KiB
Rust
//! Signature canonical text and ed25519 verification of registry entries.
|
|
|
|
use notescraft_engine::addons::{Ed25519, Entry, FakeVerifier, Index, Verifier, canonical_text};
|
|
use ring::signature::{Ed25519KeyPair, KeyPair};
|
|
use serde_json::{Value, json};
|
|
|
|
/// Test-only seed; not a secret and not the production key.
|
|
const TEST_SEED: [u8; 32] = [7; 32];
|
|
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
fn b64(bytes: &[u8]) -> String {
|
|
let mut out = String::new();
|
|
for chunk in bytes.chunks(3) {
|
|
let n = chunk.iter().enumerate().fold(0_u32, |n, (i, b)| n | u32::from(*b) << (16 - 8 * i));
|
|
for i in 0..4 {
|
|
if i <= chunk.len() {
|
|
out.push(char::from(B64[usize::try_from((n >> (18 - 6 * i)) & 63).unwrap()]));
|
|
} else {
|
|
out.push('=');
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn pair() -> Ed25519KeyPair {
|
|
Ed25519KeyPair::from_seed_unchecked(&TEST_SEED).unwrap()
|
|
}
|
|
|
|
fn verifier() -> Ed25519 {
|
|
Ed25519::new(vec![("k1".into(), pair().public_key().as_ref().try_into().unwrap())])
|
|
}
|
|
|
|
fn files(order: &[&str]) -> Value {
|
|
Value::Array(order.iter().map(|p| json!({ "path": p, "size": 5, "sha256": "c".repeat(64), "url": format!("https://h.example/{p}") })).collect())
|
|
}
|
|
|
|
fn entry_json(files: &Value, sig: &str, key_id: &str) -> Value {
|
|
json!({
|
|
"id": "p", "kind": "pack", "title": "P", "version": "1.2.3", "min_app_version": "0.1.0", "license": "MIT",
|
|
"files": files, "signature": { "key_id": key_id, "sig": sig }
|
|
})
|
|
}
|
|
|
|
fn parse(value: &Value) -> Entry {
|
|
let index = Index::parse(json!({ "version": 2, "entries": [value] }).to_string().as_bytes()).unwrap();
|
|
index.entries[0].clone()
|
|
}
|
|
|
|
fn signed(order: &[&str]) -> Entry {
|
|
let unsigned = parse(&entry_json(&files(order), "QUJD", "k1"));
|
|
let sig = b64(pair().sign(canonical_text(&unsigned).as_bytes()).as_ref());
|
|
parse(&entry_json(&files(order), &sig, "k1"))
|
|
}
|
|
|
|
const A: &str = "grammars/a.sublime-syntax";
|
|
const B: &str = "themes/b.toml";
|
|
|
|
#[test]
|
|
fn known_answer_verifies() {
|
|
let entry = signed(&[A, B]);
|
|
assert_eq!(canonical_text(&entry), format!("notescraft-registry-v1\np\npack\n1.2.3\n{A}\n{}\n5\n{B}\n{}\n5\n", "c".repeat(64), "c".repeat(64)));
|
|
verifier().verify(&entry).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn file_order_does_not_change_text_or_signature() {
|
|
let entry = signed(&[A, B]);
|
|
let mut shuffled = entry.clone();
|
|
shuffled.files.reverse();
|
|
assert_eq!(canonical_text(&entry), canonical_text(&shuffled));
|
|
verifier().verify(&shuffled).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn tampering_fails() {
|
|
let entry = signed(&[A, B]);
|
|
let mut hash = entry.clone();
|
|
hash.files[0].sha256 = format!("d{}", "c".repeat(63));
|
|
assert!(verifier().verify(&hash).is_err());
|
|
let mut size = entry.clone();
|
|
size.files[0].size += 1;
|
|
assert!(verifier().verify(&size).is_err());
|
|
let mut version = entry.clone();
|
|
version.version.patch += 1;
|
|
assert!(verifier().verify(&version).is_err());
|
|
let mut id = entry.clone();
|
|
id.id = "q".into();
|
|
assert!(verifier().verify(&id).is_err());
|
|
let mut kind = entry;
|
|
kind.kind = notescraft_engine::addons::Kind::Theme;
|
|
assert!(verifier().verify(&kind).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_key_bad_length_and_empty_trust_fail() {
|
|
let entry = signed(&[A]);
|
|
let mut other = entry.clone();
|
|
other.signature.key_id = "k2".into();
|
|
assert!(verifier().verify(&other).is_err());
|
|
let mut short = entry.clone();
|
|
short.signature.sig = b64(&[1, 2, 3]);
|
|
assert!(verifier().verify(&short).is_err());
|
|
let mut junk = entry.clone();
|
|
junk.signature.sig = "!!!!".into();
|
|
assert!(verifier().verify(&junk).is_err());
|
|
assert!(Ed25519::trusted().verify(&entry).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn fake_verifier() {
|
|
let entry = signed(&[A]);
|
|
assert!(FakeVerifier { accept: true }.verify(&entry).is_ok());
|
|
assert!(FakeVerifier { accept: false }.verify(&entry).is_err());
|
|
}
|