Ask for a signer name and key, and hold saved Markdown whole to sign it
Agents / agent (pull_request) Successful in 3m27s
Agents / agent (pull_request) Successful in 3m27s
This commit is contained in:
@@ -17,6 +17,9 @@ It registers one provider, `signature`, for the `signed-webdav` scheme.
|
||||
| Server address | The folder's address, for example `https://dav.example.com/files/` |
|
||||
| Username | Optional account name for HTTP basic authentication |
|
||||
| Password | Optional password, stored as a secret |
|
||||
| Signer name | The ENS name that signs, for example `laptop.alice.eth` |
|
||||
| Signing key | The private key of the address the name resolves to, stored as a secret |
|
||||
| Sepolia RPC address | The endpoint used to check signers against ENS, by default `https://ethereum-sepolia-rpc.publicnode.com` |
|
||||
|
||||
Listing, reading, writing, creating directories, renaming and deleting behave
|
||||
as in the WebDAV plugin: reads and writes stream in pieces of at most 1 MiB,
|
||||
@@ -24,6 +27,15 @@ conditional writes use `If-Match` and `If-None-Match`, change polling is not
|
||||
supported, a rename never replaces an existing target, and a folder with
|
||||
contents is refused rather than removed whole.
|
||||
|
||||
## Signing
|
||||
|
||||
Saving a `.md` file holds it whole, up to 8 MiB, and writes it once finished,
|
||||
with `signer` and `signature` set in its frontmatter; every other file streams
|
||||
straight through. Reading passes every file through unchanged. The signing
|
||||
itself lives behind one small interface that the md-ens-signature library
|
||||
fills in; until then a saved file is written exactly as it arrives. The RPC
|
||||
address is for verifying signers, which a later version adds to reads.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
|
||||
@@ -33,6 +33,28 @@
|
||||
"label": "Password",
|
||||
"kind": "password",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "ensName",
|
||||
"label": "Signer name",
|
||||
"description": "The ENS name that signs saved Markdown, for example laptop.alice.eth",
|
||||
"kind": "text",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "privateKey",
|
||||
"label": "Signing key",
|
||||
"description": "The private key of the address this name resolves to",
|
||||
"kind": "password",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "rpcUrl",
|
||||
"label": "Sepolia RPC address",
|
||||
"description": "The Ethereum Sepolia endpoint used to check signers against ENS",
|
||||
"kind": "text",
|
||||
"required": false,
|
||||
"default": "https://ethereum-sepolia-rpc.publicnode.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+41
-2
@@ -3,6 +3,7 @@
|
||||
|
||||
mod dav;
|
||||
mod sdk;
|
||||
mod sign;
|
||||
|
||||
use sdk::host;
|
||||
use sdk::{
|
||||
@@ -10,6 +11,7 @@ use sdk::{
|
||||
GuestReader, GuestWriter, ListPage, Locator, Reader, Setting, WriteCondition,
|
||||
Writer,
|
||||
};
|
||||
use sign::Signer;
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
/// The PROPFIND body. Asking for named properties rather than allprop keeps
|
||||
@@ -21,10 +23,13 @@ const PROPS: &str = concat!(
|
||||
"</d:prop></d:propfind>",
|
||||
);
|
||||
const MAX_CHUNK: u32 = 1024 * 1024;
|
||||
/// The largest Markdown file this plugin will hold whole to sign.
|
||||
const MAX_MARKDOWN: usize = 8 * 1024 * 1024;
|
||||
|
||||
struct Session {
|
||||
root: String,
|
||||
authorization: Option<String>,
|
||||
signer: Signer,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -179,11 +184,27 @@ impl GuestReader for DavReader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a path names a Markdown file, the only kind this plugin signs.
|
||||
fn is_markdown(path: &str) -> bool {
|
||||
path.to_ascii_lowercase().ends_with(".md")
|
||||
}
|
||||
|
||||
/// Writes one file through a PUT whose body is pushed in pieces.
|
||||
struct DavWriter {
|
||||
call: host::Exchange,
|
||||
done: RefCell<bool>,
|
||||
written: Cell<u64>,
|
||||
/// A Markdown file waits here whole until `finish`, because its signature
|
||||
/// covers the entire body and lands in the frontmatter at the front.
|
||||
markdown: Option<RefCell<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl DavWriter {
|
||||
fn send(&self, data: &[u8]) -> Result<(), ErrorCode> {
|
||||
self.call.push(data).map_err(transport)?;
|
||||
self.written.set(self.written.get() + data.len() as u64);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl GuestWriter for DavWriter {
|
||||
@@ -191,8 +212,14 @@ impl GuestWriter for DavWriter {
|
||||
if *self.done.borrow() {
|
||||
return Err(ErrorCode::InvalidInput);
|
||||
}
|
||||
self.call.push(&data).map_err(transport)?;
|
||||
self.written.set(self.written.get() + data.len() as u64);
|
||||
let Some(markdown) = &self.markdown else {
|
||||
return self.send(&data);
|
||||
};
|
||||
let mut markdown = markdown.borrow_mut();
|
||||
if markdown.len() + data.len() > MAX_MARKDOWN {
|
||||
return Err(ErrorCode::Unsupported);
|
||||
}
|
||||
markdown.extend_from_slice(&data);
|
||||
Ok(())
|
||||
}
|
||||
/// One call is enough: the request body is already complete, so the response
|
||||
@@ -203,6 +230,12 @@ impl GuestWriter for DavWriter {
|
||||
return Err(ErrorCode::InvalidInput);
|
||||
}
|
||||
*self.done.borrow_mut() = true;
|
||||
if let Some(markdown) = &self.markdown {
|
||||
let signed = session(|session| session.signer.sign(&markdown.borrow()))?;
|
||||
for piece in signed.chunks(MAX_CHUNK as usize) {
|
||||
self.send(piece)?;
|
||||
}
|
||||
}
|
||||
let head = self.call.respond().map_err(transport)?;
|
||||
dav::outcome(head.status)?;
|
||||
let revision = head
|
||||
@@ -242,9 +275,14 @@ impl Guest for Webdav {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let signer = Signer::new(
|
||||
&lookup("ensName").ok_or(ErrorCode::InvalidInput)?,
|
||||
&lookup("privateKey").ok_or(ErrorCode::InvalidInput)?,
|
||||
)?;
|
||||
SESSION.set(Some(Session {
|
||||
root,
|
||||
authorization,
|
||||
signer,
|
||||
}));
|
||||
Ok(Capabilities {
|
||||
write: true,
|
||||
@@ -358,6 +396,7 @@ impl Guest for Webdav {
|
||||
call,
|
||||
done: RefCell::new(false),
|
||||
written: Cell::new(0),
|
||||
markdown: is_markdown(&path).then(|| RefCell::new(Vec::new())),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//! Signing a saved Markdown file, the only place this plugin touches its
|
||||
//! bytes. Everything the md-ens-signature library does for the plugin goes
|
||||
//! through the interface here, so adopting the library replaces this file and
|
||||
//! nothing else.
|
||||
|
||||
use crate::sdk::ErrorCode;
|
||||
|
||||
/// One connection's signing identity: an ENS name and the private key bound
|
||||
/// to it.
|
||||
///
|
||||
/// Until the library lands, a signer accepts any identity and leaves every
|
||||
/// file exactly as it was saved.
|
||||
pub struct Signer;
|
||||
|
||||
impl Signer {
|
||||
/// Takes the configured name and key, failing with `InvalidInput` when the
|
||||
/// key cannot sign, so a bad key refuses the connection rather than every
|
||||
/// later save.
|
||||
pub fn new(_name: &str, _private_key: &str) -> Result<Self, ErrorCode> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
/// Returns the whole file with its `signer` and `signature` frontmatter set
|
||||
/// for this identity, other frontmatter and the body untouched.
|
||||
pub fn sign(&self, markdown: &[u8]) -> Result<Vec<u8>, ErrorCode> {
|
||||
Ok(markdown.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Signer;
|
||||
use crate::sdk::ErrorCode;
|
||||
|
||||
#[test]
|
||||
fn a_file_keeps_its_body() -> Result<(), ErrorCode> {
|
||||
let signer = Signer::new("laptop.alice.eth", "0x01")?;
|
||||
let signed = signer.sign(b"# Notes\n\nHello\n")?;
|
||||
assert!(signed.ends_with(b"# Notes\n\nHello\n"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user