125 lines
6.3 KiB
Markdown
125 lines
6.3 KiB
Markdown
# Memona signature plugin
|
|
|
|
Opens a WebDAV server as a folder inside Memona and signs every Markdown file
|
|
saved to it with an ENS name, writing `signer` and `signature` into the file's
|
|
frontmatter so anyone can check who wrote it against the name's current ENSv2
|
|
records. Signing comes from the
|
|
[md-ens-signature](https://github.com/capsulizers/md-ens-signature) library.
|
|
|
|
The plugin is a WASI component: it speaks no sockets of its own and makes every
|
|
request through the host's HTTP interface, under the connection's permissions.
|
|
It registers one provider, `signature`, for the `signed-webdav` scheme.
|
|
|
|
## Connecting
|
|
|
|

|
|
|
|
| Field | Key | Meaning |
|
|
| --- | --- | --- |
|
|
| Server address | `url` | The folder's address, for example `https://dav.example.com/files/` |
|
|
| Username | `username` | Optional account name for HTTP basic authentication |
|
|
| Password | `password` | Optional password, stored as a secret |
|
|
| Signer name | `ensName` | The ENS name that signs, for example `laptop.alice.eth` |
|
|
| Signing key | `privateKey` | The private key of the address that owns the name, stored as a secret |
|
|
| Sepolia RPC address | `rpcUrl` | The endpoint used to check signers against ENS, by default `https://ethereum-sepolia-rpc.publicnode.com` |
|
|
| Trusted parent name | `trustedParent` | Optional. Only signers that are this name or one of its subnames count, for example `team.eth` |
|
|
|
|
The connection's allowed hosts must include the RPC address's host as well as
|
|
the server's, or every check reads as Sepolia unreachable.
|
|
|
|
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,
|
|
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 96 KiB, and writes it once finished,
|
|
with `signer` and `signature` set in its frontmatter; every other file streams
|
|
straight through. The signature is EIP-191 `personal_sign` over the signer name
|
|
and the body's SHA-256, made with the configured key; other frontmatter and the
|
|
body stay byte for byte. A key that is not 32 bytes of hex refuses the
|
|
connection, and a `.md` file that is not UTF-8 refuses the save.
|
|
|
|

|
|
|
|
## Verifying
|
|
|
|
Reading a `.md` file checks its signature against ENSv2 on Sepolia and adds an
|
|
`ens-verification` line to the frontmatter Memona receives, which Memona shows
|
|
as a badge. The owner of the signer name comes from the Universal Resolver's
|
|
`findOwner`, so a name its parent revoked stops counting at once.
|
|
|
|
| Badge | Meaning |
|
|
| --- | --- |
|
|
| `Verified: bob.team.eth` | The name's current owner signed this exact body |
|
|
| `Tampered: signed as bob.team.eth` | The body changed after signing, or another key signed it |
|
|
| `Unauthorized: bob.team.eth is not registered` | The signature holds, but no one owns the name now: it was revoked or never granted |
|
|
| `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 |
|
|
|
|
| Verified | Tampered |
|
|
| --- | --- |
|
|
|  |  |
|
|
|
|
| Unauthorized | Unsigned |
|
|
| --- | --- |
|
|
|  |  |
|
|
|
|
The line never reaches the server: a save drops it before signing, and one
|
|
stored on the server is replaced, so a file cannot claim its own verdict.
|
|
Verdicts are reused for 30 seconds per signed body, a failed lookup included,
|
|
so an editor rereading a file does not flood the RPC. Stat, list and saves
|
|
keep reporting the stored size and the server's etag, so `If-Match` works as
|
|
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.
|
|
|
|
## Building
|
|
|
|
```sh
|
|
rustup target add wasm32-wasip2
|
|
cargo build --locked --release --target wasm32-wasip2
|
|
cargo test --locked --lib
|
|
```
|
|
|
|
The component lands at
|
|
`target/wasm32-wasip2/release/memona_plugin_signature.wasm`.
|
|
|
|
`wit/filesystem.wit` is a copy of the canonical Memona filesystem contract, and
|
|
`src/sdk.rs` generates the guest bindings from it, so this repository builds
|
|
without a Memona checkout. Refresh the copy from `plugin_system/sdk/wit` in the
|
|
[Memona repository](https://git.capsulizers.com/lead/memona) when the contract
|
|
moves, and keep `apiVersion` in `manifest.json` in step.
|
|
|
|
## Packaging for the plugin store
|
|
|
|
A `.memona-plugin` package is a ZIP archive with the files at its root, never
|
|
inside a directory:
|
|
|
|
```sh
|
|
cargo build --locked --release --target wasm32-wasip2
|
|
cp target/wasm32-wasip2/release/memona_plugin_signature.wasm plugin.wasm
|
|
zip memona-plugin-signature.memona-plugin \
|
|
manifest.json plugin.wasm README.md LICENSE
|
|
```
|
|
|
|
`manifest.json` and `plugin.wasm` are required; `README.md` becomes the guide on
|
|
the plugin's public page and `LICENSE` is shown beside it.
|
|
|
|
## Attribution
|
|
|
|
The WebDAV code in `src/lib.rs`, `src/dav.rs` and `src/sdk.rs` is copied from
|
|
the MIT-licensed
|
|
[Memona WebDAV plugin](https://git.capsulizers.com/commons/memona-plugin-webdav)
|
|
at commit `3d69750`. That crate keeps its provider, reader and writer private
|
|
and exports the component's entry points itself, so it cannot be wrapped as a
|
|
dependency without forking it; the copy stays as close to the original as the
|
|
signing hook allows, so fixes there carry over with a plain diff.
|