Markdown files up to 96 KiB show whether their ENS signature holds (#7)
Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #7.
This commit is contained in:
Generated
+1
-1
@@ -471,7 +471,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "md-ens-signature"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/capsulizers/md-ens-signature?rev=86878b144154c894948c595d2f0d66fbbd36ae4b#86878b144154c894948c595d2f0d66fbbd36ae4b"
|
||||
source = "git+https://github.com/capsulizers/md-ens-signature?rev=3a9f5e2bea1a668adc84342b82b98fee77909379#3a9f5e2bea1a668adc84342b82b98fee77909379"
|
||||
dependencies = [
|
||||
"alloy-primitives",
|
||||
"alloy-sol-types",
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
wit-bindgen = "=0.62.0"
|
||||
quick-xml = { version = "0.37", default-features = false }
|
||||
anyhow = "1.0.104"
|
||||
md-ens-signature = { git = "https://github.com/capsulizers/md-ens-signature", rev = "86878b144154c894948c595d2f0d66fbbd36ae4b" }
|
||||
md-ens-signature = { git = "https://github.com/capsulizers/md-ens-signature", rev = "3a9f5e2bea1a668adc84342b82b98fee77909379" }
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-primitives = { version = "1.7.3", default-features = false }
|
||||
|
||||
@@ -59,7 +59,7 @@ as a badge. The owner of the signer name comes from the Universal Resolver's
|
||||
| `Unauthorized: bob.team.eth is not a member of team.eth` | The signer is outside the trusted parent name |
|
||||
| `Unsigned` | The file has no `signer` and `signature` |
|
||||
| `Not checked: Sepolia unreachable` | The RPC address did not answer, so nothing was decided |
|
||||
| `Not checked: file too large` | The file is over 40 KiB, too large to hash within one call |
|
||||
| `Not checked: file too large` | The file is over 96 KiB, too large to hash within one call |
|
||||
|
||||
| Verified | Tampered |
|
||||
| --- | --- |
|
||||
@@ -78,8 +78,8 @@ before.
|
||||
|
||||
Memona gives each call into a plugin a fixed instruction budget, which is where
|
||||
the size limits come from: checking hashes the body and recovers a key, so it
|
||||
covers files up to 40 KiB; a file up to 1 MiB is marked not checked, and a
|
||||
larger one streams through without a badge.
|
||||
covers files up to 96 KiB, the same as signing; a file up to 1 MiB is marked
|
||||
not checked, and a larger one streams through without a badge.
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
+16
-8
@@ -6,7 +6,7 @@ use crate::sdk::host;
|
||||
use anyhow::{anyhow, bail};
|
||||
use md_ens_signature::document::{body_digest, read_signature};
|
||||
use md_ens_signature::ens::EthCall;
|
||||
use md_ens_signature::verify::{EthCaller, Verdict, verify};
|
||||
use md_ens_signature::verify::{EthCaller, Verdict, verify_fields};
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::pin::pin;
|
||||
@@ -20,11 +20,12 @@ const VERDICT_TTL: Duration = Duration::from_secs(30);
|
||||
const MAX_RPC_ANSWER: usize = 64 * 1024;
|
||||
/// The most bytes asked of the host per pull.
|
||||
const RPC_CHUNK: u32 = 64 * 1024;
|
||||
/// The largest file verified. A first check hashes the body twice, at about
|
||||
/// 100 WebAssembly instructions a byte, and recovers a key for about 8.5
|
||||
/// million more, all inside Memona's 20 million per call; past this size the
|
||||
/// call would run out and the file would not open at all.
|
||||
const MAX_CHECKED: usize = 40 * 1024;
|
||||
/// The largest file verified, the same as the largest one a save signs. A
|
||||
/// first check costs about 4.9 million WebAssembly instructions plus 86 a
|
||||
/// byte, or 98 with CRLF line endings, so a file this size leaves roughly 30
|
||||
/// percent of Memona's 20 million per call to spare. Past this size the
|
||||
/// call could run out and the file would not open at all.
|
||||
const MAX_CHECKED: usize = 96 * 1024;
|
||||
|
||||
/// What a cached verdict belongs to: the signed body and the claim on it.
|
||||
/// The trusted parent is fixed per connection, so it needs no place here.
|
||||
@@ -65,7 +66,11 @@ impl Checker {
|
||||
let Some(fields) = read_signature(markdown) else {
|
||||
return badge::label(&Ok(Verdict::Unsigned));
|
||||
};
|
||||
let claim = (body_digest(markdown), fields.signer, fields.signature);
|
||||
let claim = (
|
||||
body_digest(markdown),
|
||||
fields.signer.clone(),
|
||||
fields.signature.clone(),
|
||||
);
|
||||
let now = Instant::now();
|
||||
let mut verdicts = self.verdicts.borrow_mut();
|
||||
verdicts.retain(|(_, at, _)| now.duration_since(*at) < VERDICT_TTL);
|
||||
@@ -73,8 +78,11 @@ impl Checker {
|
||||
{
|
||||
return text.clone();
|
||||
}
|
||||
// The digest taken for the cache is the one verified, so the body, most
|
||||
// of the work for a large file, is hashed once.
|
||||
let parent = self.trusted_parent.as_deref();
|
||||
let text = badge::label(&run(verify(markdown, parent, caller)).flatten());
|
||||
let verdict = run(verify_fields(&fields, &claim.0, parent, caller));
|
||||
let text = badge::label(&verdict.flatten());
|
||||
verdicts.push((claim, now, text.clone()));
|
||||
text
|
||||
}
|
||||
|
||||
+6
-5
@@ -29,9 +29,10 @@ const PROPS: &str = concat!(
|
||||
);
|
||||
const MAX_CHUNK: u32 = 1024 * 1024;
|
||||
/// The largest Markdown file a save signs. Memona gives each call into the
|
||||
/// plugin 20 million WebAssembly instructions; signing costs about 6 million
|
||||
/// plus 110 per byte, so a larger file would run out mid-call and leave the
|
||||
/// save's outcome unknown. It is refused up front instead.
|
||||
/// plugin 20 million WebAssembly instructions; signing costs about 5.6
|
||||
/// million plus 85 per byte, or 97 with CRLF line endings, so a larger file
|
||||
/// could run out mid-call and leave the save's outcome unknown. It is
|
||||
/// refused up front instead.
|
||||
const MAX_SIGNED: usize = 96 * 1024;
|
||||
/// Room a save may carry beyond [`MAX_SIGNED`] before its verdict line is
|
||||
/// dropped, so a file read back at the limit can be saved unedited.
|
||||
@@ -297,8 +298,8 @@ impl GuestWriter for DavWriter {
|
||||
if stored.len() > MAX_SIGNED {
|
||||
return Err(ErrorCode::Unsupported);
|
||||
}
|
||||
let signed = session(|session| session.signer.sign(stored.as_bytes()))?;
|
||||
for piece in signed.chunks(MAX_CHUNK as usize) {
|
||||
let signed = session(|session| session.signer.sign(&stored))?;
|
||||
for piece in signed.as_bytes().chunks(MAX_CHUNK as usize) {
|
||||
self.send(piece)?;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-16
@@ -45,18 +45,17 @@ impl Signer {
|
||||
}
|
||||
|
||||
/// Returns the whole file with its `signer` and `signature` frontmatter set
|
||||
/// for this identity, other frontmatter and the body untouched. A file that
|
||||
/// is not UTF-8 is not Markdown this plugin can sign.
|
||||
pub fn sign(&self, markdown: &[u8]) -> Result<Vec<u8>, ErrorCode> {
|
||||
let text =
|
||||
std::str::from_utf8(markdown).map_err(|_| ErrorCode::InvalidInput)?;
|
||||
let signature = sign(&self.key, &self.name, &body_digest(text))
|
||||
/// for this identity, other frontmatter and the body untouched. The caller
|
||||
/// has already refused bytes that are not UTF-8, so they are not checked a
|
||||
/// second time here.
|
||||
pub fn sign(&self, markdown: &str) -> Result<String, ErrorCode> {
|
||||
let signature = sign(&self.key, &self.name, &body_digest(markdown))
|
||||
.map_err(|_| ErrorCode::Internal)?;
|
||||
let fields = SignatureFields {
|
||||
signer: self.name.clone(),
|
||||
signature,
|
||||
};
|
||||
Ok(write_signature(text, &fields).into_bytes())
|
||||
Ok(write_signature(markdown, &fields))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +74,7 @@ mod tests {
|
||||
-> Result<(), ErrorCode> {
|
||||
let signer = Signer::new(" Laptop.Alice.eth ", KEY)?;
|
||||
let saved = "---\ntitle: Notes\n---\n# Notes\n\nHello\n";
|
||||
let signed = String::from_utf8(signer.sign(saved.as_bytes())?)
|
||||
.map_err(|_| ErrorCode::Internal)?;
|
||||
let signed = signer.sign(saved)?;
|
||||
assert!(signed.starts_with("---\ntitle: Notes\n"));
|
||||
assert!(signed.ends_with("---\n# Notes\n\nHello\n"));
|
||||
// YAML reads a bare 0x... as an integer, so the signature stays a string.
|
||||
@@ -99,11 +97,4 @@ mod tests {
|
||||
assert!(Signer::new(" ", KEY).is_err());
|
||||
assert!(Signer::new("alice.eth", &KEY[2..]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_is_not_text_is_refused() -> Result<(), ErrorCode> {
|
||||
let signer = Signer::new("alice.eth", KEY)?;
|
||||
assert_eq!(signer.sign(&[0xff, 0xfe]), Err(ErrorCode::InvalidInput));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user