diff --git a/Cargo.lock b/Cargo.lock index 1d407b9..7f28ff9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,30 +2,283 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "alloy-primitives" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e9dbd49258ac3ab893a481d46be29b58be7f734dcac46cd80b6b13ee36566c" +dependencies = [ + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "itoa", + "k256", + "paste", + "ruint", + "sha3", +] + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "bitflags" version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "const-hex" +version = "1.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e59eef12462b0f9b0a3620219be5d639afd79fe39dff0a42c3997061f9298b4" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -41,6 +294,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -65,12 +336,46 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", +] + [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "log" version = "0.4.34" @@ -85,7 +390,18 @@ checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", +] + +[[package]] +name = "md-ens-signature" +version = "0.1.0" +source = "git+https://github.com/capsulizers/md-ens-signature?rev=051a5f611eeba8d61c66dbe5074a2e56517897a7#051a5f611eeba8d61c66dbe5074a2e56517897a7" +dependencies = [ + "alloy-primitives", + "anyhow", + "k256", + "sha2", ] [[package]] @@ -98,10 +414,36 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" name = "memona-plugin-signature" version = "0.1.0" dependencies = [ + "md-ens-signature", "quick-xml", "wit-bindgen", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.3.0" @@ -109,7 +451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn", + "syn 3.0.6", ] [[package]] @@ -121,6 +463,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.5", + "rand_chacha", + "rand_xorshift", + "unarray", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -139,6 +495,108 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ruint" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2973657b5127d510e230f5c63d2d106af9c8f79393d8b9f4647323e8196bdde5" +dependencies = [ + "proptest", + "rand 0.8.8", + "rand 0.9.5", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.28" @@ -171,7 +629,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] @@ -187,6 +645,54 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.6" @@ -198,12 +704,48 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasm-encoder" version = "0.259.0" @@ -269,7 +811,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 3.0.6", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -286,7 +828,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 3.0.6", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -329,6 +871,32 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "zerocopy" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 2a25cd2..b820222 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] wit-bindgen = "=0.62.0" quick-xml = { version = "0.37", default-features = false } +md-ens-signature = { git = "https://github.com/capsulizers/md-ens-signature", rev = "051a5f611eeba8d61c66dbe5074a2e56517897a7" } [lints.clippy] panic = "deny" diff --git a/README.md b/README.md index 91dce9c..5ba34ad 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # Memona signature plugin -Opens a WebDAV server as a folder inside Memona. It is the foundation for -signing Markdown files with an ENS name as they are saved, writing `signer` and -`signature` into each file's frontmatter so anyone can check who wrote it -against the name's current ENSv2 records, using the +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. -This version is the WebDAV folder alone; signing is not in it yet. 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. @@ -18,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, @@ -25,6 +27,17 @@ 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. 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. Reading passes +every file through unchanged. The RPC address is for verifying signers, which a +later version adds to reads. + ## Building ```sh diff --git a/manifest.json b/manifest.json index 7ad1a43..690981f 100644 --- a/manifest.json +++ b/manifest.json @@ -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" } ] } diff --git a/src/lib.rs b/src/lib.rs index a365775..8dc8806 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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!( "", ); 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, + 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, written: Cell, + /// 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>>, +} + +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())), })) } diff --git a/src/sign.rs b/src/sign.rs new file mode 100644 index 0000000..2fe171d --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,107 @@ +//! 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. + +use crate::sdk::ErrorCode; +use md_ens_signature::document::{ + SignatureFields, body_digest, write_signature, +}; +use md_ens_signature::signature::{address_of, sign}; + +/// One connection's signing identity: an ENS name and the private key bound +/// to it. +pub struct Signer { + name: String, + key: [u8; 32], +} + +/// Reads a 32-byte private key written as 64 hex characters, with or without +/// a `0x` prefix. +fn parse_key(text: &str) -> Option<[u8; 32]> { + let digits = text.trim(); + let digits = digits.strip_prefix("0x").unwrap_or(digits).as_bytes(); + if digits.len() != 64 { + return None; + } + let mut key = [0u8; 32]; + for (byte, pair) in key.iter_mut().zip(digits.chunks(2)) { + let text = std::str::from_utf8(pair).ok()?; + *byte = u8::from_str_radix(text, 16).ok()?; + } + Some(key) +} + +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 { + let name = name.trim().to_lowercase(); + let key = parse_key(private_key).ok_or(ErrorCode::InvalidInput)?; + if name.is_empty() || address_of(&key).is_err() { + return Err(ErrorCode::InvalidInput); + } + Ok(Self { name, key }) + } + + /// 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, ErrorCode> { + let text = + std::str::from_utf8(markdown).map_err(|_| ErrorCode::InvalidInput)?; + let signature = sign(&self.key, &self.name, &body_digest(text)) + .map_err(|_| ErrorCode::Internal)?; + let fields = SignatureFields { + signer: self.name.clone(), + signature, + }; + Ok(write_signature(text, &fields).into_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::{Signer, parse_key}; + use crate::sdk::ErrorCode; + use md_ens_signature::document::{body_digest, read_signature}; + use md_ens_signature::signature::{address_of, recover}; + + const KEY: &str = + "0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"; + + #[test] + fn a_signed_file_recovers_to_the_key_and_keeps_its_body() + -> 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)?; + assert!(signed.starts_with("---\ntitle: Notes\n")); + assert!(signed.ends_with("---\n# Notes\n\nHello\n")); + let fields = read_signature(&signed).ok_or(ErrorCode::Internal)?; + assert_eq!(fields.signer, "laptop.alice.eth"); + let key = parse_key(KEY).ok_or(ErrorCode::Internal)?; + let recovered = + recover(&fields.signature, &fields.signer, &body_digest(&signed)) + .map_err(|_| ErrorCode::Internal)?; + assert_eq!(Some(recovered), address_of(&key).ok()); + Ok(()) + } + + #[test] + fn a_key_that_cannot_sign_refuses_the_connection() { + assert!(Signer::new("alice.eth", "0x1234").is_err()); + assert!(Signer::new("alice.eth", &"zz".repeat(32)).is_err()); + assert!(Signer::new("alice.eth", &"00".repeat(32)).is_err()); + 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(()) + } +}