rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood86f08be3d6ee58bed8a356bc645ce46d4888c40d
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "24e3c53626bd5e1d4a71c188e8d897f47eddbbcb",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "crypto: Rewrite",
"state": {
"status": "open",
"conflicts": []
},
"before": "66ab7f35c933278b932657936ce0c52a02962302",
"after": "86f08be3d6ee58bed8a356bc645ce46d4888c40d",
"commits": [
"86f08be3d6ee58bed8a356bc645ce46d4888c40d",
"e866de4f4984536e6e2c901329fa3a3bddeec7ab",
"d2aa03d9dac1c6eea3ef8650f341eda116a1b0eb",
"094734a20b696e40b958999cb4783cf89a61968a",
"afee421647d46efff587a0cea0d3af5e951b6916",
"28bf25ef973b5297f4db5cba53c1db6f47481311",
"2fa4bad693f49569ba0b8f26feea1f960a7610d4",
"4dae90eceed357c3ce8ea44a8857c913bb4c3c0e",
"7ae9f8833cbe57fd7e3471974c0c4659488a8915",
"0a2610314b3377287b1b67db599285f6c2c4d626",
"cf037e3c1c544d43fc66822950e8ad9874d9449a",
"661ac2a8cbe4612b19892a426ae442a5825a408a",
"ee65406246ab44a4455ca9b38a61c8bfd59d385d",
"56ca165d0100f311bc955f21d09a2ef3b2086bf7",
"bbce28234e1204e090ec1d3f9475dce782e58249",
"88477959cd193aefeac84adf340d8fe3dd30b6db",
"977f7a17adfe92373f0b45014453b939aadb9b41",
"1458c696669863abfc9f0cc161d99edbb07806e7",
"537c8f0ffd237ba381a3aebb7bf29902779624db",
"0fd0104e8771fa7f2bec64216b3668485ff6b324",
"8ceef3c40571809910e4c207f3e55e95f8834490"
],
"target": "66ab7f35c933278b932657936ce0c52a02962302",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "24e3c53626bd5e1d4a71c188e8d897f47eddbbcb",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Reasons for the rewrite:\n - Migrate away from `ec25519` as our implementation of Ed25519,\n because that crate is unmaintained.\n Instead of `ec25519` we use `ed25519-dalek` which is the most popular\n implementation of Ed25519 in Rust.\n - Migrate away from `amplify`, because we make very little use of its\n features. Here, we only use `amplify::Bytes32`, which is just a thin\n wrapper of `[u8; 32]`.\n Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`.\n\nThe realization that `ExtendedSignature` is not really tied to SSH, but\nis merely a representation of a key together with a signature, allows to\nrelax the bounds on traits in `radicle-cob`.\n\nSeparating out uses of \"public key\" (a rather simple model of a key,\njust 32 bytes) and \"verifying key\" (an actual point on the Edwards\ncurve, ready to verify signatures, using more memory than just 32 bytes)\nallows a more sensible definition of `crypto::Signer`, which propagates\nto `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and\nsimplifies trait bounds.\n\nIn `radicle-node`, make the fact clearer that actually the secret key\nitself is required (for our implementation of Diffie-Hellman) not\n\"just\" signing. This results in removal of the generic argument\n(usually called `G`) of multiple types.\n\nConcretizing the types in `radicle-node` this way, i.e. by (morally)\nsetting `G = radicle_crypto::SigningKey`, leads to removal of\n`MemorySigner` and `MockSigner`, as they really just wrap a signing\nkey.\n\nRe-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`)\nis hopefully more future-proof than using our own type for signatures.\nHowever, the fact that `ed25519::Signature` is not serializable and\ndoes not implement `std::hash::Hash` means that we have to resort to\nad-hoc solutions in `radicle`:\n 1. The Identity COB requires serialization of signatures. These are\n generally questionable, since the security of the Identity COB\n should rely on the security of Signed References. In the medium\n term we might even want to consider ignoring signatures in actions.\n 2. When reading Signed References, detection of Replay Attacks uses\n a `HashMap` keyed by signatures. This detection only exists for\n backwards compatibility.\nThese two use cases are exotic enough to warrant ad-hoc implementations.\nBy not wrapping `ed25519::Signature`, we encourage writing more widely\ncompatible code in the future.",
"base": "027de89cdb93f9f08e46ab76ab38c62c4005724a",
"oid": "f8d4ff1fd7fd3110681659a4705bc4d0e2e5a2da",
"timestamp": 1784284925
},
{
"id": "f0c57fd16cfb88f1ca13c46a9ec930b6efb238aa",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Optimize compute-heavy dependencies",
"base": "027de89cdb93f9f08e46ab76ab38c62c4005724a",
"oid": "6ce96910bbb6d33857ce04afe27d17491b15ed7d",
"timestamp": 1784289364
},
{
"id": "ed140d9a6f2a111ea19054aa78bbebc9b5bfd15f",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "REVIEW\n\nA lot of these are nits and clarifications.\n\nHowever, there does seem to be a major problem. When I tried to push a I got test failures in `radicle`:\n\n```\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::sync::announce::test::announcer_reached_min_replication_target ... FAILED\n\n failures:\n\n failures:\n node::sync::announce::test::announcer_reached_min_replication_target\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 399 filtered out; finished in 0.06s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::sync::announce::test::announcer_reached_min_replication_target' (1087202) panicked at crates/radicle/src/node/sync/announce.rs:684:9:\n assertion `left == right` failed\n left: {PublicKey([5, 221, 106, 2, 242, 199, 33, 65, 145, 133, 231, 152, 124, 221, 98, 121, 221, 220, 3, 227, 226, 20, 123, 218, 187, 218, 83, 133, 33, 178, 59, 103]), PublicKey([23, 166, 132, 119, 154, 93, 182, 134, 242, 16, 90, 181, 129, 140, 75, 214, 100, 111, 77, 40, 166, 193, 195, 32, 155, 14, 238, 118, 41, 84, 233, 71]), PublicKey([75, 248, 102, 239, 154, 3, 237, 239, 125, 238, 107, 133, 162, 159, 138, 180, 2, 58, 59, 67, 223, 26, 98, 15, 82, 15, 176, 22, 72, 74, 130, 219]), PublicKey([96, 17, 171, 105, 226, 198, 100, 220, 161, 223, 167, 106, 196, 89, 213, 84, 128, 207, 1, 237, 232, 174, 197, 173, 56, 217, 189, 40, 10, 83, 230, 103]), PublicKey([108, 193, 114, 1, 252, 37, 158, 110, 74, 55, 0, 248, 215, 176, 195, 164, 101, 48, 247, 239, 141, 7, 239, 106, 255, 58, 174, 144, 207, 23, 190, 13]), PublicKey([157, 2, 236, 151, 9, 210, 225, 195, 187, 105, 75, 49, 11, 211, 109, 140, 194, 77, 241, 2, 97, 68, 95, 248, 96, 184, 137, 206, 174, 217, 7, 169]), PublicKey([183, 102, 88, 18, 108, 214, 199, 242, 120, 129, 243, 76, 196, 98, 239, 193, 168, 66, 79, 199, 175, 59, 132, 99, 22, 63, 190, 64, 100, 85, 210, 190]), PublicKey([189, 232, 81, 145, 168, 63, 139, 84, 87, 160, 247, 28, 67, 156, 16, 213, 28, 204, 121, 177, 75, 153, 128, 50, 145, 112, 5, 106, 22, 3, 193, 72])}\n right: {PublicKey([5, 221, 106, 2, 242, 199, 33, 65, 145, 133, 231, 152, 124, 221, 98, 121, 221, 220, 3, 227, 226, 20, 123, 218, 187, 218, 83, 133, 33, 178, 59, 103]), PublicKey([23, 166, 132, 119, 154, 93, 182, 134, 242, 16, 90, 181, 129, 140, 75, 214, 100, 111, 77, 40, 166, 193, 195, 32, 155, 14, 238, 118, 41, 84, 233, 71]), PublicKey([59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13, 115, 101, 50, 21, 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41]), PublicKey([75, 248, 102, 239, 154, 3, 237, 239, 125, 238, 107, 133, 162, 159, 138, 180, 2, 58, 59, 67, 223, 26, 98, 15, 82, 15, 176, 22, 72, 74, 130, 219]), PublicKey([96, 17, 171, 105, 226, 198, 100, 220, 161, 223, 167, 106, 196, 89, 213, 84, 128, 207, 1, 237, 232, 174, 197, 173, 56, 217, 189, 40, 10, 83, 230, 103]), PublicKey([108, 193, 114, 1, 252, 37, 158, 110, 74, 55, 0, 248, 215, 176, 195, 164, 101, 48, 247, 239, 141, 7, 239, 106, 255, 58, 174, 144, 207, 23, 190, 13]), PublicKey([157, 2, 236, 151, 9, 210, 225, 195, 187, 105, 75, 49, 11, 211, 109, 140, 194, 77, 241, 2, 97, 68, 95, 248, 96, 184, 137, 206, 174, 217, 7, 169]), PublicKey([183, 102, 88, 18, 108, 214, 199, 242, 120, 129, 243, 76, 196, 98, 239, 193, 168, 66, 79, 199, 175, 59, 132, 99, 22, 63, 190, 64, 100, 85, 210, 190]), PublicKey([189, 232, 81, 145, 168, 63, 139, 84, 87, 160, 247, 28, 67, 156, 16, 213, 28, 204, 121, 177, 75, 153, 128, 50, 145, 112, 5, 106, 22, 3, 193, 72])}\n\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds ... FAILED\n\n failures:\n\n failures:\n node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 399 filtered out; finished in 0.04s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds' (1087212) panicked at crates/radicle/src/node/sync/announce.rs:1000:21:\n assertion `left == right` failed: Should succeed with preferred seeds even with zero replication factor\n left: PreferredNodes { preferred: 1, total_nodes_synced: 1 }\n right: PreferredNodes { preferred: 2, total_nodes_synced: 2 }\n```\n\nWhen I reran just the `radicle` tests, I got flakey behaviour with two other tests failing randomly:\n\n```\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::policy::store::test::test_node_aliases ... FAILED\n\n failures:\n\n failures:\n node::policy::store::test::test_node_aliases\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 396 filtered out; finished in 0.06s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::policy::store::test::test_node_aliases' (1115523) panicked at crates/radicle/src/node.rs:1553:9:\n assertion `left == right` failed\n left: Some({PublicKey([12, 112, 75, 77, 19, 96, 38, 134, 98, 141, 230, 13, 191, 16, 6, 101, 198, 62, 95, 184, 134, 54, 54, 163, 177, 47, 254, 215, 192, 216, 85, 24]), PublicKey([34, 60, 233, 140, 45, 106, 150, 104, 75, 232, 227, 240, 199, 87, 107, 242, 128, 46, 157, 26, 159, 100, 54, 74, 113, 201, 232, 242, 96, 237, 160, 39])})\n right: Some({PublicKey([12, 112, 75, 77, 19, 96, 38, 134, 98, 141, 230, 13, 191, 16, 6, 101, 198, 62, 95, 184, 134, 54, 54, 163, 177, 47, 254, 215, 192, 216, 85, 24]), PublicKey([34, 60, 233, 140, 45, 106, 150, 104, 75, 232, 227, 240, 199, 87, 107, 242, 128, 46, 157, 26, 159, 100, 54, 74, 113, 201, 232, 242, 96, 237, 160, 39]), PublicKey([118, 161, 89, 32, 68, 166, 228, 245, 17, 38, 91, 202, 115, 166, 4, 217, 11, 5, 41, 209, 223, 96, 43, 227, 10, 25, 169, 37, 118, 96, 209, 245])})\n\n stdout \u2500\u2500\u2500\n\n running 1 test\n test node::sync::announce::test::announcer_adapts_target_to_reach ... FAILED\n\n failures:\n\n failures:\n node::sync::announce::test::announcer_adapts_target_to_reach\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 396 filtered out; finished in 0.03s\n \n stderr \u2500\u2500\u2500\n\n thread 'node::sync::announce::test::announcer_adapts_target_to_reach' (1117345) panicked at crates/radicle/src/node/sync/announce.rs:971:9:\n assertion `left == right` failed\n left: 2\n right: 3\n```",
"base": "66ab7f35c933278b932657936ce0c52a02962302",
"oid": "ee65406246ab44a4455ca9b38a61c8bfd59d385d",
"timestamp": 1784312032
},
{
"id": "eeec57800be24d12bb9c22679fd3cf897e05962b",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Respond to review",
"base": "66ab7f35c933278b932657936ce0c52a02962302",
"oid": "86f08be3d6ee58bed8a356bc645ce46d4888c40d",
"timestamp": 1784326540
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "57c396a0-5542-4bf8-a735-4da25a7267c2"
},
"info_url": "https://cci.rad.levitte.org//57c396a0-5542-4bf8-a735-4da25a7267c2.html"
}
Started at: 2026-07-18 00:16:46.366038+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/57c396a0-5542-4bf8-a735-4da25a7267c2/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 187 issues · 42 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb
✓ Switched to branch patch/24e3c53 at revision eeec578
✓ Branch patch/24e3c53 setup to track rad/patches/24e3c53626bd5e1d4a71c188e8d897f47eddbbcb
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 86f08be3d6ee58bed8a356bc645ce46d4888c40d
HEAD is now at 86f08be3 REVIEW: Response to Fintan are parents of this commit
Exit code: 0
$ rad patch show 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb -p
╭────────────────────────────────────────────────────────────────────────────────╮
│ Title crypto: Rewrite │
│ Patch 24e3c53626bd5e1d4a71c188e8d897f47eddbbcb │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 86f08be3d6ee58bed8a356bc645ce46d4888c40d │
│ Base 66ab7f35c933278b932657936ce0c52a02962302 │
│ Branches patch/24e3c53 │
│ Commits ahead 21, behind 0 │
│ Status open │
│ │
│ Reasons for the rewrite: │
│ - Migrate away from `ec25519` as our implementation of Ed25519, │
│ because that crate is unmaintained. │
│ Instead of `ec25519` we use `ed25519-dalek` which is the most popular │
│ implementation of Ed25519 in Rust. │
│ - Migrate away from `amplify`, because we make very little use of its │
│ features. Here, we only use `amplify::Bytes32`, which is just a thin │
│ wrapper of `[u8; 32]`. │
│ Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`. │
│ │
│ The realization that `ExtendedSignature` is not really tied to SSH, but │
│ is merely a representation of a key together with a signature, allows to │
│ relax the bounds on traits in `radicle-cob`. │
│ │
│ Separating out uses of "public key" (a rather simple model of a key, │
│ just 32 bytes) and "verifying key" (an actual point on the Edwards │
│ curve, ready to verify signatures, using more memory than just 32 bytes) │
│ allows a more sensible definition of `crypto::Signer`, which propagates │
│ to `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and │
│ simplifies trait bounds. │
│ │
│ In `radicle-node`, make the fact clearer that actually the secret key │
│ itself is required (for our implementation of Diffie-Hellman) not │
│ "just" signing. This results in removal of the generic argument │
│ (usually called `G`) of multiple types. │
│ │
│ Concretizing the types in `radicle-node` this way, i.e. by (morally) │
│ setting `G = radicle_crypto::SigningKey`, leads to removal of │
│ `MemorySigner` and `MockSigner`, as they really just wrap a signing │
│ key. │
│ │
│ Re-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`) │
│ is hopefully more future-proof than using our own type for signatures. │
│ However, the fact that `ed25519::Signature` is not serializable and │
│ does not implement `std::hash::Hash` means that we have to resort to │
│ ad-hoc solutions in `radicle`: │
│ 1. The Identity COB requires serialization of signatures. These are │
│ generally questionable, since the security of the Identity COB │
│ should rely on the security of Signed References. In the medium │
│ term we might even want to consider ignoring signatures in actions. │
│ 2. When reading Signed References, detection of Replay Attacks uses │
│ a `HashMap` keyed by signatures. This detection only exists for │
│ backwards compatibility. │
│ These two use cases are exotic enough to warrant ad-hoc implementations. │
│ By not wrapping `ed25519::Signature`, we encourage writing more widely │
│ compatible code in the future. │
├────────────────────────────────────────────────────────────────────────────────┤
│ 86f08be REVIEW: Response to Fintan are parents of this commit │
│ e866de4 REVIEW: Expand docs on `fn`s │
│ d2aa03d REVIEW: Expand documentation of `Signer` │
│ 094734a FIX: Simplify re-exporting │
│ afee421 REVIEW: Document `{Public,Verifying,Signing}Key` │
│ 28bf25e FIX: Visibility of dalek │
│ 2fa4bad REVIEW: Remove X25519 support │
│ 4dae90e REVIEW: Further tweak internal enum │
│ 7ae9f88 REVIEW: Response │
│ 0a26103 crypto: Document SigningKey methods │
│ cf037e3 REVIEW: Requesting documentation │
│ 661ac2a REVIEW: Requesting new struct │
│ ee65406 REVIEW: Using an internal enum │
│ 56ca165 REVIEW: I need clarification │
│ bbce282 crypto: Document existing methods │
│ 8847795 REVIEW: Document Signer trait │
│ 977f7a1 REVIEW: We might as well re-export the type │
│ 1458c69 cargo/dev: Optimize compute-heavy dependencies │
│ 537c8f0 crypto: Rewrite │
│ 0fd0104 radicle, cli/auth: Let binary crate generate seed │
│ 8ceef3c crypto/test: Diffie-Hellman Fixture │
├────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 24e3c53 @ 027de89..f8d4ff1 by lorenz z6MkkPv…WX5sTEz 11 hours ago │
│ ↑ Revision f0c57fd @ 027de89..6ce9691 by lorenz z6MkkPv…WX5sTEz 10 hours ago │
│ ↑ Revision ed140d9 @ 66ab7f3..ee65406 by fintohaps z6Mkire…SQZ3voM 4 hours ago │
│ ↑ Revision eeec578 @ 66ab7f3..86f08be by lorenz z6MkkPv…WX5sTEz 1 minute ago │
╰────────────────────────────────────────────────────────────────────────────────╯
commit 86f08be3d6ee58bed8a356bc645ce46d4888c40d
Merge: d2aa03d9d e866de4f4 4dae90ece 7ae9f8833 28bf25ef9 afee42164 094734a20
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Sat Jul 18 00:13:53 2026 +0200
REVIEW: Response to Fintan are parents of this commit
commit e866de4f4984536e6e2c901329fa3a3bddeec7ab
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Fri Jul 17 22:29:23 2026 +0200
REVIEW: Expand docs on `fn`s
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 8b46aa6ab..e91636923 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -538,7 +538,13 @@ impl SigningKey {
self.0.as_ref().as_bytes().into()
}
- /// Construct a new [`SigningKey`] from the provided [`Seed`].
+ /// Construct a new [`SigningKey`] from the provided [`Seed`] by "expanding"
+ /// `seed`. This involves hashing `seed` with SHA-512 and clamping the
+ /// resulting 32-byte digest to produce a valid key.
+ ///
+ /// See also `secret_expand` in [RFC 8032, Sec. 6].
+ ///
+ /// [RFC 8032, Sec. 6]: https://datatracker.ietf.org/doc/html/rfc8032#section-6
pub fn from_seed(seed: Seed) -> Self {
Self(dalek::ed::SigningKey::from_bytes(seed.as_ref()))
}
@@ -562,7 +568,8 @@ impl SigningKey {
self.0.to_keypair_bytes()
}
- /// Convert this [`SigningKey`] into its 32-byte representation.
+ /// Convert this [`SigningKey`] into a reference to its 32-byte
+ /// representation.
pub fn as_bytes(&self) -> &[u8; dalek::ed::SECRET_KEY_LENGTH] {
self.0.as_bytes()
}
commit d2aa03d9dac1c6eea3ef8650f341eda116a1b0eb
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Sat Jul 18 00:10:48 2026 +0200
REVIEW: Expand documentation of `Signer`
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index abc15be3b..c249a1834 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -39,11 +39,13 @@ pub use seed::Seed;
/// Output of a Diffie-Hellman key exchange.
pub type SharedSecret = [u8; 32];
-/// A super-trait that includes:
-/// - [`signature::Signer`] that can produce the exported [`Signature`] type,
-/// - [`signature::Keypair`] where the associated `VerifyingKey` is the
+/// A super-trait that requires:
+/// - [`signature::Signer`] to produce the exported [`Signature`] type,
+/// - [`signature::Keypair`] where the associated
+/// [`signature::Keypair::VerifyingKey`] is the
/// [`VerifyingKey`] defined in this crate, and
-/// - can return a reference to its [`PublicKey`].
+/// - [`AsRef<PublicKey>`] to obtain a reference to the corresponding
+/// [`PublicKey`].
///
/// A blanket implementation is provided for all types that satisfy the trait
/// bounds.
commit 094734a20b696e40b958999cb4783cf89a61968a
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Sat Jul 18 00:07:44 2026 +0200
FIX: Simplify re-exporting
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 523c0975a..270de88a1 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -23,12 +23,9 @@ mod dalek {
pub extern crate x25519_dalek as x;
}
-/// Re-export of the `signature` crate as re-exported
-/// by the `ed25519_dalek` crate.
-pub use dalek::ed::ed25519::signature;
-
-/// Re-export the `dalek` [`Signature`] type.
-pub use dalek::ed::Signature;
+/// Re-exports of the `signature` crate and `ed25519::Signature`
+/// as re-exported by the `ed25519_dalek` crate.
+pub use dalek::ed::ed25519::{Signature, signature};
#[cfg(all(feature = "ssh", feature = "alloc"))]
pub mod ssh;
commit afee421647d46efff587a0cea0d3af5e951b6916
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Fri Jul 17 23:38:45 2026 +0200
REVIEW: Document `{Public,Verifying,Signing}Key`
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 72cf799c3..9f0f4bf37 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -103,6 +103,13 @@ pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x01];
pub type PublicKeyBytes = [u8; dalek::ed::PUBLIC_KEY_LENGTH];
+/// Bytes that are intended/thought to correspond to a point on the Edwards25519
+/// curve (but not on its twist).
+///
+/// This is more compact than [`VerifyingKey`] in memory, and easier to handle,
+/// but it is not guaranteed to be a valid point on the curve, so cannot be used
+/// for actual cryptographic operations such as signature verification or
+/// Diffie-Hellman key exchange.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord)]
#[cfg_attr(
all(feature = "serde", feature = "alloc", feature = "multibase"),
@@ -387,7 +394,11 @@ impl cyphernet::EcPk for PublicKey {
}
}
-// REVIEW: Could we document this properly? Compare and contrast it to PublicKey.
+/// A (decompressed) point on the Edwards25519 curve (but not on its twist) that
+/// may be used to verify signatures.
+///
+/// It is not as compact as a [`PublicKey`] in memory and requires more costly
+/// verification/initialization, but directly corresponds to one.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub struct VerifyingKey(dalek::ed::VerifyingKey);
@@ -526,7 +537,8 @@ pub enum LoadError {
},
}
-// REVIEW: Documentation on this type would be welcome.
+/// A (decompressed) point on the Edwards25519 curve (but not on its twist) that
+/// may be used to sign data.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SigningKey(dalek::ed::SigningKey);
commit 28bf25ef973b5297f4db5cba53c1db6f47481311
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Fri Jul 17 23:22:33 2026 +0200
FIX: Visibility of dalek
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index de84bb6b9..cea973222 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -17,8 +17,8 @@ extern crate std;
/// that this crate depends on. Since both are related to Curve25519
/// in some way, the "25519" suffix is omitted from the name of the re-export.
mod dalek {
- pub extern crate curve25519_dalek as curve;
- pub extern crate ed25519_dalek as ed;
+ pub(crate) extern crate curve25519_dalek as curve;
+ pub(crate) extern crate ed25519_dalek as ed;
}
/// Re-export of the `signature` crate as re-exported
commit 2fa4bad693f49569ba0b8f26feea1f960a7610d4
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Fri Jul 17 23:18:01 2026 +0200
REVIEW: Remove X25519 support
diff --git a/Cargo.lock b/Cargo.lock
index d77cc6d34..b200983b7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -692,7 +692,6 @@ dependencies = [
"digest",
"fiat-crypto",
"rustc_version",
- "serde",
"subtle",
"zeroize",
]
@@ -3028,7 +3027,6 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"winpipe",
- "x25519-dalek",
"zeroize",
]
@@ -5239,18 +5237,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
-[[package]]
-name = "x25519-dalek"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
-dependencies = [
- "curve25519-dalek",
- "rand_core 0.6.4",
- "serde",
- "zeroize",
-]
-
[[package]]
name = "xattr"
version = "1.6.1"
@@ -5338,18 +5324,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [
"serde",
- "zeroize_derive",
-]
-
-[[package]]
-name = "zeroize_derive"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
]
[[package]]
diff --git a/crates/radicle-crypto/Cargo.toml b/crates/radicle-crypto/Cargo.toml
index bd21b141a..85e34f8ce 100644
--- a/crates/radicle-crypto/Cargo.toml
+++ b/crates/radicle-crypto/Cargo.toml
@@ -9,17 +9,16 @@ edition.workspace = true
rust-version.workspace = true
[features]
-default = ["multibase", "std", "x25519"]
-alloc = ["ed25519-dalek/alloc", "curve25519-dalek/alloc", "x25519-dalek?/alloc", "zeroize/alloc"]
+default = ["multibase", "std"]
+alloc = ["ed25519-dalek/alloc", "curve25519-dalek/alloc", "zeroize/alloc"]
cyphernet = ["dep:cyphernet", "alloc", "diffie-hellman", "multibase"]
diffie-hellman = []
multibase = ["dep:multibase", "cyphernet?/multibase"]
schemars = ["dep:schemars", "serde"]
-serde = ["dep:serde", "ed25519-dalek/serde", "x25519-dalek/serde", "zeroize/serde"]
+serde = ["dep:serde", "ed25519-dalek/serde", "zeroize/serde"]
ssh = ["ssh-agent-lib", "ssh-key", "alloc"]
std = ["alloc", "ed25519-dalek/std", "multibase?/std", "schemars?/std", "serde?/std", "ssh-key?/std", "thiserror/std", "zeroize/std" ]
test = ["alloc", "qcheck"]
-x25519 = ["dep:x25519-dalek"]
[dependencies]
cyphernet = { workspace = true, features = ["ed25519"], optional = true }
@@ -37,7 +36,6 @@ zeroize = { workspace = true, default-features = false }
# dalek
curve25519-dalek = { version = "4", default-features = false, features = ["zeroize"] }
ed25519-dalek = { version = "2", default-features = false, features = ["zeroize"] }
-x25519-dalek = { version = "2", default-features = false, features = ["zeroize"], optional = true }
[target.'cfg(windows)'.dependencies]
winpipe = { workspace = true }
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 01337ad37..de84bb6b9 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -13,14 +13,12 @@ use alloc::{
#[cfg(feature = "std")]
extern crate std;
-/// Re-exports of dalek cryptography crates (see <https://dalek.rs/>)
-/// that this crate depends on. Since they are all related to Curve25519
+/// References to of dalek cryptography crates (see <https://dalek.rs/>)
+/// that this crate depends on. Since both are related to Curve25519
/// in some way, the "25519" suffix is omitted from the name of the re-export.
mod dalek {
pub extern crate curve25519_dalek as curve;
pub extern crate ed25519_dalek as ed;
- #[cfg(feature = "x25519")]
- pub extern crate x25519_dalek as x;
}
/// Re-export of the `signature` crate as re-exported
@@ -397,24 +395,6 @@ impl VerifyingKey {
pub(crate) fn to_bytes(&self) -> PublicKeyBytes {
self.0.to_bytes()
}
-
- #[cfg(feature = "x25519")]
- // REVIEW: Could we consider a struct here, that way we could also have
- // `From` going both ways, but also just to have it clear that there is an
- // isomorphism between the two types.
- pub fn to_x25519(&self) -> (u8, dalek::x::PublicKey) {
- (
- AsRef::<PublicKeyBytes>::as_ref(self)[31] >> 7,
- dalek::x::PublicKey::from(self.0.to_montgomery().to_bytes()),
- )
- }
-
- #[cfg(feature = "x25519")]
- pub fn from_x25519(sign: u8, pk: &dalek::x::PublicKey) -> Option<Self> {
- dalek::curve::montgomery::MontgomeryPoint(pk.to_bytes())
- .to_edwards(sign)
- .map(|point| Self(dalek::ed::VerifyingKey::from(point)))
- }
}
impl TryFrom<&PublicKey> for VerifyingKey {
@@ -610,11 +590,6 @@ impl SigningKey {
.decompress()
.map(|point| (scalar * point).compress().to_bytes())
}
-
- #[cfg(feature = "x25519")]
- pub fn to_x25519(&self) -> [u8; 32] {
- dalek::curve::scalar::clamp_integer(self.0.to_scalar_bytes())
- }
}
impl AsRef<PublicKey> for SigningKey {
@@ -885,72 +860,4 @@ mod tests {
]
)
}
-
- // See <https://docs.rs/ed25519_to_curve25519/0.2.4/src/ed25519_to_curve25519/lib.rs.html#15-26>.
- #[cfg(all(feature = "alloc", feature = "x25519"))]
- #[test]
- fn x25519_pk() {
- let ed25519_pk = [
- 59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13, 115, 101, 50, 21,
- 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41,
- ];
-
- let curve25519_pk = [
- 91, 245, 92, 115, 184, 46, 190, 34, 190, 128, 243, 67, 6, 103, 175, 87, 15, 174, 37,
- 86, 166, 65, 94, 107, 48, 212, 6, 83, 0, 170, 148, 125,
- ];
-
- let ed25519_pk = VerifyingKey::try_from(&PublicKey::from(ed25519_pk)).unwrap();
- let converted = ed25519_pk.to_x25519();
-
- assert_eq!(*converted.1.as_bytes(), curve25519_pk);
-
- assert_eq!(
- VerifyingKey::from_x25519(converted.0, &converted.1).unwrap(),
- ed25519_pk
- );
- }
-
- // See <https://docs.rs/ed25519_to_curve25519/0.2.4/src/ed25519_to_curve25519/lib.rs.html#49-62>.
- #[cfg(all(feature = "alloc", feature = "x25519"))]
- #[test]
- fn x25519_sk() {
- let ed25519_sk: [u8; 32] = [
- 202, 104, 239, 81, 53, 110, 80, 252, 198, 23, 155, 162, 215, 98, 223, 173, 227, 188,
- 110, 54, 127, 45, 185, 206, 174, 29, 44, 147, 76, 66, 196, 195,
- ];
-
- let curve25519_sk: [u8; 32] = [
- 200, 255, 64, 61, 17, 52, 112, 33, 205, 71, 186, 13, 131, 12, 241, 136, 223, 5, 152,
- 40, 95, 187, 83, 168, 142, 10, 234, 215, 70, 210, 148, 104,
- ];
-
- let ed25519_sk = SigningKey::from(dalek::ed::SigningKey::from_bytes(&ed25519_sk));
-
- assert_eq!(ed25519_sk.to_x25519(), curve25519_sk);
- }
-
- /// See <https://w3c-ccg.github.io/did-key-spec/#ed25519-with-x25519>.
- #[cfg(all(feature = "alloc", feature = "x25519", feature = "multibase"))]
- #[test]
- fn ed25519_did() {
- let ed: PublicKey = DID_KEY_SAMPLE.parse().unwrap();
-
- let expected = {
- // X25519 public key, multibase encoded according to `did:key` spec.
- let key = "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p";
-
- let mut bytes = multibase::decode(key).unwrap().1;
-
- // X25519 public key, as raw bytes.
- let key = bytes.split_off(2);
-
- // The first two bytes indicate that this is an X25519 public key.
- assert_eq!(bytes, [0xEC, 0x01]);
-
- dalek::x::PublicKey::from(TryInto::<[u8; 32]>::try_into(key).unwrap())
- };
-
- assert_eq!(expected, VerifyingKey::try_from(&ed).unwrap().to_x25519().1);
- }
}
commit 4dae90eceed357c3ce8ea44a8857c913bb4c3c0e
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Fri Jul 17 22:18:09 2026 +0200
REVIEW: Further tweak internal enum
diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs
index 308d02db6..513900844 100644
--- a/crates/radicle/src/profile.rs
+++ b/crates/radicle/src/profile.rs
@@ -26,7 +26,6 @@ use thiserror::Error;
use crate::cob::migrate;
use crate::cob::store::access::{ReadOnly, WriteAs};
-use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{Keystore, Passphrase, keystore};
use crate::crypto::{PublicKey, Seed};
use crate::node::policy::config::store::Read;
@@ -210,13 +209,13 @@ pub enum SignerError {
LoadError(#[from] crypto::LoadError),
#[error(transparent)]
- Agent(#[from] crate::crypto::ssh::agent::IntoSignerError),
+ Agent(#[from] crypto::ssh::agent::IntoSignerError),
#[error(transparent)]
Keystore(#[from] keystore::Error),
#[error("error connecting to ssh-agent: {0}")]
- AgentConnection(#[from] crate::crypto::ssh::agent::ConnectError),
+ AgentConnection(#[from] crypto::ssh::agent::ConnectError),
#[error("public key is invalid: {0}")]
InvalidPublicKey(#[source] crypto::signature::Error),
@@ -323,39 +322,40 @@ impl Profile {
}
pub fn signer(&self) -> Result<Signer, SignerError> {
- // Where to obtain the signer from.
+ /// Where to obtain the signer from.
enum Source {
- // Obtain the signer from the keystore, without a passphrase, if the
- // keystore is not encrypted.
- PlainKeystore,
- // Obtain the signer from the keystore, using the passphrase from
- // the environment variable. This can only be done if the keystore
- // is known to be encrypted and the passphrase is set.
- EncryptedKeystore(Passphrase),
- // Otherwise, obtain the signer from the ssh-agent.
+ /// Load the secret key from the keystore without a passphrase.
+ /// This only works if the keystore is not encrypted.
+ KeystorePlain,
+ /// Load the secret key from the keystore using the given
+ /// passphrase to decrypt it.
+ KeystoreEncrypted(Passphrase),
+ /// Use `ssh-agent`.
Agent,
}
+
+ use Source::*;
+
let source = if self.keystore.is_encrypted()? {
- match env::passphrase() {
- Some(passphrase) => Source::EncryptedKeystore(passphrase),
- None => Source::Agent,
- }
+ env::passphrase().map(KeystoreEncrypted).unwrap_or(Agent)
} else {
- Source::PlainKeystore
+ KeystorePlain
};
+
let signer = match source {
- Source::EncryptedKeystore(passphrase) => {
+ KeystoreEncrypted(passphrase) => {
Signer::Key(crypto::SigningKey::load(&self.keystore, Some(passphrase))?)
}
- Source::PlainKeystore => Signer::Key(crypto::SigningKey::load(&self.keystore, None)?),
- Source::Agent => Signer::Agent(
- Agent::connect()?.into_signer(
+ KeystorePlain => Signer::Key(crypto::SigningKey::load(&self.keystore, None)?),
+ Agent => Signer::Agent(
+ crypto::ssh::agent::Agent::connect()?.into_signer(
self.id()
.try_into()
.map_err(SignerError::InvalidPublicKey)?,
)?,
),
};
+
Ok(signer)
}
commit 7ae9f8833cbe57fd7e3471974c0c4659488a8915
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Fri Jul 17 21:37:55 2026 +0200
REVIEW: Response
diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs
index cc0a8a77f..01896c41a 100644
--- a/crates/radicle/src/cob/identity.rs
+++ b/crates/radicle/src/cob/identity.rs
@@ -65,6 +65,8 @@ pub enum Action {
/// Parent revision that this revision replaces.
parent: Option<RevisionId>,
/// Signature over the revision blob.
+ ///
+ /// Regarding serialization, also see documentation of [`signature`].
#[serde(with = "signature")]
signature: Signature,
},
@@ -81,6 +83,8 @@ pub enum Action {
RevisionAccept {
revision: RevisionId,
/// Signature over the blob.
+ ///
+ /// Regarding serialization, also see documentation of [`signature`].
#[serde(with = "signature")]
signature: Signature,
},
@@ -893,6 +897,8 @@ impl<R: ReadRepository> cob::Evaluate<R> for Identity {
pub enum Verdict {
/// An accepting verdict must supply the [`Signature`] over the
/// new proposed [`Doc`].
+ ///
+ /// Regarding serialization, also see documentation of [`signature`].
Accept(#[serde(with = "signature")] Signature),
/// Rejecting the proposed [`Doc`].
Reject,
@@ -1312,14 +1318,32 @@ impl<Repo, Signer: crypto::Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
}
}
-// REVIEW: Just so that I have it clear, this is to remain compatible with the
-// previous encoding and decoding of signatures? If so, can we document the
-// reason here as well? If not.. then can we still document it and you tell me
-// why I am wrong? :D
+/// This module defines [`signature::serialize`] to serialize [`Signature`] by
+/// encoding it as a string [multibase format] (using `base58btc` via
+/// [`multibase::encode`] with [`multibase::Base::Base58Btc`]) and
+/// [`signature::deserialize`] to deserialize a [`Signature`] from a string
+/// in multibase format (using [`multibase::decode`]).
+///
+/// The names of these functions are so that the module can be consumed via
+/// `serde_derive`'s `with`, see <https://serde.rs/field-attrs.html#with>. This
+/// is done in particular for variants of [`Action`] and [`Verdict`] that carry
+/// a [`Signature`].
+///
+/// This module exists because, historically, [`Signature`] was a type that did
+/// implement [`serde::Serialize`] and [`serde::Deserialize`], but it was
+/// changed to refer to `ed25519::Signature` in order to maximize compatibility,
+/// which does not implement these traits.
+/// So, for backwards compatibility, this module fills the gap by providing a
+/// way to (se)serialize [`Signature`].
+///
+/// [multibase format]: https://datatracker.ietf.org/doc/draft-multiformats-multibase/
mod signature {
- pub fn serialize<S>(signature: &crypto::Signature, serializer: S) -> Result<S::Ok, S::Error>
+ pub fn serialize<Serializer>(
+ signature: &crypto::Signature,
+ serializer: Serializer,
+ ) -> Result<Serializer::Ok, Serializer::Error>
where
- S: serde::Serializer,
+ Serializer: serde::Serializer,
{
serializer.serialize_str(&multibase::encode(
multibase::Base::Base58Btc,
@@ -1327,14 +1351,16 @@ mod signature {
))
}
- pub fn deserialize<'de, D>(deserializer: D) -> Result<crypto::Signature, D::Error>
+ pub fn deserialize<'de, Deserializer>(
+ deserializer: Deserializer,
+ ) -> Result<crypto::Signature, Deserializer::Error>
where
- D: serde::Deserializer<'de>,
+ Deserializer: serde::Deserializer<'de>,
{
use serde::Deserialize;
- let s = String::deserialize(deserializer)?;
- let (_, bytes) = multibase::decode(&s).map_err(serde::de::Error::custom)?;
+ let (_, bytes) = multibase::decode(String::deserialize(deserializer)?)
+ .map_err(serde::de::Error::custom)?;
crypto::Signature::from_slice(bytes.as_slice()).map_err(serde::de::Error::custom)
}
}
commit 0a2610314b3377287b1b67db599285f6c2c4d626
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 18:21:31 2026 +0100
crypto: Document SigningKey methods
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 01337ad37..8b46aa6ab 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -538,6 +538,7 @@ impl SigningKey {
self.0.as_ref().as_bytes().into()
}
+ /// Construct a new [`SigningKey`] from the provided [`Seed`].
pub fn from_seed(seed: Seed) -> Self {
Self(dalek::ed::SigningKey::from_bytes(seed.as_ref()))
}
@@ -556,10 +557,12 @@ impl SigningKey {
))
}
+ /// Convert this [`SigningKey`] to a 64-byte keypair.
pub fn to_keypair_bytes(&self) -> [u8; dalek::ed::KEYPAIR_LENGTH] {
self.0.to_keypair_bytes()
}
+ /// Convert this [`SigningKey`] into its 32-byte representation.
pub fn as_bytes(&self) -> &[u8; dalek::ed::SECRET_KEY_LENGTH] {
self.0.as_bytes()
}
commit cf037e3c1c544d43fc66822950e8ad9874d9449a
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 18:20:51 2026 +0100
REVIEW: Requesting documentation
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 1ec948399..72cf799c3 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -387,6 +387,7 @@ impl cyphernet::EcPk for PublicKey {
}
}
+// REVIEW: Could we document this properly? Compare and contrast it to PublicKey.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub struct VerifyingKey(dalek::ed::VerifyingKey);
@@ -525,6 +526,7 @@ pub enum LoadError {
},
}
+// REVIEW: Documentation on this type would be welcome.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SigningKey(dalek::ed::SigningKey);
commit 661ac2a8cbe4612b19892a426ae442a5825a408a
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 18:21:17 2026 +0100
REVIEW: Requesting new struct
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 72cf799c3..01337ad37 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -399,6 +399,9 @@ impl VerifyingKey {
}
#[cfg(feature = "x25519")]
+ // REVIEW: Could we consider a struct here, that way we could also have
+ // `From` going both ways, but also just to have it clear that there is an
+ // isomorphism between the two types.
pub fn to_x25519(&self) -> (u8, dalek::x::PublicKey) {
(
AsRef::<PublicKeyBytes>::as_ref(self)[31] >> 7,
commit ee65406246ab44a4455ca9b38a61c8bfd59d385d
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 19:05:33 2026 +0100
REVIEW: Using an internal enum
The mixing of bool and options smelled to me.
I prefer approaching it with an internal enum that can be documented, making the
whole flow more readable, imo.
diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs
index 3524745b4..308d02db6 100644
--- a/crates/radicle/src/profile.rs
+++ b/crates/radicle/src/profile.rs
@@ -323,35 +323,40 @@ impl Profile {
}
pub fn signer(&self) -> Result<Signer, SignerError> {
- let key = {
- // First, check whether the key is encrypted.
- if self.keystore.is_encrypted()? {
- // The key is encrypted, so we need a passphrase to load it.
- if let Some(passphrase) = env::passphrase() {
- // We have a passphrase from the environment,
- // so we use it to load the key.
- (true, Some(passphrase))
- } else {
- // We do not have a passphrase, so we cannot load the key.
- (false, None)
- }
- } else {
- // The key is not encrypted, so we load it without a passphrase.
- (true, None)
+ // Where to obtain the signer from.
+ enum Source {
+ // Obtain the signer from the keystore, without a passphrase, if the
+ // keystore is not encrypted.
+ PlainKeystore,
+ // Obtain the signer from the keystore, using the passphrase from
+ // the environment variable. This can only be done if the keystore
+ // is known to be encrypted and the passphrase is set.
+ EncryptedKeystore(Passphrase),
+ // Otherwise, obtain the signer from the ssh-agent.
+ Agent,
+ }
+ let source = if self.keystore.is_encrypted()? {
+ match env::passphrase() {
+ Some(passphrase) => Source::EncryptedKeystore(passphrase),
+ None => Source::Agent,
}
- };
-
- Ok(if let (true, passphrase) = key {
- Signer::Key(crypto::SigningKey::load(&self.keystore, passphrase)?)
} else {
- Signer::Agent(
+ Source::PlainKeystore
+ };
+ let signer = match source {
+ Source::EncryptedKeystore(passphrase) => {
+ Signer::Key(crypto::SigningKey::load(&self.keystore, Some(passphrase))?)
+ }
+ Source::PlainKeystore => Signer::Key(crypto::SigningKey::load(&self.keystore, None)?),
+ Source::Agent => Signer::Agent(
Agent::connect()?.into_signer(
self.id()
.try_into()
.map_err(SignerError::InvalidPublicKey)?,
)?,
- )
- })
+ ),
+ };
+ Ok(signer)
}
/// Get Radicle home.
commit 56ca165d0100f311bc955f21d09a2ef3b2086bf7
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 19:05:18 2026 +0100
REVIEW: I need clarification
diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs
index 1384315f6..cc0a8a77f 100644
--- a/crates/radicle/src/cob/identity.rs
+++ b/crates/radicle/src/cob/identity.rs
@@ -1312,6 +1312,10 @@ impl<Repo, Signer: crypto::Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
}
}
+// REVIEW: Just so that I have it clear, this is to remain compatible with the
+// previous encoding and decoding of signatures? If so, can we document the
+// reason here as well? If not.. then can we still document it and you tell me
+// why I am wrong? :D
mod signature {
pub fn serialize<S>(signature: &crypto::Signature, serializer: S) -> Result<S::Ok, S::Error>
where
commit bbce28234e1204e090ec1d3f9475dce782e58249
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 18:19:15 2026 +0100
crypto: Document existing methods
Add more documentation to `PublicKey::to_namespace`, `PublicKey::to_component`,
and `PublicKey::from_namespaced`.
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index abc15be3b..1ec948399 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -214,6 +214,12 @@ impl PublicKey {
multibase::encode(multibase::Base::Base58Btc, buf)
}
+ /// Encode the public key to a Git reference string:
+ ///
+ /// `refs/namespaces/<public-key>`
+ ///
+ /// and `<public-key>` is encoded in human-readable format
+ /// ([`PublicKey::to_human`]).
#[cfg(all(
feature = "git-ref-format-core",
feature = "alloc",
@@ -225,6 +231,8 @@ impl PublicKey {
REFS.to_owned().and(NAMESPACES).and(self.to_component())
}
+ /// Encode the public key a Git reference component, which is equivalent to
+ /// the human-readable format ([`PublicKey::to_human`]).
#[cfg(all(
feature = "git-ref-format-core",
feature = "alloc",
@@ -234,6 +242,13 @@ impl PublicKey {
git_ref_format_core::Component::from(self)
}
+ /// Decode a [`PublicKey`] from a namespaced Git reference, expected to be
+ /// in the format:
+ ///
+ /// `refs/namespaces/<public-key>/…`
+ ///
+ /// The `<public-key>` is decoded from the human-readable format
+ /// ([`PublicKey::to_human`]).
#[cfg(all(
feature = "git-ref-format-core",
feature = "alloc",
commit 88477959cd193aefeac84adf340d8fe3dd30b6db
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 18:18:58 2026 +0100
REVIEW: Document Signer trait
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 523c0975a..abc15be3b 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -39,12 +39,23 @@ pub use seed::Seed;
/// Output of a Diffie-Hellman key exchange.
pub type SharedSecret = [u8; 32];
+/// A super-trait that includes:
+/// - [`signature::Signer`] that can produce the exported [`Signature`] type,
+/// - [`signature::Keypair`] where the associated `VerifyingKey` is the
+/// [`VerifyingKey`] defined in this crate, and
+/// - can return a reference to its [`PublicKey`].
+///
+/// A blanket implementation is provided for all types that satisfy the trait
+/// bounds.
pub trait Signer
where
Self: signature::Signer<Signature>,
Self: signature::Keypair<VerifyingKey = VerifyingKey>,
Self: AsRef<PublicKey>,
{
+ /// Return a reference to the [`PublicKey`].
+ ///
+ /// This is generally satisfied by the [`AsRef<PublicKey>`] instance.
fn public_key(&self) -> &PublicKey {
self.as_ref()
}
commit 977f7a17adfe92373f0b45014453b939aadb9b41
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jul 17 17:46:41 2026 +0100
REVIEW: We might as well re-export the type
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 07e62489c..523c0975a 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -27,6 +27,9 @@ mod dalek {
/// by the `ed25519_dalek` crate.
pub use dalek::ed::ed25519::signature;
+/// Re-export the `dalek` [`Signature`] type.
+pub use dalek::ed::Signature;
+
#[cfg(all(feature = "ssh", feature = "alloc"))]
pub mod ssh;
@@ -36,8 +39,6 @@ pub use seed::Seed;
/// Output of a Diffie-Hellman key exchange.
pub type SharedSecret = [u8; 32];
-pub type Signature = dalek::ed::Signature;
-
pub trait Signer
where
Self: signature::Signer<Signature>,
commit 1458c696669863abfc9f0cc161d99edbb07806e7
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Mon Jul 6 22:13:56 2026 +0200
cargo/dev: Optimize compute-heavy dependencies
diff --git a/Cargo.toml b/Cargo.toml
index e88f1a671..c6ddbc653 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -96,3 +96,16 @@ wildcard_enum_match_arm = "deny"
inherits = "release"
debug = true
incremental = false
+
+# For dependencies that do not change often and that are compute-heavy
+# at runtime, trade slower builds for faster runtime.
+# This is noticeable when running tests during development.
+[profile.dev.package]
+base64 = { opt-level = 3 }
+chacha20 = { opt-level = 3 }
+curve25519-dalek = { opt-level = 3 }
+ed25519 = { opt-level = 3 }
+mio = { opt-level = 3 }
+multibase = { opt-level = 3 }
+poly1305 = { opt-level = 3 }
+sha2 = { opt-level = 3 }
\ No newline at end of file
commit 537c8f0ffd237ba381a3aebb7bf29902779624db
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Mon Jun 29 14:06:31 2026 +0200
crypto: Rewrite
Reasons for the rewrite:
- Migrate away from `ec25519` as our implementation of Ed25519,
because that crate is unmaintained.
Instead of `ec25519` we use `ed25519-dalek` which is the most popular
implementation of Ed25519 in Rust.
- Migrate away from `amplify`, because we make very little use of its
features. Here, we only use `amplify::Bytes32`, which is just a thin
wrapper of `[u8; 32]`.
Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`.
The realization that `ExtendedSignature` is not really tied to SSH, but
is merely a representation of a key together with a signature, allows to
relax the bounds on traits in `radicle-cob`.
Separating out uses of "public key" (a rather simple model of a key,
just 32 bytes) and "verifying key" (an actual point on the Edwards
curve, ready to verify signatures, using more memory than just 32 bytes)
allows a more sensible definition of `crypto::Signer`, which propagates
to `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and
simplifies trait bounds.
In `radicle-node`, make the fact clearer that actually the secret key
itself is required (for our implementation of Diffie-Hellman) not
"just" signing. This results in removal of the generic argument
(usually called `G`) of multiple types.
Concretizing the types in `radicle-node` this way, i.e. by (morally)
setting `G = radicle_crypto::SigningKey`, leads to removal of
`MemorySigner` and `MockSigner`, as they really just wrap a signing
key.
Re-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`)
is hopefully more future-proof than using our own type for signatures.
However, the fact that `ed25519::Signature` is not serializable and
does not implement `std::hash::Hash` means that we have to resort to
ad-hoc solutions in `radicle`:
1. The Identity COB requires serialization of signatures. These are
generally questionable, since the security of the Identity COB
should rely on the security of Signed References. In the medium
term we might even want to consider ignoring signatures in actions.
2. When reading Signed References, detection of Replay Attacks uses
a `HashMap` keyed by signatures. This detection only exists for
backwards compatibility.
These two use cases are exotic enough to warrant ad-hoc implementations.
By not wrapping `ed25519::Signature`, we encourage writing more widely
compatible code in the future.
diff --git a/Cargo.lock b/Cargo.lock
index f2e73a09c..d77cc6d34 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -671,12 +671,6 @@ dependencies = [
"typenum",
]
-[[package]]
-name = "ct-codecs"
-version = "1.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
-
[[package]]
name = "ctr"
version = "0.9.2"
@@ -698,7 +692,9 @@ dependencies = [
"digest",
"fiat-crypto",
"rustc_version",
+ "serde",
"subtle",
+ "zeroize",
]
[[package]]
@@ -733,6 +729,7 @@ checksum = "f23b157ed1e424149e1af82c6ed8300d41941756f5e9841396fd987d946ab0fe"
dependencies = [
"amplify",
"ec25519",
+ "multibase",
"sha2",
]
@@ -884,7 +881,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfd533a2fc01178c738c99412ae1f7e1ad2cb37c2e14bfd87e9d4618171c825"
dependencies = [
- "ct-codecs",
"ed25519 1.5.3",
"getrandom 0.2.17",
]
@@ -918,6 +914,8 @@ version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
+ "pkcs8",
+ "serde",
"signature 2.2.0",
]
@@ -929,8 +927,10 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519 2.2.3",
+ "serde",
"sha2",
"subtle",
+ "zeroize",
]
[[package]]
@@ -2881,6 +2881,7 @@ dependencies = [
"jsonschema",
"libc",
"log",
+ "multibase",
"nonempty 0.9.0",
"pretty_assertions",
"qcheck",
@@ -2985,7 +2986,6 @@ dependencies = [
"radicle-oid",
"serde",
"serde_json",
- "signature 2.2.0",
"tempfile",
"thiserror 2.0.18",
]
@@ -3013,23 +3013,22 @@ dependencies = [
name = "radicle-crypto"
version = "0.17.0"
dependencies = [
- "amplify",
+ "curve25519-dalek",
"cyphernet",
- "ec25519",
- "fastrand",
+ "ed25519-dalek",
"git-ref-format-core",
"multibase",
"qcheck",
"qcheck-macros",
"schemars",
"serde",
- "signature 2.2.0",
"sqlite",
"ssh-agent-lib",
"ssh-key",
"tempfile",
"thiserror 2.0.18",
"winpipe",
+ "x25519-dalek",
"zeroize",
]
@@ -4196,7 +4195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -4556,9 +4555,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
-version = "1.19.0"
+version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
@@ -5240,6 +5239,18 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+[[package]]
+name = "x25519-dalek"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.6.4",
+ "serde",
+ "zeroize",
+]
+
[[package]]
name = "xattr"
version = "1.6.1"
@@ -5322,9 +5333,24 @@ dependencies = [
[[package]]
name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "serde",
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
name = "zerotrie"
diff --git a/Cargo.toml b/Cargo.toml
index 48ca6caba..e88f1a671 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,6 @@ version = "0.9.0"
rust-version = "1.88.0"
[workspace.dependencies]
-amplify = { version = "4.0.0", default-features = false }
backtrace = "0.3.75"
bstr = "1.3"
bytes = "1.11.1"
@@ -39,7 +38,7 @@ itertools = "0.14"
lexopt = "0.3.0"
libc = "0.2.137"
log = "0.4.17"
-multibase = "0.9.1"
+multibase = { version = "0.9.1", default-features = false }
nonempty = "0.9.0"
pretty_assertions = "1.3.0"
proptest = "1.9"
@@ -68,7 +67,6 @@ schemars = { version = "1.0.4", default-features = false }
serde = { version = "1.0", default-features = false }
serde_json = "1.0"
shlex = "1.1.0"
-signature = "2.2"
snapbox = "1.2"
sqlite = "0.37.0"
tempfile = "3.3.0"
@@ -77,7 +75,7 @@ uds_windows = "1.1.0"
windows = "0.62"
winpipe = "0.1.1"
winsplit = "0.1.0"
-zeroize = "1.5.7"
+zeroize = { version = "1.5.7", default-features = false }
# Crates from the "radicle-git" workspace. These should be synced manually.
# When updating, start from `radicle-surf`:
diff --git a/crates/radicle-cli/examples/rad-clone-partial-fail.md b/crates/radicle-cli/examples/rad-clone-partial-fail.md
index 18af1cfd8..44e6ec380 100644
--- a/crates/radicle-cli/examples/rad-clone-partial-fail.md
+++ b/crates/radicle-cli/examples/rad-clone-partial-fail.md
@@ -6,7 +6,7 @@ $ rad node routing
│ RID NID │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
-│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT │
+│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT │
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
╰──────────────────────────────────────────────────────────────────────────────────────╯
```
diff --git a/crates/radicle-cli/src/commands/auth.rs b/crates/radicle-cli/src/commands/auth.rs
index ca9374228..4caed9f02 100644
--- a/crates/radicle-cli/src/commands/auth.rs
+++ b/crates/radicle-cli/src/commands/auth.rs
@@ -4,8 +4,7 @@ use std::str::FromStr;
use anyhow::{Context, anyhow};
-use radicle::crypto::ssh;
-use radicle::crypto::ssh::Passphrase;
+use radicle::crypto;
use radicle::node::Alias;
use radicle::profile::env;
use radicle::{Profile, profile};
@@ -72,7 +71,7 @@ pub fn init(args: Args) -> anyhow::Result<()> {
spinner.finish();
if let Some(passphrase) = passphrase {
- match ssh::agent::Agent::connect() {
+ match crypto::ssh::agent::Agent::connect() {
Ok(mut agent) => {
let mut spinner = term::spinner("Adding your Radicle key to ssh-agent…");
if register(&mut agent, &profile, passphrase).is_ok() {
@@ -133,9 +132,9 @@ pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
// If our key is encrypted, we try to authenticate with SSH Agent and
// register it; only if it is running.
- match ssh::agent::Agent::connect() {
+ match crypto::ssh::agent::Agent::connect() {
Ok(mut agent) => {
- if agent.request_identities()?.contains(&profile.public_key) {
+ if agent.request_identities()?.contains(profile.id()) {
term::success!("Radicle key already in ssh-agent");
return Ok(());
}
@@ -164,7 +163,7 @@ pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
// Try RAD_PASSPHRASE fallback.
if let Some(passphrase) = profile::env::passphrase() {
- ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase))
+ crypto::SigningKey::load(&profile.keystore, Some(passphrase))
.map_err(|_| anyhow!("`{}` is invalid", env::RAD_PASSPHRASE))?;
return Ok(());
}
@@ -181,9 +180,9 @@ pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
/// Register key with ssh-agent.
pub fn register(
- agent: &mut ssh::agent::Agent,
+ agent: &mut crypto::ssh::agent::Agent,
profile: &Profile,
- passphrase: Passphrase,
+ passphrase: crypto::ssh::Passphrase,
) -> anyhow::Result<()> {
let secret = profile
.keystore
diff --git a/crates/radicle-cli/src/commands/id.rs b/crates/radicle-cli/src/commands/id.rs
index 5af4fb60b..e4e0f10a0 100644
--- a/crates/radicle-cli/src/commands/id.rs
+++ b/crates/radicle-cli/src/commands/id.rs
@@ -442,20 +442,16 @@ and description.
Ok(result)
}
-fn update<Repo, Signer>(
+fn update(
title: Option<Title>,
description: Option<String>,
doc: Doc,
- current: &mut IdentityMut<Repo, Signer>,
+ current: &mut IdentityMut<
+ impl WriteRepository + cob::Store<Namespace = NodeId>,
+ impl crypto::Signer,
+ >,
profile: &Profile,
-) -> anyhow::Result<Revision>
-where
- Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<Revision> {
if let Some((title, description)) = edit_title_description(title, description)? {
let id = current
.update(title, description, &doc)
diff --git a/crates/radicle-cli/src/commands/inbox.rs b/crates/radicle-cli/src/commands/inbox.rs
index b92935ebb..1395f55a1 100644
--- a/crates/radicle-cli/src/commands/inbox.rs
+++ b/crates/radicle-cli/src/commands/inbox.rs
@@ -8,7 +8,7 @@ use std::process;
use localtime::LocalTime;
use radicle::cob::TypedId;
use radicle::git::BranchName;
-use radicle::git::fmt::Qualified;
+use radicle::git::fmt::{Component, Qualified};
use radicle::identity::Identity;
use radicle::issue::cache::Issues as _;
use radicle::node::notifications;
@@ -438,7 +438,7 @@ fn show(
NotificationKind::Branch { .. } => {
let refstr = if let Some(remote) = n.remote {
n.qualified
- .with_namespace(remote.to_component())
+ .with_namespace(Component::from(&remote))
.to_string()
} else {
n.qualified.to_string()
diff --git a/crates/radicle-cli/src/commands/init.rs b/crates/radicle-cli/src/commands/init.rs
index 7774189e6..2e52e7f53 100644
--- a/crates/radicle-cli/src/commands/init.rs
+++ b/crates/radicle-cli/src/commands/init.rs
@@ -240,7 +240,7 @@ pub fn init_existing(
&working,
&radicle::rad::REMOTE_NAME,
&url,
- &url.clone().with_namespace(profile.public_key),
+ &url.clone().with_namespace(*profile.id()),
)?;
if args.set_upstream {
diff --git a/crates/radicle-cli/src/commands/issue.rs b/crates/radicle-cli/src/commands/issue.rs
index c52df0f68..dc42414b7 100644
--- a/crates/radicle-cli/src/commands/issue.rs
+++ b/crates/radicle-cli/src/commands/issue.rs
@@ -354,23 +354,21 @@ fn mk_issue_row(
]
}
-fn open<Repo, Signer>(
+fn open(
title: Option<Title>,
description: Option<String>,
labels: Vec<Label>,
assignees: Vec<Did>,
verbose: bool,
quiet: bool,
- cache: &mut issue::Cache<'_, Repo, WriteAs<'_, Signer>, cob::cache::StoreWriter>,
+ cache: &mut issue::Cache<
+ '_,
+ impl WriteRepository + cob::Store<Namespace = NodeId>,
+ WriteAs<'_, impl crypto::Signer>,
+ cob::cache::StoreWriter,
+ >,
profile: &Profile,
-) -> anyhow::Result<()>
-where
- Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<()> {
let (title, description) = if let (Some(t), Some(d)) = (title.as_ref(), description.as_ref()) {
(t.to_owned(), d.to_owned())
} else if let Some((t, d)) = term::issue::get_title_description(title, description)? {
@@ -401,10 +399,7 @@ fn edit<'a, 'b, 'g, Repo, Signer>(
) -> anyhow::Result<issue::IssueMut<'a, 'b, 'g, Repo, Signer, cob::cache::StoreWriter>>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let id = id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&id)?;
diff --git a/crates/radicle-cli/src/commands/issue/comment.rs b/crates/radicle-cli/src/commands/issue/comment.rs
index 03111848b..07599af80 100644
--- a/crates/radicle-cli/src/commands/issue/comment.rs
+++ b/crates/radicle-cli/src/commands/issue/comment.rs
@@ -1,6 +1,7 @@
use radicle::Profile;
use radicle::cob::store::access::WriteAs;
use radicle::cob::thread;
+use radicle::crypto;
use radicle::storage::WriteRepository;
use radicle::{cob, git, issue, storage};
@@ -9,26 +10,20 @@ use crate::terminal as term;
use crate::terminal::Element as _;
use crate::terminal::patch::Message;
-pub(super) fn comment<Signer>(
+pub(super) fn comment(
profile: &Profile,
repo: &storage::git::Repository,
issues: &mut issue::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::Store<cob::cache::Write>,
>,
id: Rev,
message: Message,
reply_to: Option<Rev>,
quiet: bool,
-) -> Result<(), anyhow::Error>
-where
- Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>,
-{
+) -> Result<(), anyhow::Error> {
let reply_to = reply_to
.map(|rev| rev.resolve::<git::Oid>(repo.raw()))
.transpose()?;
@@ -47,26 +42,20 @@ where
Ok(())
}
-pub(super) fn edit<Signer>(
+pub(super) fn edit(
profile: &Profile,
repo: &storage::git::Repository,
issues: &mut issue::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::Store<cob::cache::Write>,
>,
id: Rev,
message: Message,
comment_id: Rev,
quiet: bool,
-) -> Result<(), anyhow::Error>
-where
- Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>,
- Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>,
-{
+) -> Result<(), anyhow::Error> {
let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
let comment_id = comment_id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&issue_id)?;
diff --git a/crates/radicle-cli/src/commands/patch/edit.rs b/crates/radicle-cli/src/commands/patch/edit.rs
index 5f495195d..1d281367d 100644
--- a/crates/radicle-cli/src/commands/patch/edit.rs
+++ b/crates/radicle-cli/src/commands/patch/edit.rs
@@ -29,17 +29,18 @@ pub fn run(
}
}
-fn edit_root<Signer>(
- mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>,
+fn edit_root(
+ mut patch: patch::PatchMut<
+ '_,
+ '_,
+ '_,
+ Repository,
+ impl crypto::Signer,
+ cob::cache::StoreWriter,
+ >,
title: Title,
description: String,
-) -> anyhow::Result<()>
-where
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<()> {
let title = if title.as_ref() != patch.title() {
Some(title)
} else {
@@ -73,18 +74,19 @@ where
Ok(())
}
-fn edit_revision<Signer>(
- mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>,
+fn edit_revision(
+ mut patch: patch::PatchMut<
+ '_,
+ '_,
+ '_,
+ Repository,
+ impl crypto::Signer,
+ cob::cache::StoreWriter,
+ >,
revision: patch::RevisionId,
title: Title,
description: String,
-) -> anyhow::Result<()>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+) -> anyhow::Result<()> {
let embeds = patch.embeds().to_owned();
let mut message = title.to_string();
let message = if description.is_empty() {
diff --git a/crates/radicle-cli/src/commands/patch/review/builder.rs b/crates/radicle-cli/src/commands/patch/review/builder.rs
index bd4f0e520..09f7e4150 100644
--- a/crates/radicle-cli/src/commands/patch/review/builder.rs
+++ b/crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -24,7 +24,6 @@ use radicle::cob::{CodeLocation, CodeRange};
use radicle::crypto;
use radicle::git;
use radicle::git::Oid;
-use radicle::node::device::Device;
use radicle::prelude::*;
use radicle::storage::git::{Repository, cob::DraftStore};
use radicle_surf::diff::*;
@@ -614,15 +613,12 @@ impl<'a> ReviewBuilder<'a> {
}
/// Run the review builder for the given revision.
- pub fn run<G>(
+ pub fn run(
self,
revision: &Revision,
opts: &mut git::raw::DiffOptions,
- signer: &Device<G>,
- ) -> anyhow::Result<()>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> anyhow::Result<()> {
let repo = self.repo.raw();
let base = repo.find_commit((*revision.base()).into())?;
let patch_id = self.patch_id;
diff --git a/crates/radicle-cli/src/commands/remote/add.rs b/crates/radicle-cli/src/commands/remote/add.rs
index f4f246098..0a7f27f5e 100644
--- a/crates/radicle-cli/src/commands/remote/add.rs
+++ b/crates/radicle-cli/src/commands/remote/add.rs
@@ -4,7 +4,6 @@ use radicle::Profile;
use radicle::git;
use radicle::git::fmt::RefString;
use radicle::prelude::*;
-use radicle_crypto::PublicKey;
use crate::commands::checkout;
use crate::commands::follow;
diff --git a/crates/radicle-cli/src/commands/sync.rs b/crates/radicle-cli/src/commands/sync.rs
index 4b40bb4fb..976729819 100644
--- a/crates/radicle-cli/src/commands/sync.rs
+++ b/crates/radicle-cli/src/commands/sync.rs
@@ -196,7 +196,7 @@ fn announce_refs(
"nothing to announce, repository {rid} is not available locally"
));
};
- if let Err(e) = repo.remote(&profile.public_key) {
+ if let Err(e) = repo.remote(profile.id()) {
if e.is_not_found() {
term::println(term::format::italic(
"Nothing to announce, you don't have a fork of this repository.",
diff --git a/crates/radicle-cli/src/commands/watch.rs b/crates/radicle-cli/src/commands/watch.rs
index b0c73a206..4fa4c878a 100644
--- a/crates/radicle-cli/src/commands/watch.rs
+++ b/crates/radicle-cli/src/commands/watch.rs
@@ -21,7 +21,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
.refstr
.qualified()
.ok_or_else(|| anyhow!("reference must be fully-qualified, eg. 'refs/heads/master'"))?;
- let nid = args.node.unwrap_or(profile.public_key);
+ let nid = args.node.unwrap_or(*profile.id());
let (_, rid) = rid_or_cwd(args.repo)?;
let repo = storage.repository(rid)?;
let now = time::SystemTime::now();
diff --git a/crates/radicle-cli/src/terminal/cob.rs b/crates/radicle-cli/src/terminal/cob.rs
index c8fa266a1..85887e28e 100644
--- a/crates/radicle-cli/src/terminal/cob.rs
+++ b/crates/radicle-cli/src/terminal/cob.rs
@@ -5,6 +5,7 @@ use radicle::{
cache::{MigrateCallback, MigrateProgress},
store::access::{ReadOnly, WriteAs},
},
+ crypto,
prelude::NodeId,
profile,
storage::ReadRepository,
@@ -72,7 +73,7 @@ where
/// Return a read-write handle for the patches cache.
/// Prefer this over [`radicle::profile::Home::patches_mut`],
/// to obtain an error hint in case migrations must be run.
-pub fn patches_mut<'a, 'b, Repo, Signer>(
+pub fn patches_mut<'a, 'b, Repo, Signer: crypto::Signer>(
profile: &Profile,
repository: &'a Repo,
signer: &'b Signer,
@@ -97,7 +98,7 @@ where
/// Return a read-write handle for the issues cache.
/// Prefer this over [`radicle::profile::Home::issues_mut`],
/// to obtain an error hint in case migrations must be run.
-pub fn issues_mut<'a, 'b, Repo, Signer>(
+pub fn issues_mut<'a, 'b, Repo, Signer: crypto::Signer>(
profile: &Profile,
repository: &'a Repo,
signer: &'b Signer,
diff --git a/crates/radicle-cli/src/terminal/comment.rs b/crates/radicle-cli/src/terminal/comment.rs
index b5d0dd1c7..8791a2795 100644
--- a/crates/radicle-cli/src/terminal/comment.rs
+++ b/crates/radicle-cli/src/terminal/comment.rs
@@ -11,7 +11,7 @@ pub fn header<T>(
profile: &Profile,
) -> term::hstack::HStack<'static> {
let author = comment.author();
- let author = Author::new(&author, profile, false);
+ let author = Author::new(author, profile, false);
let (alias, nid) = author.labels();
term::hstack::HStack::default()
diff --git a/crates/radicle-cli/src/terminal/io.rs b/crates/radicle-cli/src/terminal/io.rs
index cfc26a083..6a8998426 100644
--- a/crates/radicle-cli/src/terminal/io.rs
+++ b/crates/radicle-cli/src/terminal/io.rs
@@ -2,11 +2,10 @@ use anyhow::anyhow;
use radicle::cob::Reaction;
use radicle::cob::issue::Issue;
use radicle::cob::thread::{Comment, CommentId};
+use radicle::crypto::SigningKey;
use radicle::crypto::ssh::Keystore;
-use radicle::crypto::ssh::keystore::MemorySigner;
-use radicle::node::device::{BoxedDevice, Device};
-use radicle::profile::Profile;
use radicle::profile::env::RAD_PASSPHRASE;
+use radicle::profile::{Profile, Signer, SignerError};
pub use radicle_term::io::*;
pub use radicle_term::spinner;
@@ -45,14 +44,34 @@ impl inquire::validator::StringValidator for PassphraseValidator {
/// Get the signer. First we try getting it from ssh-agent; otherwise, we prompt the user,
/// if we're connected to a TTY.
-pub fn signer(profile: &Profile) -> anyhow::Result<BoxedDevice> {
- match profile.signer() {
+pub fn signer(profile: &Profile) -> anyhow::Result<Signer> {
+ let err = match profile.signer() {
Ok(signer) => return Ok(signer),
- Err(err) if !err.prompt_for_passphrase() => return Err(anyhow!(err)),
- Err(_) => {
- // The error returned is potentially recoverable by prompting
- // the user for the correct passphrase.
+ Err(err) => err,
+ };
+
+ match err {
+ SignerError::LoadError(radicle::crypto::LoadError::InvalidPassphrase) => {
+ super::warning(format!(
+ "The passphrase for your Radicle key provided in the environment variable `{RAD_PASSPHRASE}` is invalid. Please try again."
+ ));
+ }
+ SignerError::AgentConnection(err) => {
+ super::warning(format!(
+ "Failed to connect to ssh-agent: {err}. Falling back to passphrase prompt."
+ ));
+ }
+ SignerError::Agent(radicle::crypto::ssh::agent::IntoSignerError::IdentityNotFound {
+ identity,
+ }) => {
+ super::warning(format!(
+ "The Radicle key for `{identity}` is not registered with ssh-agent. Please run `rad auth` to register it."
+ ));
}
+ err @ SignerError::LoadError(_)
+ | err @ SignerError::InvalidPublicKey(_)
+ | err @ SignerError::Agent(_)
+ | err @ SignerError::Keystore(_) => return Err(anyhow!(err)),
}
let validator = PassphraseValidator::new(profile.keystore.clone());
@@ -65,11 +84,11 @@ pub fn signer(profile: &Profile) -> anyhow::Result<BoxedDevice> {
}
};
let spinner = spinner("Unsealing key…");
- let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
+ let signer = SigningKey::load(&profile.keystore, Some(passphrase))?;
spinner.finish();
- Ok(Device::from(signer).boxed())
+ Ok(Signer::Key(signer))
}
pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
diff --git a/crates/radicle-cli/tests/commands/clone.rs b/crates/radicle-cli/tests/commands/clone.rs
index 8ccee2b80..521a6a779 100644
--- a/crates/radicle-cli/tests/commands/clone.rs
+++ b/crates/radicle-cli/tests/commands/clone.rs
@@ -119,7 +119,7 @@ fn rad_clone_partial_fail() {
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let mut eve = environment.node("eve");
- let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT").unwrap();
+ let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT").unwrap();
// Set up a test project.
let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
diff --git a/crates/radicle-cli/tests/util/environment.rs b/crates/radicle-cli/tests/util/environment.rs
index f42895b09..3494bf332 100644
--- a/crates/radicle-cli/tests/util/environment.rs
+++ b/crates/radicle-cli/tests/util/environment.rs
@@ -2,8 +2,8 @@ use std::path::PathBuf;
use std::str::FromStr;
use radicle::cob::cache::COBS_DB_FILE;
-use radicle::crypto::ssh::{Keystore, keystore::MemorySigner};
-use radicle::crypto::{KeyPair, Seed};
+use radicle::crypto::ssh::Keystore;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::git;
use radicle::node;
use radicle::node::policy::store as policy;
@@ -136,7 +136,9 @@ impl Environment {
pub fn profile_with(&mut self, config: profile::Config) -> Profile {
let alias = config.alias().clone();
let home = self.rad_home(&alias);
- let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
+ let seed = radicle::crypto::Seed::new([!(self.users as u8); 32]);
+ let keypair = SigningKey::from_seed(seed);
+ let public_key = keypair.public_key();
let policies_db = home.node().join(POLICIES_DB_FILE);
let cobs_db = home.cobs().join(COBS_DB_FILE);
@@ -146,7 +148,7 @@ impl Environment {
home.storage(),
radicle::git::UserInfo {
alias: alias.clone(),
- key: keypair.pk.into(),
+ key: *public_key,
},
)
.unwrap();
@@ -158,7 +160,7 @@ impl Environment {
home.database_mut(node::db::config::Config::default())
.unwrap()
.init(
- &keypair.pk.into(),
+ public_key,
config.node.features(),
&alias,
&config.node.user_agent(),
@@ -171,7 +173,7 @@ impl Environment {
transport::local::register(storage.clone());
let keystore = Keystore::new(&home.keys());
- keystore.store(keypair.clone(), "radicle", None).unwrap();
+ keystore.store(&keypair, "radicle", None).unwrap();
// Ensures that each user has a unique but deterministic public key.
self.users += 1;
@@ -180,7 +182,7 @@ impl Environment {
home,
storage,
keystore,
- public_key: keypair.pk.into(),
+ public_key: *public_key,
config,
}
}
@@ -197,7 +199,7 @@ impl Environment {
/// as for each of them a convenience function
/// (resp. [`Environment::node`], [`Environment::relay`], [`Environment::seed`]).
/// is provided to reduce boilerplate.
- pub fn node_with(&mut self, node: Config) -> Node<MemorySigner> {
+ pub fn node_with(&mut self, node: Config) -> Node {
let alias = node.alias.clone();
let profile = self.profile_with(profile::Config {
node,
@@ -206,21 +208,21 @@ impl Environment {
Node::new(profile)
}
- /// Convenience method for creating a [`Node<MemorySigner>`]
+ /// Convenience method for creating a [`Node`]
/// using configuration [`config::node`] within this [`Environment`].
- pub fn node(&mut self, alias: &'static str) -> Node<MemorySigner> {
+ pub fn node(&mut self, alias: &'static str) -> Node {
self.node_with(config::node(alias))
}
- /// Convenience method for creating a [`Node<MemorySigner>`]
+ /// Convenience method for creating a [`Node`]
/// using configuration [`config::relay`] within this [`Environment`].
- pub fn relay(&mut self, alias: &'static str) -> Node<MemorySigner> {
+ pub fn relay(&mut self, alias: &'static str) -> Node {
self.node_with(config::relay(alias))
}
- /// Convenience method for creating a [`Node<MemorySigner>`]
+ /// Convenience method for creating a [`Node`]
/// using configuration [`config::seed`] within this [`Environment`].
- pub fn seed(&mut self, alias: &'static str) -> Node<MemorySigner> {
+ pub fn seed(&mut self, alias: &'static str) -> Node {
self.node_with(config::seed(alias))
}
@@ -293,7 +295,7 @@ impl HasAlias for Alias {
}
}
-impl HasAlias for Node<MemorySigner> {
+impl HasAlias for Node {
fn alias(&self) -> &Alias {
&self.config.alias
}
@@ -305,7 +307,7 @@ impl HasAlias for Profile {
}
}
-impl<G> HasAlias for NodeHandle<G> {
+impl HasAlias for NodeHandle {
fn alias(&self) -> &Alias {
&self.alias
}
diff --git a/crates/radicle-cob/Cargo.toml b/crates/radicle-cob/Cargo.toml
index 89fe314aa..2eff83f36 100644
--- a/crates/radicle-cob/Cargo.toml
+++ b/crates/radicle-cob/Cargo.toml
@@ -31,7 +31,6 @@ radicle-git-metadata = { workspace = true }
radicle-oid = { workspace = true, features = ["git2", "serde", "sha1", "std"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
-signature = { workspace = true }
thiserror = { workspace = true, default-features = true }
[dev-dependencies]
diff --git a/crates/radicle-cob/src/backend/git/change.rs b/crates/radicle-cob/src/backend/git/change.rs
index 836e07573..5694456f8 100644
--- a/crates/radicle-cob/src/backend/git/change.rs
+++ b/crates/radicle-cob/src/backend/git/change.rs
@@ -94,18 +94,17 @@ impl change::Storage for git2::Repository {
type ObjectId = Oid;
type Parent = Oid;
- type Signatures = ExtendedSignature;
- fn store<Signer>(
+ type PublicKey = crypto::PublicKey;
+ type Signature = crypto::Signature;
+
+ fn store(
&self,
resource: Option<Self::Parent>,
mut related: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: store::Template<Self::ObjectId>,
- ) -> Result<Entry, Self::StoreError>
- where
- Signer: signature::Signer<Self::Signatures>,
- {
+ ) -> Result<Entry, Self::StoreError> {
let change::Template {
type_name,
tips,
@@ -116,7 +115,8 @@ impl change::Storage for git2::Repository {
let manifest = store::Manifest::new(type_name, Version::default());
let revision = write_manifest(self, &manifest, embeds, &contents)?;
let tree = self.find_tree(revision)?;
- let signature = signer.sign(revision.as_bytes());
+ let signature = ExtendedSignature::try_sign(signer, revision.as_bytes())
+ .map_err(|source| error::Create::Signer(Box::new(source)))?;
// Make sure there are no duplicates in the related list.
related.sort();
diff --git a/crates/radicle-cob/src/change.rs b/crates/radicle-cob/src/change.rs
index 70b23bdb8..c03848793 100644
--- a/crates/radicle-cob/src/change.rs
+++ b/crates/radicle-cob/src/change.rs
@@ -8,4 +8,4 @@ pub use store::{Contents, EntryId, Storage, Template, Timestamp};
use crate::signatures::ExtendedSignature;
/// A single change in the change graph.
-pub type Entry = store::Entry<Oid, Oid, ExtendedSignature>;
+pub type Entry = store::Entry<Oid, Oid, ExtendedSignature<crypto::PublicKey>>;
diff --git a/crates/radicle-cob/src/change/store.rs b/crates/radicle-cob/src/change/store.rs
index b4e9485cd..7a671ffc9 100644
--- a/crates/radicle-cob/src/change/store.rs
+++ b/crates/radicle-cob/src/change/store.rs
@@ -2,6 +2,8 @@
use std::{error::Error, fmt, num::NonZeroUsize};
+use crypto::signature::Verifier as _;
+use crypto::{ExtendedSignature, VerifyingKey};
use nonempty::NonEmpty;
use oid::Oid;
use serde::{Deserialize, Serialize};
@@ -16,26 +18,32 @@ pub trait Storage {
type ObjectId;
type Parent;
- type Signatures;
+
+ type PublicKey;
+ type Signature;
/// Store a new change entry.
#[allow(clippy::type_complexity)]
- fn store<G>(
+ fn store(
&self,
resource: Option<Self::Parent>,
related: Vec<Self::Parent>,
- signer: &G,
+ signer: &impl crypto::Signer,
template: Template<Self::ObjectId>,
- ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::StoreError>
- where
- G: signature::Signer<Self::Signatures>;
+ ) -> Result<
+ Entry<Self::Parent, Self::ObjectId, ExtendedSignature<Self::PublicKey, Self::Signature>>,
+ Self::StoreError,
+ >;
/// Load a change entry.
#[allow(clippy::type_complexity)]
fn load(
&self,
id: Self::ObjectId,
- ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::LoadError>;
+ ) -> Result<
+ Entry<Self::Parent, Self::ObjectId, ExtendedSignature<Self::PublicKey, Self::Signature>>,
+ Self::LoadError,
+ >;
/// Returns the parents of the object with the specified ID.
fn parents_of(&self, id: &Oid) -> Result<Vec<Oid>, Self::LoadError>;
@@ -120,22 +128,28 @@ where
Id: AsRef<[u8]>,
{
pub fn valid_signatures(&self) -> bool {
- self.signature
- .iter()
- .all(|(key, sig)| key.verify(self.revision.as_ref(), sig).is_ok())
+ use crypto::signature::Verifier as _;
+
+ self.signature.iter().all(|(key, sig)| {
+ VerifyingKey::try_from(key)
+ .and_then(|verifier| verifier.verify(self.revision.as_ref(), sig))
+ .is_ok()
+ })
}
}
-impl<R, Id> Entry<R, Id, signatures::ExtendedSignature>
+impl<R, Id> Entry<R, Id, signatures::ExtendedSignature<crypto::PublicKey>>
where
Id: AsRef<[u8]>,
{
pub fn valid_signatures(&self) -> bool {
- self.signature.verify(self.revision.as_ref())
+ VerifyingKey::try_from(self.signature.key())
+ .and_then(|verifier| verifier.verify(self.revision.as_ref(), self.signature.sig()))
+ .is_ok()
}
pub fn author(&self) -> &crypto::PublicKey {
- &self.signature.key
+ self.signature.key()
}
}
diff --git a/crates/radicle-cob/src/change_graph.rs b/crates/radicle-cob/src/change_graph.rs
index b7923e434..910942bf4 100644
--- a/crates/radicle-cob/src/change_graph.rs
+++ b/crates/radicle-cob/src/change_graph.rs
@@ -8,7 +8,7 @@ use oid::Oid;
use crate::{
CollaborativeObject, Entry, EntryId, History, ObjectId, TypeName, change, object,
- object::collaboration::Evaluate, signatures::ExtendedSignature,
+ object::collaboration::Evaluate,
};
#[derive(Debug, thiserror::Error)]
@@ -37,7 +37,12 @@ impl ChangeGraph {
oid: &ObjectId,
) -> Option<ChangeGraph>
where
- S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
+ S: change::Storage<
+ ObjectId = Oid,
+ Parent = Oid,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
+ >,
{
let tip_refs = tip_refs.collect::<Vec<_>>();
diff --git a/crates/radicle-cob/src/lib.rs b/crates/radicle-cob/src/lib.rs
index 90d8ff3bd..1b34e7f7e 100644
--- a/crates/radicle-cob/src/lib.rs
+++ b/crates/radicle-cob/src/lib.rs
@@ -84,7 +84,6 @@ pub mod history;
pub use history::History;
pub mod signatures;
-use signatures::ExtendedSignature;
pub mod type_name;
pub use type_name::TypeName;
@@ -116,6 +115,11 @@ mod tests;
pub trait Store
where
Self: object::Storage
- + change::Storage<ObjectId = oid::Oid, Parent = oid::Oid, Signatures = ExtendedSignature>,
+ + change::Storage<
+ ObjectId = oid::Oid,
+ Parent = oid::Oid,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
+ >,
{
}
diff --git a/crates/radicle-cob/src/object/collaboration/create.rs b/crates/radicle-cob/src/object/collaboration/create.rs
index 0ab18851d..31c1fb011 100644
--- a/crates/radicle-cob/src/object/collaboration/create.rs
+++ b/crates/radicle-cob/src/object/collaboration/create.rs
@@ -51,23 +51,23 @@ impl Create {
///
/// The `args` are the metadata for this [`CollaborativeObject`]. See
/// [`Create`] for further information.
-pub fn create<T, S, G>(
- storage: &S,
- signer: &G,
+pub fn create<T, Storage>(
+ storage: &Storage,
+ signer: &impl crypto::Signer,
resource: Option<Oid>,
related: Vec<Oid>,
- identifier: &<S as crate::object::Storage>::Namespace,
+ identifier: &<Storage as crate::object::Storage>::Namespace,
args: Create,
) -> Result<CollaborativeObject<T>, error::Create>
where
- T: Evaluate<S>,
- S: crate::object::Storage
- + crate::change::Storage<
+ T: Evaluate<Storage>,
+ Storage: crate::object::Storage,
+ Storage: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = crate::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
- G: signature::Signer<crate::ExtendedSignature>,
{
let type_name = args.type_name.clone();
let version = args.version;
diff --git a/crates/radicle-cob/src/object/collaboration/get.rs b/crates/radicle-cob/src/object/collaboration/get.rs
index 66bce507e..235a950d5 100644
--- a/crates/radicle-cob/src/object/collaboration/get.rs
+++ b/crates/radicle-cob/src/object/collaboration/get.rs
@@ -1,7 +1,5 @@
// Copyright © 2022 The Radicle Link Contributors
-use crypto::ssh::ExtendedSignature;
-
use crate::{CollaborativeObject, Evaluate, ObjectId, TypeName, change_graph::ChangeGraph};
use super::error;
@@ -26,7 +24,8 @@ where
S: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
let tip_refs = storage
diff --git a/crates/radicle-cob/src/object/collaboration/info.rs b/crates/radicle-cob/src/object/collaboration/info.rs
index 89aef0272..d96596a82 100644
--- a/crates/radicle-cob/src/object/collaboration/info.rs
+++ b/crates/radicle-cob/src/object/collaboration/info.rs
@@ -6,7 +6,6 @@
use std::collections::BTreeSet;
-use crypto::ssh::ExtendedSignature;
use oid::Oid;
use crate::{ObjectId, TypeName, change_graph::ChangeGraph};
@@ -43,7 +42,8 @@ where
S: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
let tip_refs = storage
diff --git a/crates/radicle-cob/src/object/collaboration/list.rs b/crates/radicle-cob/src/object/collaboration/list.rs
index 04c8b5865..22910020c 100644
--- a/crates/radicle-cob/src/object/collaboration/list.rs
+++ b/crates/radicle-cob/src/object/collaboration/list.rs
@@ -21,7 +21,8 @@ where
S: crate::change::Storage<
ObjectId = crate::object::Oid,
Parent = crate::object::Oid,
- Signatures = crate::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
let references = storage
diff --git a/crates/radicle-cob/src/object/collaboration/update.rs b/crates/radicle-cob/src/object/collaboration/update.rs
index 44bf7fe82..7b7a2ded4 100644
--- a/crates/radicle-cob/src/object/collaboration/update.rs
+++ b/crates/radicle-cob/src/object/collaboration/update.rs
@@ -5,8 +5,8 @@ use nonempty::NonEmpty;
use oid::Oid;
use crate::{
- CollaborativeObject, Embed, Evaluate, ExtendedSignature, ObjectId, TypeName, change,
- change_graph::ChangeGraph, history::EntryId,
+ CollaborativeObject, Embed, Evaluate, ObjectId, TypeName, change, change_graph::ChangeGraph,
+ history::EntryId,
};
use super::error;
@@ -55,19 +55,23 @@ pub struct Update {
///
/// The `args` are the metadata for this [`CollaborativeObject`]
/// update. See [`Update`] for further information.
-pub fn update<T, S, G>(
- storage: &S,
- signer: &G,
+pub fn update<T, Storage>(
+ storage: &Storage,
+ signer: &impl crypto::Signer,
resource: Option<Oid>,
related: Vec<Oid>,
- identifier: &S::Namespace,
+ identifier: &Storage::Namespace,
args: Update,
) -> Result<Updated<T>, error::Update>
where
- T: Evaluate<S>,
- S: crate::object::Storage,
- S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
- G: signature::Signer<ExtendedSignature>,
+ T: Evaluate<Storage>,
+ Storage: crate::object::Storage,
+ Storage: change::Storage<
+ ObjectId = Oid,
+ Parent = Oid,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
+ >,
{
let Update {
type_name: ref typename,
diff --git a/crates/radicle-cob/src/signatures.rs b/crates/radicle-cob/src/signatures.rs
index febdd8d90..137a6f59d 100644
--- a/crates/radicle-cob/src/signatures.rs
+++ b/crates/radicle-cob/src/signatures.rs
@@ -7,22 +7,23 @@ use std::{
ops::{Deref, DerefMut},
};
-use crypto::{PublicKey, ssh};
+use crypto::PublicKey;
use metadata::commit::{
CommitData,
headers::Signature::{Pgp, Ssh},
};
-pub use ssh::ExtendedSignature;
+pub use crypto::ExtendedSignature;
+use crypto::Signature;
pub mod error;
// FIXME(kim): This should really be a HashMap with a no-op Hasher -- PublicKey
// collisions are catastrophic
-#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct Signatures(BTreeMap<PublicKey, crypto::Signature>);
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Signatures(BTreeMap<PublicKey, Signature>);
impl Deref for Signatures {
- type Target = BTreeMap<PublicKey, crypto::Signature>;
+ type Target = BTreeMap<PublicKey, Signature>;
fn deref(&self) -> &Self::Target {
&self.0
@@ -36,20 +37,18 @@ impl DerefMut for Signatures {
}
impl From<ExtendedSignature> for Signatures {
- fn from(ExtendedSignature { key, sig }: ExtendedSignature) -> Self {
- let mut map = BTreeMap::new();
- map.insert(key, sig);
- map.into()
+ fn from(signature: ExtendedSignature) -> Self {
+ Self([signature.into_pair()].into())
}
}
-impl From<BTreeMap<PublicKey, crypto::Signature>> for Signatures {
- fn from(map: BTreeMap<PublicKey, crypto::Signature>) -> Self {
+impl From<BTreeMap<PublicKey, Signature>> for Signatures {
+ fn from(map: BTreeMap<PublicKey, Signature>) -> Self {
Self(map)
}
}
-impl From<Signatures> for BTreeMap<PublicKey, crypto::Signature> {
+impl From<Signatures> for BTreeMap<PublicKey, Signature> {
fn from(s: Signatures) -> Self {
s.0
}
@@ -67,27 +66,27 @@ impl<Tree, Parent> TryFrom<&CommitData<Tree, Parent>> for Signatures {
Pgp(_) => None,
Ssh(pem) => Some(
ExtendedSignature::from_pem(pem.as_bytes())
- .map_err(error::Signatures::from),
+ .map_err(error::Signatures::from)
+ .map(ExtendedSignature::into_pair),
),
}
})
- .map(|r| r.map(|es| (es.key, es.sig)))
.collect::<Result<_, _>>()
}
}
-impl FromIterator<(PublicKey, crypto::Signature)> for Signatures {
+impl FromIterator<(PublicKey, Signature)> for Signatures {
fn from_iter<T>(iter: T) -> Self
where
- T: IntoIterator<Item = (PublicKey, crypto::Signature)>,
+ T: IntoIterator<Item = (PublicKey, Signature)>,
{
Self(BTreeMap::from_iter(iter))
}
}
impl IntoIterator for Signatures {
- type Item = (PublicKey, crypto::Signature);
- type IntoIter = <BTreeMap<PublicKey, crypto::Signature> as IntoIterator>::IntoIter;
+ type Item = (PublicKey, Signature);
+ type IntoIter = <BTreeMap<PublicKey, Signature> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
@@ -99,19 +98,15 @@ impl Extend<ExtendedSignature> for Signatures {
where
T: IntoIterator<Item = ExtendedSignature>,
{
- for ExtendedSignature { key, sig } in iter {
- self.insert(key, sig);
- }
+ self.extend(iter.into_iter().map(ExtendedSignature::into_pair))
}
}
-impl Extend<(PublicKey, crypto::Signature)> for Signatures {
+impl Extend<(PublicKey, Signature)> for Signatures {
fn extend<T>(&mut self, iter: T)
where
- T: IntoIterator<Item = (PublicKey, crypto::Signature)>,
+ T: IntoIterator<Item = (PublicKey, Signature)>,
{
- for (key, sig) in iter {
- self.insert(key, sig);
- }
+ self.0.extend(iter)
}
}
diff --git a/crates/radicle-cob/src/signatures/error.rs b/crates/radicle-cob/src/signatures/error.rs
index e10e8e66c..a1f9793f5 100644
--- a/crates/radicle-cob/src/signatures/error.rs
+++ b/crates/radicle-cob/src/signatures/error.rs
@@ -1,6 +1,6 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
-use crypto::ssh::ExtendedSignatureError;
+use crypto::ssh::ExtendedSignaturePemError;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -17,7 +17,7 @@ pub enum Signature {
#[non_exhaustive]
pub enum Signatures {
#[error(transparent)]
- ExtendedSignature(#[from] ExtendedSignatureError),
+ ExtendedSignature(#[from] ExtendedSignaturePemError),
#[error(transparent)]
Signature(#[from] Signature),
diff --git a/crates/radicle-cob/src/test/storage.rs b/crates/radicle-cob/src/test/storage.rs
index 2df1c24c6..ae5e35ec8 100644
--- a/crates/radicle-cob/src/test/storage.rs
+++ b/crates/radicle-cob/src/test/storage.rs
@@ -8,7 +8,6 @@ use tempfile::TempDir;
use crate::{
ObjectId, Store, change,
object::{self, Reference},
- signatures,
};
pub mod error {
@@ -69,29 +68,38 @@ impl change::Storage for Storage {
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
type Parent = <git2::Repository as change::Storage>::Parent;
- type Signatures = <git2::Repository as change::Storage>::Signatures;
- fn store<Signer>(
+ type PublicKey = <git2::Repository as change::Storage>::PublicKey;
+ type Signature = <git2::Repository as change::Storage>::Signature;
+
+ fn store(
&self,
authority: Option<Self::Parent>,
parents: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: change::Template<Self::ObjectId>,
) -> Result<
- change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>,
+ change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ crypto::ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
Self::StoreError,
- >
- where
- Signer: signature::Signer<signatures::ExtendedSignature>,
- {
+ > {
self.as_raw().store(authority, parents, signer, spec)
}
fn load(
&self,
id: Self::ObjectId,
- ) -> Result<change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::LoadError>
- {
+ ) -> Result<
+ change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ crypto::ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
+ Self::LoadError,
+ > {
self.as_raw().load(id)
}
diff --git a/crates/radicle-cob/src/tests.rs b/crates/radicle-cob/src/tests.rs
index 48a0d2e54..79a747029 100644
--- a/crates/radicle-cob/src/tests.rs
+++ b/crates/radicle-cob/src/tests.rs
@@ -8,8 +8,10 @@ use crate::{ObjectId, TypeName, object, test::arbitrary::Invalid};
mod git {
use std::ops::ControlFlow;
- use crypto::test::signer::MockSigner;
- use crypto::{PublicKey, Signer};
+ use crypto::PublicKey;
+ use crypto::Signer as _;
+ use crypto::SigningKey;
+
use nonempty::{NonEmpty, nonempty};
use qcheck::Arbitrary;
@@ -22,15 +24,16 @@ mod git {
#[test]
fn roundtrip() {
let storage = test::Storage::new();
- let signer = r#gen::<MockSigner>(1);
- let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
- let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+ let signer = r#gen::<SigningKey>(1);
+ let terry = test::Person::new(&storage, "terry", signer.public_key().to_owned()).unwrap();
+ let proj =
+ test::Project::new(&storage, "discworld", signer.public_key().to_owned()).unwrap();
let proj = test::RemoteProject {
project: proj,
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let cob = create::<NonEmpty<Entry>, _, _>(
+ let cob = create::<NonEmpty<Entry>, _>(
&storage,
&signer,
Some(proj.project.content_id),
@@ -56,7 +59,7 @@ mod git {
#[test]
fn list_cobs() {
let storage = test::Storage::new();
- let signer = r#gen::<MockSigner>(1);
+ let signer = r#gen::<SigningKey>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
@@ -64,7 +67,7 @@ mod git {
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let issue_1 = create::<NonEmpty<Entry>, _, _>(
+ let issue_1 = create::<NonEmpty<Entry>, _>(
&storage,
&signer,
Some(proj.project.content_id),
@@ -108,7 +111,7 @@ mod git {
#[test]
fn update_cob() {
let storage = test::Storage::new();
- let signer = r#gen::<MockSigner>(1);
+ let signer = r#gen::<SigningKey>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
@@ -116,7 +119,7 @@ mod git {
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let cob = create::<NonEmpty<Entry>, _, _>(
+ let cob = create::<NonEmpty<Entry>, _>(
&storage,
&signer,
Some(proj.project.content_id),
@@ -163,9 +166,9 @@ mod git {
#[test]
fn traverse_cobs() {
let storage = test::Storage::new();
- let neil_signer = r#gen::<MockSigner>(2);
+ let neil_signer = r#gen::<SigningKey>(2);
let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
- let terry_signer = r#gen::<MockSigner>(1);
+ let terry_signer = r#gen::<SigningKey>(1);
let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
let terry_proj = test::RemoteProject {
@@ -177,7 +180,7 @@ mod git {
person: neil,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
- let cob = create::<NonEmpty<Entry>, _, _>(
+ let cob = create::<NonEmpty<Entry>, _>(
&storage,
&terry_signer,
Some(terry_proj.project.content_id),
@@ -201,7 +204,7 @@ mod git {
)
.unwrap();
- let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
+ let Updated { object, .. } = update::<NonEmpty<Entry>, _>(
&storage,
&neil_signer,
Some(neil_proj.project.content_id),
diff --git a/crates/radicle-core/Cargo.toml b/crates/radicle-core/Cargo.toml
index e47bdfabf..75bbe04bf 100644
--- a/crates/radicle-core/Cargo.toml
+++ b/crates/radicle-core/Cargo.toml
@@ -14,13 +14,13 @@ rust-version.workspace = true
default = ["std"]
git2 = ["dep:git2", "radicle-oid/git2"]
gix = ["dep:gix-hash", "radicle-oid/gix"]
-std = ["radicle-oid/std", "thiserror/std", "schemars/std", "serde/std"]
+std = [ "multibase/std", "radicle-oid/std", "thiserror/std", "schemars/std", "serde/std"]
qcheck = ["dep:qcheck", "radicle-oid/qcheck"]
[dependencies]
git2 = { workspace = true, optional = true }
gix-hash = { workspace = true, optional = true }
-multibase = { workspace = true }
+multibase = { workspace = true, default-features = false }
proptest = { workspace = true, optional = true }
qcheck = { workspace = true, optional = true }
radicle-crypto = { workspace = true }
diff --git a/crates/radicle-core/src/repo.rs b/crates/radicle-core/src/repo.rs
index 7e9bb1c77..92a700a1a 100644
--- a/crates/radicle-core/src/repo.rs
+++ b/crates/radicle-core/src/repo.rs
@@ -12,8 +12,8 @@ pub const RAD_PREFIX: &str = "rad:";
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum IdError {
- #[error(transparent)]
- Multibase(#[from] multibase::Error),
+ #[error("invalid multibase string: {0}")]
+ Multibase(#[cfg_attr(feature = "std", source)] multibase::Error),
#[error("invalid length: expected {} bytes, got {actual} bytes", Oid::LEN_SHA1)]
Length { actual: usize },
#[error(fmt = fmt_mismatched_base_encoding)]
@@ -106,7 +106,7 @@ impl RepoId {
///
/// [multibase]: https://github.com/multiformats/multibase?tab=readme-ov-file#multibase-table
pub fn from_canonical(input: &str) -> Result<Self, IdError> {
- let (base, bytes) = multibase::decode(input)?;
+ let (base, bytes) = multibase::decode(input).map_err(IdError::Multibase)?;
Self::guard_base_encoding(input, base)?;
let bytes: [u8; Oid::LEN_SHA1] =
bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
diff --git a/crates/radicle-crypto/Cargo.toml b/crates/radicle-crypto/Cargo.toml
index d9b2b0b9a..bd21b141a 100644
--- a/crates/radicle-crypto/Cargo.toml
+++ b/crates/radicle-crypto/Cargo.toml
@@ -5,39 +5,44 @@ homepage.workspace = true
repository.workspace = true
license.workspace = true
version = "0.17.0"
-authors = [
- "cloudhead <cloudhead@radicle.xyz>",
- "Fintan Halpenny <fintan.halpenny@gmail.com>",
-]
edition.workspace = true
rust-version.workspace = true
[features]
-test = ["fastrand", "qcheck"]
-ssh = ["ssh-agent-lib", "ssh-key"]
+default = ["multibase", "std", "x25519"]
+alloc = ["ed25519-dalek/alloc", "curve25519-dalek/alloc", "x25519-dalek?/alloc", "zeroize/alloc"]
+cyphernet = ["dep:cyphernet", "alloc", "diffie-hellman", "multibase"]
+diffie-hellman = []
+multibase = ["dep:multibase", "cyphernet?/multibase"]
+schemars = ["dep:schemars", "serde"]
+serde = ["dep:serde", "ed25519-dalek/serde", "x25519-dalek/serde", "zeroize/serde"]
+ssh = ["ssh-agent-lib", "ssh-key", "alloc"]
+std = ["alloc", "ed25519-dalek/std", "multibase?/std", "schemars?/std", "serde?/std", "ssh-key?/std", "thiserror/std", "zeroize/std" ]
+test = ["alloc", "qcheck"]
+x25519 = ["dep:x25519-dalek"]
[dependencies]
-amplify = { workspace = true }
cyphernet = { workspace = true, features = ["ed25519"], optional = true }
-ec25519 = "0.1.0"
-fastrand = { workspace = true, optional = true }
-multibase = { workspace = true }
-qcheck = { workspace = true, optional = true }
git-ref-format-core = { workspace = true, optional = true }
-schemars = { workspace = true, optional = true, features = ["derive", "std"] }
-serde = { workspace = true, features = ["derive", "std"] }
-signature = { workspace = true, features = ["std"] }
+multibase = { workspace = true, default-features = false, optional = true }
+qcheck = { workspace = true, optional = true }
+schemars = { workspace = true, default-features = false, features = ["derive"], optional = true }
+serde = { workspace = true, default-features = false, features = ["derive"], optional = true }
sqlite = { workspace = true, features = ["bundled"], optional = true }
-ssh-agent-lib = { version = "0.6", optional = true, default-features = false, features = ["log"] }
-ssh-key = { version = "0.6.3", default-features = false, features = ["std", "encryption", "getrandom"], optional = true }
-thiserror = { workspace = true, default-features = true }
-zeroize = { workspace = true }
+ssh-agent-lib = { version = "0.6", default-features = false, features = ["log"], optional = true }
+ssh-key = { version = "0.6.3", default-features = false, features = ["ed25519", "encryption"], optional = true }
+thiserror = { workspace = true, default-features = false }
+zeroize = { workspace = true, default-features = false }
+
+# dalek
+curve25519-dalek = { version = "4", default-features = false, features = ["zeroize"] }
+ed25519-dalek = { version = "2", default-features = false, features = ["zeroize"] }
+x25519-dalek = { version = "2", default-features = false, features = ["zeroize"], optional = true }
[target.'cfg(windows)'.dependencies]
winpipe = { workspace = true }
[dev-dependencies]
-fastrand = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
tempfile = { workspace = true }
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 21769df0a..07e62489c 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -1,543 +1,819 @@
-use std::cmp::Ordering;
-use std::sync::Arc;
-use std::{fmt, ops::Deref, str::FromStr};
+#![no_std]
-use ec25519 as ed25519;
-use serde::{Deserialize, Serialize};
-use thiserror::Error;
+#[cfg(any(test, feature = "alloc"))]
+extern crate alloc;
-pub use ed25519::{Error, KeyPair, Seed, edwards25519};
+#[cfg(any(test, feature = "alloc"))]
+#[allow(unused_imports)]
+use alloc::{
+ string::{String, ToString as _},
+ vec::Vec,
+};
-pub extern crate signature;
+#[cfg(feature = "std")]
+extern crate std;
-#[cfg(feature = "ssh")]
+/// Re-exports of dalek cryptography crates (see <https://dalek.rs/>)
+/// that this crate depends on. Since they are all related to Curve25519
+/// in some way, the "25519" suffix is omitted from the name of the re-export.
+mod dalek {
+ pub extern crate curve25519_dalek as curve;
+ pub extern crate ed25519_dalek as ed;
+ #[cfg(feature = "x25519")]
+ pub extern crate x25519_dalek as x;
+}
+
+/// Re-export of the `signature` crate as re-exported
+/// by the `ed25519_dalek` crate.
+pub use dalek::ed::ed25519::signature;
+
+#[cfg(all(feature = "ssh", feature = "alloc"))]
pub mod ssh;
-#[cfg(any(test, feature = "test"))]
-pub mod test;
+
+mod seed;
+pub use seed::Seed;
/// Output of a Diffie-Hellman key exchange.
pub type SharedSecret = [u8; 32];
-/// Error returned if signing fails, eg. due to an HSM or KMS.
-#[derive(Debug, Clone, Error)]
-#[error(transparent)]
-#[non_exhaustive]
-pub struct SignerError {
- #[from]
- source: Arc<dyn std::error::Error + Send + Sync>,
+pub type Signature = dalek::ed::Signature;
+
+pub trait Signer
+where
+ Self: signature::Signer<Signature>,
+ Self: signature::Keypair<VerifyingKey = VerifyingKey>,
+ Self: AsRef<PublicKey>,
+{
+ fn public_key(&self) -> &PublicKey {
+ self.as_ref()
+ }
}
-impl SignerError {
- pub fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self {
- Self {
- source: Arc::new(source),
- }
+impl<T: ?Sized> Signer for T
+where
+ Self: signature::Signer<Signature>,
+ Self: signature::Keypair<VerifyingKey = VerifyingKey>,
+ Self: AsRef<PublicKey>,
+{
+}
+
+/// This module contains compile-time checks to ensure the following:
+/// 1. [`Signer`] is compatible with `dyn` usage.
+/// 2. [`SigningKey`] and other well-known implementations of signers
+/// implement the trait.
+///
+/// As long as this module compiles, we have reasonable confidence that we
+/// can generalize to `dyn` in the future without breaking existing code.
+///
+/// Note that this module is "dead code" in the sense that it serves no
+/// purpose at runtime, but it is useful at compile-time!
+#[allow(dead_code)]
+mod future {
+ use super::*;
+
+ /// Witnesses that [`Signer`] is `dyn`-compatible.
+ fn r#dyn(_: &dyn Signer) {}
+
+ /// Witnesses that the generic argument implements [`Signer`].
+ const fn r#impl<Witness: Signer>() {}
+
+ /// Witnesses that [`SigningKey`] implements [`Signer`].
+ const IMPL_SECRET_KEY: () = r#impl::<SigningKey>();
+
+ /// Witnesses that [`ssh::agent::AgentSigner`] implements [`Signer`].
+ #[cfg(all(feature = "ssh", feature = "std"))]
+ const IMPL_AGENT_SIGNER: () = r#impl::<ssh::agent::AgentSigner>();
+}
+
+/// Multicodec key type for Ed25519 keys.
+#[cfg(feature = "multibase")]
+pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x01];
+
+pub type PublicKeyBytes = [u8; dalek::ed::PUBLIC_KEY_LENGTH];
+
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord)]
+#[cfg_attr(
+ all(feature = "serde", feature = "alloc", feature = "multibase"),
+ derive(serde::Serialize, serde::Deserialize),
+ serde(into = "String", try_from = "String")
+)]
+#[cfg_attr(
+ all(feature = "schemars", feature = "serde", feature = "alloc", feature = "multibase"),
+ derive(schemars::JsonSchema),
+ schemars(
+ title = "Ed25519",
+ description = "An Ed25519 public key in multibase encoding.",
+ extend("examples" = [
+ "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7",
+ "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C",
+ "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
+ ]),
+ ),
+)]
+#[repr(transparent)]
+pub struct PublicKey(PublicKeyBytes);
+
+impl PublicKey {
+ pub const fn from_bytes(bytes: PublicKeyBytes) -> Self {
+ Self(bytes)
+ }
+
+ pub fn into_inner(self) -> PublicKeyBytes {
+ self.0
}
}
-pub trait Signer: Send + signature::Signer<Signature> {
- /// Return this signer's public/verification key.
- fn public_key(&self) -> &PublicKey;
+impl<'a> From<&'a PublicKeyBytes> for &'a PublicKey {
+ fn from(other: &'a PublicKeyBytes) -> Self {
+ let ptr = std::ptr::from_ref(other).cast::<PublicKey>();
+ // SAFETY: `PublicKey` is `#[repr(transparent)]` over the same array type,
+ // so the cast preserves layout and alignment, and every byte pattern is valid.
+ unsafe { &*ptr }
+ }
}
-impl<S> Signer for S
-where
- S: Send,
- S: signature::Signer<Signature>,
- S: signature::KeypairRef<VerifyingKey = PublicKey>,
-{
- fn public_key(&self) -> &PublicKey {
- self.as_ref()
+impl From<PublicKeyBytes> for PublicKey {
+ fn from(bytes: PublicKeyBytes) -> Self {
+ Self(bytes)
}
}
-/// Cryptographic signature.
-#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
-#[serde(into = "String", try_from = "String")]
-pub struct Signature(pub ed25519::Signature);
+#[cfg(feature = "alloc")]
+impl alloc::borrow::Borrow<PublicKeyBytes> for PublicKey {
+ fn borrow(&self) -> &PublicKeyBytes {
+ &self.0
+ }
+}
-impl AsRef<[u8]> for Signature {
- fn as_ref(&self) -> &[u8] {
- self.0.as_ref()
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::fmt::Display for PublicKey {
+ fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
+ write!(f, "{}", self.to_human())
}
}
-impl fmt::Display for Signature {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let base = multibase::Base::Base58Btc;
- write!(f, "{}", multibase::encode(base, self.deref()))
+#[cfg(feature = "ssh")]
+impl From<PublicKey> for ssh_key::public::Ed25519PublicKey {
+ fn from(key: PublicKey) -> Self {
+ ssh_key::public::Ed25519PublicKey(key.0)
}
}
-impl fmt::Debug for Signature {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Signature({self})")
+#[cfg(feature = "ssh")]
+impl From<ssh_key::public::Ed25519PublicKey> for PublicKey {
+ fn from(key: ssh_key::public::Ed25519PublicKey) -> Self {
+ Self(key.0)
}
}
-#[derive(Error, Debug)]
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
-pub enum SignatureError {
+pub enum PublicKeyError {
+ #[error("invalid length {0}")]
+ InvalidLength(usize),
#[error("invalid multibase string: {0}")]
- Multibase(#[from] multibase::Error),
- #[error("invalid signature: {0}")]
- Invalid(#[from] ed25519::Error),
+ Multibase(#[cfg_attr(feature = "std", source)] multibase::Error),
+ #[error("invalid multicodec prefix, expected {0:?}")]
+ Multicodec([u8; 2]),
+ #[error("invalid public key")]
+ Invalid(#[cfg_attr(feature = "std", source)] signature::Error),
}
-impl From<ed25519::Signature> for Signature {
- fn from(other: ed25519::Signature) -> Self {
- Self(other)
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl From<PublicKey> for String {
+ fn from(other: PublicKey) -> Self {
+ other.to_human()
}
}
-impl FromStr for Signature {
- type Err = SignatureError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (_, bytes) = multibase::decode(s)?;
- let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
+impl PublicKey {
+ /// Encode public key in human-readable format.
+ ///
+ /// `MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))`
+ ///
+ #[cfg(all(feature = "alloc", feature = "multibase"))]
+ pub fn to_human(&self) -> String {
+ let mut buf = [0; 2 + dalek::ed::PUBLIC_KEY_LENGTH];
+ buf[..2].copy_from_slice(&MULTICODEC_TYPE);
+ buf[2..].copy_from_slice(&self.0);
- Ok(Self(sig))
+ multibase::encode(multibase::Base::Base58Btc, buf)
}
-}
-impl Deref for Signature {
- type Target = ed25519::Signature;
+ #[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+ ))]
+ pub fn to_namespace(&self) -> git_ref_format_core::RefString {
+ use alloc::borrow::ToOwned as _;
+ use git_ref_format_core::name::{NAMESPACES, REFS};
+ REFS.to_owned().and(NAMESPACES).and(self.to_component())
+ }
- fn deref(&self) -> &Self::Target {
- &self.0
+ #[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+ ))]
+ pub fn to_component(&self) -> git_ref_format_core::Component<'_> {
+ git_ref_format_core::Component::from(self)
}
-}
-impl From<[u8; 64]> for Signature {
- fn from(bytes: [u8; 64]) -> Self {
- Self(ed25519::Signature::new(bytes))
+ #[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+ ))]
+ pub fn from_namespaced(
+ refstr: &git_ref_format_core::Namespaced,
+ ) -> Result<Self, PublicKeyError> {
+ use alloc::str::FromStr as _;
+
+ let name = refstr.namespace().into_inner();
+ Self::from_str(name.as_str())
}
}
-impl TryFrom<&[u8]> for Signature {
- type Error = ed25519::Error;
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::str::FromStr for PublicKey {
+ type Err = PublicKeyError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (_, bytes) = multibase::decode(s).map_err(PublicKeyError::Multibase)?;
- fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
- ed25519::Signature::from_slice(bytes).map(Self)
+ if bytes.len() < 2 {
+ return Err(PublicKeyError::InvalidLength(bytes.len()));
+ }
+
+ if bytes[..MULTICODEC_TYPE.len()] != MULTICODEC_TYPE {
+ return Err(PublicKeyError::Multicodec(MULTICODEC_TYPE));
+ }
+
+ Ok(PublicKey(
+ bytes[MULTICODEC_TYPE.len()..]
+ .try_into()
+ .map_err(|_| PublicKeyError::InvalidLength(bytes.len()))?,
+ ))
}
}
-impl From<Signature> for String {
- fn from(s: Signature) -> Self {
- s.to_string()
+#[cfg(all(
+ feature = "git-ref-format-core",
+ feature = "alloc",
+ feature = "multibase"
+))]
+impl From<&PublicKey> for git_ref_format_core::Component<'_> {
+ fn from(id: &PublicKey) -> Self {
+ use git_ref_format_core::{Component, RefString};
+ let refstr =
+ RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
+ Component::from_refstr(refstr).expect("encoded public keys are valid refname components")
}
}
-impl TryFrom<String> for Signature {
- type Error = SignatureError;
+#[cfg(all(feature = "sqlite", feature = "alloc", feature = "multibase"))]
+impl TryFrom<&sqlite::Value> for PublicKey {
+ type Error = sqlite::Error;
- fn try_from(s: String) -> Result<Self, Self::Error> {
- Self::from_str(&s)
+ fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
+ use alloc::str::FromStr as _;
+
+ match value {
+ sqlite::Value::String(s) => Self::from_str(s).map_err(|e| sqlite::Error {
+ code: None,
+ message: Some(e.to_string()),
+ }),
+ _ => Err(sqlite::Error {
+ code: None,
+ message: Some(String::from("sql: invalid type for public key")),
+ }),
+ }
}
}
-/// The public/verification key.
-#[derive(Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
-#[serde(into = "String", try_from = "String")]
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[cfg_attr(
- feature = "schemars",
- schemars(
- title = "Ed25519",
- description = "An Ed25519 public key in multibase encoding.",
- extend("examples" = [
- "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7",
- "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C",
- "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
- "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
- ]),
- ),
-)]
-pub struct PublicKey(amplify::Bytes32);
-
-impl PublicKey {
- /// Verify the signature for a given payload.
- pub fn verify(
- &self,
- payload: impl AsRef<[u8]>,
- signature: &ed25519::Signature,
- ) -> Result<(), ed25519::Error> {
- ed25519::PublicKey::new(self.0.to_byte_array()).verify(payload, signature)
+#[cfg(all(feature = "sqlite", feature = "alloc", feature = "multibase"))]
+impl sqlite::BindableWithIndex for &PublicKey {
+ fn bind<I: sqlite::ParameterIndex>(
+ self,
+ stmt: &mut sqlite::Statement<'_>,
+ i: I,
+ ) -> sqlite::Result<()> {
+ sqlite::Value::from(self).bind(stmt, i)
}
+}
- /// Returns a byte array representation of the public key.
- #[inline]
- pub fn to_byte_array(&self) -> [u8; 32] {
- self.0.to_byte_array()
+#[cfg(all(feature = "sqlite", feature = "alloc", feature = "multibase"))]
+impl From<&PublicKey> for sqlite::Value {
+ fn from(pk: &PublicKey) -> Self {
+ sqlite::Value::String(pk.to_human())
}
}
-impl signature::Verifier<Signature> for PublicKey {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> {
- self.verify(msg, signature)
- .map_err(signature::Error::from_source)
+#[cfg(feature = "cyphernet")]
+impl AsRef<[u8]> for PublicKey {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
}
}
-#[cfg(feature = "cyphernet")]
+#[cfg(all(feature = "cyphernet", feature = "alloc", feature = "multibase"))]
impl cyphernet::display::MultiDisplay<cyphernet::display::Encoding> for PublicKey {
type Display = String;
- fn display_fmt(&self, _: &cyphernet::display::Encoding) -> Self::Display {
- self.to_string()
- }
-}
-
-#[cfg(feature = "ssh")]
-impl From<PublicKey> for ssh_key::PublicKey {
- fn from(key: PublicKey) -> Self {
- ssh_key::PublicKey::from(ssh_key::public::Ed25519PublicKey(key.to_byte_array()))
+ fn display_fmt(&self, encoding: &cyphernet::display::Encoding) -> Self::Display {
+ match encoding {
+ cyphernet::display::Encoding::Base58
+ | cyphernet::display::Encoding::Multibase(multibase::Base::Base58Btc) => {
+ self.to_string()
+ }
+ _ => unimplemented!(),
+ }
}
}
-#[cfg(feature = "cyphernet")]
+#[cfg(all(feature = "cyphernet", feature = "alloc"))]
impl cyphernet::EcPk for PublicKey {
- const COMPRESSED_LEN: usize = 32;
+ const COMPRESSED_LEN: usize = dalek::ed::PUBLIC_KEY_LENGTH;
const CURVE_NAME: &'static str = "Edwards25519";
- type Compressed = amplify::Bytes32;
+ type Compressed = PublicKey;
fn base_point() -> Self {
unimplemented!()
}
fn to_pk_compressed(&self) -> Self::Compressed {
- amplify::Bytes32::from_byte_array(self.to_byte_array())
+ *self
}
fn from_pk_compressed(pk: Self::Compressed) -> Result<Self, cyphernet::EcPkInvalid> {
- Ok(PublicKey::from(pk.to_byte_array()))
+ Ok(pk)
}
- fn from_pk_compressed_slice(slice: &[u8]) -> Result<Self, cyphernet::EcPkInvalid> {
- ed25519::PublicKey::from_slice(slice)
- .map_err(|_| cyphernet::EcPkInvalid::default())
- .map(Self::from)
+ fn from_pk_compressed_slice(pk: &[u8]) -> Result<Self, cyphernet::EcPkInvalid> {
+ Ok(PublicKey(
+ PublicKeyBytes::try_from(pk).map_err(|_| cyphernet::EcPkInvalid::default())?,
+ ))
}
}
-/// The private/signing key.
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub struct SecretKey(ed25519::SecretKey);
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
+pub struct VerifyingKey(dalek::ed::VerifyingKey);
-impl SecretKey {
- /// Elliptic-curve Diffie-Hellman.
- pub fn ecdh(&self, pk: &PublicKey) -> Result<[u8; 32], ed25519::Error> {
- let scalar = self.seed().scalar();
- let ge = edwards25519::GeP3::from_bytes_vartime(&pk.to_byte_array())
- .ok_or(Error::InvalidPublicKey)?;
+impl VerifyingKey {
+ #[allow(clippy::wrong_self_convention)] // Name copied from dalek.
+ #[inline]
+ pub(crate) fn to_bytes(&self) -> PublicKeyBytes {
+ self.0.to_bytes()
+ }
- Ok(edwards25519::ge_scalarmult(&scalar, &ge).to_bytes())
+ #[cfg(feature = "x25519")]
+ pub fn to_x25519(&self) -> (u8, dalek::x::PublicKey) {
+ (
+ AsRef::<PublicKeyBytes>::as_ref(self)[31] >> 7,
+ dalek::x::PublicKey::from(self.0.to_montgomery().to_bytes()),
+ )
+ }
+
+ #[cfg(feature = "x25519")]
+ pub fn from_x25519(sign: u8, pk: &dalek::x::PublicKey) -> Option<Self> {
+ dalek::curve::montgomery::MontgomeryPoint(pk.to_bytes())
+ .to_edwards(sign)
+ .map(|point| Self(dalek::ed::VerifyingKey::from(point)))
}
}
-impl PartialOrd for SecretKey {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
+impl TryFrom<&PublicKey> for VerifyingKey {
+ type Error = signature::Error;
+
+ fn try_from(key: &PublicKey) -> Result<Self, Self::Error> {
+ dalek::ed::VerifyingKey::from_bytes(&key.0).map(Self)
}
}
-impl Ord for SecretKey {
- fn cmp(&self, other: &Self) -> Ordering {
- self.0.cmp(&other.0)
+impl<'a> VerifyingKey {
+ pub fn public_key(&'a self) -> &'a PublicKey {
+ self.0.as_bytes().into()
}
}
-impl zeroize::Zeroize for SecretKey {
- fn zeroize(&mut self) {
- self.0.zeroize();
+impl AsRef<PublicKeyBytes> for VerifyingKey {
+ fn as_ref(&self) -> &PublicKeyBytes {
+ self.0.as_bytes()
}
}
-impl TryFrom<&[u8]> for SecretKey {
- type Error = ed25519::Error;
+impl From<dalek::ed::VerifyingKey> for VerifyingKey {
+ fn from(other: dalek::ed::VerifyingKey) -> Self {
+ Self(other)
+ }
+}
- fn try_from(bytes: &[u8]) -> Result<Self, ed25519::Error> {
- ed25519::SecretKey::from_slice(bytes).map(Self)
+impl signature::Verifier<Signature> for VerifyingKey {
+ fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> {
+ self.0.verify(msg, signature)
}
}
-impl AsRef<[u8]> for SecretKey {
- fn as_ref(&self) -> &[u8] {
- &*self.0
+#[cfg(all(feature = "alloc", feature = "multibase"))]
+impl alloc::fmt::Display for VerifyingKey {
+ fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
+ self.public_key().fmt(f)
}
}
-impl From<[u8; 64]> for SecretKey {
- fn from(bytes: [u8; 64]) -> Self {
- Self(ed25519::SecretKey::new(bytes))
+#[cfg(all(feature = "cyphernet", feature = "alloc", feature = "multibase"))]
+impl cyphernet::display::MultiDisplay<cyphernet::display::Encoding> for VerifyingKey {
+ type Display = String;
+
+ fn display_fmt(&self, encoding: &cyphernet::display::Encoding) -> Self::Display {
+ self.public_key().display_fmt(encoding)
}
}
-impl From<ed25519::SecretKey> for SecretKey {
- fn from(other: ed25519::SecretKey) -> Self {
- Self(other)
+#[cfg(feature = "cyphernet")]
+impl From<&cyphernet::ed25519::PublicKey> for PublicKey {
+ fn from(value: &cyphernet::ed25519::PublicKey) -> Self {
+ use core::ops::Deref as _;
+ Self(*value.deref().deref())
}
}
-impl From<SecretKey> for ed25519::SecretKey {
- fn from(other: SecretKey) -> Self {
- other.0
+#[cfg(feature = "cyphernet")]
+impl From<PublicKey> for cyphernet::ed25519::PublicKey {
+ fn from(value: PublicKey) -> Self {
+ use cyphernet::EcPk as _;
+ cyphernet::ed25519::PublicKey::from_pk_compressed(value.into_inner().into())
+ .expect("implementation is infallible")
}
}
-impl Deref for SecretKey {
- type Target = ed25519::SecretKey;
+#[cfg(all(feature = "cyphernet", feature = "alloc"))]
+impl cyphernet::EcPk for VerifyingKey {
+ const COMPRESSED_LEN: usize = dalek::ed::PUBLIC_KEY_LENGTH;
+ const CURVE_NAME: &'static str = "Edwards25519";
- fn deref(&self) -> &Self::Target {
- &self.0
+ type Compressed = PublicKey;
+
+ fn base_point() -> Self {
+ unimplemented!()
+ }
+
+ fn to_pk_compressed(&self) -> Self::Compressed {
+ *self.public_key()
+ }
+
+ fn from_pk_compressed(pk: Self::Compressed) -> Result<Self, cyphernet::EcPkInvalid> {
+ dalek::ed::VerifyingKey::from_bytes(&pk.0)
+ .map_err(|_| cyphernet::EcPkInvalid::default())
+ .map(Self)
+ }
+
+ fn from_pk_compressed_slice(slice: &[u8]) -> Result<Self, cyphernet::EcPkInvalid> {
+ Self::from_pk_compressed(PublicKey(
+ slice
+ .try_into()
+ .map_err(|_| cyphernet::EcPkInvalid::default())?,
+ ))
}
}
-#[derive(Error, Debug)]
+#[cfg(all(feature = "ssh", feature = "std"))]
+#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
-pub enum PublicKeyError {
- #[error("invalid length {0}")]
- InvalidLength(usize),
- #[error("invalid multibase string: {0}")]
- Multibase(#[from] multibase::Error),
- #[error("invalid multicodec prefix, expected {0:?}")]
- Multicodec([u8; 2]),
- #[error("invalid key: {0}")]
- InvalidKey(#[from] ed25519::Error),
-}
+pub enum LoadError {
+ #[error(transparent)]
+ Keystore(#[from] ssh::keystore::Error),
+ #[error("key not found in '{0}'")]
+ NotFound(std::path::PathBuf),
+ #[error("invalid passphrase")]
+ InvalidPassphrase,
+ #[error("secret key '{secret}' and public key '{public}' do not match")]
+ KeyMismatch {
+ secret: std::path::PathBuf,
+ public: std::path::PathBuf,
+ },
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SigningKey(dalek::ed::SigningKey);
+
+impl SigningKey {
+ fn public_key(&self) -> &PublicKey {
+ self.0.as_ref().as_bytes().into()
+ }
-impl fmt::Display for PublicKey {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.to_human())
+ pub fn from_seed(seed: Seed) -> Self {
+ Self(dalek::ed::SigningKey::from_bytes(seed.as_ref()))
}
-}
-impl From<PublicKey> for String {
- fn from(other: PublicKey) -> Self {
- other.to_human()
+ #[cfg(any(test, all(feature = "test", feature = "alloc")))]
+ pub fn mock(id: usize) -> Self {
+ let bytes = core::iter::repeat_n(
+ id,
+ dalek::ed::SECRET_KEY_LENGTH / core::mem::size_of::<usize>(),
+ )
+ .flat_map(|id| id.to_be_bytes())
+ .collect::<Vec<_>>();
+
+ Self(dalek::ed::SigningKey::from_bytes(
+ bytes.as_slice().try_into().unwrap(),
+ ))
+ }
+
+ pub fn to_keypair_bytes(&self) -> [u8; dalek::ed::KEYPAIR_LENGTH] {
+ self.0.to_keypair_bytes()
+ }
+
+ pub fn as_bytes(&self) -> &[u8; dalek::ed::SECRET_KEY_LENGTH] {
+ self.0.as_bytes()
+ }
+
+ /// Load this signer from a keystore, given a secret key passphrase.
+ #[cfg(all(feature = "ssh", feature = "std"))]
+ pub fn load(
+ keystore: &ssh::Keystore,
+ passphrase: Option<ssh::Passphrase>,
+ ) -> Result<Self, LoadError> {
+ let secret = keystore
+ .secret_key(passphrase)
+ .map_err(|e| {
+ if e.is_crypto_err() {
+ LoadError::InvalidPassphrase
+ } else {
+ e.into()
+ }
+ })?
+ .ok_or_else(|| LoadError::NotFound(keystore.secret_key_path().to_path_buf()))?;
+
+ let Some(public_path) = keystore.public_key_path() else {
+ // There is no public key in the key store, so there's nothing
+ // to validate. Derive it from the secret key.
+ return Ok(secret);
+ };
+
+ let public = keystore
+ .public_key()?
+ .ok_or_else(|| LoadError::NotFound(public_path.to_path_buf()))?;
+
+ if secret.public_key() != &public {
+ return Err(LoadError::KeyMismatch {
+ secret: keystore.secret_key_path().to_path_buf(),
+ public: public_path.to_path_buf(),
+ });
+ }
+
+ Ok(secret)
+ }
+
+ /// Elliptic-curve Diffie-Hellman.
+ #[cfg(feature = "diffie-hellman")]
+ pub fn diffie_hellman(&self, their_public: &VerifyingKey) -> Option<SharedSecret> {
+ let scalar = self.0.to_scalar();
+
+ dalek::curve::edwards::CompressedEdwardsY(their_public.to_bytes())
+ .decompress()
+ .map(|point| (scalar * point).compress().to_bytes())
+ }
+
+ #[cfg(feature = "x25519")]
+ pub fn to_x25519(&self) -> [u8; 32] {
+ dalek::curve::scalar::clamp_integer(self.0.to_scalar_bytes())
}
}
-impl fmt::Debug for PublicKey {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "PublicKey({self})")
+impl AsRef<PublicKey> for SigningKey {
+ fn as_ref(&self) -> &PublicKey {
+ self.public_key()
}
}
-impl From<ed25519::PublicKey> for PublicKey {
- fn from(other: ed25519::PublicKey) -> Self {
- Self(amplify::Bytes32::from_byte_array(*other.deref()))
+impl PartialOrd for SigningKey {
+ fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
+ Some(self.cmp(other))
}
}
-impl From<PublicKey> for ed25519::PublicKey {
- fn from(val: PublicKey) -> Self {
- ed25519::PublicKey::new(val.to_byte_array())
+impl Ord for SigningKey {
+ fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+ self.0.as_bytes().cmp(other.0.as_bytes())
}
}
-impl From<[u8; 32]> for PublicKey {
- fn from(other: [u8; 32]) -> Self {
- Self(amplify::Bytes32::from_byte_array(other))
+impl TryFrom<[u8; dalek::ed::KEYPAIR_LENGTH]> for SigningKey {
+ type Error = signature::Error;
+
+ fn try_from(bytes: [u8; dalek::ed::KEYPAIR_LENGTH]) -> Result<Self, Self::Error> {
+ dalek::ed::SigningKey::from_keypair_bytes(&bytes).map(Self)
}
}
-impl TryFrom<&[u8]> for PublicKey {
- type Error = ed25519::Error;
+impl From<dalek::ed::SigningKey> for SigningKey {
+ fn from(other: dalek::ed::SigningKey) -> Self {
+ Self(other)
+ }
+}
- fn try_from(other: &[u8]) -> Result<Self, Self::Error> {
- ed25519::PublicKey::from_slice(other).map(Self::from)
+impl From<SigningKey> for dalek::ed::SigningKey {
+ fn from(other: SigningKey) -> Self {
+ other.0
}
}
-impl PublicKey {
- /// Multicodec key type for Ed25519 keys.
- pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x1];
+impl signature::Signer<Signature> for SigningKey {
+ fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+ self.0.try_sign(msg)
+ }
+}
- /// Encode public key in human-readable format.
- ///
- /// `MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))`
- ///
- pub fn to_human(&self) -> String {
- let mut buf = [0; 2 + ed25519::PublicKey::BYTES];
- buf[..2].copy_from_slice(&Self::MULTICODEC_TYPE);
- buf[2..].copy_from_slice(self.to_byte_array().as_slice());
+impl signature::Keypair for SigningKey {
+ type VerifyingKey = VerifyingKey;
- multibase::encode(multibase::Base::Base58Btc, buf)
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ VerifyingKey(self.0.verifying_key())
}
+}
- #[cfg(feature = "git-ref-format-core")]
- pub fn to_namespace(&self) -> git_ref_format_core::RefString {
- use git_ref_format_core::name::{Component, NAMESPACES, REFS};
- REFS.to_owned().and(NAMESPACES).and(Component::from(self))
+#[cfg(all(feature = "cyphernet", feature = "alloc"))]
+impl cyphernet::EcSk for SigningKey {
+ type Pk = VerifyingKey;
+
+ fn generate_keypair() -> (Self, Self::Pk)
+ where
+ Self: Sized,
+ {
+ let signing_key = dalek::ed::SigningKey::from_bytes(&[154; 32]);
+ let verifying_key = signature::Keypair::verifying_key(&signing_key);
+ (SigningKey(signing_key), VerifyingKey(verifying_key))
}
- #[cfg(feature = "git-ref-format-core")]
- pub fn to_component(&self) -> git_ref_format_core::Component<'_> {
- git_ref_format_core::Component::from(self)
+ fn to_pk(&self) -> Result<Self::Pk, cyphernet::EcSkInvalid> {
+ use signature::Keypair as _;
+
+ Ok(self.verifying_key())
}
+}
- #[cfg(feature = "git-ref-format-core")]
- pub fn from_namespaced(
- refstr: &git_ref_format_core::Namespaced,
- ) -> Result<Self, PublicKeyError> {
- let name = refstr.namespace().into_inner();
+#[cfg(all(feature = "cyphernet", feature = "diffie-hellman"))]
+impl cyphernet::Ecdh for SigningKey {
+ type SharedSecret = SharedSecret;
- Self::from_str(name.deref().as_str())
+ fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
+ self.diffie_hellman(pk)
+ .ok_or(cyphernet::EcdhError::InvalidPk(
+ cyphernet::EcPkInvalid::default(),
+ ))
}
}
-impl FromStr for PublicKey {
+#[cfg(any(test, feature = "test"))]
+impl qcheck::Arbitrary for SigningKey {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ SigningKey::mock(usize::arbitrary(g))
+ }
+}
+
+#[cfg(all(feature = "alloc", feature = "multibase", feature = "cyphernet"))]
+impl alloc::str::FromStr for VerifyingKey {
type Err = PublicKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (_, bytes) = multibase::decode(s)?;
-
- if let Some(bytes) = bytes.strip_prefix(&Self::MULTICODEC_TYPE) {
- let key = ed25519::PublicKey::from_slice(bytes)?;
-
- Ok(key.into())
- } else {
- Err(PublicKeyError::Multicodec(Self::MULTICODEC_TYPE))
- }
+ let pk = PublicKey::from_str(s)?;
+ dalek::ed::VerifyingKey::from_bytes(&pk.0)
+ .map(Self)
+ .map_err(PublicKeyError::Invalid)
}
}
+#[cfg(all(feature = "alloc", feature = "multibase"))]
impl TryFrom<String> for PublicKey {
type Error = PublicKeyError;
fn try_from(value: String) -> Result<Self, Self::Error> {
+ use alloc::str::FromStr as _;
+
Self::from_str(&value)
}
}
-#[cfg(feature = "git-ref-format-core")]
-impl From<&PublicKey> for git_ref_format_core::Component<'_> {
- fn from(id: &PublicKey) -> Self {
- use git_ref_format_core::{Component, RefString};
- let refstr =
- RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
- Component::from_refstr(refstr).expect("encoded public keys are valid refname components")
+#[cfg(all(feature = "alloc", feature = "multibase", feature = "cyphernet"))]
+impl TryFrom<String> for VerifyingKey {
+ type Error = PublicKeyError;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ use alloc::str::FromStr as _;
+
+ Self::from_str(&value)
}
}
-#[cfg(feature = "sqlite")]
-impl From<&PublicKey> for sqlite::Value {
- fn from(pk: &PublicKey) -> Self {
- sqlite::Value::String(pk.to_human())
+#[cfg(any(test, feature = "test"))]
+impl qcheck::Arbitrary for PublicKey {
+ fn arbitrary(g: &mut qcheck::Gen) -> Self {
+ *SigningKey::arbitrary(g).public_key()
}
}
-#[cfg(feature = "sqlite")]
-impl TryFrom<&sqlite::Value> for PublicKey {
- type Error = sqlite::Error;
+/// An extended signature carries the key that may be used to verify the
+/// signature along with the signature itself.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ExtendedSignature<PublicKey = crate::PublicKey, Signature = crate::Signature> {
+ key: PublicKey,
+ sig: Signature,
+}
- fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
- match value {
- sqlite::Value::String(s) => Self::from_str(s).map_err(|e| sqlite::Error {
- code: None,
- message: Some(e.to_string()),
- }),
- _ => Err(sqlite::Error {
- code: None,
- message: Some("sql: invalid type for public key".to_owned()),
- }),
- }
+impl ExtendedSignature {
+ pub fn try_sign(signer: &impl Signer, payload: &[u8]) -> Result<Self, signature::Error> {
+ Ok(Self {
+ key: *signer.public_key(),
+ sig: signer.try_sign(payload)?,
+ })
}
}
-#[cfg(feature = "sqlite")]
-impl sqlite::BindableWithIndex for &PublicKey {
- fn bind<I: sqlite::ParameterIndex>(
- self,
- stmt: &mut sqlite::Statement<'_>,
- i: I,
- ) -> sqlite::Result<()> {
- sqlite::Value::from(self).bind(stmt, i)
+impl<VerifyingKey, Signature> ExtendedSignature<VerifyingKey, Signature>
+where
+ VerifyingKey: signature::Verifier<Signature>,
+{
+ /// Verify the signature for a given payload.
+ pub fn verify(&self, msg: &[u8]) -> Result<(), signature::Error> {
+ self.key.verify(msg, &self.sig)
}
}
-#[cfg(feature = "sqlite")]
-impl From<&Signature> for sqlite::Value {
- fn from(sig: &Signature) -> Self {
- sqlite::Value::Binary(sig.to_vec())
+impl<VerifyingKey, Signature> ExtendedSignature<VerifyingKey, Signature> {
+ /// Create a new extended signature.
+ pub fn new(key: VerifyingKey, sig: Signature) -> Self {
+ Self { key, sig }
}
-}
-#[cfg(feature = "sqlite")]
-impl TryFrom<&sqlite::Value> for Signature {
- type Error = sqlite::Error;
+ pub fn key(&self) -> &VerifyingKey {
+ &self.key
+ }
- fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
- match value {
- sqlite::Value::Binary(s) => ed25519::Signature::from_slice(s)
- .map_err(|e| sqlite::Error {
- code: None,
- message: Some(e.to_string()),
- })
- .map(Self),
- _ => Err(sqlite::Error {
- code: None,
- message: Some("sql: invalid column type for signature".to_owned()),
- }),
- }
+ pub fn sig(&self) -> &Signature {
+ &self.sig
}
-}
-#[cfg(feature = "sqlite")]
-impl sqlite::BindableWithIndex for &Signature {
- fn bind<I: sqlite::ParameterIndex>(
- self,
- stmt: &mut sqlite::Statement<'_>,
- i: I,
- ) -> sqlite::Result<()> {
- sqlite::Value::from(self).bind(stmt, i)
+ pub fn into_pair(self) -> (VerifyingKey, Signature) {
+ (self.key, self.sig)
}
}
#[cfg(test)]
mod tests {
- use super::KeyPair;
- use crate::{PublicKey, SecretKey};
+ use super::*;
+
use qcheck_macros::quickcheck;
- use std::str::FromStr;
- #[test]
- fn test_e25519_dh() {
- let kp_a = KeyPair::generate();
- let kp_b = KeyPair::generate();
+ use crate::{SigningKey, VerifyingKey};
+
+ /// See <https://w3c-ccg.github.io/did-key-spec/#example-a-simple-ed25519-did-key-value>.
+ const DID_KEY_SAMPLE: &str = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
- let output_a = SecretKey::from(kp_b.sk).ecdh(&kp_a.pk.into()).unwrap();
- let output_b = SecretKey::from(kp_a.sk).ecdh(&kp_b.pk.into()).unwrap();
+ #[cfg(feature = "diffie-hellman")]
+ #[quickcheck]
+ fn diffie_hellman(sk_a: SigningKey, sk_b: SigningKey) {
+ use signature::Keypair as _;
+
+ let output_a = sk_b.diffie_hellman(&sk_a.verifying_key()).unwrap();
+ let output_b = sk_a.diffie_hellman(&sk_b.verifying_key()).unwrap();
assert_eq!(output_a, output_b);
}
+ #[cfg(feature = "alloc")]
#[quickcheck]
fn prop_encode_decode(input: PublicKey) {
+ use alloc::str::FromStr as _;
+
let encoded = input.to_string();
let decoded = PublicKey::from_str(&encoded).unwrap();
assert_eq!(input, decoded);
}
+ #[cfg(feature = "alloc")]
#[test]
- fn test_encode_decode() {
- let input = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
- let key = PublicKey::from_str(input).unwrap();
+ fn did_key_sample() {
+ use alloc::str::FromStr as _;
- assert_eq!(key.to_string(), input);
+ let key = PublicKey::from_str(DID_KEY_SAMPLE).unwrap();
+
+ assert_eq!(key.to_string(), DID_KEY_SAMPLE);
}
+ #[cfg(feature = "std")]
#[quickcheck]
fn prop_key_equality(a: PublicKey, b: PublicKey) {
- use std::collections::HashSet;
-
- assert_ne!(a, b);
+ if a == b {
+ return;
+ }
- let mut hm = HashSet::new();
+ let mut hm = std::collections::HashSet::new();
assert!(hm.insert(a));
assert!(hm.insert(b));
@@ -545,8 +821,9 @@ mod tests {
assert!(!hm.insert(b));
}
+ #[cfg(feature = "diffie-hellman")]
#[test]
- fn e25519_dh_fixture() {
+ fn diffie_hellman_fixture() {
let sk_a: [u8; 32] = [
92, 136, 18, 88, 112, 205, 201, 68, 109, 197, 130, 211, 179, 138, 197, 113, 120, 55,
104, 139, 208, 184, 178, 157, 120, 11, 60, 13, 91, 30, 213, 38,
@@ -556,13 +833,18 @@ mod tests {
21, 202, 228, 123, 193, 140, 252, 63, 72, 5, 137, 36, 245,
];
- let kp_a = KeyPair::from_seed(ec25519::Seed::from(sk_a));
- let kp_b = KeyPair::from_seed(ec25519::Seed::from(sk_b));
+ let kp_a = dalek::ed::SigningKey::from_bytes(&sk_a);
+ let kp_b = dalek::ed::SigningKey::from_bytes(&sk_b);
- let output_a = SecretKey::from(kp_b.sk).ecdh(&kp_a.pk.into()).unwrap();
- let output_b = SecretKey::from(kp_a.sk).ecdh(&kp_b.pk.into()).unwrap();
+ let output_a = SigningKey::from(kp_b.clone())
+ .diffie_hellman(&kp_a.verifying_key().into())
+ .unwrap();
+ let output_b = SigningKey::from(kp_a)
+ .diffie_hellman(&kp_b.verifying_key().into())
+ .unwrap();
assert_eq!(output_a, output_b);
+
assert_eq!(
output_a,
[
@@ -571,4 +853,72 @@ mod tests {
]
)
}
+
+ // See <https://docs.rs/ed25519_to_curve25519/0.2.4/src/ed25519_to_curve25519/lib.rs.html#15-26>.
+ #[cfg(all(feature = "alloc", feature = "x25519"))]
+ #[test]
+ fn x25519_pk() {
+ let ed25519_pk = [
+ 59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13, 115, 101, 50, 21,
+ 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41,
+ ];
+
+ let curve25519_pk = [
+ 91, 245, 92, 115, 184, 46, 190, 34, 190, 128, 243, 67, 6, 103, 175, 87, 15, 174, 37,
+ 86, 166, 65, 94, 107, 48, 212, 6, 83, 0, 170, 148, 125,
+ ];
+
+ let ed25519_pk = VerifyingKey::try_from(&PublicKey::from(ed25519_pk)).unwrap();
+ let converted = ed25519_pk.to_x25519();
+
+ assert_eq!(*converted.1.as_bytes(), curve25519_pk);
+
+ assert_eq!(
+ VerifyingKey::from_x25519(converted.0, &converted.1).unwrap(),
+ ed25519_pk
+ );
+ }
+
+ // See <https://docs.rs/ed25519_to_curve25519/0.2.4/src/ed25519_to_curve25519/lib.rs.html#49-62>.
+ #[cfg(all(feature = "alloc", feature = "x25519"))]
+ #[test]
+ fn x25519_sk() {
+ let ed25519_sk: [u8; 32] = [
+ 202, 104, 239, 81, 53, 110, 80, 252, 198, 23, 155, 162, 215, 98, 223, 173, 227, 188,
+ 110, 54, 127, 45, 185, 206, 174, 29, 44, 147, 76, 66, 196, 195,
+ ];
+
+ let curve25519_sk: [u8; 32] = [
+ 200, 255, 64, 61, 17, 52, 112, 33, 205, 71, 186, 13, 131, 12, 241, 136, 223, 5, 152,
+ 40, 95, 187, 83, 168, 142, 10, 234, 215, 70, 210, 148, 104,
+ ];
+
+ let ed25519_sk = SigningKey::from(dalek::ed::SigningKey::from_bytes(&ed25519_sk));
+
+ assert_eq!(ed25519_sk.to_x25519(), curve25519_sk);
+ }
+
+ /// See <https://w3c-ccg.github.io/did-key-spec/#ed25519-with-x25519>.
+ #[cfg(all(feature = "alloc", feature = "x25519", feature = "multibase"))]
+ #[test]
+ fn ed25519_did() {
+ let ed: PublicKey = DID_KEY_SAMPLE.parse().unwrap();
+
+ let expected = {
+ // X25519 public key, multibase encoded according to `did:key` spec.
+ let key = "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p";
+
+ let mut bytes = multibase::decode(key).unwrap().1;
+
+ // X25519 public key, as raw bytes.
+ let key = bytes.split_off(2);
+
+ // The first two bytes indicate that this is an X25519 public key.
+ assert_eq!(bytes, [0xEC, 0x01]);
+
+ dalek::x::PublicKey::from(TryInto::<[u8; 32]>::try_into(key).unwrap())
+ };
+
+ assert_eq!(expected, VerifyingKey::try_from(&ed).unwrap().to_x25519().1);
+ }
}
diff --git a/crates/radicle-crypto/src/seed.rs b/crates/radicle-crypto/src/seed.rs
new file mode 100644
index 000000000..c682cbc3f
--- /dev/null
+++ b/crates/radicle-crypto/src/seed.rs
@@ -0,0 +1,22 @@
+use zeroize::Zeroizing;
+
+pub struct Seed(Zeroizing<[u8; Self::BYTES]>);
+
+impl Seed {
+ pub const BYTES: usize = 32;
+
+ pub fn new(seed: [u8; Self::BYTES]) -> Self {
+ Seed(Zeroizing::from(seed))
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn mock(byte: u8) -> Self {
+ Self::new([byte; Self::BYTES])
+ }
+}
+
+impl AsRef<[u8; Self::BYTES]> for Seed {
+ fn as_ref(&self) -> &[u8; Self::BYTES] {
+ &self.0
+ }
+}
diff --git a/crates/radicle-crypto/src/ssh.rs b/crates/radicle-crypto/src/ssh.rs
index d6b436aa5..8d0ff8160 100644
--- a/crates/radicle-crypto/src/ssh.rs
+++ b/crates/radicle-crypto/src/ssh.rs
@@ -1,103 +1,85 @@
-pub mod agent;
-pub mod keystore;
+extern crate alloc;
+
+use alloc::string::{String, ToString as _};
use thiserror::Error;
-use crate as crypto;
+#[cfg(feature = "std")]
+pub mod agent;
+
+#[cfg(feature = "std")]
+pub mod keystore;
+#[cfg(feature = "std")]
pub use keystore::{Keystore, Passphrase};
#[derive(Debug, Error)]
#[non_exhaustive]
-pub enum ExtendedSignatureError {
+pub enum ExtendedSignaturePemError {
#[error(transparent)]
Ssh(#[from] ssh_key::Error),
#[error(transparent)]
- Crypto(#[from] crypto::Error),
- #[error("unsupported signature algorithm")]
- UnsupportedAlgorithm,
-}
-
-/// Signature with public key, used for SSH signing.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ExtendedSignature {
- pub key: crypto::PublicKey,
- pub sig: crypto::Signature,
-}
-
-impl From<ExtendedSignature> for crypto::Signature {
- fn from(ExtendedSignature { sig, .. }: ExtendedSignature) -> Self {
- sig
- }
+ Signature(#[from] crate::signature::Error),
+ #[error("unsupported signature algorithm: {algorithm}")]
+ UnsupportedAlgorithm { algorithm: ssh_key::Algorithm },
}
-impl ExtendedSignature {
- /// Create a new extended signature.
- pub fn new(public_key: crypto::PublicKey, signature: crypto::Signature) -> Self {
- Self {
- key: public_key,
- sig: signature,
- }
- }
-
+impl crate::ExtendedSignature {
/// Convert to OpenSSH standard PEM format.
- pub fn to_pem(&self) -> Result<String, ExtendedSignatureError> {
+ pub fn to_pem(&self) -> Result<String, ExtendedSignaturePemError> {
ssh_key::SshSig::new(
- ssh_key::public::KeyData::from(ssh_key::public::Ed25519PublicKey(
- self.key.to_byte_array(),
- )),
+ ssh_key::public::KeyData::from(ssh_key::public::Ed25519PublicKey::from(self.key)),
String::from("radicle"),
ssh_key::HashAlg::Sha256,
- ssh_key::Signature::new(ssh_key::Algorithm::Ed25519, **self.sig)?,
+ ssh_key::Signature::new(ssh_key::Algorithm::Ed25519, self.sig.to_vec())?,
)?
.to_pem(ssh_key::LineEnding::default())
- .map_err(ExtendedSignatureError::from)
+ .map_err(ExtendedSignaturePemError::from)
}
/// Create from OpenSSH PEM format.
- pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self, ExtendedSignatureError> {
+ pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self, ExtendedSignaturePemError> {
let sig = ssh_key::SshSig::from_pem(pem)?;
+ let key = match sig.public_key() {
+ ssh_key::public::KeyData::Ed25519(key) => key.0,
+ key_data => {
+ return Err(ExtendedSignaturePemError::UnsupportedAlgorithm {
+ algorithm: key_data.algorithm(),
+ });
+ }
+ };
+
Ok(Self {
- key: crypto::PublicKey::from(
- sig.public_key()
- .ed25519()
- .ok_or(ExtendedSignatureError::UnsupportedAlgorithm)?
- .0,
- ),
- sig: crypto::Signature::try_from(sig.signature().as_bytes())?,
+ key: crate::PublicKey(key),
+ sig: crate::Signature::try_from(sig.signature().as_bytes())?,
})
}
-
- /// Verify the signature for a given payload.
- pub fn verify(&self, payload: &[u8]) -> bool {
- self.key.verify(payload, &self.sig).is_ok()
- }
}
pub mod fmt {
+ use super::*;
use crate::PublicKey;
/// Get the SSH long key from a public key.
/// This is the output of `ssh-add -L`.
pub fn key(key: &PublicKey) -> String {
- ssh_key::PublicKey::from(*key).to_string()
+ ssh_key::PublicKey::from(ssh_key::public::Ed25519PublicKey(key.0)).to_string()
}
/// Get the SSH key fingerprint from a public key.
/// This is the output of `ssh-add -l`.
pub fn fingerprint(key: &PublicKey) -> String {
- ssh_key::PublicKey::from(*key)
+ ssh_key::PublicKey::from(ssh_key::public::Ed25519PublicKey(key.0))
.fingerprint(Default::default())
.to_string()
}
#[cfg(test)]
mod test {
- use std::str::FromStr;
-
use super::*;
- use crate::PublicKey;
+
+ use alloc::str::FromStr;
#[test]
fn test_key() {
@@ -114,6 +96,7 @@ pub mod fmt {
fn test_fingerprint() {
let pk =
PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+
assert_eq!(
fingerprint(&pk),
"SHA256:gE/Ty4fuXzww49lcnNe9/GI0L7xSEQdFp/v9tOjFwB4"
diff --git a/crates/radicle-crypto/src/ssh/agent.rs b/crates/radicle-crypto/src/ssh/agent.rs
index fe48bb8df..8d07ff1a2 100644
--- a/crates/radicle-crypto/src/ssh/agent.rs
+++ b/crates/radicle-crypto/src/ssh/agent.rs
@@ -1,7 +1,12 @@
+extern crate std;
+
+use std::borrow::ToOwned as _;
use std::cell::RefCell;
use std::env::VarError;
use std::path::Path;
use std::path::PathBuf;
+use std::string::{String, ToString as _};
+use std::vec::Vec;
use proto::{PrivateCredential, PublicCredential};
use ssh_agent_lib::blocking::Client;
@@ -10,9 +15,7 @@ use ssh_agent_lib::proto;
use ssh_key::public::{Ed25519PublicKey, KeyData};
use thiserror::Error;
-use crate::{PublicKey, SecretKey, Signature, Signer};
-
-use super::ExtendedSignature;
+use crate::{PublicKey, Signature, SigningKey, VerifyingKey};
#[cfg(unix)]
use std::os::unix::net::UnixStream as Stream;
@@ -50,6 +53,14 @@ impl ConnectError {
}
}
+#[derive(Debug, Error)]
+pub enum IntoSignerError {
+ #[error(transparent)]
+ Agent(#[from] AgentError),
+ #[error("Identity {identity} not found in agent.")]
+ IdentityNotFound { identity: PublicKey },
+}
+
pub struct Agent {
path: PathBuf,
client: Client<Stream>,
@@ -82,17 +93,22 @@ impl Agent {
}
/// Register a key with the agent.
- pub fn register(&mut self, key: &SecretKey) -> Result<(), AgentError> {
+ pub fn register(&mut self, key: &SigningKey) -> Result<(), AgentError> {
use ssh_key::private::{Ed25519Keypair, KeypairData};
self.client.add_identity(proto::AddIdentity {
credential: PrivateCredential::Key {
- privkey: KeypairData::Ed25519(Ed25519Keypair::from_bytes(key).unwrap()),
+ privkey: KeypairData::Ed25519(
+ Ed25519Keypair::from_bytes(
+ &ed25519_dalek::SigningKey::from(key.clone()).to_keypair_bytes(),
+ )
+ .unwrap(),
+ ),
comment: "".into(),
},
})
}
- pub fn unregister(&mut self, key: &PublicKey) -> Result<(), AgentError> {
+ pub fn unregister(&mut self, key: &VerifyingKey) -> Result<(), AgentError> {
self.client.remove_identity(proto::RemoveIdentity {
credential: PublicCredential::Key(Self::key_data(key)),
})
@@ -102,7 +118,7 @@ impl Agent {
self.client.remove_all_identities()
}
- pub fn sign(&mut self, key: &PublicKey, data: &[u8]) -> Result<[u8; 64], AgentError> {
+ pub fn sign(&mut self, key: &VerifyingKey, data: &[u8]) -> Result<[u8; 64], AgentError> {
let sig = self.client.sign(proto::SignRequest {
credential: PublicCredential::Key(Self::key_data(key)),
data: data.to_vec(),
@@ -112,9 +128,16 @@ impl Agent {
Ok(sig.as_bytes().to_owned().try_into().unwrap())
}
- /// Get a signer from this agent, given the public key.
- pub fn signer(self, key: PublicKey) -> AgentSigner {
- AgentSigner::new(self, key)
+ /// Transform this [`Agent`] into an [`AgentSigner`] scoped to the given
+ /// `identity`.
+ pub fn into_signer(mut self, identity: VerifyingKey) -> Result<AgentSigner, IntoSignerError> {
+ if self.request_identities()?.contains(identity.public_key()) {
+ Ok(AgentSigner::new(self, identity))
+ } else {
+ Err(IntoSignerError::IdentityNotFound {
+ identity: *identity.public_key(),
+ })
+ }
}
pub fn path(&self) -> &Path {
@@ -136,79 +159,68 @@ impl Agent {
.collect())
}
- fn key_data(key: &PublicKey) -> KeyData {
- KeyData::Ed25519(Ed25519PublicKey(key.to_byte_array()))
+ fn key_data(key: &VerifyingKey) -> KeyData {
+ KeyData::Ed25519(Ed25519PublicKey(key.to_bytes()))
}
}
-/// A [`Signer`] that uses `ssh-agent`.
+/// A [`crate::signature::Signer`] that uses `ssh-agent`.
pub struct AgentSigner {
agent: RefCell<Agent>,
- public: PublicKey,
+ public: VerifyingKey,
}
-impl signature::Signer<Signature> for AgentSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+impl crate::signature::Signer<Signature> for AgentSigner {
+ fn try_sign(&self, msg: &[u8]) -> Result<Signature, crate::signature::Error> {
let sig = self
.agent
.borrow_mut()
.sign(&self.public, msg)
- .map_err(signature::Error::from_source)?;
+ .map_err(crate::signature::Error::from_source)?;
Ok(Signature::from(sig))
}
}
-impl signature::Signer<ExtendedSignature> for AgentSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
- use signature::Keypair as _;
- Ok(ExtendedSignature {
- key: self.verifying_key(),
- sig: self.try_sign(msg)?,
- })
+impl AsRef<VerifyingKey> for AgentSigner {
+ fn as_ref(&self) -> &VerifyingKey {
+ &self.public
}
}
-impl AsRef<PublicKey> for AgentSigner {
- fn as_ref(&self) -> &PublicKey {
- &self.public
+impl AsRef<crate::PublicKey> for AgentSigner {
+ fn as_ref(&self) -> &crate::PublicKey {
+ self.public.public_key()
}
}
-impl signature::KeypairRef for AgentSigner {
- type VerifyingKey = PublicKey;
+impl crate::signature::KeypairRef for AgentSigner {
+ type VerifyingKey = VerifyingKey;
}
impl AgentSigner {
- pub fn new(agent: Agent, public: PublicKey) -> Self {
+ pub fn new(agent: Agent, public: VerifyingKey) -> Self {
let agent = RefCell::new(agent);
Self { agent, public }
}
-
- pub fn is_ready(&self) -> Result<bool, AgentError> {
- let ids = self.agent.borrow_mut().request_identities()?;
-
- Ok(ids.contains(&self.public))
- }
-
- /// Box this signer into a [`Signer`].
- pub fn boxed(self) -> Box<dyn Signer> {
- Box::new(self)
- }
}
#[cfg(test)]
mod test {
- use crate::PublicKey;
+ use super::*;
+
use ssh_agent_lib::blocking::Client;
use ssh_agent_lib::proto::{PublicCredential, SignRequest};
use ssh_agent_lib::ssh_key::public::{Ed25519PublicKey, KeyData};
+ use crate::VerifyingKey;
+
#[test]
fn test_agent_encoding_remove() {
use std::str::FromStr;
- let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+ let pk =
+ VerifyingKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
let expected = [
0, 0, 0, 56, // Message length
18, // Message type (remove identity)
@@ -220,14 +232,14 @@ mod test {
37, 145, 51, 176, 174, 61, 136, 160, 107, 4, 95, 175, 144, // Key
];
- let mut client = Client::new(std::io::Cursor::new(vec![]));
+ let mut client = Client::new(std::io::Cursor::new(Vec::new()));
// We expect this to fail with an unexpected EOF, since the client will
// attempt to read a response from the stream, but the stream is empty,
// since we are not actually connected to SSH agent.
assert!(
matches!(client.remove_identity(ssh_agent_lib::proto::RemoveIdentity {
- credential: PublicCredential::Key(KeyData::Ed25519(Ed25519PublicKey(pk.to_byte_array()))),
+ credential: PublicCredential::Key(KeyData::Ed25519(Ed25519PublicKey(pk.to_bytes()))),
}),
Err(
super::AgentError::Proto(ssh_agent_lib::proto::ProtoError::IO(err)),
@@ -242,7 +254,8 @@ mod test {
fn test_agent_encoding_sign() {
use std::str::FromStr;
- let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
+ let pk =
+ VerifyingKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap();
let expected = [
0, 0, 0, 73, // Message length
13, // Message type (sign request)
@@ -257,13 +270,13 @@ mod test {
0, 0, 0, 0, // Signature flags
];
- let mut client = Client::new(std::io::Cursor::new(vec![]));
- let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
+ let mut client = Client::new(std::io::Cursor::new(Vec::new()));
+ let data: Vec<u8> = [1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().collect();
client
.sign(SignRequest {
credential: PublicCredential::Key(KeyData::Ed25519(Ed25519PublicKey(
- pk.to_byte_array(),
+ pk.to_bytes(),
))),
data,
flags: 0,
diff --git a/crates/radicle-crypto/src/ssh/keystore.rs b/crates/radicle-crypto/src/ssh/keystore.rs
index c899d5f44..e43a80232 100644
--- a/crates/radicle-crypto/src/ssh/keystore.rs
+++ b/crates/radicle-crypto/src/ssh/keystore.rs
@@ -1,15 +1,13 @@
-use std::ops::Deref;
+extern crate std;
+
use std::path::{Path, PathBuf};
+use std::string::String;
use std::{fs, io};
-#[cfg(feature = "cyphernet")]
-use cyphernet::{EcSk, EcSkInvalid, Ecdh};
use thiserror::Error;
use zeroize::Zeroizing;
-use crate::{KeyPair, PublicKey, SecretKey, Signature, Signer};
-
-use super::ExtendedSignature;
+use crate::{PublicKey, Seed, SigningKey};
/// A secret key passphrase.
pub type Passphrase = Zeroizing<String>;
@@ -27,6 +25,10 @@ pub enum Error {
AlreadyInitialized { exists: PathBuf },
#[error("keystore is encrypted; a passphrase is required")]
PassphraseMissing,
+ #[error(transparent)]
+ Signature(#[from] crate::signature::Error),
+ #[error("invalid key")]
+ Invalid,
}
impl Error {
@@ -91,19 +93,22 @@ impl Keystore {
&self,
comment: &str,
passphrase: Option<Passphrase>,
- seed: ec25519::Seed,
+ seed: Seed,
) -> Result<PublicKey, Error> {
- self.store(KeyPair::from_seed(seed), comment, passphrase)
+ let signing_key = SigningKey::from_seed(seed);
+ self.store(&signing_key, comment, passphrase)?;
+ Ok(*signing_key.public_key())
}
/// Store a keypair on disk. Returns an error if any of the two key files already exist.
pub fn store(
&self,
- keypair: KeyPair,
+ keypair: &SigningKey,
comment: &str,
passphrase: Option<Passphrase>,
- ) -> Result<PublicKey, Error> {
- let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&keypair)?;
+ ) -> Result<(), Error> {
+ let keypair_bytes = keypair.to_keypair_bytes();
+ let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&keypair_bytes)?;
let ssh_pair = ssh_key::private::KeypairData::Ed25519(ssh_pair);
let secret = ssh_key::PrivateKey::new(ssh_pair, comment)?;
let secret = if let Some(p) = passphrase {
@@ -160,11 +165,14 @@ impl Keystore {
public.write_openssh_file(path_public)?;
}
- Ok(keypair.pk.into())
+ Ok(())
}
/// Load the public key from the store. Returns `None` if it wasn't found.
pub fn public_key(&self) -> Result<Option<PublicKey>, Error> {
+ use KeyData::*;
+ use ssh_key::{PublicKey as SshPublicKey, public::KeyData};
+
let Some(path_public) = &self.path_public else {
return Ok(None);
};
@@ -173,24 +181,24 @@ impl Keystore {
return Ok(None);
}
- let public = ssh_key::PublicKey::read_openssh_file(path_public)?;
- PublicKey::try_from(public)
- .map(Some)
- .map_err(|_| Error::InvalidKeyType)
+ match KeyData::from(SshPublicKey::read_openssh_file(path_public)?) {
+ Ed25519(key) => Ok(Some(PublicKey::from(key))),
+ _ => Err(Error::InvalidKeyType),
+ }
}
/// Load the secret key from the store, decrypting it with the given passphrase.
/// Returns `None` if it wasn't found.
- pub fn secret_key(
- &self,
- passphrase: Option<Passphrase>,
- ) -> Result<Option<Zeroizing<SecretKey>>, Error> {
+ pub fn secret_key(&self, passphrase: Option<Passphrase>) -> Result<Option<SigningKey>, Error> {
+ use KeypairData::*;
+ use ssh_key::{PrivateKey, private::KeypairData};
+
let path = &self.path_secret;
if !path.exists() {
return Ok(None);
}
- let secret = ssh_key::PrivateKey::read_openssh_file(path)?;
+ let secret = PrivateKey::read_openssh_file(path)?;
let secret = if let Some(p) = passphrase {
secret.decrypt(p)?
@@ -200,9 +208,7 @@ impl Keystore {
secret
};
match secret.key_data() {
- ssh_key::private::KeypairData::Ed25519(pair) => {
- Ok(Some(SecretKey::from(pair.to_bytes()).into()))
- }
+ Ed25519(pair) => Ok(Some(SigningKey::try_from(pair.to_bytes())?)),
_ => Err(Error::InvalidKeyType),
}
}
@@ -227,174 +233,19 @@ impl Keystore {
}
}
-#[derive(Debug, Error)]
-#[non_exhaustive]
-pub enum MemorySignerError {
- #[error(transparent)]
- Keystore(#[from] Error),
- #[error("key not found in '{0}'")]
- NotFound(PathBuf),
- #[error("invalid passphrase")]
- InvalidPassphrase,
- #[error("secret key '{secret}' and public key '{public}' do not match")]
- KeyMismatch { secret: PathBuf, public: PathBuf },
-}
-
-/// An in-memory signer that keeps its secret key internally
-/// so that signing never fails.
-///
-/// Can be created from a [`Keystore`] with the [`MemorySigner::load`] function.
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct MemorySigner {
- public: PublicKey,
- secret: Zeroizing<SecretKey>,
-}
-
-impl signature::Signer<Signature> for MemorySigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
- Ok(Signature::from(self.secret.deref().deref().sign(msg, None)))
- }
-}
-
-impl signature::Signer<ExtendedSignature> for MemorySigner {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
- use signature::Keypair as _;
- Ok(ExtendedSignature {
- key: self.verifying_key(),
- sig: self.try_sign(msg)?,
- })
- }
-}
-
-impl AsRef<PublicKey> for MemorySigner {
- fn as_ref(&self) -> &PublicKey {
- &self.public
- }
-}
-
-impl signature::KeypairRef for MemorySigner {
- type VerifyingKey = PublicKey;
-}
-
-#[cfg(feature = "cyphernet")]
-impl EcSk for MemorySigner {
- type Pk = PublicKey;
-
- fn generate_keypair() -> (Self, Self::Pk)
- where
- Self: Sized,
- {
- let ms = Self::r#gen();
- let pk = ms.public;
-
- (ms, pk)
- }
-
- fn to_pk(&self) -> Result<Self::Pk, EcSkInvalid> {
- Ok(self.public)
- }
-}
-
-#[cfg(feature = "cyphernet")]
-impl Ecdh for MemorySigner {
- type SharedSecret = [u8; 32];
-
- fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
- self.secret.ecdh(pk).map_err(cyphernet::EcdhError::from)
- }
-}
-
-impl MemorySigner {
- /// Load this signer from a keystore, given a secret key passphrase.
- pub fn load(
- keystore: &Keystore,
- passphrase: Option<Passphrase>,
- ) -> Result<Self, MemorySignerError> {
- let secret = keystore
- .secret_key(passphrase)
- .map_err(|e| {
- if e.is_crypto_err() {
- MemorySignerError::InvalidPassphrase
- } else {
- e.into()
- }
- })?
- .ok_or_else(|| MemorySignerError::NotFound(keystore.secret_key_path().to_path_buf()))?;
-
- let Some(public_path) = keystore.public_key_path() else {
- // There is no public key in the key store, so there's nothing
- // to validate. Derive it from the secret key.
- return Ok(Self::from_secret(secret));
- };
-
- let public = keystore
- .public_key()?
- .ok_or_else(|| MemorySignerError::NotFound(public_path.to_path_buf()))?;
-
- secret
- .validate_public_key(&public.into())
- .map_err(|_| MemorySignerError::KeyMismatch {
- secret: keystore.secret_key_path().to_path_buf(),
- public: public_path.to_path_buf(),
- })?;
-
- Ok(Self { public, secret })
- }
-
- /// Create a new memory signer from the given secret key, deriving
- /// the public key from the secret key.
- pub fn from_secret(secret: Zeroizing<SecretKey>) -> Self {
- Self {
- public: secret.public_key().into(),
- secret,
- }
- }
-
- /// Box this signer into a trait object.
- pub fn boxed(self) -> Box<dyn Signer> {
- Box::new(self)
- }
-
- /// Generate a new memory signer.
- pub fn r#gen() -> Self {
- let keypair = KeyPair::generate();
- let sk = keypair.sk;
-
- Self {
- public: sk.public_key().into(),
- secret: Zeroizing::new(sk.into()),
- }
- }
-}
-
-impl TryFrom<ssh_key::PublicKey> for PublicKey {
- type Error = Error;
-
- fn try_from(public: ssh_key::PublicKey) -> Result<Self, Self::Error> {
- match public.key_data() {
- ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => {
- Ok(Self::from(*data))
- }
- _ => Err(Error::InvalidKeyType),
- }
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use std::borrow::ToOwned as _;
+
#[test]
fn test_init_passphrase() {
let tmp = tempfile::tempdir().unwrap();
let store = Keystore::new(&tmp);
let public = store
- .init(
- "test",
- Some("hunter".to_owned().into()),
- ec25519::Seed::default(),
- )
+ .init("test", Some("hunter".to_owned().into()), Seed::mock(1))
.unwrap();
assert_eq!(public, store.public_key().unwrap().unwrap());
assert!(store.is_encrypted().unwrap());
@@ -403,7 +254,10 @@ mod tests {
.secret_key(Some("hunter".to_owned().into()))
.unwrap()
.unwrap();
- assert_eq!(PublicKey::from(secret.public_key()), public);
+
+ let secret_public = secret.public_key();
+
+ assert_eq!(secret_public, &public);
store
.secret_key(Some("blunder".to_owned().into()))
@@ -415,12 +269,13 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let store = Keystore::new(&tmp);
- let public = store.init("test", None, ec25519::Seed::default()).unwrap();
+ let public = store.init("test", None, Seed::mock(1)).unwrap();
assert_eq!(public, store.public_key().unwrap().unwrap());
assert!(!store.is_encrypted().unwrap());
let secret = store.secret_key(None).unwrap().unwrap();
- assert_eq!(PublicKey::from(secret.public_key()), public);
+ let secret_public = secret.public_key();
+ assert_eq!(secret_public, &public);
}
#[test]
@@ -429,14 +284,10 @@ mod tests {
let store = Keystore::new(&tmp);
let public = store
- .init(
- "test",
- Some("hunter".to_owned().into()),
- ec25519::Seed::default(),
- )
+ .init("test", Some("hunter".to_owned().into()), Seed::mock(1))
.unwrap();
- let signer = MemorySigner::load(&store, Some("hunter".to_owned().into())).unwrap();
+ let signer = SigningKey::load(&store, Some("hunter".to_owned().into())).unwrap();
- assert_eq!(public, *signer.public_key());
+ assert_eq!(&public, signer.public_key());
}
}
diff --git a/crates/radicle-crypto/src/test.rs b/crates/radicle-crypto/src/test.rs
deleted file mode 100644
index cadc1f12a..000000000
--- a/crates/radicle-crypto/src/test.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod arbitrary;
-pub mod signer;
diff --git a/crates/radicle-crypto/src/test/arbitrary.rs b/crates/radicle-crypto/src/test/arbitrary.rs
deleted file mode 100644
index fdc072f78..000000000
--- a/crates/radicle-crypto/src/test/arbitrary.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use qcheck::Arbitrary;
-
-use crate::{KeyPair, PublicKey, SecretKey, Seed, test::signer::MockSigner};
-
-impl Arbitrary for MockSigner {
- fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let bytes: [u8; 32] = Arbitrary::arbitrary(g);
- let seed = Seed::new(bytes);
- let sk = KeyPair::from_seed(seed).sk;
-
- MockSigner::from(SecretKey::from(sk))
- }
-}
-
-impl Arbitrary for PublicKey {
- fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let bytes: [u8; 32] = Arbitrary::arbitrary(g);
- let seed = Seed::new(bytes);
- let keypair = KeyPair::from_seed(seed);
-
- keypair.pk.into()
- }
-}
diff --git a/crates/radicle-crypto/src/test/signer.rs b/crates/radicle-crypto/src/test/signer.rs
deleted file mode 100644
index 181eea1de..000000000
--- a/crates/radicle-crypto/src/test/signer.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-#[cfg(feature = "ssh")]
-use crate::ssh::ExtendedSignature;
-use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature};
-
-#[derive(Debug, Clone)]
-pub struct MockSigner {
- pk: PublicKey,
- sk: SecretKey,
-}
-
-#[cfg(feature = "ssh")]
-impl signature::Signer<ExtendedSignature> for MockSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
- use signature::Keypair as _;
- Ok(ExtendedSignature {
- key: self.verifying_key(),
- sig: self.try_sign(msg)?,
- })
- }
-}
-
-impl signature::Signer<Signature> for MockSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
- Ok(Signature(self.sk.sign(msg, None)))
- }
-}
-
-impl signature::Verifier<Signature> for MockSigner {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> {
- self.pk
- .verify(msg, signature)
- .map_err(signature::Error::from_source)
- }
-}
-
-impl AsRef<PublicKey> for MockSigner {
- fn as_ref(&self) -> &PublicKey {
- &self.pk
- }
-}
-
-impl signature::KeypairRef for MockSigner {
- type VerifyingKey = PublicKey;
-}
-
-impl MockSigner {
- pub fn new(rng: &mut fastrand::Rng) -> Self {
- let mut seed: [u8; 32] = [0; 32];
-
- for byte in &mut seed {
- *byte = rng.u8(..);
- }
- Self::from_seed(seed)
- }
-
- pub fn from_seed(seed: [u8; 32]) -> Self {
- let seed = Seed::new(seed);
- let keypair = KeyPair::from_seed(seed);
-
- Self::from(SecretKey::from(keypair.sk))
- }
-}
-
-impl From<SecretKey> for MockSigner {
- fn from(sk: SecretKey) -> Self {
- let pk = sk.public_key().into();
- Self { sk, pk }
- }
-}
-
-impl Default for MockSigner {
- fn default() -> Self {
- let seed = Seed::generate();
- let keypair = KeyPair::from_seed(seed);
- let sk = keypair.sk;
-
- Self {
- pk: sk.public_key().into(),
- sk: sk.into(),
- }
- }
-}
-
-impl PartialEq for MockSigner {
- fn eq(&self, other: &Self) -> bool {
- self.pk == other.pk
- }
-}
-
-impl Eq for MockSigner {}
-
-impl std::hash::Hash for MockSigner {
- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- self.pk.hash(state)
- }
-}
-
-#[cfg(feature = "cyphernet")]
-impl cyphernet::EcSk for MockSigner {
- type Pk = PublicKey;
-
- // TODO: Should be renamed to 'generate'.
- fn generate_keypair() -> (Self, Self::Pk)
- where
- Self: Sized,
- {
- let kp = Self::default();
- let pk = kp.pk;
-
- (kp, pk)
- }
-
- fn to_pk(&self) -> Result<Self::Pk, cyphernet::EcSkInvalid> {
- Ok(self.pk)
- }
-}
-
-#[cfg(feature = "cyphernet")]
-impl cyphernet::Ecdh for MockSigner {
- type SharedSecret = [u8; 32];
-
- fn ecdh(&self, pk: &Self::Pk) -> Result<Self::SharedSecret, cyphernet::EcdhError> {
- self.sk.ecdh(pk).map_err(|_| cyphernet::EcdhError::WeakPk)
- }
-}
diff --git a/crates/radicle-node/src/fingerprint.rs b/crates/radicle-node/src/fingerprint.rs
index cd765a939..ef215340e 100644
--- a/crates/radicle-node/src/fingerprint.rs
+++ b/crates/radicle-node/src/fingerprint.rs
@@ -22,6 +22,7 @@
use thiserror::Error;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::profile::Home;
/// Fingerprint of a public key.
@@ -64,30 +65,24 @@ impl Fingerprint {
}
/// Initialize the fingerprint of the node with given public key.
- pub fn init(
- home: &Home,
- secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
- ) -> Result<(), Error> {
- let public_key = secret_key.deref().public_key().into();
+ pub fn init(home: &Home, secret_key: &crypto::SigningKey) -> Result<(), Error> {
+ let public_key = secret_key.public_key();
let mut file = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(path(home))?;
{
use std::io::Write as _;
- file.write_all(crypto::ssh::fmt::fingerprint(&public_key).as_ref())?;
+ file.write_all(crypto::ssh::fmt::fingerprint(public_key).as_ref())?;
}
Ok(())
}
/// Verify that the fingerprint of given public key matches self.
- pub fn verify(
- &self,
- secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
- ) -> FingerprintVerification {
- let public_key = secret_key.deref().public_key().into();
- if crypto::ssh::fmt::fingerprint(&public_key) == self.0 {
+ pub fn verify(&self, secret_key: &crypto::SigningKey) -> FingerprintVerification {
+ let public_key = secret_key.public_key();
+ if crypto::ssh::fmt::fingerprint(public_key) == self.0 {
FingerprintVerification::Match
} else {
FingerprintVerification::Mismatch
@@ -104,7 +99,7 @@ fn path(home: &Home) -> std::path::PathBuf {
mod tests {
use super::*;
- use crypto::ssh::Keystore;
+ use crypto::{Seed, ssh::Keystore};
#[test]
fn matching() {
@@ -112,7 +107,7 @@ mod tests {
let home = Home::new(tmp.path()).unwrap();
let store = Keystore::new(&home.keys());
- store.init("test 1", None, crypto::Seed::default()).unwrap();
+ store.init("test 1", None, Seed::mock(1)).unwrap();
let secret = store.secret_key(None).unwrap().unwrap();
assert_eq!(Fingerprint::read(&home).unwrap(), None);
@@ -124,7 +119,7 @@ mod tests {
// Generate a new keypair, which does not match the fingerprint.
// This simulates the user modifying `~/.radicle/keys`.
std::fs::remove_dir_all(home.keys()).unwrap();
- store.init("test 1", None, crypto::Seed::default()).unwrap();
+ store.init("test 1", None, Seed::mock(2)).unwrap();
let other_secret = store.secret_key(None).unwrap().unwrap();
assert_ne!(secret, other_secret);
diff --git a/crates/radicle-node/src/main.rs b/crates/radicle-node/src/main.rs
index c758f0477..38ec395d4 100644
--- a/crates/radicle-node/src/main.rs
+++ b/crates/radicle-node/src/main.rs
@@ -6,10 +6,11 @@ use std::str::FromStr;
use thiserror::Error;
-use radicle::node::device::Device;
+use radicle::crypto::Signer as _;
+use radicle::node::NodeId;
use radicle::profile;
-use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
+use radicle_node::crypto::ssh::keystore::Keystore;
use radicle_node::fingerprint::{Fingerprint, FingerprintVerification};
use radicle_node::{Runtime, VERSION};
use radicle_signals as signals;
@@ -313,7 +314,7 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
let keystore = Keystore::from_secret_path(&secret_path);
- let secret_key = keystore
+ let signer = keystore
.secret_key(passphrase.clone())
.map_err(|err| ExecutionError::SecretLoading {
path: secret_path.clone(),
@@ -325,7 +326,7 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
if let Some(fp) = Fingerprint::read(&home)? {
log::debug!(target: "node", "Verifying fingerprint..");
- if fp.verify(&secret_key) != FingerprintVerification::Match {
+ if fp.verify(&signer) != FingerprintVerification::Match {
return Err(ExecutionError::FingerprintMismatch {
secret: keystore.secret_key_path().to_path_buf(),
fingerprint: fp,
@@ -333,11 +334,10 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
}
} else {
log::info!(target: "node", "Initializing fingerprint..");
- Fingerprint::init(&home, &secret_key)?;
+ Fingerprint::init(&home, &signer)?;
}
- let signer = Device::from(MemorySigner::from_secret(secret_key));
- log::info!(target: "node", "Node ID is {}", signer.public_key());
+ log::info!(target: "node", "Node ID is {}", NodeId::from(*signer.public_key()));
// Add the preferred seeds as persistent peers so that we reconnect to them automatically.
config.node.connect.extend(config.preferred_seeds);
diff --git a/crates/radicle-node/src/runtime.rs b/crates/radicle-node/src/runtime.rs
index 6f142b704..8fd326597 100644
--- a/crates/radicle-node/src/runtime.rs
+++ b/crates/radicle-node/src/runtime.rs
@@ -12,13 +12,11 @@ use std::os::unix::net::UnixListener;
#[cfg(windows)]
use uds_windows::UnixListener;
-use cyphernet::Ecdh;
use radicle::cob::migrate;
-use radicle::crypto;
-use radicle::node::device::Device;
use radicle_signals::Signal;
use thiserror::Error;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::node;
use radicle::node::Event;
use radicle::node::UserAgent;
@@ -124,22 +122,15 @@ impl Runtime {
/// Initialize the runtime.
///
/// This function spawns threads.
- pub fn init<G>(
+ pub fn init(
home: Home,
config: radicle::node::Config,
socket: PathBuf,
listen: Vec<net::SocketAddr>,
signals: mpsc::Receiver<Signal>,
- signer: Device<G>,
- ) -> Result<Runtime, Error>
- where
- G: crypto::signature::Signer<crypto::Signature>
- + Ecdh<Pk = NodeId>
- + Clone
- + Debug
- + 'static,
- {
- let id = *signer.public_key();
+ secret_key: SigningKey,
+ ) -> Result<Runtime, Error> {
+ let id = NodeId::from(*secret_key.public_key());
let alias = config.alias.clone();
let network = config.network;
let rng = fastrand::Rng::new();
@@ -215,7 +206,7 @@ impl Runtime {
stores,
storage.clone(),
policies,
- signer.clone(),
+ secret_key.clone(),
rng,
announcement,
emitter.clone(),
@@ -224,7 +215,7 @@ impl Runtime {
let (worker_send, worker_recv) =
crossbeam_channel::bounded::<worker::Task>(MAX_PENDING_TASKS);
- let mut wire = Wire::new(service, worker_send, signer.clone());
+ let mut wire = Wire::new(service, worker_send, secret_key.clone());
let mut local_addrs = Vec::new();
for addr in listen {
@@ -237,7 +228,7 @@ impl Runtime {
let reactor = Reactor::new(wire, thread::name(&id, "service"))?;
let handle = Handle::new(home.clone(), socket.clone(), reactor.controller(), emitter);
- let nid = *signer.public_key();
+ let nid = NodeId::from(*secret_key.public_key());
let fetch = worker::FetchConfig {
local: nid,
expiry: worker::garbage::Expiry::default(),
diff --git a/crates/radicle-node/src/test/gossip.rs b/crates/radicle-node/src/test/gossip.rs
index 4a774a32f..69a23dbd6 100644
--- a/crates/radicle-node/src/test/gossip.rs
+++ b/crates/radicle-node/src/test/gossip.rs
@@ -1,6 +1,6 @@
+use radicle::crypto::SigningKey;
use radicle::node;
use radicle::node::UserAgent;
-use radicle::node::device::Device;
use radicle::test::fixtures::r#gen;
use crate::test::arbitrary;
@@ -15,7 +15,7 @@ pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Messa
let mut msgs = Vec::new();
for _ in 0..count {
- let signer = Device::mock_rng(&mut rng);
+ let signer = SigningKey::mock(rng.usize(0x1000..0xFFFF));
let time = if delta == LocalDuration::from_secs(0) {
now
} else {
diff --git a/crates/radicle-node/src/test/node.rs b/crates/radicle-node/src/test/node.rs
index 3591bc8a6..0545fc069 100644
--- a/crates/radicle-node/src/test/node.rs
+++ b/crates/radicle-node/src/test/node.rs
@@ -1,4 +1,3 @@
-use std::fmt::Debug;
use std::io::BufRead as _;
use std::mem::ManuallyDrop;
use std::net::Ipv4Addr;
@@ -11,16 +10,12 @@ use std::{
};
use crate::node::NodeId;
-use crate::node::device::Device;
use crate::storage::git::transport;
use crate::{Runtime, runtime, runtime::Handle, service};
use radicle::Storage;
use radicle::cob;
use radicle::cob::issue;
-use radicle::crypto::Signature;
-use radicle::crypto::signature::Signer;
-use radicle::crypto::ssh::keystore::MemorySigner;
-use radicle::crypto::test::signer::MockSigner;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::git;
use radicle::git::fmt::refname;
use radicle::identity::{RepoId, Visibility};
@@ -38,19 +33,19 @@ use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository a
use radicle::test::fixtures;
/// A node that can be run.
-pub struct Node<G> {
+pub struct Node {
pub id: NodeId,
pub home: Home,
- pub signer: Device<G>,
+ pub secret_key: SigningKey,
pub storage: Storage,
pub config: Config,
pub db: service::Stores<Database>,
pub policies: policy::Store<policy::Write>,
}
-impl Node<MemorySigner> {
+impl Node {
pub fn new(profile: Profile) -> Self {
- let signer = Device::from(MemorySigner::load(&profile.keystore, None).unwrap());
+ let secret_key = profile.keystore.secret_key(None).unwrap().unwrap();
let id = *profile.id();
let policies_db = profile.home.node().join(POLICIES_DB_FILE);
let policies = policy::Store::open(policies_db).unwrap();
@@ -61,7 +56,7 @@ impl Node<MemorySigner> {
id,
home: profile.home,
config: profile.config.node,
- signer,
+ secret_key,
db,
policies,
storage: profile.storage,
@@ -70,18 +65,18 @@ impl Node<MemorySigner> {
}
/// Handle to a running node.
-pub struct NodeHandle<G: 'static> {
+pub struct NodeHandle {
pub id: NodeId,
pub alias: Alias,
pub storage: Storage,
- pub signer: Device<G>,
+ pub signer: SigningKey,
pub home: Home,
pub addr: net::SocketAddr,
pub thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
pub handle: ManuallyDrop<Handle>,
}
-impl<G: 'static> Drop for NodeHandle<G> {
+impl Drop for NodeHandle {
fn drop(&mut self) {
log::debug!(target: "test", "Node {} shutting down..", self.id);
@@ -95,12 +90,12 @@ impl<G: 'static> Drop for NodeHandle<G> {
}
}
-impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
+impl NodeHandle {
/// Connect this node to another node, and wait for the connection to be established both ways.
///
/// If the remote has blocked this node, then the remote event will be
/// [`Event::PeerDisconnected`].
- pub fn connect(&mut self, remote: &NodeHandle<G>) -> &mut Self {
+ pub fn connect(&mut self, remote: &NodeHandle) -> &mut Self {
let local_events = self.handle.events();
let remote_events = remote.handle.events();
@@ -130,7 +125,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
self
}
- pub fn disconnect(&mut self, remote: &NodeHandle<G>) {
+ pub fn disconnect(&mut self, remote: &NodeHandle) {
self.handle.disconnect(remote.id).unwrap();
}
@@ -176,7 +171,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
/// Wait until this node's routing table matches the remotes.
pub fn converge<'a>(
&'a self,
- remotes: impl IntoIterator<Item = &'a NodeHandle<G>>,
+ remotes: impl IntoIterator<Item = &'a NodeHandle>,
) -> BTreeSet<(RepoId, NodeId)> {
converge(iter::once(self).chain(remotes))
}
@@ -381,7 +376,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
None => git::fmt::lit::refs_heads(refname).into(),
Some(refname) => refname,
};
- let refname = refname.with_namespace(self.id.to_component());
+ let refname = refname.with_namespace(git::fmt::Component::from(&self.id));
let repo = self.storage.repository(rid).unwrap();
let raw = &repo.backend;
@@ -436,21 +431,18 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
}
}
-impl Node<MockSigner> {
+impl Node {
/// Create a new node.
- pub fn init(base: &Path, config: Config) -> Self {
- let home = base.join(
- iter::repeat_with(fastrand::alphanumeric)
- .take(8)
- .collect::<String>(),
- );
+ pub fn init(base: &Path, config: Config, id: usize) -> Self {
+ let home = base.join(config.alias.to_string());
let home = Home::new(home).unwrap();
- let signer = Device::mock();
+ let secret_key = SigningKey::mock(id);
+ let nid = NodeId::from(*secret_key.public_key());
let storage = Storage::open(
home.storage(),
git::UserInfo {
alias: config.alias.clone(),
- key: *signer.public_key(),
+ key: nid,
},
)
.unwrap();
@@ -460,11 +452,11 @@ impl Node<MockSigner> {
.unwrap();
let db = service::Stores::from(db);
- log::debug!(target: "test", "Node::init {}: {}", config.alias, signer.public_key());
+ log::debug!(target: "test", "Node::init {}: {}", config.alias, nid);
Self {
- id: *signer.public_key(),
+ id: nid,
home,
- signer,
+ secret_key,
storage,
config,
db,
@@ -473,10 +465,11 @@ impl Node<MockSigner> {
}
}
-impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G> {
+impl Node {
/// Spawn a node in its own thread.
- pub fn spawn(self) -> NodeHandle<G> {
+ pub fn spawn(self) -> NodeHandle {
let alias = self.config.alias.clone();
+
let listen = vec![(Ipv4Addr::LOCALHOST, 0).into()];
let (_, signals) = mpsc::sync_channel(1);
let rt = Runtime::init(
@@ -485,11 +478,12 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
self.home.socket_default(),
listen,
signals,
- self.signer.clone(),
+ self.secret_key.clone(),
)
.unwrap();
+
let addr = *rt.local_addrs.first().unwrap();
- let id = *self.signer.public_key();
+ let id = NodeId::from(*self.secret_key.public_key());
let handle = ManuallyDrop::new(rt.handle.clone());
let thread = ManuallyDrop::new(runtime::thread::spawn(&id, "runtime", move || rt.run()));
@@ -497,7 +491,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
id,
alias,
storage: self.storage,
- signer: self.signer,
+ signer: self.secret_key,
home: self.home,
addr,
handle,
@@ -521,7 +515,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
description,
branch.clone(),
Visibility::default(),
- &self.signer,
+ &self.secret_key,
&self.storage,
)
.map(|(id, _, _)| id)
@@ -531,7 +525,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
log::debug!(
target: "test",
- "Initialized project {id} for node {}", self.signer.public_key()
+ "Initialized project {id} for node {}", NodeId::from(*self.secret_key.public_key())
);
// Push local branches to storage.
@@ -558,7 +552,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
self.storage
.repository(id)
.unwrap()
- .sign_refs(&self.signer)
+ .sign_refs(&self.secret_key)
.unwrap();
id
@@ -575,9 +569,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
/// Checks whether the nodes have converged in their routing tables.
#[track_caller]
-pub fn converge<'a, G: Signer<Signature> + cyphernet::Ecdh + 'static>(
- nodes: impl IntoIterator<Item = &'a NodeHandle<G>>,
-) -> BTreeSet<(RepoId, NodeId)> {
+pub fn converge<'a>(nodes: impl IntoIterator<Item = &'a NodeHandle>) -> BTreeSet<(RepoId, NodeId)> {
let nodes = nodes.into_iter().collect::<Vec<_>>();
let mut all_routes = BTreeSet::<(RepoId, NodeId)>::new();
diff --git a/crates/radicle-node/src/test/peer.rs b/crates/radicle-node/src/test/peer.rs
index 97fe7a3a5..b14fa238d 100644
--- a/crates/radicle-node/src/test/peer.rs
+++ b/crates/radicle-node/src/test/peer.rs
@@ -7,20 +7,18 @@ use std::str::FromStr;
use log::*;
use radicle::Storage;
-use radicle::crypto::Signer as _;
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::git::Oid;
use radicle::identity::Visibility;
use radicle::node::Database;
use radicle::node::UserAgent;
use radicle::node::address::Store as _;
-use radicle::node::device::Device;
use radicle::node::{Alias, ConnectOptions, address};
use radicle::rad;
use radicle::storage::refs;
use radicle::storage::refs::{RefsAt, SignedRefs};
use radicle::storage::{ReadRepository, RemoteRepository};
-use crate::crypto::test::signer::MockSigner;
use crate::identity::RepoId;
use crate::node;
use crate::node::routing::Store as _;
@@ -42,7 +40,7 @@ use radicle::node::policy::{Scope, SeedingPolicy};
use radicle_protocol::bounded::BoundedVec;
/// Service instantiation used for testing.
-pub type Service<S> = service::Service<Database, S, MockSigner>;
+pub type Service<S> = service::Service<Database, S>;
pub const AMY: u8 = 0x0A;
pub const BOB: u8 = 0x0B;
@@ -124,24 +122,18 @@ pub struct Config {
pub(crate) config: radicle::node::Config,
pub(crate) local_time: LocalTime,
pub(crate) policy: SeedingPolicy,
- pub(crate) signer: MockSigner,
+ pub(crate) secret_key: SigningKey,
}
impl Config {
pub(crate) fn new(id: usize) -> Self {
let config = radicle::node::Config::test(Alias::from_str("mocky").unwrap());
- let seed = std::iter::repeat_n(id, 32 / std::mem::size_of::<usize>())
- .flat_map(|i| i.to_le_bytes())
- .collect::<Vec<_>>()
- .try_into()
- .unwrap();
-
Config {
config,
local_time: LocalTime::now(),
policy: SeedingPolicy::default(),
- signer: MockSigner::from_seed(seed),
+ secret_key: SigningKey::mock(id),
}
}
}
@@ -156,7 +148,7 @@ impl Peer<Storage> {
description,
radicle::git::fmt::refname!("master"),
Visibility::default(),
- self.signer(),
+ self.secret_key(),
self.storage(),
)
.unwrap();
@@ -198,7 +190,7 @@ where
let tempdir = tempfile::TempDir::with_prefix(name).unwrap();
- let nid = *config.signer.public_key();
+ let nid = *config.secret_key.public_key();
// Initialize database.
let db = Database::open(
@@ -226,7 +218,7 @@ where
db,
storage,
policies,
- Device::new(config.signer),
+ config.secret_key,
fastrand::Rng::with_seed(id as u64),
announcement,
emitter,
@@ -305,7 +297,7 @@ where
inventory: arbitrary::vec(3).try_into().unwrap(),
timestamp: Timestamp::from(*self.clock()),
},
- self.signer(),
+ self.secret_key(),
)
}
@@ -322,7 +314,7 @@ where
}
.solve(0)
.unwrap(),
- self.signer(),
+ self.secret_key(),
)
}
@@ -355,12 +347,12 @@ where
}
pub fn announcement(&self, ann: impl Into<AnnouncementMessage>) -> Message {
- ann.into().signed(self.signer()).into()
+ ann.into().signed(self.secret_key()).into()
}
pub fn signed_refs_at(&self, root: Oid) -> SignedRefs {
arbitrary::with_gen(8, |g| {
- refs::arbitrary::signed_refs_at(g, root, self.signer())
+ refs::arbitrary::signed_refs_at(g, root, self.secret_key())
})
}
diff --git a/crates/radicle-node/src/tests.rs b/crates/radicle-node/src/tests.rs
index 5ed13be4b..c046575e8 100644
--- a/crates/radicle-node/src/tests.rs
+++ b/crates/radicle-node/src/tests.rs
@@ -8,12 +8,11 @@ use std::sync::Arc;
use std::sync::LazyLock;
use std::time;
-use radicle::node::device::Device;
use radicle::storage::ReadRepository;
use test_log::test;
use radicle::cob;
-use radicle::crypto::test::signer::MockSigner;
+use radicle::crypto::SigningKey;
use radicle::identity::Visibility;
use radicle::node::Link;
use radicle::node::address::Store as _;
@@ -259,7 +258,7 @@ fn inventory_sync() {
Storage::open(tmp.path().join("amy"), fixtures::user()).unwrap(),
);
let bob_secret = BOB;
- let bob_signer = Device::mock_from_seed([bob_secret; 32]);
+ let bob_signer = SigningKey::mock(bob_secret as usize);
let bob_storage = fixtures::storage(tmp.path().join("bob"), &bob_signer).unwrap();
let bob = Peer::with_storage("bob", bob_secret, bob_storage);
let now = LocalTime::now().into();
@@ -273,7 +272,7 @@ fn inventory_sync() {
inventory: repos.clone().try_into().unwrap(),
timestamp: now,
},
- bob.signer(),
+ bob.secret_key(),
),
);
@@ -378,7 +377,7 @@ fn inventory_pruning() {
.unwrap(),
timestamp: bob.local_time().into(),
},
- peer.signer(),
+ peer.secret_key(),
),
);
}
@@ -434,7 +433,7 @@ fn inventory_relay_bad_timestamp() {
inventory: BoundedVec::new(),
timestamp,
},
- bob.signer(),
+ bob.secret_key(),
),
);
assert_matches!(
@@ -669,13 +668,9 @@ fn refs_announcement_relay_public() {
let bob = {
const ID: u8 = BOB;
- let secret_key = MockSigner::from_seed([ID; 32]);
- let storage =
- fixtures::storage(tmp.path().join("bob"), &Device::new(secret_key.clone())).unwrap();
-
- Peer::new_with("bob", ID, storage, move |config| {
- config.signer = secret_key;
- })
+ let secret_key = SigningKey::mock(ID as usize);
+ let storage = fixtures::storage(tmp.path().join("bob"), &secret_key).unwrap();
+ Peer::with_storage("bob", ID, storage)
};
let bob_inv = bob.inventory().into_iter().collect::<Vec<_>>();
@@ -741,10 +736,9 @@ fn refs_announcement_relay_private() {
);
let bob = {
- let signer = MockSigner::from_seed([BOB; 32]);
+ let signer = SigningKey::mock(BOB as usize);
- let storage =
- fixtures::storage(tmp.path().join("bob"), &Device::new(signer.clone())).unwrap();
+ let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
Peer::with_storage("bob", BOB, storage)
};
@@ -819,7 +813,7 @@ fn refs_announcement_fetch_trusted_no_inventory() {
let bob = {
let mut rng = fastrand::Rng::new();
let id = rng.u8(100..200);
- let secret_key = Device::mock_from_seed([id; 32]);
+ let secret_key = SigningKey::mock(id as usize);
let storage = fixtures::storage(tmp.path().join("bob"), &secret_key).unwrap();
Peer::with_storage("bob", id, storage)
@@ -917,13 +911,10 @@ fn refs_announcement_no_subscribe() {
fn refs_announcement_offline() {
let tmp = tempfile::tempdir().unwrap();
let mut amy = {
- const ID: u8 = AMY;
- let signer = MockSigner::from_seed([ID; 32]);
- let secret_key = Device::new(signer.clone());
+ let id = AMY;
+ let secret_key = SigningKey::mock(id as usize);
let storage = fixtures::storage(tmp.path().join("amy"), &secret_key).unwrap();
- Peer::new_with("amy", ID, storage, move |config| {
- config.signer = signer;
- })
+ Peer::with_storage("amy", id, storage)
};
let mut bob = Peer::bob();
@@ -955,7 +946,7 @@ fn refs_announcement_offline() {
// Create an issue without telling the node.
let repo = amy.storage().repository(rid).unwrap();
let old_refs = RefsAt::new(&repo, *amy.nid()).unwrap();
- let mut issues = radicle::issue::Cache::no_cache(&repo, amy.signer()).unwrap();
+ let mut issues = radicle::issue::Cache::no_cache(&repo, amy.secret_key()).unwrap();
issues
.create(
cob::Title::new("Issue while offline!").unwrap(),
@@ -1028,7 +1019,7 @@ fn inventory_relay() {
inventory: inv.clone(),
timestamp: now,
},
- bob.signer(),
+ bob.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1055,7 +1046,7 @@ fn inventory_relay() {
inventory: inv.clone(),
timestamp: now,
},
- bob.signer(),
+ bob.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1073,7 +1064,7 @@ fn inventory_relay() {
inventory: inv.clone(),
timestamp: now + 1,
},
- bob.signer(),
+ bob.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1097,7 +1088,7 @@ fn inventory_relay() {
inventory: inv,
timestamp: now,
},
- cid.signer(),
+ cid.secret_key(),
),
)
.elapse(service::GOSSIP_INTERVAL);
@@ -1383,7 +1374,7 @@ fn fetch_missing_inventory_on_gossip() {
inventory: vec![rid].try_into().unwrap(),
timestamp: now.into(),
},
- bob.signer(),
+ bob.secret_key(),
),
);
amy.outbox()
@@ -1407,7 +1398,7 @@ fn fetch_missing_inventory_on_schedule() {
inventory: vec![rid].try_into().unwrap(),
timestamp: now.into(),
},
- bob.signer(),
+ bob.secret_key(),
),
);
amy.fetched(
@@ -1680,7 +1671,7 @@ fn refs_synced_event() {
.unwrap(),
timestamp: Timestamp::from(*bob.clock()),
});
- let msg = ann.signed(bob.signer());
+ let msg = ann.signed(bob.secret_key());
amy.seed(&acme, policy::Scope::All).unwrap();
amy.connect_to(&bob);
@@ -1753,7 +1744,7 @@ fn init_and_seed() {
"amy's repo",
git::fmt::refname!("master"),
Visibility::default(),
- amy.signer(),
+ amy.secret_key(),
amy.storage(),
)
.unwrap();
diff --git a/crates/radicle-node/src/tests/e2e.rs b/crates/radicle-node/src/tests/e2e.rs
index 6ab9579d5..ef0eef878 100644
--- a/crates/radicle-node/src/tests/e2e.rs
+++ b/crates/radicle-node/src/tests/e2e.rs
@@ -3,12 +3,12 @@ use std::{collections::HashSet, thread, time};
use radicle::cob;
use radicle::cob::Title;
use radicle::cob::store::access::{ReadOnly, WriteAs};
-use radicle_crypto::test::signer::MockSigner;
+use radicle::crypto::{Signer as _, SigningKey};
+use radicle::git::fmt::Component;
use test_log::test;
use radicle::git::raw::ErrorExt as _;
use radicle::node::Event;
-use radicle::node::device::Device;
use radicle::node::policy::Scope;
use radicle::node::{Alias, ConnectResult, DEFAULT_TIMEOUT, FetchResult, Handle as _};
use radicle::storage::{
@@ -56,8 +56,8 @@ mod config {
fn test_inventory_sync_basic() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
alice.project("alice", "");
bob.project("bob", "");
@@ -78,9 +78,9 @@ fn test_inventory_sync_basic() {
fn test_inventory_sync_bridge() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let mut eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let mut eve = Node::init(tmp.path(), config::relay("eve"), 42);
alice.project("alice", "");
bob.project("bob", "");
@@ -106,10 +106,10 @@ fn test_inventory_sync_bridge() {
fn test_inventory_sync_ring() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let mut eve = Node::init(tmp.path(), config::relay("eve"));
- let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let mut eve = Node::init(tmp.path(), config::relay("eve"), 42);
+ let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")), 73);
alice.project("alice", "");
bob.project("bob", "");
@@ -141,11 +141,11 @@ fn test_inventory_sync_ring() {
fn test_inventory_sync_star() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let mut eve = Node::init(tmp.path(), config::relay("eve"));
- let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
- let mut dave = Node::init(tmp.path(), Config::test(Alias::new("dave")));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let mut eve = Node::init(tmp.path(), config::relay("eve"), 42);
+ let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")), 73);
+ let mut dave = Node::init(tmp.path(), Config::test(Alias::new("dave")), 91);
alice.project("alice", "");
bob.project("bob", "");
@@ -171,8 +171,8 @@ fn test_inventory_sync_star() {
#[test]
fn test_replication() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -247,8 +247,8 @@ fn test_replication() {
#[test]
fn test_replication_ref_in_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
// Delete one of the signed refs.
@@ -298,9 +298,9 @@ fn test_replication_ref_in_sigrefs() {
#[test]
fn test_replication_invalid() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let carol = Device::mock();
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let carol = SigningKey::mock(8);
let acme = bob.project("acme", "");
let repo = bob.storage.repository_mut(acme).unwrap();
let (_, head) = repo.head().unwrap();
@@ -309,7 +309,8 @@ fn test_replication_invalid() {
// Create some unsigned refs for Carol in Bob's storage.
repo.raw()
.reference(
- &git::fmt::qualified!("refs/heads/carol").with_namespace(carol.public_key().into()),
+ &git::fmt::qualified!("refs/heads/carol")
+ .with_namespace(Component::from(carol.public_key())),
head.into(),
true,
&String::default(),
@@ -352,8 +353,8 @@ fn test_replication_invalid() {
#[test]
fn test_migrated_clone() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
@@ -409,8 +410,8 @@ fn test_migrated_clone() {
#[test]
fn test_dont_fetch_owned_refs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
@@ -440,13 +441,13 @@ fn test_dont_fetch_owned_refs() {
#[test]
fn test_fetch_followed_remotes() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut signers = Vec::with_capacity(5);
{
- for _ in 0..5 {
- let signer = Device::mock();
+ for i in 0..5 {
+ let signer = SigningKey::mock(i);
rad::fork_remote(acme, &alice.id, &signer, &alice.storage).unwrap();
signers.push(signer);
}
@@ -496,13 +497,13 @@ fn test_fetch_followed_remotes() {
#[test]
fn test_missing_remote() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
let mut bob = bob.spawn();
- let carol = Device::mock();
+ let carol = SigningKey::mock(98);
alice.connect(&bob);
converge([&alice, &bob]);
@@ -534,8 +535,8 @@ fn test_missing_remote() {
#[test]
fn test_fetch_preserve_owned_refs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
let mut bob = bob.spawn();
@@ -584,8 +585,8 @@ fn test_fetch_preserve_owned_refs() {
#[test]
fn test_clone() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -645,8 +646,8 @@ fn test_clone() {
#[test]
fn test_fetch_up_to_date() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -678,8 +679,8 @@ fn test_fetch_up_to_date() {
#[test]
fn test_fetch_unseeded() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -712,8 +713,8 @@ fn test_fetch_unseeded() {
fn test_large_fetch() {
let tmp = tempfile::tempdir().unwrap();
let scale = config::scale();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let (repo, _) = fixtures::repository(tmp.path());
fixtures::populate(&repo, scale.max(3));
@@ -763,6 +764,7 @@ fn test_concurrent_fetches() {
relay: radicle::node::config::Relay::Always,
..config::relay("alice")
},
+ 13,
);
let mut bob = Node::init(
tmp.path(),
@@ -771,6 +773,7 @@ fn test_concurrent_fetches() {
relay: radicle::node::config::Relay::Always,
..config::relay("bob")
},
+ 37,
);
for i in 0..repos {
@@ -864,8 +867,8 @@ fn test_concurrent_fetches() {
#[test]
fn test_connection_crossing() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let alice = alice.spawn();
let bob = bob.spawn();
@@ -946,9 +949,9 @@ fn test_connection_crossing() {
fn test_non_fast_forward_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = bob.project("acme", "");
@@ -1060,9 +1063,9 @@ fn test_non_fast_forward_sigrefs() {
fn test_outdated_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = alice.project("acme", "");
@@ -1169,9 +1172,9 @@ fn test_outdated_sigrefs() {
fn test_outdated_delegate_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = alice.project("acme", "");
@@ -1268,8 +1271,8 @@ fn test_outdated_delegate_sigrefs() {
fn missing_default_branch() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let rid = alice.project("acme", "");
@@ -1334,9 +1337,9 @@ fn missing_delegate_default_branch() {
use radicle::storage::git::Repository;
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let seed = Node::init(tmp.path(), config::relay("seed"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let seed = Node::init(tmp.path(), config::relay("seed"), 7);
let rid = alice.project("acme", "");
@@ -1401,7 +1404,7 @@ fn missing_delegate_default_branch() {
let new = repo.identity_doc().unwrap().doc;
assert!(
- new.is_delegate(&bob.signer.public_key().into()),
+ new.is_delegate(&bob_key.into()),
"Bob must be a delegate after the update"
);
}
@@ -1467,9 +1470,9 @@ fn missing_delegate_default_branch() {
fn test_background_foreground_fetch() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
- let eve = Node::init(tmp.path(), config::relay("eve"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
+ let eve = Node::init(tmp.path(), config::relay("eve"), 42);
let rid = alice.project("acme", "");
@@ -1563,10 +1566,10 @@ fn test_background_foreground_fetch() {
/// she is made aware of the changes via the `subscribe` message, and fetches from the seed.
fn test_catchup_on_refs_announcements() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let bob_id = bob.id;
- let seed = Node::init(tmp.path(), config::relay("seed"));
+ let seed = Node::init(tmp.path(), config::relay("seed"), 7);
let acme = alice.project("acme", "");
let mut alice = alice.spawn();
@@ -1600,8 +1603,8 @@ fn test_catchup_on_refs_announcements() {
fn test_multiple_offline_inits() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let acme = alice.project("acme", "");
let radcliffe = alice.project("radcliffe", "");
@@ -1625,7 +1628,7 @@ fn test_multiple_offline_inits() {
#[test]
fn test_channel_reader_limit() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
let limits = radicle::node::config::Limits {
fetch_pack_receive: radicle::node::config::FetchPackSizeLimit::bytes(1000),
..radicle::node::config::Limits::default()
@@ -1636,6 +1639,7 @@ fn test_channel_reader_limit() {
limits,
..config::relay("bob")
},
+ 37,
);
let acme = alice.project("acme", "");
@@ -1670,8 +1674,8 @@ fn test_channel_reader_limit() {
fn test_fetch_emits_canonical_ref_update() {
let tmp = tempfile::tempdir().unwrap();
let scale = config::scale();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let (repo, _) = fixtures::repository(tmp.path());
fixtures::populate(&repo, scale.max(3));
@@ -1722,10 +1726,10 @@ fn test_non_fast_forward_identity_doc() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
- let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
- let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
- let alice_laptop = Node::init(tmp.path(), Config::test(Alias::new("alice-laptop")));
+ let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")), 13);
+ let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")), 37);
+ let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")), 42);
+ let alice_laptop = Node::init(tmp.path(), Config::test(Alias::new("alice-laptop")), 113);
let rid = alice.project("acme", "");
@@ -1735,7 +1739,7 @@ fn test_non_fast_forward_identity_doc() {
let bob_events = bob.handle.events();
let mut eve = eve.spawn();
- let has_issue = |node: &NodeHandle<MockSigner>, issue: &cob::ObjectId| -> bool {
+ let has_issue = |node: &NodeHandle, issue: &cob::ObjectId| -> bool {
let repo = node.storage.repository(rid).unwrap();
repo.contains(**issue).unwrap()
};
@@ -1837,8 +1841,8 @@ fn test_non_fast_forward_identity_doc() {
#[test]
fn test_block_active_connection() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let mut alice = alice.spawn();
let bob = bob.spawn();
@@ -1863,8 +1867,8 @@ fn test_block_active_connection() {
#[test]
fn test_block_prevents_connection() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let mut alice = alice.spawn();
let mut bob = bob.spawn();
@@ -1896,8 +1900,8 @@ fn test_block_prevents_connection() {
#[test]
fn test_block_prevents_fetch() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Node::init(tmp.path(), config::relay("alice"));
- let mut bob = Node::init(tmp.path(), config::relay("bob"));
+ let alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let mut bob = Node::init(tmp.path(), config::relay("bob"), 37);
let rid = bob.project("acme", "");
let mut alice = alice.spawn();
@@ -1919,8 +1923,8 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
let tmp = tempfile::tempdir().unwrap();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let rid = alice.project("acme", "");
@@ -1970,8 +1974,8 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
fn test_fetch_emits_canonical_ref_update_partial_glob() {
let tmp = tempfile::tempdir().unwrap();
let scale = config::scale();
- let mut alice = Node::init(tmp.path(), config::relay("alice"));
- let bob = Node::init(tmp.path(), config::relay("bob"));
+ let mut alice = Node::init(tmp.path(), config::relay("alice"), 13);
+ let bob = Node::init(tmp.path(), config::relay("bob"), 37);
let (repo, _) = fixtures::repository(tmp.path());
let rid = alice.project_from("acme", "", &repo);
diff --git a/crates/radicle-node/src/wire.rs b/crates/radicle-node/src/wire.rs
index 4fbf66d7d..9c4145d16 100644
--- a/crates/radicle-node/src/wire.rs
+++ b/crates/radicle-node/src/wire.rs
@@ -12,13 +12,12 @@ use crossbeam_channel as chan;
use cyphernet::addr::{HostName, InetHost, NetAddr};
use cyphernet::encrypt::noise::{HandshakePattern, Keyset, NoiseState};
use cyphernet::proxy::socks5;
-use cyphernet::{Digest, EcSk, Ecdh, Sha256};
+use cyphernet::{Digest, Sha256};
use localtime::{LocalDuration, LocalTime};
use mio::net::TcpStream;
-use radicle::node::device::Device;
use radicle::collections::{RandomMap, RandomSet};
-use radicle::crypto;
+use radicle::crypto::{self, Signer as _};
use radicle::node::Link;
use radicle::node::NodeId;
#[cfg(any(feature = "i2p", feature = "tor"))]
@@ -66,10 +65,10 @@ pub enum Control {
}
/// Peer session type.
-type WireSession<G> = NoiseSession<G, Sha256, Socks5Session<TcpStream>>;
+type WireSession = NoiseSession<crypto::SigningKey, Sha256, Socks5Session<TcpStream>>;
/// Reactor action.
-type Action<G> = reactor::Action<Listener, Transport<WireSession<G>>>;
+type Action = reactor::Action<Listener, Transport<WireSession>>;
/// A worker stream.
struct Stream {
@@ -293,17 +292,17 @@ impl Peers {
}
/// Wire protocol implementation for a set of peers.
-pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + Ecdh> {
+pub(crate) struct Wire<D, S> {
/// Backing service instance.
- service: Service<D, S, G>,
+ service: Service<D, S>,
/// Worker pool interface.
worker: chan::Sender<Task>,
/// Used for authentication.
- signer: Device<G>,
+ secret_key: radicle::crypto::SigningKey,
/// Node metrics.
metrics: service::Metrics,
/// Internal queue of actions to send to the reactor.
- actions: VecDeque<Action<G>>,
+ actions: VecDeque<Action>,
/// Outbound attempted peers without a session.
outbound: RandomMap<Token, Outbound>,
/// Inbound peers without a session.
@@ -316,19 +315,22 @@ pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + E
tokens: Tokens,
}
-impl<D, S, G> Wire<D, S, G>
+impl<D, S> Wire<D, S>
where
D: service::Store,
S: WriteStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId>,
{
- pub fn new(service: Service<D, S, G>, worker: chan::Sender<Task>, signer: Device<G>) -> Self {
+ pub fn new(
+ service: Service<D, S>,
+ worker: chan::Sender<Task>,
+ secret_key: crypto::SigningKey,
+ ) -> Self {
assert!(service.started().is_some(), "Service must be initialized");
Self {
service,
worker,
- signer,
+ secret_key,
metrics: Metrics::default(),
actions: VecDeque::new(),
inbound: RandomSet::default(),
@@ -482,14 +484,13 @@ where
}
}
-impl<D, S, G> reactor::ReactionHandler for Wire<D, S, G>
+impl<D, S> reactor::ReactionHandler for Wire<D, S>
where
D: service::Store + Send,
S: WriteStorage + Send + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + Send + Debug,
{
type Listener = Listener;
- type Transport = Transport<WireSession<G>>;
+ type Transport = Transport<WireSession>;
fn tick(&mut self) {
self.metrics.open_channels = self
@@ -538,11 +539,7 @@ where
return;
}
- let session = accept::<G>(
- remote.clone().into(),
- connection,
- self.signer.clone().into_inner(),
- );
+ let session = accept(remote.clone().into(), connection, self.secret_key.clone());
let transport = match Transport::with_session(session) {
Ok(transport) => transport,
Err(err) => {
@@ -580,13 +577,13 @@ where
}
}
- fn transport_reacted(&mut self, token: Token, event: SessionEvent<WireSession<G>>, _: Instant) {
+ fn transport_reacted(&mut self, token: Token, event: SessionEvent<WireSession>, _: Instant) {
match event {
SessionEvent::Established(ProtocolArtifact { state, session }) => {
// SAFETY: With the NoiseXK protocol, there is always a remote static key.
- let nid: NodeId = state.remote_static_key.unwrap();
+ let nid: NodeId = NodeId::from(*state.remote_static_key.unwrap().public_key());
// Make sure we don't try to connect to ourselves by mistake.
- if &nid == self.signer.public_key() {
+ if &nid == self.secret_key.public_key() {
log::warn!(target: "wire", "Self-connection detected, disconnecting..");
self.disconnect(token, DisconnectReason::SelfConnection);
@@ -628,7 +625,7 @@ where
use Precedence::*;
// Whether we have precedence in case of conflicting connections.
- let precedence = if *self.signer.public_key() > nid {
+ let precedence = if NodeId::from(*self.secret_key.public_key()) > nid {
Ours
} else {
Theirs
@@ -830,7 +827,7 @@ where
}
}
- fn handle_error(&mut self, err: reactor::Error<Listener, Transport<WireSession<G>>>) {
+ fn handle_error(&mut self, err: reactor::Error<Listener, Transport<WireSession>>) {
match err {
reactor::Error::Poll(err) | reactor::Error::Registration(err) => {
// TODO: This should be a fatal error, there's nothing we can do here.
@@ -912,13 +909,12 @@ where
}
}
-impl<D, S, G> Iterator for Wire<D, S, G>
+impl<D, S> Iterator for Wire<D, S>
where
D: service::Store,
S: WriteStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone,
{
- type Item = Action<G>;
+ type Item = Action;
fn next(&mut self) -> Option<Self::Item> {
while let Some(ev) = self.service.next() {
@@ -964,13 +960,13 @@ where
self.service.attempted(node_id, addr.clone());
self.metrics.peer(node_id).outbound_connection_attempts += 1;
- match dial::<G>(
+ match dial(
(*addr).clone(),
node_id,
- self.signer.clone().into_inner(),
+ self.secret_key.clone(),
self.service.config(),
)
- .and_then(Transport::<WireSession<G>>::with_session)
+ .and_then(Transport::<WireSession>::with_session)
{
Ok(transport) => {
let token = self.tokens.advance();
@@ -1087,12 +1083,16 @@ where
}
/// Establish a new outgoing connection.
-pub fn dial<G: Ecdh<Pk = NodeId>>(
+pub fn dial(
remote_addr: NetAddr<HostName>,
- remote_id: <G as EcSk>::Pk,
- signer: G,
+ remote_id: crypto::PublicKey,
+ signer: crypto::SigningKey,
config: &radicle::node::Config,
-) -> io::Result<WireSession<G>> {
+) -> io::Result<WireSession> {
+ // TODO: Nicer error.
+ let remote_id = crypto::VerifyingKey::try_from(&remote_id)
+ .map_err(|source| io::Error::new(io::ErrorKind::InvalidInput, source.to_string()))?;
+
#[cfg(any(feature = "i2p", feature = "tor"))]
fn proxy_or_forward<H: std::fmt::Display>(
config: &AddressConfig,
@@ -1159,7 +1159,7 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
// Whether to tunnel regular connections through the proxy.
let force_proxy = config.proxy.is_some();
- Ok(session::<G>(
+ Ok(session(
remote_addr,
Some(remote_id),
connection,
@@ -1169,22 +1169,22 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
}
/// Accept a new connection.
-pub fn accept<G: Ecdh<Pk = NodeId>>(
+pub fn accept(
remote_addr: NetAddr<HostName>,
connection: TcpStream,
- signer: G,
-) -> WireSession<G> {
- session::<G>(remote_addr, None, connection, false, signer)
+ secret_key: crypto::SigningKey,
+) -> WireSession {
+ session(remote_addr, None, connection, false, secret_key)
}
/// Create a new [`WireSession`].
-fn session<G: Ecdh<Pk = NodeId>>(
+fn session(
remote_addr: NetAddr<HostName>,
- remote_id: Option<NodeId>,
+ remote_id: Option<crypto::VerifyingKey>,
connection: TcpStream,
force_proxy: bool,
- signer: G,
-) -> WireSession<G> {
+ secret_key: crypto::SigningKey,
+) -> WireSession {
if let Err(e) = connection.set_nodelay(true) {
log::warn!(target: "wire", "Unable to set TCP_NODELAY on socket {connection:?}: {e}");
}
@@ -1256,11 +1256,9 @@ fn session<G: Ecdh<Pk = NodeId>>(
};
let noise = {
- let pair = G::generate_keypair();
-
let keyset = Keyset {
- e: pair.0,
- s: Some(signer),
+ e: crypto::SigningKey::from_seed(crypto::Seed::new([91; 32])),
+ s: Some(secret_key),
re: None,
rs: remote_id,
};
@@ -1289,7 +1287,6 @@ mod logger {
#[cfg(test)]
mod test {
use super::*;
- use crate::crypto::test::signer::MockSigner;
use radicle_protocol::service::ServiceState as _;
@@ -1430,7 +1427,7 @@ mod test {
// Returns the wire, the repo id, and bob's id/address.
#[allow(clippy::type_complexity)]
fn wire_with_active_fetch() -> (
- Wire<crate::node::Database, MockStorage, MockSigner>,
+ Wire<crate::node::Database, MockStorage>,
RepoId,
NodeId,
NetAddr<HostName>,
@@ -1459,7 +1456,11 @@ mod test {
assert!(alice.fetcher().active_fetches().contains_key(&rid));
let (worker_tx, _worker_rx) = chan::unbounded::<Task>();
- let wire = Wire::new(alice.into_service(), worker_tx, Device::mock());
+ let wire = Wire::new(
+ alice.into_service(),
+ worker_tx,
+ crypto::SigningKey::mock(100),
+ );
(wire, rid, bob_id, bob_addr)
}
diff --git a/crates/radicle-protocol/src/fetcher/test/state/multinode.rs b/crates/radicle-protocol/src/fetcher/test/state/multinode.rs
index fb5c3c4bc..8dedccd3c 100644
--- a/crates/radicle-protocol/src/fetcher/test/state/multinode.rs
+++ b/crates/radicle-protocol/src/fetcher/test/state/multinode.rs
@@ -1,4 +1,6 @@
+use radicle::crypto::{Signer as _, SigningKey};
use radicle::test::arbitrary;
+
use radicle_core::{NodeId, RepoId};
use crate::fetcher::state::{command, event};
@@ -62,11 +64,13 @@ fn independent_queues() {
#[test]
fn high_count() {
+ const N: usize = 100;
+
let mut state = FetcherState::new(helpers::config(1, 10));
let config = FetchConfig::default();
- for i in 0..100 {
- let node: NodeId = arbitrary::r#gen(i + 1);
+ for i in 0..N {
+ let node: NodeId = *SigningKey::mock(i * 20).public_key();
let repo: RepoId = arbitrary::r#gen(i + 1);
let event = state.fetch(command::Fetch {
from: node,
@@ -77,5 +81,5 @@ fn high_count() {
assert!(matches!(event, event::Fetch::Started { .. }));
}
- assert_eq!(state.active_fetches().len(), 100);
+ assert_eq!(state.active_fetches().len(), N);
}
diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs
index 5f541cd2f..2b7115787 100644
--- a/crates/radicle-protocol/src/service.rs
+++ b/crates/radicle-protocol/src/service.rs
@@ -29,7 +29,6 @@ use radicle::node::address;
use radicle::node::address::Store as _;
use radicle::node::address::{AddressBook, AddressType, KnownAddress};
use radicle::node::config::{PeerConfig, RateLimit};
-use radicle::node::device::Device;
use radicle::node::refs::Store as _;
use radicle::node::routing::Store as _;
use radicle::node::seed;
@@ -59,6 +58,7 @@ use radicle::storage::{Namespaces, ReadStorage, refs::RefsAt};
// use radicle::worker::fetch;
// use crate::worker::FetchError;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::node::Link;
use radicle::node::PROTOCOL_VERSION;
@@ -324,11 +324,11 @@ impl<D> From<D> for Stores<D> {
/// The node service.
#[derive(Debug)]
-pub struct Service<D, S, G> {
+pub struct Service<D, S> {
/// Service configuration.
config: Config,
/// Our cryptographic signer and key.
- signer: Device<G>,
+ secret_key: crypto::SigningKey,
/// Project storage.
storage: S,
/// Node database.
@@ -381,12 +381,7 @@ pub struct Service<D, S, G> {
metrics: Metrics,
}
-impl<D, S, G> Service<D, S, G> {
- /// Get the local node id.
- pub fn node_id(&self) -> NodeId {
- *self.signer.public_key()
- }
-
+impl<D, S> Service<D, S> {
/// Get the local service time.
pub fn local_time(&self) -> LocalTime {
self.clock
@@ -397,18 +392,17 @@ impl<D, S, G> Service<D, S, G> {
}
}
-impl<D, S, G> Service<D, S, G>
+impl<D, S> Service<D, S>
where
D: Store,
S: WriteStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature>,
{
/// Initialize service with current time. Call this once.
pub fn initialize(&mut self, time: LocalTime) -> Result<(), Error> {
debug!(target: "service", "Init @{}", time.as_millis());
assert_ne!(time, LocalTime::default());
- let nid = self.node_id();
+ let nid = *self.nid();
self.clock = time;
self.started_at = Some(time);
@@ -533,7 +527,7 @@ where
let repo = self.storage.repository_mut(rid)?;
// NOTE: We assume to reach `FeatureLevel::LATEST` by signing refs.
- let refs = repo.force_sign_refs(&self.signer)?;
+ let refs = repo.force_sign_refs(&self.secret_key)?;
let repo = self.storage.repository(rid)?;
let synced_at = SyncedAt::new(refs.at, &repo)?;
@@ -545,18 +539,17 @@ where
}
}
-impl<D, S, G> Service<D, S, G>
+impl<D, S> Service<D, S>
where
D: Store,
S: ReadStorage + 'static,
- G: crypto::signature::Signer<crypto::Signature>,
{
pub fn new(
config: Config,
db: Stores<D>,
storage: S,
policies: policy::Config<Write>,
- signer: Device<G>,
+ secret_key: crypto::SigningKey,
rng: Rng,
node: NodeAnnouncement,
emitter: Emitter<Event>,
@@ -579,7 +572,7 @@ where
config,
storage,
policies,
- signer,
+ secret_key,
rng,
inventory,
node,
@@ -679,9 +672,10 @@ where
&self.policies
}
- /// Get the local signer.
- pub fn signer(&self) -> &Device<G> {
- &self.signer
+ /// Get the local secret key.
+ #[cfg(any(test, feature = "test"))]
+ pub fn secret_key(&self) -> &radicle::crypto::SigningKey {
+ &self.secret_key
}
/// Subscriber to inner `Emitter` events.
@@ -1756,7 +1750,7 @@ where
remote: &NodeId,
message: Message,
) -> Result<(), session::Error> {
- let local = self.node_id();
+ let local = *self.nid();
let relay = self.config.is_relay();
let Some(peer) = self.sessions.get_mut(remote) else {
debug!(target: "service", "Session not found for {remote}");
@@ -1918,7 +1912,7 @@ where
},
};
// Remove our own remote, we don't want to fetch that.
- refs.want.retain(|r| r.remote != self.node_id());
+ refs.want.retain(|r| r.remote != *self.nid());
Ok(refs)
}
@@ -1955,8 +1949,8 @@ where
debug!(target: "service", "Subscribing to messages since timestamp {since}..");
vec![
- Message::node(self.node.clone(), &self.signer),
- Message::inventory(self.inventory.clone(), &self.signer),
+ Message::node(self.node.clone(), &self.secret_key),
+ Message::inventory(self.inventory.clone(), &self.secret_key),
Message::subscribe(filter, since, Timestamp::MAX),
]
}
@@ -1972,7 +1966,7 @@ where
/// Remove a local repository from our inventory.
fn remove_inventory(&mut self, rid: &RepoId) -> Result<bool, Error> {
- let node = self.node_id();
+ let node = *self.nid();
let now = self.timestamp();
let removed = self.db.routing_mut().remove_inventory(rid, &node)?;
@@ -1984,7 +1978,7 @@ where
/// Add a local repository to our inventory.
fn add_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
- let node = self.node_id();
+ let node = *self.nid();
let now = self.timestamp();
if !self.storage.contains(&rid)? {
@@ -2119,7 +2113,7 @@ where
refs: refs.clone(),
timestamp,
});
- Ok((msg.signed(&self.signer), refs.into()))
+ Ok((msg.signed(&self.secret_key), refs.into()))
}
/// Announce our own refs for the given repo.
@@ -2208,7 +2202,7 @@ where
fn connect(&mut self, nid: NodeId, addr: Address) -> Result<(), ConnectError> {
debug!(target: "service", "Connecting to {nid} ({addr})..");
- if nid == self.node_id() {
+ if nid == *self.nid() {
return Err(ConnectError::SelfConnection);
}
if let Ok(true) = self.policies.is_blocked(&nid) {
@@ -2358,7 +2352,7 @@ where
fn relay_announcements(&mut self) -> Result<(), Error> {
let now = self.clock.into();
let rows = self.database_mut().gossip_mut().relays(now)?;
- let local = self.node_id();
+ let local = *self.nid();
for (id, msg) in rows {
let announcer = msg.node;
@@ -2382,7 +2376,7 @@ where
let msg = AnnouncementMessage::from(self.inventory.clone());
self.outbox.announce(
- msg.signed(&self.signer),
+ msg.signed(&self.secret_key),
self.sessions.connected().map(|(_, p)| p),
self.db.gossip_mut(),
);
@@ -2396,7 +2390,7 @@ where
}
let delta = count - usize::from(self.config.limits.routing_max_size);
- let nid = self.node_id();
+ let nid = *self.nid();
self.db.routing_mut().prune(
(*now - LocalDuration::from(self.config.limits.routing_max_age)).into(),
Some(delta),
@@ -2501,7 +2495,7 @@ where
continue;
}
}
- match self.seeds(&rid, [self.node_id()].into()) {
+ match self.seeds(&rid, [*self.nid()].into()) {
Ok(seeds) => {
if let Some(connected) = NonEmpty::from_vec(seeds.connected().collect()) {
for seed in connected {
@@ -2697,14 +2691,13 @@ pub trait ServiceState {
fn metrics(&self) -> &Metrics;
}
-impl<D, S, G> ServiceState for Service<D, S, G>
+impl<D, S> ServiceState for Service<D, S>
where
D: routing::Store,
- G: crypto::signature::Signer<crypto::Signature>,
S: ReadStorage,
{
fn nid(&self) -> &NodeId {
- self.signer.public_key()
+ self.secret_key.public_key()
}
fn sessions(&self) -> &Sessions {
diff --git a/crates/radicle-protocol/src/service/gossip/store.rs b/crates/radicle-protocol/src/service/gossip/store.rs
index 914b1b564..27762a926 100644
--- a/crates/radicle-protocol/src/service/gossip/store.rs
+++ b/crates/radicle-protocol/src/service/gossip/store.rs
@@ -130,7 +130,7 @@ impl Store for Database {
stmt.bind((4, &msg.encode_to_vec()[..]))?;
}
}
- stmt.bind((5, &ann.signature))?;
+ stmt.bind((5, sql::Value::Binary(ann.signature.to_vec())))?;
stmt.bind((6, &ann.message.timestamp()))?;
if let Some(row) = stmt.into_iter().next() {
@@ -341,7 +341,7 @@ impl TryFrom<&sql::Value> for GossipType {
mod parse {
use super::*;
- pub fn announcement(row: sql::Row) -> Result<(AnnouncementId, Announcement), Error> {
+ pub fn announcement(mut row: sql::Row) -> Result<(AnnouncementId, Announcement), Error> {
let id = row.try_read::<i64, _>("rowid")? as AnnouncementId;
let node = row.try_read::<NodeId, _>("node")?;
let gt = row.try_read::<GossipType, _>("type")?;
@@ -359,7 +359,25 @@ mod parse {
AnnouncementMessage::Node(ann)
}
};
- let signature = row.try_read::<Signature, _>("signature")?;
+
+ let signature = match row.take("signature") {
+ sql::Value::Binary(bytes) => {
+ Signature::try_from(bytes.as_slice()).map_err(|source| sql::Error {
+ code: None,
+ message: Some(format!("invalid signature: {source}")),
+ })?
+ }
+ value => {
+ return Err(sql::Error {
+ code: None,
+ message: Some(format!(
+ "invalid value of type '{:?}' for row signature",
+ value.kind()
+ )),
+ })?;
+ }
+ };
+
let timestamp = row.try_read::<Timestamp, _>("timestamp")?;
debug_assert_eq!(timestamp, message.timestamp());
@@ -382,8 +400,8 @@ mod test {
use crate::bounded::BoundedVec;
use localtime::LocalTime;
use radicle::assert_matches;
+ use radicle::crypto::SigningKey;
use radicle::identity::RepoId;
- use radicle::node::device::Device;
use radicle::test::arbitrary;
#[test]
@@ -392,18 +410,18 @@ mod test {
let nid = arbitrary::r#gen::<NodeId>(1);
let rid = arbitrary::r#gen::<RepoId>(1);
let timestamp = LocalTime::now().into();
- let signer = Device::mock();
+ let secret_key = SigningKey::mock(100);
let refs = AnnouncementMessage::Refs(RefsAnnouncement {
rid,
refs: BoundedVec::new(),
timestamp,
})
- .signed(&signer);
+ .signed(&secret_key);
let inv = AnnouncementMessage::Inventory(InventoryAnnouncement {
inventory: BoundedVec::new(),
timestamp,
})
- .signed(&signer);
+ .signed(&secret_key);
// Only the first announcement of each type is recognized as new.
let id1 = db.announced(&nid, &refs).unwrap().unwrap();
diff --git a/crates/radicle-protocol/src/service/message.rs b/crates/radicle-protocol/src/service/message.rs
index 945fd0e6e..705b27abc 100644
--- a/crates/radicle-protocol/src/service/message.rs
+++ b/crates/radicle-protocol/src/service/message.rs
@@ -5,10 +5,10 @@ use bytes::{Buf, BufMut};
use nonempty::NonEmpty;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::git;
use radicle::identity::RepoId;
use radicle::node;
-use radicle::node::device::Device;
use radicle::node::{Address, Alias, UserAgent};
use radicle::storage;
use radicle::storage::refs::RefsAt;
@@ -261,18 +261,15 @@ pub enum AnnouncementMessage {
impl AnnouncementMessage {
/// Sign this announcement message.
- pub fn signed<G>(self, signer: &Device<G>) -> Announcement
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ pub fn signed(self, secret_key: &crypto::SigningKey) -> Announcement {
use crypto::signature::Signer as _;
let msg = self.encode_to_vec();
- let signature = signer.sign(&msg);
+ let signature = secret_key.sign(&msg);
Announcement {
- node: *signer.public_key(),
+ node: *secret_key.public_key(),
message: self,
signature,
}
@@ -368,8 +365,15 @@ impl Announcement {
/// Verify this announcement's signature.
pub fn verify(&self) -> bool {
+ use crypto::signature::Verifier as _;
+
+ let Ok(verifier) = crypto::VerifyingKey::try_from(&self.node) else {
+ // Public key is not a valid verifying key, so the signature cannot be valid.
+ return false;
+ };
+
let msg = self.message.encode_to_vec();
- self.node.verify(msg, &self.signature).is_ok()
+ verifier.verify(&msg, &self.signature).is_ok()
}
pub fn matches(&self, filter: &Filter) -> bool {
@@ -449,18 +453,12 @@ impl Message {
.into()
}
- pub fn node<G: crypto::signature::Signer<crypto::Signature>>(
- message: NodeAnnouncement,
- signer: &Device<G>,
- ) -> Self {
- AnnouncementMessage::from(message).signed(signer).into()
+ pub fn node(message: NodeAnnouncement, secret_key: &crypto::SigningKey) -> Self {
+ AnnouncementMessage::from(message).signed(secret_key).into()
}
- pub fn inventory<G: crypto::signature::Signer<crypto::Signature>>(
- message: InventoryAnnouncement,
- signer: &Device<G>,
- ) -> Self {
- AnnouncementMessage::from(message).signed(signer).into()
+ pub fn inventory(message: InventoryAnnouncement, secret_key: &crypto::SigningKey) -> Self {
+ AnnouncementMessage::from(message).signed(secret_key).into()
}
pub fn subscribe(filter: Filter, since: Timestamp, until: Timestamp) -> Self {
@@ -684,9 +682,9 @@ impl qcheck::Arbitrary for ZeroBytes {
#[allow(clippy::unwrap_used)]
mod tests {
- use fastrand;
use localtime::LocalTime;
use qcheck_macros::quickcheck;
+ use radicle::crypto::SigningKey;
use radicle::test::arbitrary;
use crate::wire::Decode as _;
@@ -696,7 +694,7 @@ mod tests {
#[test]
fn test_ref_remote_limit() {
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
- let signer = Device::mock();
+ let signer = SigningKey::mock(18);
let at = git::Oid::ZERO_SHA1;
assert_eq!(refs.capacity(), REF_REMOTE_LIMIT);
@@ -714,7 +712,7 @@ mod tests {
refs,
timestamp: LocalTime::now().into(),
})
- .signed(&Device::mock())
+ .signed(&crypto::SigningKey::mock(93))
.into();
let mut buf = Vec::new();
@@ -734,7 +732,7 @@ mod tests {
.expect("size within bounds limit"),
timestamp: LocalTime::now().into(),
},
- &Device::mock(),
+ &crypto::SigningKey::mock(218),
);
let mut buf: Vec<u8> = Vec::new();
msg.encode(&mut buf);
@@ -753,12 +751,12 @@ mod tests {
#[quickcheck]
fn prop_refs_announcement_signing(rid: RepoId) {
- let signer = Device::mock_rng(&mut fastrand::Rng::new());
+ let secret_key = crypto::SigningKey::mock(242);
let timestamp = Timestamp::EPOCH;
let at = git::Oid::ZERO_SHA1;
let refs = BoundedVec::collect_from(
&mut [RefsAt {
- remote: *signer.public_key(),
+ remote: *secret_key.public_key(),
at,
}]
.into_iter(),
@@ -768,7 +766,7 @@ mod tests {
refs,
timestamp,
});
- let ann = message.signed(&signer);
+ let ann = message.signed(&secret_key);
assert!(ann.verify());
}
diff --git a/crates/radicle-protocol/src/wire.rs b/crates/radicle-protocol/src/wire.rs
index 578b4deae..a45e5acf0 100644
--- a/crates/radicle-protocol/src/wire.rs
+++ b/crates/radicle-protocol/src/wire.rs
@@ -18,7 +18,7 @@ use cypheraddr::i2p;
#[cfg(feature = "tor")]
use cypheraddr::tor;
-use radicle::crypto::{PublicKey, Signature};
+use radicle::crypto::{PublicKey, PublicKeyBytes, Signature};
use radicle::git;
use radicle::git::fmt;
use radicle::identity::RepoId;
@@ -181,7 +181,7 @@ impl Encode for u64 {
impl Encode for PublicKey {
fn encode(&self, buf: &mut impl BufMut) {
- self.to_byte_array().encode(buf)
+ std::borrow::Borrow::<PublicKeyBytes>::borrow(self).encode(buf)
}
}
@@ -310,7 +310,8 @@ impl Encode for git::fmt::RefString {
impl Encode for Signature {
fn encode(&self, buf: &mut impl BufMut) {
- self.deref().encode(buf)
+ let bytes: [u8; 64] = self.to_bytes();
+ bytes.encode(buf)
}
}
@@ -326,9 +327,7 @@ impl Encode for git::Oid {
impl Decode for PublicKey {
fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
- let buf: [u8; 32] = Decode::decode(buf)?;
-
- Ok(PublicKey::from(buf))
+ Ok(PublicKey::from(<[u8; 32]>::decode(buf)?))
}
}
diff --git a/crates/radicle-protocol/src/wire/message.rs b/crates/radicle-protocol/src/wire/message.rs
index f574a17f9..9e615db99 100644
--- a/crates/radicle-protocol/src/wire/message.rs
+++ b/crates/radicle-protocol/src/wire/message.rs
@@ -409,7 +409,6 @@ impl wire::Decode for ZeroBytes {
mod tests {
use qcheck_macros::quickcheck;
use radicle::node::UserAgent;
- use radicle::node::device::Device;
use radicle::storage::refs::RefsAt;
use radicle::test::arbitrary;
@@ -424,7 +423,7 @@ mod tests {
#[test]
fn test_refs_ann_max_size() {
- let signer = Device::mock();
+ let signer = radicle::crypto::SigningKey::mock(235);
let refs: [RefsAt; REF_REMOTE_LIMIT] = arbitrary::r#gen(1);
let ann = AnnouncementMessage::Refs(RefsAnnouncement {
rid: arbitrary::r#gen(1),
@@ -440,7 +439,7 @@ mod tests {
#[test]
fn test_inv_ann_max_size() {
- let signer = Device::mock();
+ let signer = radicle::crypto::SigningKey::mock(147);
let inv: [RepoId; INVENTORY_LIMIT] = arbitrary::r#gen(1);
let ann = AnnouncementMessage::Inventory(InventoryAnnouncement {
inventory: BoundedVec::collect_from(&mut inv.into_iter()),
@@ -455,7 +454,7 @@ mod tests {
#[test]
fn test_node_ann_max_size() {
- let signer = Device::mock();
+ let signer = radicle::crypto::SigningKey::mock(247);
let addrs: [Address; ADDRESS_LIMIT] = arbitrary::r#gen(1);
let alias = ['@'; radicle::node::MAX_ALIAS_LENGTH];
let ann = AnnouncementMessage::Node(NodeAnnouncement {
diff --git a/crates/radicle-remote-helper/src/main.rs b/crates/radicle-remote-helper/src/main.rs
index 5fc451959..68b2c9a01 100644
--- a/crates/radicle-remote-helper/src/main.rs
+++ b/crates/radicle-remote-helper/src/main.rs
@@ -28,9 +28,10 @@ use std::process;
use std::str::FromStr;
use std::{env, fmt};
-use radicle::cob::store::access::{ReadOnly, WriteAs};
use thiserror::Error;
+use radicle::cob::store::access::{ReadOnly, WriteAs};
+use radicle::crypto;
use radicle::prelude::NodeId;
use radicle::storage::git::transport::local::{Url, UrlError};
use radicle::storage::{ReadRepository, WriteStorage};
@@ -482,7 +483,7 @@ pub(crate) fn patches<'a, Repo: ReadRepository + cob::Store<Namespace = NodeId>>
}
/// Get the mutable patch store.
-pub(crate) fn patches_mut<'a, 'b, Signer>(
+pub(crate) fn patches_mut<'a, 'b, Signer: crypto::Signer>(
profile: &Profile,
repo: &'a storage::git::Repository,
signer: &'b Signer,
diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs
index 2efac2d57..7bc6380da 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -16,6 +16,7 @@ use radicle::cob::object::ParseObjectId;
use radicle::cob::patch;
use radicle::cob::patch::cache::Patches as _;
use radicle::crypto;
+use radicle::crypto::Signer as _;
use radicle::explorer::ExplorerResource;
use radicle::identity::Did;
use radicle::node;
@@ -287,7 +288,7 @@ pub(super) fn run(
// won't match the remote we're pushing to.
// 3. The URL namespace is not set.
let nid = url.namespace.ok_or(Error::NoKey).and_then(|ns| {
- (profile.public_key == ns)
+ (profile.id() == &ns)
.then_some(ns)
.ok_or(Error::KeyMismatch(ns.into()))
})?;
@@ -568,7 +569,7 @@ impl<'a, G> Drop for TempPatchRef<'a, G> {
}
/// Open a new patch.
-fn patch_open<S, Signer>(
+fn patch_open(
head: &git::Oid,
upstream: &Option<git::fmt::RefString>,
nid: &NodeId,
@@ -577,20 +578,13 @@ fn patch_open<S, Signer>(
mut patches: patch::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::StoreWriter,
>,
profile: &Profile,
opts: Options,
- git: &S,
-) -> Result<Option<ExplorerResource>, Error>
-where
- S: GitService,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+ git: &impl GitService,
+) -> Result<Option<ExplorerResource>, Error> {
let temp = TempPatchRef::new(stored, head, nid, git);
temp.push(head, opts.verbosity)?;
let base = patch_base(head, &opts, stored)?;
@@ -692,7 +686,7 @@ where
/// Update an existing patch.
#[allow(clippy::too_many_arguments)]
-fn patch_update<S, Signer>(
+fn patch_update<Signer>(
head: &git::Oid,
dst: &git::fmt::Qualified,
force: bool,
@@ -709,14 +703,10 @@ fn patch_update<S, Signer>(
signer: &Signer,
opts: Options,
expected_refs: &[String],
- git: &S,
+ git: &impl GitService,
) -> Result<Option<ExplorerResource>, Error>
where
- S: GitService,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let Ok(Some(patch)) = patches.get(&patch_id) else {
return Err(Error::NotFound(patch_id));
@@ -786,7 +776,7 @@ where
Ok(Some(ExplorerResource::Patch { id: patch_id }))
}
-fn push<S, Signer>(
+fn push<Signer>(
src: &git::Oid,
dst: &git::fmt::Qualified,
force: bool,
@@ -802,14 +792,10 @@ fn push<S, Signer>(
signer: &Signer,
verbosity: Verbosity,
expected_refs: &[String],
- git: &S,
+ git: &impl GitService,
) -> Result<Option<ExplorerResource>, Error>
where
- S: GitService,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let head = *src;
let namespaced_dst = dst.with_namespace(nid.into());
@@ -859,7 +845,7 @@ where
}
/// Revert all patches that are no longer included in the base branch.
-fn patch_revert_all<Signer>(
+fn patch_revert_all(
old: git::Oid,
new: git::Oid,
pushed_dst: &git::fmt::Qualified,
@@ -867,15 +853,11 @@ fn patch_revert_all<Signer>(
patches: &mut patch::Cache<
'_,
storage::git::Repository,
- WriteAs<'_, Signer>,
+ WriteAs<'_, impl crypto::Signer>,
cob::cache::StoreWriter,
>,
identity: &radicle::identity::Identity,
-) -> Result<(), Error>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
-{
+) -> Result<(), Error> {
// Find all commits reachable from the old OID but not from the new OID.
let mut revwalk = stored.revwalk()?;
revwalk.push(old.into())?;
@@ -961,10 +943,7 @@ fn patch_merge_all<Signer>(
identity: &radicle::identity::Identity,
) -> Result<(), Error>
where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let mut revwalk = working.revwalk()?;
revwalk.push_range(&format!("{old}..{new}"))?;
@@ -1011,19 +990,22 @@ where
Ok(())
}
-fn patch_merge<Signer, C>(
- mut patch: patch::PatchMut<'_, '_, '_, storage::git::Repository, Signer, C>,
+fn patch_merge<Signer>(
+ mut patch: patch::PatchMut<
+ '_,
+ '_,
+ '_,
+ storage::git::Repository,
+ Signer,
+ impl cob::cache::Update<patch::Patch>,
+ >,
revision: patch::RevisionId,
commit: git::Oid,
working: &git::raw::Repository,
signer: &Signer,
) -> Result<(), Error>
where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- C: cob::cache::Update<patch::Patch>,
+ Signer: crypto::Signer,
{
let (latest, _) = patch.latest();
let merged = patch.merge(revision, commit)?;
diff --git a/crates/radicle/Cargo.toml b/crates/radicle/Cargo.toml
index 529c0996e..2c1193d74 100644
--- a/crates/radicle/Cargo.toml
+++ b/crates/radicle/Cargo.toml
@@ -39,11 +39,12 @@ fastrand = { workspace = true, features = ["std"] }
git2 = { workspace = true, features = ["vendored-libgit2"] }
indexmap = { workspace = true, features = ["serde"] }
log = { workspace = true, features = ["std"] }
+multibase = { workspace = true, features = ["std"] }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle-cob = { workspace = true, features = ["git2"] }
radicle-core = { workspace = true, features = ["git2", "serde", "sqlite"] }
-radicle-crypto = { workspace = true, features = ["git-ref-format-core", "ssh", "sqlite", "cyphernet"] }
+radicle-crypto = { workspace = true, features = ["git-ref-format-core", "serde", "ssh", "sqlite", "cyphernet"] }
radicle-git-ref-format = { workspace = true, features = ["macro", "serde"] }
radicle-git-metadata = { workspace = true }
radicle-localtime = { workspace = true, features = ["serde"] }
diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs
index 6429fea5a..1384315f6 100644
--- a/crates/radicle/src/cob/identity.rs
+++ b/crates/radicle/src/cob/identity.rs
@@ -65,6 +65,7 @@ pub enum Action {
/// Parent revision that this revision replaces.
parent: Option<RevisionId>,
/// Signature over the revision blob.
+ #[serde(with = "signature")]
signature: Signature,
},
RevisionEdit {
@@ -80,6 +81,7 @@ pub enum Action {
RevisionAccept {
revision: RevisionId,
/// Signature over the blob.
+ #[serde(with = "signature")]
signature: Signature,
},
#[serde(rename = "revision.reject")]
@@ -214,10 +216,7 @@ impl Identity {
) -> Result<IdentityMut<'a, 'b, Repo, Signer>, cob::store::Error>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let mut store = cob::store::Store::open(store, WriteAs::new(signer))?;
@@ -262,7 +261,7 @@ impl Identity {
) -> Result<IdentityMut<'a, 'b, Repo, Signer>, store::Error>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
let obj = Self::get(id, repo)?;
let store = cob::store::Store::open(repo, WriteAs::new(signer))?;
@@ -287,7 +286,7 @@ impl Identity {
) -> Result<IdentityMut<'a, 'b, Repo, Signer>, RepositoryError>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
let oid = repo.identity_root()?;
let oid = ObjectId::from(oid);
@@ -894,7 +893,7 @@ impl<R: ReadRepository> cob::Evaluate<R> for Identity {
pub enum Verdict {
/// An accepting verdict must supply the [`Signature`] over the
/// new proposed [`Doc`].
- Accept(Signature),
+ Accept(#[serde(with = "signature")] Signature),
/// Rejecting the proposed [`Doc`].
Reject,
}
@@ -1067,10 +1066,7 @@ impl Revision {
})
}
- pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
- &self,
- signer: &G,
- ) -> Result<Signature, DocError> {
+ pub fn sign(&self, signer: &impl crypto::Signer) -> Result<Signature, DocError> {
self.doc.signature_of(signer)
}
}
@@ -1141,14 +1137,14 @@ impl<R: ReadRepository> store::Transaction<Identity, R> {
}
}
-impl<R: WriteRepository> store::Transaction<Identity, R> {
- pub fn new_revision<G: crypto::signature::Signer<crypto::Signature>>(
+impl<Repo: WriteRepository> store::Transaction<Identity, Repo> {
+ pub fn new_revision(
title: cob::Title,
description: impl ToString,
doc: &Doc,
parent: Option<RevisionId>,
- repo: &R,
- signer: &G,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
) -> Result<Self, store::Error> {
let mut tx = Transaction::default();
@@ -1175,14 +1171,14 @@ impl<R: WriteRepository> store::Transaction<Identity, R> {
}
}
-pub struct IdentityMut<'a, 'b, Repo, Signer> {
+pub struct IdentityMut<'a, 'b, Repo, Signer: crypto::Signer> {
pub id: ObjectId,
identity: Identity,
store: store::Store<'a, Identity, Repo, WriteAs<'b, Signer>>,
}
-impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
+impl<Repo, Signer: crypto::Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IdentityMut")
.field("id", &self.id)
@@ -1191,13 +1187,9 @@ impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
}
}
-impl<Repo, Signer> IdentityMut<'_, '_, Repo, Signer>
+impl<Repo, Signer: crypto::Signer> IdentityMut<'_, '_, Repo, Signer>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
{
/// Reload the identity data from storage.
#[cfg(test)]
@@ -1240,7 +1232,7 @@ where
doc: &Doc,
) -> Result<RevisionId, Error> {
#[allow(deprecated)]
- let did: Did = self.store.signer().verifying_key().into();
+ let did: Did = self.store.signer().public_key().into();
if let Some(revision_id) = self.has_accepted_active_sibling(&self.current, &did) {
return Err(Error::Apply(ApplyError::SiblingAccepted {
revision: revision_id,
@@ -1275,7 +1267,7 @@ where
if let Some(parent_id) = revision.parent {
#[allow(deprecated)]
- let did: Did = self.store.signer().verifying_key().into();
+ let did: Did = self.store.signer().public_key().into();
if let Some(revision_id) = self.has_accepted_active_sibling(&parent_id, &did) {
return Err(Error::Apply(ApplyError::SiblingAccepted {
revision: revision_id,
@@ -1312,7 +1304,7 @@ where
}
}
-impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
+impl<Repo, Signer: crypto::Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
type Target = Identity;
fn deref(&self) -> &Self::Target {
@@ -1320,6 +1312,29 @@ impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
}
}
+mod signature {
+ pub fn serialize<S>(signature: &crypto::Signature, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&multibase::encode(
+ multibase::Base::Base58Btc,
+ signature.to_bytes(),
+ ))
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<crypto::Signature, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ use serde::Deserialize;
+
+ let s = String::deserialize(deserializer)?;
+ let (_, bytes) = multibase::decode(&s).map_err(serde::de::Error::custom)?;
+ crypto::Signature::from_slice(bytes.as_slice()).map_err(serde::de::Error::custom)
+ }
+}
+
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
@@ -1328,11 +1343,10 @@ mod test {
use qcheck_macros::quickcheck;
use crate::cob::{self, Title};
- use crate::crypto::PublicKey;
+ use crate::crypto::{PublicKey, Signer as _, SigningKey};
use crate::identity::Visibility;
use crate::identity::did::Did;
use crate::identity::doc::PayloadId;
- use crate::node::device::Device;
use crate::rad;
use crate::storage::ReadStorage as _;
use crate::storage::git::Storage;
@@ -1356,7 +1370,7 @@ mod test {
#[test]
fn test_identity_updates() {
let NodeWithRepo { node, repo } = NodeWithRepo::default();
- let bob = Device::mock();
+ let bob = SigningKey::mock(103);
let signer = &node.signer;
let mut identity = Identity::load_mut(&*repo, signer).unwrap();
let mut doc = identity.doc().clone().edit();
@@ -1419,8 +1433,8 @@ mod test {
#[test]
fn test_identity_update_rejected() {
let NodeWithRepo { node, repo } = NodeWithRepo::default();
- let bob = Device::mock();
- let eve = Device::mock();
+ let bob = SigningKey::mock(200);
+ let eve = SigningKey::mock(201);
let signer = &node.signer;
let mut identity = Identity::load_mut(&*repo, signer).unwrap();
@@ -2531,9 +2545,9 @@ mod test {
let tempdir = tempfile::tempdir().unwrap();
let mut rng = fastrand::Rng::new();
- let alice = Device::mock_rng(&mut rng);
- let bob = Device::mock_rng(&mut rng);
- let eve = Device::mock_rng(&mut rng);
+ let alice = SigningKey::mock(rng.usize(..));
+ let bob = SigningKey::mock(rng.usize(..));
+ let eve = SigningKey::mock(rng.usize(..));
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
let (id, _, _, _) =
@@ -2717,7 +2731,6 @@ mod test {
/// enables a queued child to be automatically adopted.
#[test]
fn evaluates_queued_children_with_new_delegate() {
- use crate::crypto::test::signer::MockSigner;
use crate::test::setup::{Node, NodeRepo};
use tempfile::tempdir;
@@ -2727,7 +2740,7 @@ mod test {
let eve = &network.eve;
// Create Dave as a 4th participant.
- let mut dave_node = Node::new(tempdir().unwrap(), MockSigner::from_seed([!3; 32]), "dave");
+ let mut dave_node = Node::new(tempdir().unwrap(), SigningKey::mock(3), "dave");
dave_node.clone(network.rid, alice);
let dave_repo = NodeRepo {
repo: dave_node.storage.repository(network.rid).unwrap(),
@@ -2829,7 +2842,6 @@ mod test {
/// by its parent, not by the currently accepted identity document.
#[test]
fn authorization_based_on_parent_not_current() {
- use crate::crypto::test::signer::MockSigner;
use crate::test::setup::{Node, NodeRepo};
use tempfile::tempdir;
@@ -2839,7 +2851,7 @@ mod test {
let eve = &network.eve;
// Create Dave as a 4th participant.
- let mut dave_node = Node::new(tempdir().unwrap(), MockSigner::from_seed([!3; 32]), "dave");
+ let mut dave_node = Node::new(tempdir().unwrap(), SigningKey::mock(3), "dave");
dave_node.clone(network.rid, alice);
let dave_repo = NodeRepo {
repo: dave_node.storage.repository(network.rid).unwrap(),
diff --git a/crates/radicle/src/cob/identity/test/property.rs b/crates/radicle/src/cob/identity/test/property.rs
index 3e79d0965..1d1b4cc31 100644
--- a/crates/radicle/src/cob/identity/test/property.rs
+++ b/crates/radicle/src/cob/identity/test/property.rs
@@ -4,6 +4,7 @@ use crate::assert_matches;
use crate::cob;
use crate::cob::identity::{Did, Identity, RedactedBy, RejectedBy, RevisionId, State};
use crate::cob::store::Transaction;
+use crate::crypto::Signer as _;
use crate::identity::doc::PayloadId;
use crate::test::arbitrary::BoundedVec;
use crate::test::setup::Network;
@@ -192,7 +193,7 @@ impl Harness {
'_,
'_,
crate::storage::git::Repository,
- crate::node::device::Device<crypto::test::signer::MockSigner>,
+ crypto::SigningKey,
>,
) -> Result<T, cob::identity::Error>,
{
@@ -465,12 +466,8 @@ impl Harness {
fn identity_for(
&self,
actor: &Actor,
- ) -> cob::identity::IdentityMut<
- '_,
- '_,
- crate::storage::git::Repository,
- crate::node::device::Device<crypto::test::signer::MockSigner>,
- > {
+ ) -> cob::identity::IdentityMut<'_, '_, crate::storage::git::Repository, crypto::SigningKey>
+ {
let (_, identity, _) = self.signer_identity_doc_for(actor);
identity
@@ -480,13 +477,8 @@ impl Harness {
&self,
actor: &Actor,
) -> (
- &crate::node::device::Device<crypto::test::signer::MockSigner>,
- cob::identity::IdentityMut<
- '_,
- '_,
- crate::storage::git::Repository,
- crate::node::device::Device<crypto::test::signer::MockSigner>,
- >,
+ &crypto::SigningKey,
+ cob::identity::IdentityMut<'_, '_, crate::storage::git::Repository, crypto::SigningKey>,
crate::prelude::RawDoc,
) {
let (repo, signer) = match actor {
diff --git a/crates/radicle/src/cob/issue.rs b/crates/radicle/src/cob/issue.rs
index d7214e52d..6f74cf76e 100644
--- a/crates/radicle/src/cob/issue.rs
+++ b/crates/radicle/src/cob/issue.rs
@@ -375,7 +375,7 @@ impl Issue {
Action::CommentEdit { id, .. } | Action::CommentRedact { id, .. } => {
if let Some(comment) = self.thread.comments.get(id) {
if let Some(comment) = comment {
- Authorization::from(*actor == comment.author())
+ Authorization::from(actor == comment.author())
} else {
Authorization::Unknown
}
@@ -472,8 +472,8 @@ impl Issue {
}
}
-impl<'a, 'b, 'g, Repo, Signer, Cache> From<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>>
- for (IssueId, Issue)
+impl<'a, 'b, 'g, Repo, Signer: crypto::Signer, Cache>
+ From<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>> for (IssueId, Issue)
{
fn from(value: IssueMut<'a, 'b, 'g, Repo, Signer, Cache>) -> Self {
(value.id, value.issue)
@@ -581,14 +581,16 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
}
}
-pub struct IssueMut<'a, 'b, 'g, Repo, Signer, Cache> {
+pub struct IssueMut<'a, 'b, 'g, Repo, Signer: crypto::Signer, Cache> {
id: ObjectId,
issue: Issue,
store: &'g mut Issues<'a, Repo, WriteAs<'b, Signer>>,
cache: &'g mut Cache,
}
-impl<Repo, Signer, Cache> std::fmt::Debug for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
+impl<Repo, Signer: crypto::Signer, Cache> std::fmt::Debug
+ for IssueMut<'_, '_, '_, Repo, Signer, Cache>
+{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("IssueMut")
.field("id", &self.id)
@@ -597,13 +599,9 @@ impl<Repo, Signer, Cache> std::fmt::Debug for IssueMut<'_, '_, '_, Repo, Signer,
}
}
-impl<Repo, Signer, Cache> IssueMut<'_, '_, '_, Repo, Signer, Cache>
+impl<Repo, Signer: crypto::Signer, Cache> IssueMut<'_, '_, '_, Repo, Signer, Cache>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
Cache: cob::cache::Update<Issue>,
{
/// Reload the issue data from storage.
@@ -694,10 +692,6 @@ where
pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error>
where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
F: FnOnce(&mut Transaction<Issue, Repo>) -> Result<(), store::Error>,
{
let mut tx = Transaction::default();
@@ -716,7 +710,7 @@ where
}
}
-impl<Repo, Signer, Cache> Deref for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
+impl<Repo, Signer: crypto::Signer, Cache> Deref for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
type Target = Issue;
fn deref(&self) -> &Self::Target {
@@ -777,10 +771,7 @@ where
impl<'a, 'b, Repo, Signer> Issues<'a, Repo, WriteAs<'b, Signer>>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Get an issue mutably.
pub fn get_mut<'g, Cache>(
@@ -947,9 +938,9 @@ mod test {
use super::*;
use crate::cob::{ActorId, Reaction, store::CobWithType};
+ use crate::crypto::{Signer as _, SigningKey};
use crate::git::Oid;
use crate::issue::cache::Issues as _;
- use crate::node::device::Device;
use crate::test::arbitrary;
use crate::{assert_matches, test};
@@ -1169,8 +1160,8 @@ mod test {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
- let assignee = Did::from(arbitrary::r#gen::<ActorId>(1));
- let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1));
+ let [assignee, assignee_two] = arbitrary::vec_distinct::<Did>(2..=2).try_into().unwrap();
+
let mut issue = issues
.create(
cob::Title::new("My first issue").unwrap(),
@@ -1264,7 +1255,7 @@ mod test {
let reactions = issue.comment(&comment).unwrap().reactions();
let authors = reactions.get(&reaction).unwrap();
- assert_eq!(authors.first().unwrap(), &node.signer.public_key());
+ assert_eq!(*authors.first().unwrap(), node.signer.public_key());
// TODO: Test multiple reactions from same author and different authors
}
@@ -1349,7 +1340,7 @@ mod test {
#[test]
fn test_issue_comment() {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
- let author = *node.signer.public_key();
+ let author = node.signer.public_key();
let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
let mut issue = issues
.create(
@@ -1667,7 +1658,7 @@ mod test {
use nonempty::NonEmpty;
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
- let eve = Device::mock();
+ let eve = SigningKey::mock(126);
let identity = repo.identity().unwrap().head();
let missing = arbitrary::oid();
let type_name = Issue::type_name().clone();
diff --git a/crates/radicle/src/cob/issue/cache.rs b/crates/radicle/src/cob/issue/cache.rs
index 94617ad9f..d4af802ab 100644
--- a/crates/radicle/src/cob/issue/cache.rs
+++ b/crates/radicle/src/cob/issue/cache.rs
@@ -97,13 +97,7 @@ where
}
}
-impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+impl<'a, 'b, Repo, Signer: crypto::Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C> {
/// Create a new [`Issue`] using the [`super::Issues`] as the
/// main storage, and writing the update to the `cache`.
pub fn create<'g>(
@@ -210,7 +204,7 @@ where
impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
where
Repo: WriteRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Get a `Cache` that does no write-through modifications and
/// uses the [`super::Issues`] store for all reads and writes.
@@ -253,7 +247,7 @@ impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> {
}
}
-impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
+impl<'a, 'b, Repo, Signer: crypto::Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
diff --git a/crates/radicle/src/cob/op.rs b/crates/radicle/src/cob/op.rs
index 4c49b807a..0bf976c58 100644
--- a/crates/radicle/src/cob/op.rs
+++ b/crates/radicle/src/cob/op.rs
@@ -124,7 +124,8 @@ impl<A> Op<A> {
S: cob::change::Storage<
ObjectId = git::Oid,
Parent = git::Oid,
- Signatures = crypto::ssh::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
{
store.manifest_of(id).map_err(|err| ManifestError {
@@ -139,7 +140,8 @@ impl<A> Op<A> {
S: cob::change::Storage<
ObjectId = git::Oid,
Parent = git::Oid,
- Signatures = crypto::ssh::ExtendedSignature,
+ PublicKey = crypto::PublicKey,
+ Signature = crypto::Signature,
>,
for<'de> A: serde::Deserialize<'de>,
{
diff --git a/crates/radicle/src/cob/patch.rs b/crates/radicle/src/cob/patch.rs
index d378e764d..451bd3349 100644
--- a/crates/radicle/src/cob/patch.rs
+++ b/crates/radicle/src/cob/patch.rs
@@ -416,17 +416,12 @@ impl<R: WriteRepository> Merged<'_, R> {
///
/// This removes Git refs relating to the patch, both in the working copy,
/// and the stored copy; and updates `rad/sigrefs`.
- pub fn cleanup<Signer>(
+ pub fn cleanup(
self,
working: &git::raw::Repository,
- signer: &Signer,
- ) -> Result<(), storage::RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
- let nid = &signer.verifying_key();
+ signer: &impl crypto::Signer,
+ ) -> Result<(), storage::RepositoryError> {
+ let nid = signer.public_key();
let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into());
let working_ref = git::refs::workdir::patch_upstream(&self.patch);
@@ -899,7 +894,7 @@ impl Patch {
if let Some((_, review)) = lookup::review(self, review)?
&& let Some(comment) = review.comments.comment(comment)
{
- return Ok(Authorization::from(*actor == comment.author()));
+ return Ok(Authorization::from(actor == comment.author()));
}
// Redacted.
Authorization::Unknown
@@ -913,7 +908,7 @@ impl Patch {
&& let Some(comment) = review.comments.comment(comment)
{
return Ok(Authorization::from(
- actor == &comment.author()
+ actor == comment.author()
|| actor == review.author.public_key()
|| actor == revision.author.public_key(),
));
@@ -946,7 +941,7 @@ impl Patch {
if let Some(revision) = lookup::revision(self, revision)?
&& let Some(comment) = revision.discussion.comment(comment)
{
- return Ok(Authorization::from(actor == &comment.author()));
+ return Ok(Authorization::from(actor == comment.author()));
}
// Redacted.
Authorization::Unknown
@@ -2207,7 +2202,7 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
}
}
-pub struct PatchMut<'a, 'b, 'g, Repo, Signer, Cache> {
+pub struct PatchMut<'a, 'b, 'g, Repo, Signer: crypto::Signer, Cache> {
pub id: ObjectId,
patch: Patch,
@@ -2215,13 +2210,9 @@ pub struct PatchMut<'a, 'b, 'g, Repo, Signer, Cache> {
cache: &'g mut Cache,
}
-impl<'a, 'b, 'g, Repo, Signer, Update> PatchMut<'a, 'b, 'g, Repo, Signer, Update>
+impl<'a, 'b, 'g, Repo, Signer: crypto::Signer, Update> PatchMut<'a, 'b, 'g, Repo, Signer, Update>
where
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
Update: cob::cache::Update<Patch>,
{
pub fn new(
@@ -2575,7 +2566,7 @@ where
}
}
-impl<Repo, Signer, Cache> Deref for PatchMut<'_, '_, '_, Repo, Signer, Cache> {
+impl<Repo, Signer: crypto::Signer, Cache> Deref for PatchMut<'_, '_, '_, Repo, Signer, Cache> {
type Target = Patch;
fn deref(&self) -> &Self::Target {
@@ -2725,8 +2716,7 @@ where
impl<'a, 'b, Repo, Signer> Patches<'a, Repo, WriteAs<'b, Signer>>
where
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Open a new patch.
pub fn create<'g, Cache>(
@@ -2741,9 +2731,7 @@ where
) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
where
Cache: cob::cache::Update<Patch>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
self._create(
title,
@@ -2770,8 +2758,6 @@ where
) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
where
Cache: cob::cache::Update<Patch>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
{
self._create(
title,
@@ -2818,10 +2804,7 @@ where
) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
where
Cache: cob::cache::Update<Patch>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
let (id, patch) = Transaction::initial("Create patch", &mut self.raw, |tx, _| {
tx.revision(description, base, oid)?;
@@ -3039,8 +3022,8 @@ mod test {
use super::*;
use crate::cob::common::CodeRange;
- use crate::cob::test::Actor;
- use crate::crypto::test::signer::MockSigner;
+ use crate::cob::test::SignerOpExt as _;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::git::BranchName;
use crate::identity;
use crate::identity::doc::RawDoc;
@@ -3133,7 +3116,7 @@ mod test {
#[test]
fn test_merge_target_resolution() {
- let alice = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(23);
let project = Project::new(
ProjectName::from_str("test_merge_target_resolution").unwrap(),
String::from(""),
@@ -3190,7 +3173,7 @@ mod test {
fn test_patch_merge_authorization_ref_formats() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let alice = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(14);
let mut raw_doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3273,8 +3256,8 @@ mod test {
fn test_patch_merge_custom_destination_authorized() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let alice = Actor::<MockSigner>::default();
- let bob = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(14);
+ let bob = SigningKey::mock(23);
let mut raw_doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3332,8 +3315,8 @@ mod test {
fn test_patch_merge_custom_destination_unauthorized() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let alice = Actor::<MockSigner>::default();
- let bob = Actor::<MockSigner>::default();
+ let alice = SigningKey::mock(14);
+ let bob = SigningKey::mock(23);
let mut raw_doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3627,7 +3610,7 @@ mod test {
fn test_revision_review_merge_redacted() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(14);
let rid = r#gen::<RepoId>(1);
let doc = RawDoc::new(
r#gen::<Project>(1),
@@ -3687,8 +3670,8 @@ mod test {
let oid = arbitrary::oid();
let repo = r#gen::<MockRepository>(1);
let time = env::local_time();
- let alice = MockSigner::default();
- let bob = MockSigner::default();
+ let alice = SigningKey::mock(38);
+ let bob = SigningKey::mock(39);
let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history(
&[
Action::Revision {
@@ -3746,7 +3729,7 @@ mod test {
fn test_revision_reaction() {
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(35);
let repo = r#gen::<MockRepository>(1);
let reaction = Reaction::new('👍').expect("failed to create a reaction");
diff --git a/crates/radicle/src/cob/patch/cache.rs b/crates/radicle/src/cob/patch/cache.rs
index 71f390074..3b0541567 100644
--- a/crates/radicle/src/cob/patch/cache.rs
+++ b/crates/radicle/src/cob/patch/cache.rs
@@ -107,13 +107,7 @@ where
}
}
-impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C>
-where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
-{
+impl<'a, 'b, Repo, Signer: crypto::Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C> {
/// Create a new [`Patch`] using the [`super::Patches`] as the
/// main storage, and writing the update to the `cache`.
pub fn create<'g>(
@@ -258,7 +252,7 @@ impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> {
}
}
-impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
+impl<'a, 'b, Repo, Signer: crypto::Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
@@ -280,7 +274,7 @@ where
}
}
-impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
+impl<'a, 'b, Repo, Signer: crypto::Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
diff --git a/crates/radicle/src/cob/store.rs b/crates/radicle/src/cob/store.rs
index 41b5941f2..3a0fd102c 100644
--- a/crates/radicle/src/cob/store.rs
+++ b/crates/radicle/src/cob/store.rs
@@ -233,7 +233,7 @@ where
}
}
-impl<'a, 'b, T, Repo, Signer> Store<'a, T, Repo, WriteAs<'b, Signer>>
+impl<'a, 'b, T, Repo, Signer: crypto::Signer> Store<'a, T, Repo, WriteAs<'b, Signer>>
where
T: Cob + cob::Evaluate<Repo>,
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
@@ -262,10 +262,7 @@ where
T: Cob + cob::Evaluate<Repo>,
T::Action: Serialize,
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
+ Signer: crypto::Signer,
{
/// Update an object.
pub fn update(
@@ -288,13 +285,13 @@ where
})
})
.collect::<Result<_, _>>()?;
- let namespace = self.access.signer.verifying_key();
+ let namespace = self.access.signer.public_key();
let updated = cob::update(
self.repo,
self.access.signer,
self.identity,
related,
- &namespace,
+ namespace,
Update {
object_id,
type_name: type_name.clone(),
@@ -329,13 +326,13 @@ where
})
})
.collect::<Result<_, _>>()?;
- let namespace = self.access.signer.verifying_key();
- let cob = cob::create::<T, _, _>(
+ let namespace = self.access.signer.public_key();
+ let cob = cob::create::<T, _>(
self.repo,
self.access.signer,
self.identity,
parents,
- &namespace,
+ namespace,
Create {
type_name: self.scope.type_name.clone(),
version: Version::default(),
@@ -357,11 +354,11 @@ where
/// Remove an object.
pub fn remove(&mut self, id: &ObjectId) -> Result<(), Error> {
- let namespace = self.access.signer.verifying_key();
- let name = git::refs::storage::cob(&namespace, self.scope.type_name, id);
- match self.repo.reference_oid(&namespace, &name.strip_namespace()) {
+ let namespace = self.access.signer.public_key();
+ let name = git::refs::storage::cob(namespace, self.scope.type_name, id);
+ match self.repo.reference_oid(namespace, &name.strip_namespace()) {
Ok(_) => {
- cob::remove(self.repo, &namespace, self.scope.type_name, id)?;
+ cob::remove(self.repo, namespace, self.scope.type_name, id)?;
self.repo
.sign_refs(self.access.signer)
.map_err(|e| Error::SignRefs(Box::new(e)))?;
@@ -469,18 +466,14 @@ where
T: Cob + CobWithType + cob::Evaluate<Repo>,
{
/// Create a new transaction to be used as the initial set of operations for a COB.
- pub fn initial<'a, 'b, Signer, Tx, F>(
+ pub fn initial<'a, 'b, Tx, F>(
message: &str,
- store: &mut Store<'a, T, Repo, WriteAs<'b, Signer>>,
+ store: &mut Store<'a, T, Repo, WriteAs<'b, impl crypto::Signer>>,
operations: F,
) -> Result<(ObjectId, T), Error>
where
T::Action: Serialize + Clone,
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
Tx: From<Self>,
Self: From<Tx>,
F: FnOnce(&mut Tx, &Repo) -> Result<(), Error>,
@@ -538,19 +531,15 @@ where
/// Commit transaction.
///
/// Returns an operation that can be applied onto an in-memory state.
- pub fn commit<'a, Signer>(
+ pub fn commit<'a>(
self,
msg: &str,
id: ObjectId,
- store: &mut Store<T, Repo, WriteAs<'a, Signer>>,
+ store: &mut Store<T, Repo, WriteAs<'a, impl crypto::Signer>>,
) -> Result<(T, EntryId), Error>
where
T::Action: Serialize + Clone,
Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
{
let actions = NonEmpty::from_vec(self.actions)
.expect("Transaction::commit: transaction must not be empty");
diff --git a/crates/radicle/src/cob/store/access.rs b/crates/radicle/src/cob/store/access.rs
index afccd4eed..c589f9efa 100644
--- a/crates/radicle/src/cob/store/access.rs
+++ b/crates/radicle/src/cob/store/access.rs
@@ -7,11 +7,11 @@ pub use seal::Access;
pub struct ReadOnly;
/// [`WriteAs`] is used for write [`Access`].
-pub struct WriteAs<'a, Signer> {
+pub struct WriteAs<'a, Signer: crypto::Signer> {
pub(super) signer: &'a Signer,
}
-impl<'a, Signer> WriteAs<'a, Signer> {
+impl<'a, Signer: crypto::Signer> WriteAs<'a, Signer> {
pub fn new(signer: &'a Signer) -> Self {
Self { signer }
}
@@ -31,7 +31,7 @@ mod seal {
fn seal(&self, _: Seal) {}
}
- impl<Signer> Access for super::WriteAs<'_, Signer> {
+ impl<Signer: crypto::Signer> Access for super::WriteAs<'_, Signer> {
fn seal(&self, _: Seal) {}
}
}
diff --git a/crates/radicle/src/cob/stream.rs b/crates/radicle/src/cob/stream.rs
index 54e2383ac..6a37f06cd 100644
--- a/crates/radicle/src/cob/stream.rs
+++ b/crates/radicle/src/cob/stream.rs
@@ -175,7 +175,7 @@ mod tests {
use serde_json as json;
use crate::cob::change::Storage as _;
- use crate::crypto::test::signer::MockSigner;
+ use crate::crypto::SigningKey;
use crate::test::arbitrary;
use crate::test::arbitrary::r#gen;
use crate::{cob, test};
@@ -186,7 +186,7 @@ mod tests {
"xyz.radicle.test".parse::<TypeName>().unwrap()
}
- fn gen_ops(repo: &git::raw::Repository, signer: &MockSigner) -> Vec<cob::Entry> {
+ fn gen_ops(repo: &git::raw::Repository, signer: &SigningKey) -> Vec<cob::Entry> {
// Number of ops
let n = r#gen::<u8>(1).clamp(1, 10);
let mut entries = Vec::with_capacity(n.into());
@@ -215,7 +215,7 @@ mod tests {
fn create_entry(
repo: &git::raw::Repository,
- signer: &MockSigner,
+ signer: &SigningKey,
contents: NonEmpty<Vec<u8>>,
parent: Option<Oid>,
) -> cob::Entry {
@@ -332,7 +332,7 @@ mod tests {
fn test_all_from() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(38);
let ops = gen_ops(&repo, &signer);
let history = CobRange {
root: ops.first().unwrap().id,
@@ -346,7 +346,7 @@ mod tests {
fn test_all_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(39);
let ops = gen_ops(&repo, &signer);
let tip = ops.last().unwrap().id;
let history = CobRange {
@@ -361,7 +361,7 @@ mod tests {
fn test_all_from_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(40);
let ops = gen_ops(&repo, &signer);
let tip = ops.last().unwrap().id;
let history = CobRange {
@@ -376,7 +376,7 @@ mod tests {
fn test_from_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(41);
let ops = gen_ops(&repo, &signer);
let history = CobRange {
root: ops.first().unwrap().id,
@@ -399,7 +399,7 @@ mod tests {
fn test_regression_from_until() {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = test::fixtures::repository(tmp.path());
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(42);
// Set up 3 entries that make up the COB history
let op1 = create_entry(
&repo,
diff --git a/crates/radicle/src/cob/test.rs b/crates/radicle/src/cob/test.rs
index e976cbb0a..90182ea59 100644
--- a/crates/radicle/src/cob/test.rs
+++ b/crates/radicle/src/cob/test.rs
@@ -2,20 +2,15 @@ use std::marker::PhantomData;
use std::ops::Deref;
use nonempty::NonEmpty;
-use radicle_crypto::ssh::ExtendedSignature;
use serde::{Deserialize, Serialize};
use crate::cob::op::Op;
-use crate::cob::patch::Patch;
use crate::cob::store::encoding;
use crate::cob::{Entry, History, Manifest, Timestamp, Version};
-use crate::cob::{Title, patch};
-use crate::crypto::Signer;
+use crate::crypto::ExtendedSignature;
use crate::git::{self, Oid};
-use crate::node::device::Device;
use crate::prelude::Did;
use crate::profile::env;
-use crate::storage::ReadRepository;
use crate::test::arbitrary;
use super::store::{Cob, CobWithType};
@@ -37,11 +32,11 @@ impl<T> AsRef<History> for HistoryBuilder<T> {
}
impl HistoryBuilder<thread::Thread> {
- pub fn comment<G: Signer>(
+ pub fn comment(
&mut self,
body: impl ToString,
reply_to: Option<thread::CommentId>,
- signer: &G,
+ signer: &impl crypto::Signer,
) -> Oid {
let action = thread::Action::Comment {
body: body.to_string(),
@@ -56,18 +51,21 @@ where
T: CobWithType,
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
{
- pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
+ pub fn new(
+ actions: &[T::Action],
+ time: Timestamp,
+ signer: &impl crypto::Signer,
+ ) -> HistoryBuilder<T> {
let resource = Some(arbitrary::oid());
let revision = arbitrary::oid();
let (contents, oids): (Vec<Vec<u8>>, Vec<Oid>) = actions
.iter()
- .map(|a| encoded::<T, _>(a, time, [], signer))
+ .map(|a| encoded::<T>(a, time, [], signer))
.unzip();
let contents = NonEmpty::from_vec(contents).unwrap();
let root = oids.first().unwrap();
let manifest = Manifest::new(T::type_name().clone(), Version::default());
- let signature = signer.sign(&[0]);
- let signature = ExtendedSignature::new(*signer.public_key(), signature);
+ let signature = ExtendedSignature::try_sign(signer, &[0]).unwrap();
let change = Entry {
id: *root,
signature,
@@ -96,14 +94,13 @@ where
self.history.merge(other.history);
}
- pub fn commit<G: Signer>(&mut self, action: &T::Action, signer: &G) -> crate::git::Oid {
+ pub fn commit(&mut self, action: &T::Action, signer: &impl crypto::Signer) -> crate::git::Oid {
let timestamp = self.time;
let tips = self.tips();
let revision = arbitrary::oid();
- let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer);
+ let (data, oid) = encoded::<T>(action, timestamp, tips, signer);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
- let signature = signer.sign(data.as_slice());
- let signature = ExtendedSignature::new(*signer.public_key(), signature);
+ let signature = ExtendedSignature::try_sign(signer, data.as_slice()).unwrap();
let change = Entry {
id: oid,
signature,
@@ -130,10 +127,10 @@ impl<A> Deref for HistoryBuilder<A> {
}
/// Create a new test history.
-pub fn history<T, G: Signer>(
+pub fn history<T>(
actions: &[T::Action],
time: Timestamp,
- signer: &G,
+ signer: &impl crypto::Signer,
) -> HistoryBuilder<T>
where
T: Cob + CobWithType,
@@ -142,26 +139,10 @@ where
HistoryBuilder::new(actions, time, signer)
}
-/// An object that can be used to create and sign operations.
-pub struct Actor<G> {
- pub signer: Device<G>,
-}
-
-impl<G: Default + Signer> Default for Actor<G> {
- fn default() -> Self {
- Self::new(Device::default())
- }
-}
-
-impl<G> Actor<G> {
- pub fn new(signer: Device<G>) -> Self {
- Self { signer }
- }
-}
-
-impl<G: Signer> Actor<G> {
+/// An extension trait that provides convenience methods for creating operations.
+pub trait SignerOpExt: crypto::Signer {
/// Create a new operation.
- pub fn op_with<T>(
+ fn op_with<T>(
&mut self,
actions: impl IntoIterator<Item = T::Action>,
identity: Option<Oid>,
@@ -180,7 +161,7 @@ impl<G: Signer> Actor<G> {
let oid =
crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
let id = oid.into();
- let author = *self.signer.public_key();
+ let author = self.did().into();
let actions = NonEmpty::from_vec(actions).unwrap();
let manifest = Manifest::new(T::type_name().clone(), Version::default());
let parents = vec![];
@@ -199,7 +180,7 @@ impl<G: Signer> Actor<G> {
}
/// Create a new operation.
- pub fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
+ fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
where
T: Cob + CobWithType,
T::Action: Clone + Serialize,
@@ -210,49 +191,23 @@ impl<G: Signer> Actor<G> {
self.op_with::<T>(actions, Some(identity), timestamp.into())
}
- /// Get the actor's DID.
- pub fn did(&self) -> Did {
- self.signer.public_key().into()
+ /// Get the [`Did`] corresponding to the verifying key of the signer.
+ fn did(&self) -> Did {
+ Did::from(self.verifying_key().public_key())
}
}
-impl<G: Signer> Actor<G> {
- /// Create a patch.
- pub fn patch<R: ReadRepository>(
- &mut self,
- title: Title,
- description: impl ToString,
- base: crate::git::Oid,
- oid: crate::git::Oid,
- repo: &R,
- ) -> Result<Patch, patch::Error> {
- Patch::from_root(
- self.op::<Patch>([
- patch::Action::Revision {
- description: description.to_string(),
- base,
- oid,
- resolves: Default::default(),
- },
- patch::Action::Edit {
- title,
- target: patch::MergeTarget::default(),
- },
- ]),
- repo,
- )
- }
-}
+impl<Signer> SignerOpExt for Signer where Signer: crypto::Signer {}
/// Encode an action and return its hash.
///
/// Doesn't encode in the same way as we do in production, but attempts to include the same data
/// that feeds into the hash entropy, so that changing any input will change the resulting oid.
-fn encoded<T: Cob, G: Signer>(
+fn encoded<T: Cob>(
action: &T::Action,
timestamp: Timestamp,
parents: impl IntoIterator<Item = Oid>,
- signer: &G,
+ signer: &impl crypto::Signer,
) -> (Vec<u8>, crate::git::Oid) {
use radicle_git_metadata::{
author::{Author, Time},
@@ -264,7 +219,7 @@ fn encoded<T: Cob, G: Signer>(
let parents = parents.into_iter().map(|o| o.into());
let author = Author {
name: "radicle".to_owned(),
- email: signer.public_key().to_human(),
+ email: signer.verifying_key().public_key().to_human(),
time: Time::new(timestamp.as_secs() as i64, 0),
};
let commit = CommitData::<git::raw::Oid, git::raw::Oid>::new::<_, _, OwnedTrailer>(
diff --git a/crates/radicle/src/cob/thread.rs b/crates/radicle/src/cob/thread.rs
index 6b96d808c..e15dd1289 100644
--- a/crates/radicle/src/cob/thread.rs
+++ b/crates/radicle/src/cob/thread.rs
@@ -174,8 +174,8 @@ impl<L> Comment<L> {
}
/// Return the comment author.
- pub fn author(&self) -> ActorId {
- self.author
+ pub fn author(&self) -> &ActorId {
+ &self.author
}
/// Return the comment this is a reply to. Returns nothing if this is the root comment.
@@ -624,8 +624,6 @@ pub fn unresolve<T>(
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
- use std::ops::{Deref, DerefMut};
-
use pretty_assertions::assert_eq;
use qcheck_macros::quickcheck;
@@ -633,36 +631,17 @@ mod tests {
use crate as radicle;
use crate::cob::store::Cob;
use crate::cob::test;
- use crate::crypto::Signer;
- use crate::crypto::test::signer::MockSigner;
- use crate::node::device::Device;
+ use crate::cob::test::SignerOpExt;
+ use crate::crypto::SigningKey;
use crate::profile::env;
use crate::test::arbitrary;
use crate::test::arbitrary::r#gen;
use crate::test::storage::MockRepository;
- /// An object that can be used to create and sign changes.
- pub struct Actor<G> {
- inner: cob::test::Actor<G>,
- }
-
- impl<G: Default + Signer> Default for Actor<G> {
- fn default() -> Self {
- Self {
- inner: cob::test::Actor::<G>::default(),
- }
- }
- }
-
- impl<G: Signer> Actor<G> {
- pub fn new(signer: Device<G>) -> Self {
- Self {
- inner: cob::test::Actor::new(signer),
- }
- }
-
+ /// An extension trait that provides convenience methods for handling thread operations.
+ trait SignerThreadOpExt: SignerOpExt {
/// Create a new comment.
- pub fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> {
+ fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> {
self.op::<Thread>([Action::Comment {
body: String::from(body),
reply_to,
@@ -670,12 +649,12 @@ mod tests {
}
/// Create a new redaction.
- pub fn redact(&mut self, id: CommentId) -> Op<Action> {
+ fn redact(&mut self, id: CommentId) -> Op<Action> {
self.op::<Thread>([Action::Redact { id }])
}
/// Edit a comment.
- pub fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
+ fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
self.op::<Thread>([Action::Edit {
id,
body: body.to_owned(),
@@ -683,25 +662,13 @@ mod tests {
}
}
- impl<G> Deref for Actor<G> {
- type Target = cob::test::Actor<G>;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
- }
-
- impl<G> DerefMut for Actor<G> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
- }
+ impl<T> SignerThreadOpExt for T where T: SignerOpExt {}
#[test]
fn test_redact_comment() {
let radicle::test::setup::Node { signer, .. } = radicle::test::setup::Node::default();
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::new(signer);
+ let mut alice = signer;
let a0 = alice.comment("First comment", None);
let a1 = alice.comment("Second comment", Some(a0.id()));
@@ -724,7 +691,7 @@ mod tests {
#[test]
fn test_edit_comment() {
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(49);
let repo = r#gen::<MockRepository>(1);
let c0 = alice.comment("Hello world!", None);
@@ -744,13 +711,13 @@ mod tests {
#[test]
fn test_timeline() {
- let alice = MockSigner::default();
- let bob = MockSigner::default();
- let eve = MockSigner::default();
+ let alice = SigningKey::mock(94);
+ let bob = SigningKey::mock(103);
+ let eve = SigningKey::mock(104);
let repo = r#gen::<MockRepository>(1);
let time = env::local_time();
- let mut a = test::history::<Thread, _>(
+ let mut a = test::history::<Thread>(
&[Action::Comment {
body: "Thread root".to_owned(),
reply_to: None,
@@ -813,11 +780,12 @@ mod tests {
#[test]
fn test_duplicate_comments() {
let repo = r#gen::<MockRepository>(1);
- let alice = MockSigner::default();
- let bob = MockSigner::default();
+ let alice = SigningKey::mock(94);
+ let bob = SigningKey::mock(103);
+ let _eve = SigningKey::mock(104);
let time = env::local_time();
- let mut a = test::history::<Thread, _>(
+ let mut a = test::history::<Thread>(
&[Action::Comment {
body: "Thread root".to_owned(),
reply_to: None,
@@ -857,11 +825,12 @@ mod tests {
#[quickcheck]
fn prop_ordering(timestamp: u64) {
let repo = r#gen::<MockRepository>(1);
- let alice = MockSigner::default();
- let bob = MockSigner::default();
+ let alice = SigningKey::mock(94);
+ let bob = SigningKey::mock(103);
+ let _eve = SigningKey::mock(104);
let timestamp = Timestamp::from_secs(timestamp);
- let h0 = test::history::<Thread, _>(
+ let h0 = test::history::<Thread>(
&[Action::Comment {
body: "Thread root".to_owned(),
reply_to: None,
@@ -916,7 +885,7 @@ mod tests {
#[test]
fn test_comment_redact_missing() {
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(94);
let mut t = Thread::default();
let id = arbitrary::entry_id();
@@ -926,7 +895,7 @@ mod tests {
#[test]
fn test_comment_edit_missing() {
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(94);
let mut t = Thread::default();
let id = arbitrary::entry_id();
@@ -936,7 +905,7 @@ mod tests {
#[test]
fn test_comment_edit_redacted() {
let repo = r#gen::<MockRepository>(1);
- let mut alice = Actor::<MockSigner>::default();
+ let mut alice = SigningKey::mock(94);
let a1 = alice.comment("Hi", None);
let a2 = alice.redact(a1.id);
diff --git a/crates/radicle/src/git/canonical.rs b/crates/radicle/src/git/canonical.rs
index ce49a0e48..ea3f3db51 100644
--- a/crates/radicle/src/git/canonical.rs
+++ b/crates/radicle/src/git/canonical.rs
@@ -507,8 +507,8 @@ mod tests {
use super::*;
use crate::assert_matches;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::git;
- use crate::node::device::Device;
use crate::test::fixtures;
/// Test helper to construct a Canonical and get the quorum
@@ -521,7 +521,7 @@ mod tests {
let mut delegates = Vec::new();
for (i, head) in heads.iter().enumerate() {
- let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
+ let signer = SigningKey::mock(i);
let did = Did::from(signer.public_key());
delegates.push(did);
let ns = git::fmt::Component::from(signer.public_key());
diff --git a/crates/radicle/src/git/canonical/rules/test.rs b/crates/radicle/src/git/canonical/rules/test.rs
index 07887b11b..9931d1fac 100644
--- a/crates/radicle/src/git/canonical/rules/test.rs
+++ b/crates/radicle/src/git/canonical/rules/test.rs
@@ -6,12 +6,11 @@ use std::collections::BTreeMap;
use nonempty::nonempty;
use crate::Storage;
-use crate::crypto::{Signer, test::signer::MockSigner};
+use crate::crypto::{Signer as _, SigningKey};
use crate::git;
use crate::git::fmt::qualified_pattern;
use crate::identity::Visibility;
use crate::identity::doc::Doc;
-use crate::node::device::Device;
use crate::rad;
use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH, SIGREFS_PARENT};
use crate::storage::{ReadStorage, git::transport};
@@ -295,23 +294,25 @@ fn rule_validate_failures() {
Err(ValidationError::Threshold(_))
));
- let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
+ let delegates = NonEmpty::from_vec(
+ arbitrary::set::<Did>(256..=256)
+ .into_iter()
+ .collect::<Vec<_>>(),
+ )
+ .unwrap();
+ assert_eq!(delegates.len(), 256);
assert!(matches!(
Rule::new(delegates.into(), 1).validate(&mut || resolve_from_doc(&doc)),
Err(ValidationError::Delegates(_))
));
- let delegates = nonempty![
- did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
- did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
- ];
+ let delegate = Did::from(SigningKey::mock(22).public_key());
+
+ let delegates = nonempty![delegate, delegate];
+ assert_eq!(delegates.len(), 2);
+
let expected = Rule {
- allow: ResolvedDelegates::Set(
- doc::Delegates::new(nonempty![did(
- "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
- )])
- .unwrap(),
- ),
+ allow: ResolvedDelegates::Set(doc::Delegates::new(nonempty![delegate]).unwrap()),
threshold: doc::Threshold::MIN,
extensions: json::Map::new(),
};
@@ -357,8 +358,8 @@ fn canonical() {
transport::local::register(storage.clone());
- let delegate = Device::mock_from_seed([0xff; 32]);
- let contributor = MockSigner::from_seed([0xfe; 32]);
+ let delegate = SigningKey::mock(0xff);
+ let contributor = SigningKey::mock(0xfe);
let (repo, head) = fixtures::repository(tempdir.path().join("working"));
let (rid, doc, _) = rad::init(
&repo,
diff --git a/crates/radicle/src/git/raw.rs b/crates/radicle/src/git/raw.rs
index 68f589ed0..4fc73ce3e 100644
--- a/crates/radicle/src/git/raw.rs
+++ b/crates/radicle/src/git/raw.rs
@@ -5,9 +5,7 @@
// Re-exports created by manually scanning the `heartwood` workspace on 2025-10-04.
// Re-exports that are only used within this crate.
-pub(crate) use git2::{
- AutotagOption, Blob, FetchOptions, FetchPrune, Object, Revwalk, Sort, message_trailers_strs,
-};
+pub(crate) use git2::{AutotagOption, Blob, FetchOptions, FetchPrune, Object, Revwalk, Sort};
#[cfg(unix)]
pub(crate) use git2::Config;
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 7230ff2bd..a6147124b 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -26,11 +26,10 @@ use crate::git::fmt::RefString;
use crate::git::raw::ErrorExt as _;
use crate::identity::crefs;
use crate::identity::{Did, project::Project};
-use crate::node::device::Device;
use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};
-pub use crypto::PublicKey;
+pub use crypto::{PublicKey, VerifyingKey};
pub use radicle_core::repo::*;
use super::CanonicalRefs;
@@ -959,13 +958,16 @@ impl Doc {
signature: &Signature,
blob: Oid,
) -> Result<(), PublicKey> {
+ use crypto::signature::Verifier as _;
+
if !self.is_delegate(&key.into()) {
return Err(*key);
}
- if key.verify(AsRef::<[u8]>::as_ref(&blob), signature).is_err() {
- return Err(*key);
- }
- Ok(())
+
+ VerifyingKey::try_from(key)
+ .map_err(|_| *key)?
+ .verify(AsRef::<[u8]>::as_ref(&blob), signature)
+ .map_err(|_| *key)
}
/// Check the provided `votes` passes the [`Doc::majority`].
@@ -1002,10 +1004,10 @@ impl Doc {
/// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
/// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
- pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ pub fn sign(
+ &self,
+ signer: &impl crypto::Signer,
+ ) -> Result<(git::Oid, Vec<u8>, Signature), DocError> {
let (oid, bytes) = self.encode()?;
let sig = signer.sign(oid.as_ref());
@@ -1013,10 +1015,7 @@ impl Doc {
}
/// Similar to [`Doc::sign`], but only returning the [`Signature`].
- pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ pub fn signature_of(&self, signer: &impl crypto::Signer) -> Result<Signature, DocError> {
let (_, _, sig) = self.sign(signer)?;
Ok(sig)
@@ -1037,21 +1036,15 @@ impl Doc {
/// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
/// document.
- pub fn init<G>(
+ pub fn init(
&self,
repo: &storage::git::Repository,
- signer: &Device<G>,
- ) -> Result<git::Oid, RepositoryError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> Result<git::Oid, RepositoryError> {
let cob = identity::Identity::initialize(self, repo, signer)?;
- let id_ref = git::refs::storage::id(signer.public_key());
- let cob_ref = git::refs::storage::cob(
- signer.public_key(),
- &crate::cob::identity::TYPENAME,
- &cob.id,
- );
+ let public_key = signer.public_key();
+ let id_ref = git::refs::storage::id(public_key);
+ let cob_ref = git::refs::storage::cob(public_key, &crate::cob::identity::TYPENAME, &cob.id);
// Set `…/refs/rad/id` -> `…/refs/cobs/xyz.radicle.id/<id>`
repo.backend.reference_symbolic(
id_ref.as_str(),
@@ -1129,6 +1122,7 @@ impl GetRawCanonicalRefs for RawDoc {}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
+ use crypto::{Signer as _, SigningKey};
use serde_json::json;
use crate::assert_matches;
@@ -1145,7 +1139,7 @@ mod test {
#[test]
fn test_duplicate_dids() {
- let delegate = Device::mock_from_seed([0xff; 32]);
+ let delegate = SigningKey::mock(usize::MAX);
let did = Did::from(delegate.public_key());
let mut doc = RawDoc::new(r#gen::<Project>(1), vec![did], 1, Visibility::Public);
doc.delegate(did);
@@ -1157,7 +1151,9 @@ mod test {
#[test]
fn test_max_delegates() {
// Generate more than the max delegates
- let delegates = (0..MAX_DELEGATES + 1).map(r#gen).collect::<Vec<Did>>();
+ let delegates = arbitrary::set::<Did>(MAX_DELEGATES + 1..=MAX_DELEGATES + 1)
+ .into_iter()
+ .collect::<Vec<_>>();
// A document with max delegates will be fine
let doc = RawDoc::new(
@@ -1280,7 +1276,7 @@ mod test {
transport::local::register(storage.clone());
- let delegate = Device::mock_from_seed([0xff; 32]);
+ let delegate = SigningKey::mock(usize::MAX);
let (repo, _) = fixtures::repository(tempdir.path().join("working"));
let (id, _, _) = rad::init(
&repo,
@@ -1331,7 +1327,7 @@ mod test {
let (working, _) = fixtures::repository(tempdir.path().join("working"));
- let delegate = Device::mock_from_seed([0xff; 32]);
+ let delegate = SigningKey::mock(usize::MAX);
let (rid, doc, _) = rad::init(
&working,
"heartwood".try_into().unwrap(),
diff --git a/crates/radicle/src/identity/project.rs b/crates/radicle/src/identity/project.rs
index 6509e7de7..c530870e0 100644
--- a/crates/radicle/src/identity/project.rs
+++ b/crates/radicle/src/identity/project.rs
@@ -12,7 +12,7 @@ use crate::git::{fmt::Qualified, refs::branch};
use crate::identity::doc;
use crate::identity::doc::Payload;
-pub use crypto::PublicKey;
+pub use crypto::VerifyingKey;
/// A project-related error.
#[derive(Debug, Error)]
diff --git a/crates/radicle/src/node.rs b/crates/radicle/src/node.rs
index 7503522a8..18b7f2679 100644
--- a/crates/radicle/src/node.rs
+++ b/crates/radicle/src/node.rs
@@ -6,7 +6,6 @@ use address::AddressType;
pub mod command;
pub mod config;
pub mod db;
-pub mod device;
pub mod events;
pub mod notifications;
pub mod policy;
diff --git a/crates/radicle/src/node/device.rs b/crates/radicle/src/node/device.rs
deleted file mode 100644
index 76a715757..000000000
--- a/crates/radicle/src/node/device.rs
+++ /dev/null
@@ -1,220 +0,0 @@
-use std::fmt;
-use std::ops::Deref;
-
-use crypto::{
- Signature,
- signature::{Keypair, KeypairRef, Signer, Verifier},
- ssh::ExtendedSignature,
-};
-
-use crate::crypto;
-
-use super::NodeId;
-
-/// A `Device` identifies the local node through its [`NodeId`], and carries its
-/// signing mechanism.
-///
-/// The signing mechanism is for node specific cryptography, e.g. signing
-/// `rad/sigrefs`, COB commits, node messages, etc.
-///
-/// Note that a `Device` can create [`Signature`]s and [`ExtendedSignature`]s.
-/// It can achieve this as long as `S` implements `Signer<Signature>`.
-#[derive(Clone)]
-pub struct Device<S> {
- node: NodeId,
- signer: S,
-}
-
-impl<S> fmt::Debug for Device<S> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Device")
- .field("node", &self.node.to_human())
- .finish()
- }
-}
-
-impl<S: crypto::Signer + Default> Default for Device<S> {
- fn default() -> Self {
- Self::from(S::default())
- }
-}
-
-impl<S: crypto::Signer> Device<S> {
- /// Construct a new `Device`.
- pub fn new(signer: S) -> Self {
- Self {
- node: *signer.public_key(),
- signer,
- }
- }
-}
-
-impl<S> Device<S> {
- /// Return the [`NodeId`] of the `Device.`
- pub fn node_id(&self) -> &NodeId {
- &self.node
- }
-
- /// Return the [`crypto::PublicKey`] of the `Device.`
- pub fn public_key(&self) -> &crypto::PublicKey {
- &self.node
- }
-
- /// Convert the `Device` into its signer.
- ///
- /// This consumes the `Device`.
- pub fn into_inner(self) -> S {
- self.signer
- }
-}
-
-#[cfg(any(test, feature = "test"))]
-impl Device<crypto::test::signer::MockSigner> {
- /// Construct a `Device` using a default `MockSigner` as the device signer.
- pub fn mock() -> Self {
- Device::from(crypto::test::signer::MockSigner::default())
- }
-
- /// Construct a `Device`, constructing an RNG'd `MockSigner` for the signer.
- pub fn mock_rng(rng: &mut fastrand::Rng) -> Self {
- Device::from(crypto::test::signer::MockSigner::new(rng))
- }
-
- /// Construct a `Device`, constructing a seeded `MockSigner` for the signer.
- pub fn mock_from_seed(seed: [u8; 32]) -> Self {
- Device::from(crypto::test::signer::MockSigner::from_seed(seed))
- }
-}
-
-impl<S: BoxableSigner + 'static> Device<S> {
- /// Construct a [`BoxedDevice`] from a given `Device`.
- pub fn boxed(self) -> BoxedDevice {
- BoxedDevice(Device {
- node: self.node,
- signer: BoxedSigner(Box::new(self.signer)),
- })
- }
-}
-
-impl<S> AsRef<NodeId> for Device<S> {
- fn as_ref(&self) -> &NodeId {
- &self.node
- }
-}
-
-impl<S> KeypairRef for Device<S> {
- type VerifyingKey = NodeId;
-}
-
-impl<S> Verifier<Signature> for Device<S> {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> {
- self.node
- .verify(msg, signature)
- .map_err(crypto::signature::Error::from_source)
- }
-}
-
-impl<S: crypto::Signer> From<S> for Device<S> {
- fn from(signer: S) -> Self {
- Self {
- node: *signer.public_key(),
- signer,
- }
- }
-}
-
-impl<S: crypto::Signer + Clone> From<&S> for Device<S> {
- fn from(signer: &S) -> Self {
- Self::from(signer.clone())
- }
-}
-
-impl<S: Signer<Signature>> Signer<Signature> for Device<S> {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
- self.signer.try_sign(msg)
- }
-}
-
-impl<S: Signer<Signature>> Signer<ExtendedSignature> for Device<S> {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, crypto::signature::Error> {
- Ok(ExtendedSignature {
- key: *self.public_key(),
- sig: self.signer.try_sign(msg)?,
- })
- }
-}
-
-pub trait BoxableSigner: Signer<Signature> + Keypair<VerifyingKey = crypto::PublicKey> {}
-
-impl<S: Signer<Signature> + Keypair<VerifyingKey = crypto::PublicKey>> BoxableSigner for S {}
-
-/// A `Signer<Signature>` that is packed in a [`Box`] for dynamic dispatch.
-pub struct BoxedSigner(Box<dyn BoxableSigner + 'static>);
-
-impl Signer<Signature> for BoxedSigner {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
- self.0.try_sign(msg)
- }
-}
-
-impl Keypair for BoxedSigner {
- type VerifyingKey = crypto::PublicKey;
-
- fn verifying_key(&self) -> Self::VerifyingKey {
- self.0.verifying_key()
- }
-}
-
-/// A `Device` where the signer is a dynamic `Signer<Signature>`, in the form of
-/// a [`BoxedSigner`].
-///
-/// This can be constructed via [`Device::boxed`].
-pub struct BoxedDevice(Device<BoxedSigner>);
-
-impl AsRef<Device<BoxedSigner>> for BoxedDevice {
- fn as_ref(&self) -> &Device<BoxedSigner> {
- &self.0
- }
-}
-
-impl Deref for BoxedDevice {
- type Target = Device<BoxedSigner>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl Signer<Signature> for BoxedDevice {
- fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
- self.0.signer.try_sign(msg)
- }
-}
-
-impl Signer<ExtendedSignature> for BoxedDevice {
- fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, crypto::signature::Error> {
- Ok(ExtendedSignature {
- key: *self.0.public_key(),
- sig: self.0.signer.try_sign(msg)?,
- })
- }
-}
-
-impl AsRef<crypto::PublicKey> for BoxedDevice {
- fn as_ref(&self) -> &crypto::PublicKey {
- &self.0.node
- }
-}
-
-impl KeypairRef for BoxedDevice {
- type VerifyingKey = crypto::PublicKey;
-}
-
-impl Verifier<Signature> for BoxedDevice {
- fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> {
- self.0
- .node
- .verify(msg, signature)
- .map_err(crypto::signature::Error::from_source)
- }
-}
diff --git a/crates/radicle/src/node/routing.rs b/crates/radicle/src/node/routing.rs
index 008477263..d3390d5e6 100644
--- a/crates/radicle/src/node/routing.rs
+++ b/crates/radicle/src/node/routing.rs
@@ -473,7 +473,7 @@ mod test {
let mut rng = fastrand::Rng::new();
let now = LocalTime::now();
let ids = arbitrary::vec::<RepoId>(10);
- let nodes = arbitrary::vec::<NodeId>(10);
+ let nodes = arbitrary::vec_distinct::<NodeId>(10..=10);
let mut db = database(":memory:");
for node in &nodes {
diff --git a/crates/radicle/src/node/sync/announce.rs b/crates/radicle/src/node/sync/announce.rs
index 602a961a4..12b7d7689 100644
--- a/crates/radicle/src/node/sync/announce.rs
+++ b/crates/radicle/src/node/sync/announce.rs
@@ -840,10 +840,15 @@ mod test {
#[test]
fn announcer_reached_preferred_seeds() {
- let local = arbitrary::r#gen::<NodeId>(0);
let seeds = arbitrary::set::<NodeId>(10..=10);
- let unsynced = seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
- let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
+ let local = seeds.iter().next().copied().unwrap();
+ let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
+ let preferred_seeds = seeds
+ .iter()
+ .skip(1)
+ .take(2)
+ .copied()
+ .collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(11),
@@ -1555,7 +1560,7 @@ mod test {
#[test]
fn synced_with_local_node_is_ignored() {
let local = arbitrary::r#gen::<NodeId>(0);
- let unsynced = arbitrary::set::<NodeId>(3..=3).into_iter().collect();
+ let unsynced = BTreeSet::from_iter(arbitrary::set::<NodeId>(3..=3));
let config = AnnouncerConfig::public(
local,
diff --git a/crates/radicle/src/node/sync/fetch.rs b/crates/radicle/src/node/sync/fetch.rs
index ff26c8c79..c8789a0ce 100644
--- a/crates/radicle/src/node/sync/fetch.rs
+++ b/crates/radicle/src/node/sync/fetch.rs
@@ -550,7 +550,18 @@ mod test {
let seeds = arbitrary::set::<NodeId>(3..=6)
.into_iter()
.collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ // Make sure that `extra_candidates` does not contain any of the preferred seeds.
+ let extra_candidates = loop {
+ let extra_candidates = arbitrary::set::<NodeId>(3..=3)
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+
+ if seeds.intersection(&extra_candidates).count() == 0 {
+ break extra_candidates;
+ }
+ };
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
@@ -574,10 +585,11 @@ mod test {
#[test]
fn ignores_duplicates_and_local_node() {
- let local = arbitrary::r#gen::<NodeId>(0);
let replicas = ReplicationFactor::default();
- let bob = arbitrary::r#gen::<NodeId>(1);
- let eve = arbitrary::r#gen::<NodeId>(2);
+
+ // Ensure that the three nodes in this test have pairwise distinct IDs.
+ let [local, bob, eve] = arbitrary::vec::<NodeId>(3).try_into().unwrap();
+
let seeds = [bob].into_iter().collect::<BTreeSet<_>>();
let extra_candidates = vec![bob, local, eve];
let config = FetcherConfig::public(seeds.clone(), replicas, local)
@@ -631,7 +643,18 @@ mod test {
let seeds = arbitrary::set::<NodeId>(3..=3)
.into_iter()
.collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ // Make sure that `extra_candidates` does not contain any of the preferred seeds.
+ let extra_candidates = loop {
+ let extra_candidates = arbitrary::set::<NodeId>(3..=3)
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+
+ if seeds.intersection(&extra_candidates).count() == 0 {
+ break extra_candidates;
+ }
+ };
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
@@ -673,12 +696,26 @@ mod test {
let seeds = arbitrary::set::<NodeId>(3..=3)
.into_iter()
.collect::<BTreeSet<_>>();
- let extra_candidates = arbitrary::vec::<NodeId>(3);
+
+ // Make sure that `extra_candidates` does not contain any of the preferred seeds.
+ let extra_candidates = loop {
+ let extra_candidates = arbitrary::set::<NodeId>(3..=3)
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+
+ if seeds.intersection(&extra_candidates).count() == 0 {
+ break extra_candidates;
+ }
+ };
+
+ assert_eq!(extra_candidates.len(), replicas.lower_bound());
+
let config = FetcherConfig::public(seeds.clone(), replicas, local)
.with_candidates(extra_candidates.clone().into_iter().map(Candidate::new));
let mut fetcher = Fetcher::new(config).expect("fetcher should be constructed correctly");
let mut result = Vec::with_capacity(extra_candidates.len());
+
let expected = extra_candidates
.clone()
.into_iter()
diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs
index 24c6bfde5..3524745b4 100644
--- a/crates/radicle/src/profile.rs
+++ b/crates/radicle/src/profile.rs
@@ -14,6 +14,9 @@
pub mod config;
pub use config::{Config, WriteError};
+mod signer;
+pub use signer::Signer;
+
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::{fs, io};
@@ -23,10 +26,9 @@ use thiserror::Error;
use crate::cob::migrate;
use crate::cob::store::access::{ReadOnly, WriteAs};
-use crate::crypto::PublicKey;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{Keystore, Passphrase, keystore};
-use crate::node::device::{BoxedDevice, Device};
+use crate::crypto::{PublicKey, Seed};
use crate::node::policy::config::store::Read;
use crate::node::{Alias, AliasStore, Handle as _, Node, notifications, policy, policy::Scope};
use crate::prelude::{Did, NodeId, RepoId};
@@ -205,32 +207,19 @@ pub enum Error {
#[derive(Debug, Error)]
pub enum SignerError {
#[error(transparent)]
- MemorySigner(#[from] keystore::MemorySignerError),
+ LoadError(#[from] crypto::LoadError),
#[error(transparent)]
- Agent(#[from] crate::crypto::ssh::agent::AgentError),
-
- #[error("Radicle key `{0}` is not registered; run `rad auth` to register it with ssh-agent")]
- KeyNotRegistered(PublicKey),
+ Agent(#[from] crate::crypto::ssh::agent::IntoSignerError),
#[error(transparent)]
Keystore(#[from] keystore::Error),
- #[error("error connecting to ssh-agent: {source}")]
- AgentConnection {
- source: crate::crypto::ssh::agent::ConnectError,
- },
-}
+ #[error("error connecting to ssh-agent: {0}")]
+ AgentConnection(#[from] crate::crypto::ssh::agent::ConnectError),
-impl SignerError {
- /// Some signer errors are potentially recoverable by prompting the user
- /// for a password.
- pub fn prompt_for_passphrase(&self) -> bool {
- matches!(
- self,
- Self::AgentConnection { .. } | Self::KeyNotRegistered(_)
- )
- }
+ #[error("public key is invalid: {0}")]
+ InvalidPublicKey(#[source] crypto::signature::Error),
}
#[derive(Debug, Clone)]
@@ -247,7 +236,7 @@ impl Profile {
home: Home,
alias: Alias,
passphrase: Option<Passphrase>,
- seed: crypto::Seed,
+ seed: Seed,
) -> Result<Self, Error> {
let keystore = Keystore::new(&home.keys());
let public_key = keystore.init("radicle", passphrase, seed)?;
@@ -333,24 +322,36 @@ impl Profile {
Did::from(self.public_key)
}
- pub fn signer(&self) -> Result<BoxedDevice, SignerError> {
- if !self.keystore.is_encrypted()? {
- let signer = keystore::MemorySigner::load(&self.keystore, None)?;
- return Ok(Device::from(signer).boxed());
- }
-
- if let Some(passphrase) = env::passphrase() {
- let signer = keystore::MemorySigner::load(&self.keystore, Some(passphrase))?;
- return Ok(Device::from(signer).boxed());
- }
+ pub fn signer(&self) -> Result<Signer, SignerError> {
+ let key = {
+ // First, check whether the key is encrypted.
+ if self.keystore.is_encrypted()? {
+ // The key is encrypted, so we need a passphrase to load it.
+ if let Some(passphrase) = env::passphrase() {
+ // We have a passphrase from the environment,
+ // so we use it to load the key.
+ (true, Some(passphrase))
+ } else {
+ // We do not have a passphrase, so we cannot load the key.
+ (false, None)
+ }
+ } else {
+ // The key is not encrypted, so we load it without a passphrase.
+ (true, None)
+ }
+ };
- let agent = Agent::connect().map_err(|source| SignerError::AgentConnection { source })?;
- let signer = agent.signer(self.public_key);
- if signer.is_ready()? {
- Ok(Device::from(signer).boxed())
+ Ok(if let (true, passphrase) = key {
+ Signer::Key(crypto::SigningKey::load(&self.keystore, passphrase)?)
} else {
- Err(SignerError::KeyNotRegistered(self.public_key))
- }
+ Signer::Agent(
+ Agent::connect()?.into_signer(
+ self.id()
+ .try_into()
+ .map_err(SignerError::InvalidPublicKey)?,
+ )?,
+ )
+ })
}
/// Get Radicle home.
@@ -797,7 +798,7 @@ impl Home {
}
/// Return a read-write handle for the issues cache.
- pub fn issues_mut<'a, 'b, Repo, Signer>(
+ pub fn issues_mut<'a, 'b, Repo, Signer: crypto::Signer>(
&self,
repository: &'a Repo,
signer: &'b Signer,
@@ -830,7 +831,7 @@ impl Home {
}
/// Return a read-write handle for the patches cache.
- pub fn patches_mut<'a, 'b, Repo, Signer>(
+ pub fn patches_mut<'a, 'b, Repo, Signer: crypto::Signer>(
&self,
repository: &'a Repo,
signer: &'b Signer,
diff --git a/crates/radicle/src/profile/signer.rs b/crates/radicle/src/profile/signer.rs
new file mode 100644
index 000000000..d1f2e0226
--- /dev/null
+++ b/crates/radicle/src/profile/signer.rs
@@ -0,0 +1,41 @@
+use crypto::{PublicKey, Signature, SigningKey, VerifyingKey, signature, ssh::agent::AgentSigner};
+
+/// Wraps well-known implementations of [`signature::Signer`] and
+/// [`signature::Keypair`].
+///
+/// This is used to abstract over the different ways of signing (references and
+/// COB operations), such as using a [`SigningKey`] directly (usually loaded via
+/// [`crypto::ssh::Keystore`]) or SSH Agent (via [`AgentSigner`]).
+pub enum Signer {
+ Key(SigningKey),
+ Agent(AgentSigner),
+}
+
+impl signature::Signer<Signature> for Signer {
+ fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+ match self {
+ Signer::Key(key) => key.try_sign(msg),
+ Signer::Agent(agent) => agent.try_sign(msg),
+ }
+ }
+}
+
+impl signature::Keypair for Signer {
+ type VerifyingKey = VerifyingKey;
+
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ match self {
+ Signer::Key(key) => key.verifying_key(),
+ Signer::Agent(agent) => agent.verifying_key(),
+ }
+ }
+}
+
+impl AsRef<PublicKey> for Signer {
+ fn as_ref(&self) -> &PublicKey {
+ match self {
+ Signer::Key(key) => key.as_ref(),
+ Signer::Agent(agent) => agent.as_ref(),
+ }
+ }
+}
diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs
index 74d89fd8f..91ca24356 100644
--- a/crates/radicle/src/rad.rs
+++ b/crates/radicle/src/rad.rs
@@ -11,7 +11,6 @@ use crate::git::BranchName;
use crate::identity::doc;
use crate::identity::doc::{DocError, RepoId, Visibility};
use crate::identity::project::{Project, ProjectName};
-use crate::node::device::Device;
use crate::storage::RepositoryError;
use crate::storage::git::Repository;
use crate::storage::git::transport;
@@ -46,19 +45,15 @@ pub enum InitError {
}
/// Initialize a new Radicle project from a git repository.
-pub fn init<G, S>(
+pub fn init(
repo: &git::raw::Repository,
name: ProjectName,
description: &str,
default_branch: BranchName,
visibility: Visibility,
- signer: &Device<G>,
- storage: S,
-) -> Result<(RepoId, identity::Doc, SignedRefs), InitError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
- S: WriteStorage,
-{
+ signer: &impl crypto::Signer,
+ storage: &impl WriteStorage,
+) -> Result<(RepoId, identity::Doc, SignedRefs), InitError> {
// TODO: Better error when project id already exists in storage, but remote doesn't.
let delegate: identity::Did = signer.public_key().into();
let proj = Project::new(
@@ -76,7 +71,7 @@ where
})?;
let doc = identity::Doc::initial(proj, delegate, visibility);
- let (project, identity) = Repository::init(&doc, &storage, signer)?;
+ let (project, identity) = Repository::init(&doc, storage, signer)?;
let url = git::Url::from(project.id);
match init_configure(repo, &project, &default_branch, &url, identity, signer) {
@@ -95,17 +90,14 @@ where
}
}
-fn init_configure<G>(
+fn init_configure(
repo: &git::raw::Repository,
stored: &Repository,
default_branch: &BranchName,
url: &git::Url,
identity: git::Oid,
- signer: &Device<G>,
-) -> Result<SignedRefs, InitError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
-{
+ signer: &impl crypto::Signer,
+) -> Result<SignedRefs, InitError> {
let pk = signer.public_key();
git::configure_repository(repo)?;
@@ -166,16 +158,12 @@ pub enum ForkError {
/// Create a local tree for an existing project, from an existing remote.
#[cfg(any(test, feature = "test"))]
-pub fn fork_remote<G, S>(
+pub fn fork_remote(
proj: RepoId,
remote: &RemoteId,
- signer: &Device<G>,
- storage: S,
-) -> Result<(), ForkError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
- S: storage::WriteStorage,
-{
+ signer: &impl crypto::Signer,
+ storage: impl storage::WriteStorage,
+) -> Result<(), ForkError> {
// TODO: Copy tags over?
// Creates or copies the following references:
@@ -205,11 +193,11 @@ where
Ok(())
}
-pub fn fork<G, S>(rid: RepoId, signer: &Device<G>, storage: &S) -> Result<(), ForkError>
-where
- G: crypto::signature::Signer<crypto::Signature>,
- S: storage::WriteStorage,
-{
+pub fn fork(
+ rid: RepoId,
+ signer: &impl crypto::Signer,
+ storage: &impl storage::WriteStorage,
+) -> Result<(), ForkError> {
let me = signer.public_key();
let repository = storage.repository_mut(rid)?;
let (canonical_branch, canonical_head) = repository.head()?;
@@ -510,6 +498,7 @@ mod tests {
use pretty_assertions::assert_eq;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::identity::Did;
use crate::storage::git::Storage;
use crate::storage::git::transport;
@@ -522,8 +511,8 @@ mod tests {
#[test]
fn test_init() {
let tempdir = tempfile::tempdir().unwrap();
- let signer = Device::mock();
- let public_key = *signer.public_key();
+ let signer = SigningKey::mock(73);
+ let public_key = signer.public_key();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
@@ -564,7 +553,7 @@ mod tests {
.unwrap(),
);
- assert_eq!(remotes[&public_key].refs.refs(), refs.refs());
+ assert_eq!(remotes[public_key].refs.refs(), refs.refs());
assert_eq!(project.name(), "acme");
assert_eq!(project.description(), "Acme's repo");
assert_eq!(project.default_branch(), &git::fmt::refname!("master"));
@@ -575,8 +564,8 @@ mod tests {
fn test_fork() {
let mut rng = fastrand::Rng::new();
let tempdir = tempfile::tempdir().unwrap();
- let alice = Device::mock_rng(&mut rng);
- let bob = Device::mock_rng(&mut rng);
+ let alice = SigningKey::mock(rng.usize(..));
+ let bob = SigningKey::mock(rng.usize(..));
let bob_id = bob.public_key();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
@@ -613,7 +602,7 @@ mod tests {
#[test]
fn test_checkout() {
let tempdir = tempfile::tempdir().unwrap();
- let signer = Device::mock();
+ let signer = SigningKey::mock(73);
let remote_id = signer.public_key();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index 99c99a69c..a11bb9fe0 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -728,20 +728,12 @@ pub trait WriteRepository: ReadRepository + SignRepository {
/// Allows signing refs.
pub trait SignRepository {
/// Sign the repository's refs under the `refs/rad/sigrefs` branch.
- fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>;
+ fn sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError>;
/// Sign the repository's refs under the `refs/rad/sigrefs` branch, even if unchanged.
///
/// Most users will prefer [`Self::sign_refs`].
- fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>;
+ fn force_sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError>;
}
impl<T, S> ReadStorage for T
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index d130e51d7..956a013fc 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -5,7 +5,7 @@ pub mod transport;
pub mod temp;
pub use temp::TempRepository;
-use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::collections::{BTreeMap, BTreeSet};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
@@ -16,7 +16,6 @@ use crate::git::raw::ErrorExt as _;
use crate::identity::doc::DocError;
use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
-use crate::node::device::Device;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs};
use crate::storage::{
ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
@@ -522,15 +521,11 @@ impl Repository {
}
/// Create the repository's identity branch.
- pub fn init<G, S>(
+ pub fn init(
doc: &Doc,
- storage: &S,
- signer: &Device<G>,
- ) -> Result<(Self, crate::git::Oid), RepositoryError>
- where
- G: crypto::signature::Signer<crypto::Signature>,
- S: WriteStorage,
- {
+ storage: &impl WriteStorage,
+ signer: &impl crypto::Signer,
+ ) -> Result<(Self, crate::git::Oid), RepositoryError> {
let (doc_oid, doc_bytes) = doc.encode()?;
let id = RepoId::from(doc_oid);
let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
@@ -1032,52 +1027,37 @@ impl WriteRepository for Repository {
}
impl SignRepository for Repository {
- fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ fn sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError> {
self.sign_refs_with(signer, false)
}
- fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ fn force_sign_refs(&self, signer: &impl crypto::Signer) -> Result<SignedRefs, RepositoryError> {
self.sign_refs_with(signer, true)
}
}
impl Repository {
- fn sign_refs_with<Signer>(
+ fn sign_refs_with(
&self,
- signer: &Signer,
+ signer: &impl crypto::Signer,
force: bool,
- ) -> Result<SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
- let remote = signer.verifying_key();
+ ) -> Result<SignedRefs, RepositoryError> {
+ let remote = signer.public_key();
// Ensure the root reference is set, which is checked during sigref verification.
if self
- .reference_oid(&remote, &git::refs::storage::IDENTITY_ROOT)
+ .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT)
.is_err()
{
- self.set_remote_identity_root(&remote)?;
+ self.set_remote_identity_root(remote)?;
}
- let committer = refs::sigrefs::git::Committer::from_env_or_now(&remote);
+ let committer = refs::sigrefs::git::Committer::from_env_or_now(remote);
- let refs = self.references_of(&remote)?;
+ let refs = self.references_of(remote)?;
let signed = if force {
- refs.force_save(remote, committer, self, signer)?
+ refs.force_save(*remote, committer, self, signer)?
} else {
- refs.save(remote, committer, self, signer)?
+ refs.save(*remote, committer, self, signer)?
};
Ok(signed)
@@ -1137,48 +1117,6 @@ impl sigrefs::git::reference::Writer for Repository {
}
}
-pub mod trailers {
- use std::str::FromStr;
-
- use thiserror::Error;
-
- use super::*;
- use crypto::{PublicKey, PublicKeyError};
- use crypto::{Signature, SignatureError};
-
- pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
-
- #[derive(Error, Debug)]
- pub enum Error {
- #[error("invalid format for signature trailer")]
- SignatureTrailerFormat,
- #[error("invalid public key in signature trailer")]
- PublicKey(#[from] PublicKeyError),
- #[error("invalid signature in trailer")]
- Signature(#[from] SignatureError),
- }
-
- pub fn parse_signatures(msg: &str) -> Result<HashMap<PublicKey, Signature>, Error> {
- let trailers =
- git::raw::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
- let mut signatures = HashMap::with_capacity(trailers.len());
-
- for (key, val) in trailers.iter() {
- if key == SIGNATURE_TRAILER {
- if let Some((pk, sig)) = val.split_once(' ') {
- let pk = PublicKey::from_str(pk)?;
- let sig = Signature::from_str(sig)?;
-
- signatures.insert(pk, sig);
- } else {
- return Err(Error::SignatureTrailerFormat);
- }
- }
- }
- Ok(signatures)
- }
-}
-
pub mod paths {
use std::path::PathBuf;
@@ -1197,13 +1135,14 @@ mod tests {
use super::*;
use crate::git;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::storage::{ReadRepository, ReadStorage};
use crate::test::fixtures;
#[test]
fn test_references_of() {
let tmp = tempfile::tempdir().unwrap();
- let signer = Device::mock();
+ let signer = SigningKey::mock(54);
let storage = Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
@@ -1237,10 +1176,9 @@ mod tests {
#[test]
fn test_sign_refs() {
let tmp = tempfile::tempdir().unwrap();
- let mut rng = fastrand::Rng::new();
- let signer = Device::mock_rng(&mut rng);
+ let signer = SigningKey::mock(0xb4);
let storage = Storage::open(tmp.path(), fixtures::user()).unwrap();
- let alice = *signer.public_key();
+ let alice = signer.public_key();
let (rid, _, working, _) =
fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
let stored = storage.repository(rid).unwrap();
@@ -1259,8 +1197,8 @@ mod tests {
.unwrap();
let signed = stored.sign_refs(&signer).unwrap();
- let remote = stored.remote(&alice).unwrap();
- let mut unsigned = stored.references_of(&alice).unwrap();
+ let remote = stored.remote(alice).unwrap();
+ let mut unsigned = stored.references_of(alice).unwrap();
// The signed refs doesn't contain the signature ref itself.
unsigned.remove_sigrefs().unwrap();
diff --git a/crates/radicle/src/storage/git/cob.rs b/crates/radicle/src/storage/git/cob.rs
index 2ca9eef7c..01f191e8f 100644
--- a/crates/radicle/src/storage/git/cob.rs
+++ b/crates/radicle/src/storage/git/cob.rs
@@ -3,7 +3,6 @@ use std::collections::BTreeMap;
use std::path::Path;
use cob::object::Objects;
-use cob::signatures::ExtendedSignature;
use radicle_cob as cob;
use radicle_cob::change;
use storage::RemoteRepository;
@@ -43,8 +42,6 @@ pub enum TypesError {
#[error(transparent)]
Git(#[from] git::raw::Error),
#[error(transparent)]
- ParseKey(#[from] crypto::Error),
- #[error(transparent)]
ParseObjectId(#[from] cob::object::ParseObjectId),
#[error(transparent)]
RefFormat(#[from] git::fmt::Error),
@@ -58,18 +55,17 @@ impl change::Storage for Repository {
type ObjectId = <git::raw::Repository as change::Storage>::ObjectId;
type Parent = <git::raw::Repository as change::Storage>::Parent;
- type Signatures = <git::raw::Repository as change::Storage>::Signatures;
- fn store<Signer>(
+ type PublicKey = <git::raw::Repository as change::Storage>::PublicKey;
+ type Signature = <git::raw::Repository as change::Storage>::Signature;
+
+ fn store(
&self,
authority: Option<Self::Parent>,
parents: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: change::Template<Self::ObjectId>,
- ) -> Result<cob::Entry, Self::StoreError>
- where
- Signer: crypto::signature::Signer<ExtendedSignature>,
- {
+ ) -> Result<cob::Entry, Self::StoreError> {
self.backend.store(authority, parents, signer, spec)
}
@@ -203,18 +199,17 @@ impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> {
type ObjectId = <git::raw::Repository as change::Storage>::ObjectId;
type Parent = <git::raw::Repository as change::Storage>::Parent;
- type Signatures = <git::raw::Repository as change::Storage>::Signatures;
- fn store<Signer>(
+ type PublicKey = <git::raw::Repository as change::Storage>::PublicKey;
+ type Signature = <git::raw::Repository as change::Storage>::Signature;
+
+ fn store(
&self,
authority: Option<Self::Parent>,
parents: Vec<Self::Parent>,
- signer: &Signer,
+ signer: &impl crypto::Signer,
spec: change::Template<Self::ObjectId>,
- ) -> Result<cob::Entry, Self::StoreError>
- where
- Signer: crypto::signature::Signer<ExtendedSignature>,
- {
+ ) -> Result<cob::Entry, Self::StoreError> {
self.repo.raw().store(authority, parents, signer, spec)
}
@@ -235,31 +230,21 @@ impl<Repo> SignRepository for DraftStore<'_, Repo>
where
Repo: storage::ReadRepository,
{
- fn sign_refs<Signer>(
+ fn sign_refs(
&self,
- signer: &Signer,
- ) -> Result<storage::refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> Result<storage::refs::SignedRefs, RepositoryError> {
// Since this is a draft store, we do not actually want to sign the refs.
// Instead, we just return the existing signed refs.
- let remote = self.repo.remote(&signer.verifying_key())?;
+ let remote = self.repo.remote(signer.public_key())?;
Ok(remote.refs)
}
- fn force_sign_refs<Signer>(
+ fn force_sign_refs(
&self,
- signer: &Signer,
- ) -> Result<storage::refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ signer: &impl crypto::Signer,
+ ) -> Result<storage::refs::SignedRefs, RepositoryError> {
self.sign_refs(signer)
}
}
diff --git a/crates/radicle/src/storage/refs.rs b/crates/radicle/src/storage/refs.rs
index 639dd5f92..1f4b0a156 100644
--- a/crates/radicle/src/storage/refs.rs
+++ b/crates/radicle/src/storage/refs.rs
@@ -10,8 +10,8 @@ use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::str::FromStr;
-use crypto::signature;
-use crypto::{PublicKey, Signature};
+use crypto::PublicKey;
+use crypto::Signature;
use radicle_core::NodeId;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -70,55 +70,49 @@ impl Refs {
/// Save the signed refs to disk.
/// This creates a new commit on the signed refs branch, and updates the branch pointer.
- pub fn save<R, S>(
+ pub fn save<Repo>(
self,
namespace: NodeId,
committer: sigrefs::git::Committer,
- repo: &R,
- signer: &S,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
) -> Result<SignedRefs, Error>
where
- R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
- R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
- R: HasRepoId,
- S: signature::Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+ Repo: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+ Repo: HasRepoId,
{
self.save_with(namespace, committer, repo, signer, false)
}
/// Save the signed refs to disk, even if the refs are unchanged.
- pub fn force_save<R, S>(
+ pub fn force_save<Repo>(
self,
namespace: NodeId,
committer: sigrefs::git::Committer,
- repo: &R,
- signer: &S,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
) -> Result<SignedRefs, Error>
where
- R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
- R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
- R: HasRepoId,
- S: signature::Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+ Repo: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+ Repo: HasRepoId,
{
self.save_with(namespace, committer, repo, signer, true)
}
- fn save_with<R, S>(
+ fn save_with<Repo>(
self,
namespace: NodeId,
committer: sigrefs::git::Committer,
- repo: &R,
- signer: &S,
+ repo: &Repo,
+ signer: &impl crypto::Signer,
force: bool,
) -> Result<SignedRefs, Error>
where
- R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
- R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
- R: HasRepoId,
- S: signature::Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+ Repo: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+ Repo: HasRepoId,
{
let msg = "Update signed refs\n";
let reflog = format!("Save {} signed references", self.len());
@@ -452,7 +446,12 @@ impl SignedRefs {
R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
{
let root = repo.rid();
- match sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read() {
+
+ let verifier = crypto::VerifyingKey::try_from(&remote).map_err(|source| {
+ sigrefs::read::error::Read::Verify(sigrefs::read::error::Verify::Signature(source))
+ })?;
+
+ match sigrefs::SignedRefsReader::new(root, tip, repo, &verifier).read() {
Ok(latest) => Ok(Some(latest.into_sigrefs_at(remote))),
Err(sigrefs::read::error::Read::MissingSigrefs { namespace }) => {
debug_assert_eq!(namespace, remote);
@@ -500,7 +499,7 @@ mod tests {
use super::*;
use crate::assert_matches;
- use crate::node::device::Device;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::storage::WriteRepository as _;
use crate::{Storage, cob::Title, cob::identity::Identity, rad, test::fixtures};
@@ -522,8 +521,8 @@ mod tests {
// `paris` repo. We also don't expected the signed refs to validate without error.
fn test_rid_verification() {
let tmp = tempfile::tempdir().unwrap();
- let alice = Device::mock();
- let bob = Device::mock();
+ let alice = SigningKey::mock(9);
+ let bob = SigningKey::mock(10);
let storage = &Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
@@ -665,7 +664,7 @@ mod tests {
// under her copy of Bob's namespace, and this would only be rejected during signed ref
// validation.
{
- let name = &SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(bob.node_id()));
+ let name = &SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(bob.public_key()));
let id = paris.backend.refname_to_id(name.as_str()).unwrap();
london
.backend
diff --git a/crates/radicle/src/storage/refs/arbitrary.rs b/crates/radicle/src/storage/refs/arbitrary.rs
index 5cb40075c..67f07d0de 100644
--- a/crates/radicle/src/storage/refs/arbitrary.rs
+++ b/crates/radicle/src/storage/refs/arbitrary.rs
@@ -2,14 +2,9 @@
use qcheck::Arbitrary;
-use crate::node::device::Device;
-
use super::*;
-pub fn signed_refs_at<S>(g: &mut qcheck::Gen, root: Oid, signer: &Device<S>) -> SignedRefs
-where
- S: crypto::signature::Signer<crypto::Signature>,
-{
+pub fn signed_refs_at(g: &mut qcheck::Gen, root: Oid, signer: &impl crypto::Signer) -> SignedRefs {
let mut refs = Refs::arbitrary(g);
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
@@ -25,7 +20,7 @@ where
SignedRefs {
refs,
signature,
- id: *signer.node_id(),
+ id: *signer.public_key(),
level: level.unwrap_or_else(|| FeatureLevel::arbitrary(g)),
parent: Arbitrary::arbitrary(g),
at: Oid::arbitrary(g),
diff --git a/crates/radicle/src/storage/refs/sigrefs/git/properties.rs b/crates/radicle/src/storage/refs/sigrefs/git/properties.rs
index 5c3d636b4..d7ef6c8b6 100644
--- a/crates/radicle/src/storage/refs/sigrefs/git/properties.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/git/properties.rs
@@ -1,7 +1,7 @@
use std::path::Path;
-use crypto::test::signer::MockSigner;
-use crypto::{PublicKey, Signer as _, signature};
+use crypto::{Signer as _, SigningKey};
+use crypto::{signature, signature::Keypair as _};
use qcheck::TestResult;
use qcheck_macros::quickcheck;
use radicle_core::{NodeId, RepoId};
@@ -20,26 +20,6 @@ use super::Committer;
type BoundedVec<T> = arbitrary::BoundedVec<T, 24>;
-struct Verifier {
- key: PublicKey,
-}
-
-impl Verifier {
- fn new(signer: &MockSigner) -> Self {
- Self {
- key: *signer.public_key(),
- }
- }
-}
-
-impl signature::Verifier<crypto::Signature> for Verifier {
- fn verify(&self, msg: &[u8], signature: &crypto::Signature) -> Result<(), signature::Error> {
- self.key
- .verify(msg, signature)
- .map_err(signature::Error::from_source)
- }
-}
-
fn mock_author() -> Author {
Author {
name: "testy".to_string(),
@@ -111,7 +91,7 @@ fn write_log(
refs: Refs,
rid: RepoId,
namespace: NodeId,
- signer: &MockSigner,
+ signer: &SigningKey,
repo: &git::raw::Repository,
) -> Update {
SignedRefsWriter::new(refs, rid, namespace, repo, signer)
@@ -123,12 +103,15 @@ fn write_log(
.unwrap()
}
-fn read_log(
+fn read_log<Verifier>(
rid: RepoId,
namespace: NodeId,
verifier: &Verifier,
repo: &git::raw::Repository,
-) -> VerifiedCommit {
+) -> VerifiedCommit
+where
+ Verifier: signature::Verifier<crypto::Signature>,
+{
SignedRefsReader::new(rid, Tip::Reference(namespace), repo, verifier)
.read()
.unwrap()
@@ -143,9 +126,9 @@ fn initial_commit_roundtrip(mut refs: Refs) -> bool {
_tmp,
} = Repository::new();
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(73);
let namespace = *signer.public_key();
- let verifier = Verifier::new(&signer);
+ let verifier = signer.verifying_key();
let update = write_log(refs.clone(), rid, namespace, &signer, &repo);
let head_oid = match update {
@@ -174,9 +157,9 @@ fn chain_roundtrip(chain: BoundedVec<Refs>) -> TestResult {
root,
_tmp,
} = Repository::new();
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(98);
let namespace = *signer.public_key();
- let verifier = Verifier::new(&signer);
+ let verifier = signer.verifying_key();
let mut last_changed_head = None;
let mut expected_parent = None;
@@ -225,7 +208,7 @@ fn idempotent_write(mut refs: Refs) -> bool {
_tmp,
} = Repository::new();
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(83);
let namespace = *signer.public_key();
let first = write_log(refs.clone(), rid, namespace, &signer, &repo);
diff --git a/crates/radicle/src/storage/refs/sigrefs/property.rs b/crates/radicle/src/storage/refs/sigrefs/property.rs
index 917e7e674..188c43202 100644
--- a/crates/radicle/src/storage/refs/sigrefs/property.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/property.rs
@@ -1,8 +1,8 @@
mod mock;
use mock::*;
-use crypto::Signer as _;
-use crypto::test::signer::MockSigner;
+use crypto::signature::Keypair as _;
+use crypto::{Signer as _, SigningKey};
use qcheck::TestResult;
use qcheck_macros::quickcheck;
@@ -17,8 +17,8 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
}
let fixture = Fixture::new();
- let signer = MockSigner::default();
- let node_id = *signer.public_key();
+ let signer = SigningKey::mock(34);
+ let node_id = signer.public_key();
for refs in all_refs {
let refs = fixture.with_identity_root(refs);
@@ -26,7 +26,7 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
let writer = SignedRefsWriter::new(
refs.clone(),
fixture.rid(),
- node_id,
+ *node_id,
fixture.repo(),
&signer,
);
@@ -46,11 +46,13 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
assert_eq!(refs, written_refs);
+ let verifier = signer.verifying_key();
+
let reader = SignedRefsReader::new(
fixture.rid(),
- Tip::Reference(node_id),
+ Tip::Reference(*node_id),
fixture.repo(),
- &node_id,
+ &verifier,
);
let verified = match reader.read() {
Ok(v) => v,
@@ -69,7 +71,7 @@ fn roundtrip(BoundedVec(all_refs): BoundedVec<Refs>) -> TestResult {
fn idempotent(refs: Refs) -> TestResult {
let fixture = Fixture::new();
let refs = fixture.with_identity_root(refs);
- let signer = MockSigner::default();
+ let signer = SigningKey::mock(83);
let node_id = *signer.public_key();
if let Err(e) = SignedRefsWriter::new(
diff --git a/crates/radicle/src/storage/refs/sigrefs/read.rs b/crates/radicle/src/storage/refs/sigrefs/read.rs
index a22bd3703..ad3baa805 100644
--- a/crates/radicle/src/storage/refs/sigrefs/read.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/read.rs
@@ -9,7 +9,7 @@ use std::collections::{BTreeMap, HashMap};
use std::num::NonZeroUsize;
use std::path::Path;
-use crypto::{PublicKey, signature};
+use crypto::{PublicKey, VerifyingKey, signature};
use nonempty::NonEmpty;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::commit::CommitData;
@@ -72,11 +72,11 @@ impl VerifiedCommit {
/// - A [`Tip`] which describes where and how to start the verification.
/// - A `repository` which is the Git repository that is being used for the reading.
/// - A `verifier` which is the entity that verifies the cryptographic signatures.
-pub struct SignedRefsReader<'a, R, V> {
+pub struct SignedRefsReader<'a, Repository, Verifier = VerifyingKey> {
rid: RepoId,
tip: Tip,
- repository: &'a R,
- verifier: &'a V,
+ repository: &'a Repository,
+ verifier: &'a Verifier,
}
/// Describe where to start a [`SignedRefsReader`]'s commit chain.
@@ -126,13 +126,13 @@ impl std::fmt::Display for FeatureLevels {
}
}
-impl<'a, R, V> SignedRefsReader<'a, R, V>
+impl<'a, Repository, Verifier> SignedRefsReader<'a, Repository, Verifier>
where
- R: object::Reader + reference::Reader,
- V: signature::Verifier<crypto::Signature>,
+ Repository: object::Reader + reference::Reader,
+ Verifier: signature::Verifier<crypto::Signature>,
{
/// Construct a new [`SignedRefsReader`].
- pub fn new(rid: RepoId, tip: Tip, repository: &'a R, verifier: &'a V) -> Self {
+ pub fn new(rid: RepoId, tip: Tip, repository: &'a Repository, verifier: &'a Verifier) -> Self {
Self {
rid,
tip,
@@ -191,6 +191,19 @@ where
return Ok(head);
}
+ /// A wrapper around a [`crypto::Signature`] that implements
+ /// [`std::hash::Hash`] so that we can use it as a key in a [`HashMap`].
+ #[derive(PartialEq, Eq)]
+ #[repr(transparent)]
+ struct HashSignature(crypto::Signature);
+
+ impl std::hash::Hash for HashSignature {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ state.write(self.0.r_bytes());
+ state.write(self.0.s_bytes());
+ }
+ }
+
// `seen` maps from signatures to the `NonEmpty` of commits they were
// seen in. Note that for all sets of commits which share the same
// signature, the `NonEmpty` in `seen` will be in reverse order of the
@@ -201,13 +214,13 @@ where
// the maximum feature level over all commits in the history.
let (seen, levels) = iter::Walk::new(head.commit.oid, self.repository).try_fold(
(
- HashMap::<crypto::Signature, NonEmpty<Oid>>::new(),
+ HashMap::<HashSignature, NonEmpty<Oid>>::new(),
FeatureLevels::new(),
),
|(mut seen, mut levels), commit| {
let commit = commit.map_err(error::Read::Commit)?;
- seen.entry(commit.signature)
+ seen.entry(HashSignature(commit.signature))
.and_modify(|value| value.push(commit.oid))
.or_insert_with(|| NonEmpty::new(commit.oid));
@@ -242,7 +255,7 @@ where
}
if seen
- .get(&head.commit.signature)
+ .get(&HashSignature(head.commit.signature))
.expect(SIGNATURES_COLLECTED)
.len_nonzero()
== ONE
@@ -272,7 +285,9 @@ where
let commit = verified.commit();
- let commits = seen.get(&commit.signature).expect(SIGNATURES_COLLECTED);
+ let commits = seen
+ .get(&HashSignature(commit.signature))
+ .expect(SIGNATURES_COLLECTED);
if commits.len_nonzero() == ONE {
return Ok(verified);
}
@@ -345,13 +360,13 @@ impl Commit {
&self.signature
}
- pub(super) fn verify<V>(
+ pub(super) fn verify<Verifier>(
mut self,
expected: RepoId,
- verifier: &V,
+ verifier: &Verifier,
) -> Result<VerifiedCommit, error::Verify>
where
- V: signature::Verifier<crypto::Signature>,
+ Verifier: signature::Verifier<crypto::Signature>,
{
verifier
.verify(&self.refs.canonical(), &self.signature)
@@ -433,16 +448,13 @@ impl Commit {
}
}
-pub(super) struct CommitReader<'a, R> {
+pub(super) struct CommitReader<'a, Repository: object::Reader> {
commit: Oid,
- repository: &'a R,
+ repository: &'a Repository,
}
-impl<'a, R> CommitReader<'a, R>
-where
- R: object::Reader,
-{
- pub(super) fn new(commit: Oid, repository: &'a R) -> Self {
+impl<'a, Repo: object::Reader> CommitReader<'a, Repo> {
+ pub(super) fn new(commit: Oid, repository: &'a Repo) -> Self {
Self { commit, repository }
}
@@ -502,16 +514,13 @@ struct Tree {
signature: crypto::Signature,
}
-struct TreeReader<'a, R> {
+struct TreeReader<'a, Repo: object::Reader> {
commit: Oid,
- repository: &'a R,
+ repository: &'a Repo,
}
-impl<'a, R> TreeReader<'a, R>
-where
- R: object::Reader,
-{
- fn new(commit: Oid, repository: &'a R) -> Self {
+impl<'a, Repo: object::Reader> TreeReader<'a, Repo> {
+ fn new(commit: Oid, repository: &'a Repo) -> Self {
Self { commit, repository }
}
@@ -566,16 +575,13 @@ pub(super) struct IdentityRoot {
rid: RepoId,
}
-struct IdentityRootReader<'a, 'b, R> {
+struct IdentityRootReader<'a, 'b, Repo: object::Reader> {
refs: &'a Refs,
- repository: &'b R,
+ repository: &'b Repo,
}
-impl<'a, 'b, R> IdentityRootReader<'a, 'b, R>
-where
- R: object::Reader,
-{
- fn new(refs: &'a Refs, repository: &'b R) -> Self {
+impl<'a, 'b, Repo: object::Reader> IdentityRootReader<'a, 'b, Repo> {
+ fn new(refs: &'a Refs, repository: &'b Repo) -> Self {
Self { refs, repository }
}
diff --git a/crates/radicle/src/storage/refs/sigrefs/read/error.rs b/crates/radicle/src/storage/refs/sigrefs/read/error.rs
index c7a8c0626..dfb66520b 100644
--- a/crates/radicle/src/storage/refs/sigrefs/read/error.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/read/error.rs
@@ -81,7 +81,7 @@ pub enum Tree {
#[error(transparent)]
ParseRefs(canonical::Error),
#[error(transparent)]
- ParseSignature(crypto::Error),
+ ParseSignature(crypto::signature::Error),
#[error(transparent)]
MissingBlobs(#[from] MissingBlobs),
}
diff --git a/crates/radicle/src/storage/refs/sigrefs/write.rs b/crates/radicle/src/storage/refs/sigrefs/write.rs
index a427fb7fb..674f5c0c3 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write.rs
@@ -6,7 +6,7 @@ mod test;
use std::path::Path;
use crypto::PublicKey;
-use crypto::signature::{self, Signer};
+use crypto::signature;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::author::Author;
use radicle_git_metadata::commit::{CommitData, headers::Headers, trailers::OwnedTrailer};
@@ -62,19 +62,17 @@ impl Update {
/// writing.
/// - A `signer` which is the entity that produces the cryptographic signature
/// over the [`Refs`].
-pub struct SignedRefsWriter<'a, R, S> {
+pub struct SignedRefsWriter<'a, Repo, Signer: crypto::Signer> {
refs: Refs,
rid: RepoId,
namespace: NodeId,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
}
-impl<'a, R, S> SignedRefsWriter<'a, R, S>
+impl<'a, Repo, Signer: crypto::Signer> SignedRefsWriter<'a, Repo, Signer>
where
- R: object::Writer + object::Reader + reference::Writer + reference::Reader,
- S: Signer<crypto::Signature>,
- S: signature::Verifier<crypto::Signature>,
+ Repo: object::Writer + object::Reader + reference::Writer + reference::Reader,
{
/// Construct a new [`SignedRefsWriter`].
///
@@ -87,8 +85,8 @@ where
mut refs: Refs,
rid: RepoId,
namespace: NodeId,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
) -> Self {
debug_assert!(refs.get(&IDENTITY_ROOT).is_some());
debug_assert!(refs.get(&SIGREFS_PARENT).is_none());
@@ -157,7 +155,8 @@ where
} = self;
let reference = SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&namespace));
- let head = HeadReader::new(&reference, repository, rid, self.signer).read();
+ let verifier = signer.verifying_key();
+ let head = HeadReader::new(&reference, repository, rid, &verifier).read();
let commit_writer = match head {
Ok(Some(head)) if !force && head.is_unchanged(&refs) => {
@@ -221,21 +220,23 @@ impl Commit {
}
}
-struct CommitWriter<'a, R, S> {
+struct CommitWriter<'a, Repo, Signer: crypto::Signer> {
refs: Refs,
parent: Option<Oid>,
author: Author,
message: String,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
}
-impl<'a, R, S> CommitWriter<'a, R, S>
-where
- R: object::Writer,
- S: Signer<crypto::Signature>,
-{
- fn root(refs: Refs, author: Author, message: String, repository: &'a R, signer: &'a S) -> Self {
+impl<'a, Repo: object::Writer, Signer: crypto::Signer> CommitWriter<'a, Repo, Signer> {
+ fn root(
+ refs: Refs,
+ author: Author,
+ message: String,
+ repository: &'a Repo,
+ signer: &'a Signer,
+ ) -> Self {
Self {
refs,
parent: None,
@@ -251,8 +252,8 @@ where
parent: Oid,
author: Author,
message: String,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
) -> Self {
Self {
refs,
@@ -307,18 +308,14 @@ struct Tree {
signature: crypto::Signature,
}
-struct TreeWriter<'a, R, S> {
+struct TreeWriter<'a, Repo, Signer: crypto::Signer> {
refs: Refs,
- repository: &'a R,
- signer: &'a S,
+ repository: &'a Repo,
+ signer: &'a Signer,
}
-impl<'a, R, S> TreeWriter<'a, R, S>
-where
- R: object::Writer,
- S: Signer<crypto::Signature>,
-{
- fn new(refs: Refs, repository: &'a R, signer: &'a S) -> Self {
+impl<'a, Repo: object::Writer, Signer: crypto::Signer> TreeWriter<'a, Repo, Signer> {
+ fn new(refs: Refs, repository: &'a Repo, signer: &'a Signer) -> Self {
Self {
refs,
repository,
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs
index 8d82731d7..3d0582270 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/commit_writer.rs
@@ -24,7 +24,7 @@ fn tree_error() {
mock::author(),
"msg".into(),
&repo,
- &mock::NeverSign,
+ &mock::NeverSign::new(),
)
.write();
assert!(matches!(result, Err(error::Commit::Tree(_))));
@@ -40,7 +40,7 @@ fn write_commit_error() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write();
assert!(matches!(result, Err(error::Commit::Write(_))));
@@ -58,7 +58,7 @@ fn write_root_ok() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write()
.unwrap();
@@ -82,7 +82,7 @@ fn write_with_parent_ok() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write()
.unwrap();
@@ -105,7 +105,7 @@ fn write_empty_refs() {
mock::author(),
"msg".into(),
&repo,
- &mock::AlwaysSign,
+ &mock::AlwaysSign::new(),
)
.write()
.unwrap();
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs
index 448c21988..4736438fb 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/head_reader.rs
@@ -9,7 +9,7 @@ use crate::storage::refs::{IDENTITY_ROOT, Refs, SIGREFS_BRANCH};
fn read(repo: &MockRepository) -> Result<Option<Head>, error::Head> {
let namespace = mock::node_id();
let reference = SIGREFS_BRANCH.with_namespace(Component::from(&namespace));
- HeadReader::new(&reference, repo, mock::rid(), &mock::AlwaysSign).read()
+ HeadReader::new(&reference, repo, mock::rid(), &mock::AlwaysSign::new()).read()
}
fn refs() -> [(crate::git::fmt::RefString, radicle_oid::Oid); 2] {
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
index 23ff266a4..a7bfc2a99 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
@@ -1,6 +1,5 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
-use std::str::FromStr as _;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::author::{Author, Time};
@@ -132,6 +131,14 @@ impl MockRepository {
self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), vec![sig_id; 64])
}
+ pub fn with_valid_signature(self, commit: Oid, refs: &Refs) -> MockRepository {
+ let signer = signing_key();
+ let signature = crypto::signature::Signer::try_sign(&signer, &refs.canonical())
+ .expect("mock signing key must sign refs");
+
+ self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), signature.to_vec())
+ }
+
pub fn with_signature_error(self, commit: Oid) -> MockRepository {
self.with_blob_error(commit, Path::new(SIGNATURE_BLOB_PATH))
}
@@ -142,7 +149,6 @@ impl MockRepository {
pub fn with_invalid_signature(self, commit: Oid) -> MockRepository {
let bytes = vec![0u8; 1];
- assert!(crypto::Signature::from_str(std::str::from_utf8(&bytes).unwrap()).is_err());
self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), bytes)
}
@@ -285,12 +291,21 @@ impl reference::Writer for MockRepository {
}
}
+fn signing_key() -> crypto::SigningKey {
+ crypto::SigningKey::mock(1)
+}
+
/// Always signs successfully, returning a fixed 64-byte signature.
-pub struct AlwaysSign;
+#[derive(Clone)]
+pub struct AlwaysSign(crypto::SigningKey);
impl AlwaysSign {
const SIGNATURE: [u8; 64] = [1u8; 64];
+ pub fn new() -> Self {
+ AlwaysSign(signing_key())
+ }
+
pub fn signature() -> crypto::Signature {
crypto::Signature::from(Self::SIGNATURE)
}
@@ -312,8 +327,36 @@ impl crypto::signature::Verifier<crypto::Signature> for AlwaysSign {
}
}
+impl crypto::signature::Keypair for AlwaysSign {
+ type VerifyingKey = crypto::VerifyingKey;
+
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ self.0.verifying_key()
+ }
+}
+
+impl AsRef<crypto::PublicKey> for AlwaysSign {
+ fn as_ref(&self) -> &crypto::PublicKey {
+ self.0.as_ref()
+ }
+}
+
/// Always fails to sign.
-pub struct NeverSign;
+pub struct NeverSign(crypto::SigningKey);
+
+impl NeverSign {
+ pub fn new() -> Self {
+ NeverSign(signing_key())
+ }
+}
+
+impl crypto::signature::Keypair for NeverSign {
+ type VerifyingKey = crypto::VerifyingKey;
+
+ fn verifying_key(&self) -> Self::VerifyingKey {
+ self.0.verifying_key()
+ }
+}
impl crypto::signature::Signer<crypto::Signature> for NeverSign {
fn try_sign(&self, _msg: &[u8]) -> Result<crypto::Signature, crypto::signature::Error> {
@@ -321,6 +364,12 @@ impl crypto::signature::Signer<crypto::Signature> for NeverSign {
}
}
+impl AsRef<crypto::PublicKey> for NeverSign {
+ fn as_ref(&self) -> &crypto::PublicKey {
+ self.0.as_ref()
+ }
+}
+
/// Construct an [`Oid`] from a single repeated byte.
///
/// `oid(1) != oid(2)` is guaranteed; use distinct values for distinct objects.
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs
index d58015a08..2d20abffb 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/signed_refs_writer.rs
@@ -39,7 +39,14 @@ fn refs_with_rad_sigrefs() -> Refs {
}
fn write(refs: Refs, repo: &MockRepository) -> Result<Update, error::Write> {
- SignedRefsWriter::new(refs, mock::rid(), mock::node_id(), repo, &mock::AlwaysSign).write(
+ SignedRefsWriter::new(
+ refs,
+ mock::rid(),
+ mock::node_id(),
+ repo,
+ &mock::AlwaysSign::new(),
+ )
+ .write(
Committer::new(mock::author()),
"msg".into(),
"reflog".into(),
@@ -47,7 +54,14 @@ fn write(refs: Refs, repo: &MockRepository) -> Result<Update, error::Write> {
}
fn force_write(refs: Refs, repo: &MockRepository) -> Result<Update, error::Write> {
- SignedRefsWriter::new(refs, mock::rid(), mock::node_id(), repo, &mock::AlwaysSign).force_write(
+ SignedRefsWriter::new(
+ refs,
+ mock::rid(),
+ mock::node_id(),
+ repo,
+ &mock::AlwaysSign::new(),
+ )
+ .force_write(
Committer::new(mock::author()),
"msg".into(),
"reflog".into(),
@@ -71,7 +85,7 @@ fn unchanged() {
.with_rad_sigrefs(&mock::node_id(), head)
.with_commit(head, mock::commit_data([]))
.with_refs(head, refs.clone())
- .with_signature(head, 1);
+ .with_valid_signature(head, &refs);
match write(refs.clone(), &repo).unwrap() {
Update::Unchanged { verified } => {
diff --git a/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs b/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs
index 91fd2b2a5..06e8dbddc 100644
--- a/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs
+++ b/crates/radicle/src/storage/refs/sigrefs/write/test/tree_writer.rs
@@ -11,14 +11,15 @@ fn mock_refs() -> Refs {
fn sign_error() {
// `NeverSign` fails before `write_tree` is ever called, so the
// repository needs no configuration.
- let result = TreeWriter::new(mock_refs(), &MockRepository::new(), &mock::NeverSign).write();
+ let result =
+ TreeWriter::new(mock_refs(), &MockRepository::new(), &mock::NeverSign::new()).write();
assert!(matches!(result, Err(error::Tree::Sign(_))));
}
#[test]
fn write_tree_error() {
let repo = MockRepository::new().with_write_tree_error();
- let result = TreeWriter::new(mock_refs(), &repo, &mock::AlwaysSign).write();
+ let result = TreeWriter::new(mock_refs(), &repo, &mock::AlwaysSign::new()).write();
assert!(matches!(result, Err(error::Tree::Write(_))));
}
@@ -27,7 +28,7 @@ fn write_ok() {
let refs = mock_refs();
let expected_oid = mock::oid(1);
let repo = MockRepository::new().with_write_tree_ok(expected_oid);
- let tree = TreeWriter::new(refs.clone(), &repo, &mock::AlwaysSign)
+ let tree = TreeWriter::new(refs.clone(), &repo, &mock::AlwaysSign::new())
.write()
.unwrap();
assert_eq!(
diff --git a/crates/radicle/src/test.rs b/crates/radicle/src/test.rs
index d21c3d95d..c47136226 100644
--- a/crates/radicle/src/test.rs
+++ b/crates/radicle/src/test.rs
@@ -75,8 +75,7 @@ pub mod setup {
use tempfile::{TempDir, tempdir};
use super::storage::{Namespaces, RefUpdate};
- use crate::crypto::test::signer::MockSigner;
- use crate::node::device::Device;
+ use crate::crypto::{Signer as _, SigningKey};
use crate::storage::git::Repository;
use crate::storage::git::transport::remote;
use crate::{Storage, git, profile::Home, rad::REMOTE_NAME, test::fixtures};
@@ -89,20 +88,20 @@ pub mod setup {
pub tmp: TempDir,
pub root: PathBuf,
pub storage: Storage,
- pub signer: Device<MockSigner>,
+ pub signer: SigningKey,
}
impl Default for Node {
fn default() -> Self {
let root = tempdir().unwrap();
- Self::new(root, MockSigner::default(), "Radcliff")
+ Self::new(root, SigningKey::mock(73), "Radcliff")
}
}
impl Node {
- pub fn new(tmp: TempDir, signer: MockSigner, alias: &str) -> Self {
- let signer = Device::from(signer);
+ pub fn new(tmp: TempDir, signer: SigningKey, alias: &str) -> Self {
+ let signer = signer;
let root = tmp.path().to_path_buf();
let home = root.join("home");
let paths = Home::new(home.as_path()).unwrap();
@@ -251,10 +250,10 @@ pub mod setup {
impl Default for Network {
fn default() -> Self {
- let alice = Node::new(tempdir().unwrap(), MockSigner::from_seed([!0; 32]), "alice");
- let mut bob = Node::new(tempdir().unwrap(), MockSigner::from_seed([!1; 32]), "bob");
- let mut eve = Node::new(tempdir().unwrap(), MockSigner::from_seed([!2; 32]), "eve");
- let mut dave = Node::new(tempdir().unwrap(), MockSigner::from_seed([!3; 32]), "dave");
+ let alice = Node::new(tempdir().unwrap(), SigningKey::mock(!0), "alice");
+ let mut bob = Node::new(tempdir().unwrap(), SigningKey::mock(!1), "bob");
+ let mut eve = Node::new(tempdir().unwrap(), SigningKey::mock(!2), "eve");
+ let mut dave = Node::new(tempdir().unwrap(), SigningKey::mock(!3), "dave");
let repo = alice.project();
let rid = repo.id;
diff --git a/crates/radicle/src/test/arbitrary.rs b/crates/radicle/src/test/arbitrary.rs
index 903e1d43c..20b959fe8 100644
--- a/crates/radicle/src/test/arbitrary.rs
+++ b/crates/radicle/src/test/arbitrary.rs
@@ -65,6 +65,10 @@ pub fn vec<T: Eq + Arbitrary>(size: usize) -> Vec<T> {
vec
}
+pub fn vec_distinct<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> Vec<T> {
+ set(range).into_iter().collect::<Vec<_>>()
+}
+
pub fn nonempty_storage(size: usize) -> MockStorage {
let mut storage = r#gen::<MockStorage>(size);
for _ in 0..size {
@@ -166,11 +170,8 @@ impl Arbitrary for RawDoc {
impl Arbitrary for Doc {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let mut rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let project = Project::arbitrary(g);
- let delegates = iter::repeat_with(|| Did::arbitrary(g))
- .take(rng.usize(1..6))
- .collect::<Vec<_>>();
+ let delegates = vec_distinct::<Did>(1..6);
let threshold = delegates.len() / 2 + 1;
let visibility = Visibility::arbitrary(g);
let doc = RawDoc::new(project, delegates, threshold, visibility);
@@ -243,7 +244,7 @@ impl Arbitrary for Address {
AddressType::Onion => {
let pk = PublicKey::arbitrary(g);
let addr = OnionAddrV3::from(
- cyphernet::ed25519::PublicKey::from_pk_compressed(pk.to_byte_array().into())
+ cyphernet::ed25519::PublicKey::from_pk_compressed(pk.into_inner().into())
.unwrap(),
);
cyphernet::addr::HostName::Tor(addr)
diff --git a/crates/radicle/src/test/fixtures.rs b/crates/radicle/src/test/fixtures.rs
index 496c96b6f..c2578c5f3 100644
--- a/crates/radicle/src/test/fixtures.rs
+++ b/crates/radicle/src/test/fixtures.rs
@@ -6,7 +6,6 @@ use crate::git;
use crate::identity::RepoId;
use crate::identity::doc::Visibility;
use crate::node::Alias;
-use crate::node::device::Device;
use crate::rad;
use crate::storage::git::Storage;
use crate::storage::git::transport;
@@ -30,22 +29,24 @@ pub fn user() -> git::UserInfo {
}
/// Create a new storage with a project.
-pub fn storage<P, G>(path: P, signer: &Device<G>) -> Result<Storage, rad::InitError>
-where
- P: AsRef<Path>,
- G: crypto::signature::Signer<crypto::Signature>,
-{
+pub fn storage(
+ path: impl AsRef<Path>,
+ signer: &impl crypto::Signer,
+) -> Result<Storage, rad::InitError> {
let path = path.as_ref();
+
+ let key = signer.public_key();
+
let storage = Storage::open(
path.join("storage"),
git::UserInfo {
alias: Alias::new("Radcliff"),
- key: *signer.public_key(),
+ key: *key,
},
)?;
transport::local::register(storage.clone());
- transport::remote::mock::register(signer.public_key(), storage.path());
+ transport::remote::mock::register(key, storage.path());
for (name, desc) in [
("acme", "Acme's repository"),
@@ -68,15 +69,11 @@ where
}
/// Create a new repository at the given path, and initialize it into a project.
-pub fn project<P, G>(
- path: P,
+pub fn project(
+ path: impl AsRef<Path>,
storage: &Storage,
- signer: &Device<G>,
-) -> Result<(RepoId, SignedRefs, git::raw::Repository, git::raw::Oid), rad::InitError>
-where
- P: AsRef<Path>,
- G: crypto::signature::Signer<crypto::Signature>,
-{
+ signer: &impl crypto::Signer,
+) -> Result<(RepoId, SignedRefs, git::raw::Repository, git::raw::Oid), rad::InitError> {
transport::local::register(storage.clone());
let (working, head) = repository(path);
diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs
index fb5ee4978..7755828d1 100644
--- a/crates/radicle/src/test/storage.rs
+++ b/crates/radicle/src/test/storage.rs
@@ -6,6 +6,7 @@ use std::str::FromStr;
use crypto::PublicKey;
+use crate::crypto::ExtendedSignature;
pub use crate::git;
use crate::git::fmt;
@@ -379,23 +380,17 @@ impl WriteRepository for MockRepository {
}
impl SignRepository for MockRepository {
- fn sign_refs<Signer>(
+ fn sign_refs(
&self,
- _signer: &Signer,
- ) -> Result<crate::storage::refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- {
+ _signer: &impl crypto::Signer,
+ ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> {
todo!()
}
- fn force_sign_refs<Signer>(&self, _signer: &Signer) -> Result<refs::SignedRefs, RepositoryError>
- where
- Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
- Signer: crypto::signature::Signer<crypto::Signature>,
- Signer: crypto::signature::Verifier<crypto::Signature>,
- {
+ fn force_sign_refs(
+ &self,
+ _signer: &impl crypto::Signer,
+ ) -> Result<refs::SignedRefs, RepositoryError> {
todo!()
}
}
@@ -453,21 +448,24 @@ impl radicle_cob::change::Storage for MockRepository {
type LoadError = radicle_cob::git::change::error::Load;
type ObjectId = Oid;
type Parent = Oid;
- type Signatures = radicle_cob::signatures::ExtendedSignature;
- fn store<G>(
+ type PublicKey = crypto::PublicKey;
+ type Signature = crypto::Signature;
+
+ fn store(
&self,
_resource: Option<Self::Parent>,
_related: Vec<Self::Parent>,
- _signer: &G,
+ _signer: &impl crypto::Signer,
_template: radicle_cob::change::Template<Self::ObjectId>,
) -> Result<
- radicle_cob::change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>,
+ radicle_cob::change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
Self::StoreError,
- >
- where
- G: radicle_crypto::signature::Signer<Self::Signatures>,
- {
+ > {
todo!()
}
@@ -475,7 +473,11 @@ impl radicle_cob::change::Storage for MockRepository {
&self,
_id: Self::ObjectId,
) -> Result<
- radicle_cob::change::store::Entry<Self::Parent, Self::ObjectId, Self::Signatures>,
+ radicle_cob::change::store::Entry<
+ Self::Parent,
+ Self::ObjectId,
+ ExtendedSignature<Self::PublicKey, Self::Signature>,
+ >,
Self::LoadError,
> {
todo!()
commit 0fd0104e8771fa7f2bec64216b3668485ff6b324
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Mon Jun 29 14:06:31 2026 +0200
radicle, cli/auth: Let binary crate generate seed
In preparation of a rewrite of `radicle-crypto`, move the
responsibility for generating a cryptographically secure
seed from `radicle` (transitively via dependency on
`radicle-crypto` and `ec25519`) to the dependent of `radicle`.
Applications should decide how to obtain randomness, not `radicle` via
some far-removed dependency.
Within the workspace, the only dependent using `Profile::seed` is
`radicle-cli`, in particular for the `rad auth` subcommand. To obtain
random bytes, `radicle-cli` depends on `getrandom` and uses
`getrandom::fill`.
diff --git a/Cargo.lock b/Cargo.lock
index 2e7448e7c..f2e73a09c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1134,12 +1134,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -1199,15 +1193,13 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
- "wasip2",
- "wasip3",
]
[[package]]
@@ -1445,7 +1437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b"
dependencies = [
"gix-hash",
- "hashbrown 0.16.1",
+ "hashbrown",
"parking_lot",
]
@@ -1805,15 +1797,6 @@ dependencies = [
"byteorder",
]
-[[package]]
-name = "hashbrown"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
-dependencies = [
- "foldhash",
-]
-
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1972,12 +1955,6 @@ dependencies = [
"zerovec",
]
-[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
[[package]]
name = "idna"
version = "1.1.0"
@@ -2006,7 +1983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
- "hashbrown 0.16.1",
+ "hashbrown",
"serde",
"serde_core",
]
@@ -2174,12 +2151,6 @@ dependencies = [
"spin",
]
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
[[package]]
name = "lexopt"
version = "0.3.2"
@@ -2779,16 +2750,6 @@ dependencies = [
"yansi",
]
-[[package]]
-name = "prettyplease"
-version = "0.2.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
-dependencies = [
- "proc-macro2",
- "syn 2.0.117",
-]
-
[[package]]
name = "primeorder"
version = "0.13.6"
@@ -2953,6 +2914,7 @@ dependencies = [
"clap",
"clap_complete",
"dunce",
+ "getrandom 0.4.3",
"human-panic",
"humantime",
"itertools",
@@ -4234,7 +4196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.4.2",
+ "getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -4651,12 +4613,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
-[[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
[[package]]
name = "unit-prefix"
version = "0.5.2"
@@ -4703,7 +4659,7 @@ version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
"js-sys",
"wasm-bindgen",
]
@@ -4813,15 +4769,6 @@ dependencies = [
"wit-bindgen",
]
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen",
-]
-
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
@@ -4867,40 +4814,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
-]
-
[[package]]
name = "web-time"
version = "1.1.0"
@@ -5320,88 +5233,6 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck",
- "indexmap",
- "prettyplease",
- "syn 2.0.117",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
[[package]]
name = "writeable"
diff --git a/crates/radicle-cli/Cargo.toml b/crates/radicle-cli/Cargo.toml
index afe6f91b8..89896ade1 100644
--- a/crates/radicle-cli/Cargo.toml
+++ b/crates/radicle-cli/Cargo.toml
@@ -24,6 +24,7 @@ chrono = { workspace = true, features = ["clock", "std"] }
clap = { version = "4.5.44", features = ["derive"] }
clap_complete = "4.5"
dunce = { workspace = true }
+getrandom = "0.4.3"
human-panic.workspace = true
humantime.workspace = true
itertools.workspace = true
diff --git a/crates/radicle-cli/src/commands/auth.rs b/crates/radicle-cli/src/commands/auth.rs
index cd5a49915..ca9374228 100644
--- a/crates/radicle-cli/src/commands/auth.rs
+++ b/crates/radicle-cli/src/commands/auth.rs
@@ -56,7 +56,18 @@ pub fn init(args: Args) -> anyhow::Result<()> {
};
let passphrase = passphrase.filter(|passphrase| !passphrase.trim().is_empty());
let spinner = term::spinner("Creating your Ed25519 keypair…");
- let profile = Profile::init(home, alias, passphrase.clone(), env::seed())?;
+ let profile = Profile::init(
+ home,
+ alias,
+ passphrase.clone(),
+ env::seed().unwrap_or_else(|| {
+ use radicle::crypto::Seed;
+
+ let mut seed = [0; Seed::BYTES];
+ getrandom::fill(&mut seed).expect("failed get random bytes from the operating system");
+ Seed::new(seed)
+ }),
+ )?;
let mut agent = true;
spinner.finish();
diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs
index 64f286856..24c6bfde5 100644
--- a/crates/radicle/src/profile.rs
+++ b/crates/radicle/src/profile.rs
@@ -39,6 +39,8 @@ use crate::{cob, git, node, storage};
pub mod env {
pub use std::env::*;
+ use crypto::Seed;
+
/// Path to the Radicle home folder.
pub const RAD_HOME: &str = "RAD_HOME";
/// Path to the Radicle node socket file.
@@ -129,22 +131,31 @@ pub mod env {
}
/// Return the seed stored in the [`RAD_KEYGEN_SEED`] environment variable,
- /// or generate a random one.
- pub fn seed() -> crypto::Seed {
- if let Ok(seed) = var(RAD_KEYGEN_SEED) {
- let Ok(seed) = (0..seed.len())
- .step_by(2)
- .map(|i| u8::from_str_radix(&seed[i..i + 2], 16))
- .collect::<Result<Vec<u8>, _>>()
- else {
- panic!("env::seed: invalid hexadecimal value set in `{RAD_KEYGEN_SEED}`");
- };
- let Ok(seed): Result<[u8; 32], _> = seed.try_into() else {
- panic!("env::seed: invalid seed length set in `{RAD_KEYGEN_SEED}`");
- };
- crypto::Seed::new(seed)
- } else {
- crypto::Seed::generate()
+ /// if set. Otherwise, return `None`.
+ ///
+ /// # Panics
+ ///
+ /// If the environment value [`RAD_KEYGEN_SEED`] is set but malformed,
+ /// i.e., not Unicode, not hexadecimal, or not 32 bytes long.
+ pub fn seed() -> Option<Seed> {
+ match var(RAD_KEYGEN_SEED) {
+ Err(VarError::NotPresent) => None,
+ Err(VarError::NotUnicode(_)) => {
+ panic!("env::seed: invalid Unicode value set in `{RAD_KEYGEN_SEED}`")
+ }
+ Ok(seed) => Some(Seed::new(
+ (0..seed.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&seed[i..i + 2], 16))
+ .collect::<Result<Vec<_>, _>>()
+ .unwrap_or_else(|_| {
+ panic!("env::seed: invalid hexadecimal value set in `{RAD_KEYGEN_SEED}`")
+ })
+ .try_into()
+ .unwrap_or_else(|_| {
+ panic!("env::seed: invalid seed length set in `{RAD_KEYGEN_SEED}`")
+ }),
+ )),
}
}
commit 8ceef3c40571809910e4c207f3e55e95f8834490
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Fri Jul 3 22:41:25 2026 +0200
crypto/test: Diffie-Hellman Fixture
Before rewriting `radicle-crypto`, add a fixture to have some assurance
on compatibility.
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index a8d3ec4ba..21769df0a 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -544,4 +544,31 @@ mod tests {
assert!(!hm.insert(a));
assert!(!hm.insert(b));
}
+
+ #[test]
+ fn e25519_dh_fixture() {
+ let sk_a: [u8; 32] = [
+ 92, 136, 18, 88, 112, 205, 201, 68, 109, 197, 130, 211, 179, 138, 197, 113, 120, 55,
+ 104, 139, 208, 184, 178, 157, 120, 11, 60, 13, 91, 30, 213, 38,
+ ];
+ let sk_b: [u8; 32] = [
+ 202, 152, 225, 201, 169, 81, 217, 16, 235, 104, 91, 252, 52, 113, 81, 190, 68, 250, 86,
+ 21, 202, 228, 123, 193, 140, 252, 63, 72, 5, 137, 36, 245,
+ ];
+
+ let kp_a = KeyPair::from_seed(ec25519::Seed::from(sk_a));
+ let kp_b = KeyPair::from_seed(ec25519::Seed::from(sk_b));
+
+ let output_a = SecretKey::from(kp_b.sk).ecdh(&kp_a.pk.into()).unwrap();
+ let output_b = SecretKey::from(kp_a.sk).ecdh(&kp_b.pk.into()).unwrap();
+
+ assert_eq!(output_a, output_b);
+ assert_eq!(
+ output_a,
+ [
+ 159, 131, 169, 27, 132, 202, 47, 250, 112, 247, 176, 222, 213, 220, 147, 216, 53,
+ 7, 33, 232, 232, 77, 254, 105, 125, 237, 61, 243, 209, 172, 93, 100
+ ]
+ )
+ }
}
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 57c396a0-5542-4bf8-a735-4da25a7267c2 -v /opt/radcis/ci.rad.levitte.org/cci/state/57c396a0-5542-4bf8-a735-4da25a7267c2/s:/57c396a0-5542-4bf8-a735-4da25a7267c2/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/57c396a0-5542-4bf8-a735-4da25a7267c2/w:/57c396a0-5542-4bf8-a735-4da25a7267c2/w -w /57c396a0-5542-4bf8-a735-4da25a7267c2/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /57c396a0-5542-4bf8-a735-4da25a7267c2/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.97.1-x86_64-unknown-linux-gnu'
info: latest update on 2026-07-16, rust version 1.97.1 (8bab26f4f 2026-07-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.97.1 (c980f4866 2026-06-30)
+ rustc --version
rustc 1.97.1 (8bab26f4f 2026-07-14)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- --deny warnings
Updating crates.io index
Downloading crates ...
Downloaded diff v0.1.13
Downloaded crypto-bigint v0.5.5
Downloaded fluent-uri v0.3.2
Downloaded bstr v1.12.1
Downloaded const-str v0.4.3
Downloaded gix-negotiate v0.31.0
Downloaded potential_utf v0.1.4
Downloaded console v0.16.3
Downloaded clap_lex v1.1.0
Downloaded salsa20 v0.10.2
Downloaded pkg-config v0.3.32
Downloaded poly1305 v0.8.0
Downloaded polyval v0.6.2
Downloaded derive_more-impl v2.1.1
Downloaded crypto-common v0.1.7
Downloaded convert_case v0.10.0
Downloaded cypheraddr v0.4.1
Downloaded structured-logger v1.0.5
Downloaded data-encoding-macro-internal v0.1.17
Downloaded portable-atomic v1.13.1
Downloaded bytes v1.11.1
Downloaded crossbeam-channel v0.5.15
Downloaded ctr v0.9.2
Downloaded ecdsa v0.16.9
Downloaded fastrand v2.3.0
Downloaded ec25519 v0.1.0
Downloaded proc-macro-error-attr3 v3.0.2
Downloaded derive_more v2.1.1
Downloaded curve25519-dalek v4.1.3
Downloaded rustc_version v0.4.1
Downloaded proc-macro-error3 v3.0.2
Downloaded rand_chacha v0.9.0
Downloaded form_urlencoded v1.2.2
Downloaded der v0.7.10
Downloaded semver v1.0.27
Downloaded sha1 v0.10.6
Downloaded primeorder v0.13.6
Downloaded crc32fast v1.5.0
Downloaded sem_safe v0.2.1
Downloaded scopeguard v1.2.0
Downloaded bytesize v2.3.1
Downloaded const-oid v0.9.6
Downloaded cpufeatures v0.2.17
Downloaded crossbeam-utils v0.8.21
Downloaded curve25519-dalek-derive v0.1.1
Downloaded cyphergraphy v0.3.1
Downloaded env_logger v0.11.9
Downloaded ff v0.13.1
Downloaded flate2 v1.1.9
Downloaded generic-array v0.14.7
Downloaded radicle-std-ext v0.2.0
Downloaded ref-cast v1.0.25
Downloaded rsa v0.9.10
Downloaded schemars_derive v1.2.1
Downloaded shell-words v1.1.1
Downloaded bytecount v0.6.9
Downloaded ppv-lite86 v0.2.21
Downloaded rand_core v0.9.5
Downloaded sha2 v0.10.9
Downloaded data-encoding-macro v0.1.19
Downloaded pretty_assertions v1.4.1
Downloaded qcheck-macros v1.0.0
Downloaded same-file v1.0.6
Downloaded serde_fmt v1.1.0
Downloaded smallvec v1.15.1
Downloaded clap_complete v4.6.0
Downloaded getrandom v0.2.17
Downloaded ryu v1.0.23
Downloaded cbc v0.1.2
Downloaded radicle-git-ext v0.13.0
Downloaded fnv v1.0.7
Downloaded git-ref-format v0.7.0
Downloaded git-ref-format-core v0.6.0
Downloaded document-features v0.2.12
Downloaded quote v1.0.45
Downloaded rusty-fork v0.3.1
Downloaded byteorder v1.5.0
Downloaded clap_builder v4.6.0
Downloaded displaydoc v0.2.5
Downloaded dunce v1.0.5
Downloaded dyn-clone v1.0.20
Downloaded ed25519 v1.5.3
Downloaded either v1.15.0
Downloaded filetime v0.2.27
Downloaded ghash v0.5.1
Downloaded git-ref-format-macro v0.7.0
Downloaded gix-glob v0.26.0
Downloaded qcheck v1.0.0
Downloaded quick-error v1.2.3
Downloaded secrecy v0.10.3
Downloaded base256emoji v1.0.2
Downloaded env_filter v1.0.0
Downloaded errno v0.3.14
Downloaded fast-glob v0.3.3
Downloaded gix-hashtable v0.15.0
Downloaded gix-actor v0.41.0
Downloaded gix-lock v23.0.0
Downloaded find-msvc-tools v0.1.9
Downloaded rand_chacha v0.3.1
Downloaded siphasher v0.3.11
Downloaded stable_deref_trait v1.2.1
Downloaded subtle v2.6.1
Downloaded cfg-if v1.0.4
Downloaded gix-chunk v0.7.1
Downloaded gix-config-value v0.18.0
Downloaded equivalent v1.0.2
Downloaded signature v2.2.0
Downloaded socks5-client v0.4.3
Downloaded sval_fmt v2.17.0
Downloaded test-log-macros v0.2.19
Downloaded autocfg v1.5.0
Downloaded cipher v0.4.4
Downloaded data-encoding v2.10.0
Downloaded escargot v0.5.15
Downloaded faster-hex v0.10.0
Downloaded serde_spanned v1.0.4
Downloaded gix-hash v0.25.0
Downloaded colorchoice v1.0.5
Downloaded gix-command v0.9.0
Downloaded gix-shallow v0.12.0
Downloaded is_terminal_polyfill v1.70.2
Downloaded rand v0.8.5
Downloaded sval_dynamic v2.17.0
Downloaded chacha20 v0.9.1
Downloaded digest v0.10.7
Downloaded ed25519 v2.2.3
Downloaded gix-date v0.15.3
Downloaded siphasher v1.0.2
Downloaded snapbox-macros v1.1.0
Downloaded base64ct v1.8.3
Downloaded gix-commitgraph v0.37.0
Downloaded gix-fs v0.21.1
Downloaded match-lookup v0.1.2
Downloaded rand_xorshift v0.4.0
Downloaded scrypt v0.11.0
Downloaded clap_derive v4.6.0
Downloaded gix-credentials v0.38.0
Downloaded ref-cast-impl v1.0.25
Downloaded email_address v0.2.9
Downloaded ssh-encoding v0.2.0
Downloaded nonempty v0.12.0
Downloaded jobserver v0.1.34
Downloaded num-cmp v0.1.0
Downloaded num-iter v0.1.45
Downloaded erased-serde v0.4.10
Downloaded gix-error v0.2.3
Downloaded gix-features v0.48.0
Downloaded bitflags v2.11.0
Downloaded pbkdf2 v0.12.2
Downloaded signal-hook-mio v0.2.5
Downloaded blowfish v0.9.1
Downloaded hash32 v0.3.1
Downloaded rustversion v1.0.22
Downloaded rfc6979 v0.4.0
Downloaded rustc-demangle v0.1.27
Downloaded signature v1.6.4
Downloaded serde-untagged v0.1.9
Downloaded idna_adapter v1.2.1
Downloaded test-log v0.2.19
Downloaded gix-diff v0.63.0
Downloaded getrandom v0.4.3
Downloaded sqlite v0.37.0
Downloaded borrow-or-share v0.2.4
Downloaded elliptic-curve v0.13.8
Downloaded clap v4.6.0
Downloaded colored v2.2.0
Downloaded phf_shared v0.11.3
Downloaded ssh-cipher v0.2.0
Downloaded typeid v1.0.3
Downloaded rand_core v0.6.4
Downloaded strsim v0.11.1
Downloaded shlex v1.3.0
Downloaded simd-adler32 v0.3.8
Downloaded serde_derive_internals v0.29.1
Downloaded gix-traverse v0.57.0
Downloaded sec1 v0.7.3
Downloaded getrandom v0.3.4
Downloaded systemd-journal-logger v2.2.2
Downloaded gix-object v0.60.0
Downloaded sqlite3-sys v0.18.0
Downloaded streaming-iterator v0.1.9
Downloaded sval_json v2.17.0
Downloaded base16ct v0.2.0
Downloaded unarray v0.1.4
Downloaded unicode-display-width v0.3.0
Downloaded emojis v0.6.4
Downloaded memmap2 v0.9.10
Downloaded spki v0.7.3
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded percent-encoding v2.3.2
Downloaded gix-quote v0.7.1
Downloaded sval_ref v2.17.0
Downloaded sval_serde v2.17.0
Downloaded cc v1.2.57
Downloaded miniz_oxide v0.8.9
Downloaded crossterm v0.29.0
Downloaded fancy-regex v0.14.0
Downloaded fraction v0.15.3
Downloaded base32 v0.4.0
Downloaded base-x v0.2.11
Downloaded pastey v0.2.1
Downloaded universal-hash v0.5.1
Downloaded inout v0.1.4
Downloaded lock_api v0.4.14
Downloaded sval_nested v2.17.0
Downloaded timeago v0.4.2
Downloaded tracing-core v0.1.36
Downloaded tree-sitter-highlight v0.24.7
Downloaded icu_properties v2.1.2
Downloaded gix-sec v0.14.0
Downloaded referencing v0.30.0
Downloaded spin v0.9.8
Downloaded pem-rfc7468 v0.7.0
Downloaded chacha20poly1305 v0.10.1
Downloaded matchers v0.2.0
Downloaded signal-hook-registry v1.4.8
Downloaded sval_buffer v2.17.0
Downloaded lazy_static v1.5.0
Downloaded signals_receipts v0.2.5
Downloaded uuid-simd v0.8.0
Downloaded serde_core v1.0.228
Downloaded snapbox v1.2.2
Downloaded proc-macro2 v1.0.106
Downloaded socket2 v0.5.10
Downloaded tinyvec_macros v0.1.1
Downloaded tree-sitter-language v0.1.7
Downloaded gix-trace v0.1.19
Downloaded tree-sitter-json v0.24.8
Downloaded xattr v1.6.1
Downloaded gix-utils v0.3.2
Downloaded keccak v0.1.6
Downloaded gix-ref v0.63.0
Downloaded phf v0.11.3
Downloaded regex v1.12.3
Downloaded gix-refspec v0.41.0
Downloaded gix-revision v0.45.0
Downloaded unit-prefix v0.5.2
Downloaded yoke-derive v0.8.1
Downloaded utf8parse v0.2.2
Downloaded gix-prompt v0.15.0
Downloaded zerovec-derive v0.11.2
Downloaded num-integer v0.1.46
Downloaded pkcs1 v0.7.5
Downloaded unicode-ident v1.0.24
Downloaded num-rational v0.4.2
Downloaded lexopt v0.3.2
Downloaded num v0.4.3
Downloaded pin-project-lite v0.2.17
Downloaded pkcs8 v0.10.2
Downloaded gix-revwalk v0.31.0
Downloaded gix-odb v0.80.0
Downloaded signal-hook v0.3.18
Downloaded amplify_num v0.5.3
Downloaded human-panic v2.0.6
Downloaded sharded-slab v0.1.7
Downloaded utf8_iter v1.0.4
Downloaded zerofrom v0.1.6
Downloaded gix-tempfile v23.0.0
Downloaded synstructure v0.13.2
Downloaded ed25519-dalek v2.2.0
Downloaded tree-sitter-html v0.23.2
Downloaded maybe-async v0.2.10
Downloaded yoke v0.8.1
Downloaded log v0.4.29
Downloaded anyhow v1.0.102
Downloaded gix-packetline v0.21.3
Downloaded version_check v0.9.5
Downloaded parking_lot_core v0.9.12
Downloaded amplify_derive v4.0.1
Downloaded serde_derive v1.0.228
Downloaded once_cell v1.21.4
Downloaded rand v0.9.2
Downloaded outref v0.5.2
Downloaded block-buffer v0.10.4
Downloaded nu-ansi-term v0.50.3
Downloaded value-bag-sval2 v1.12.0
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded zerofrom-derive v0.1.6
Downloaded block-padding v0.3.3
Downloaded noise-framework v0.4.1
Downloaded toml v0.9.12+spec-1.1.0
Downloaded multibase v0.9.2
Downloaded aead v0.5.2
Downloaded gix-validate v0.11.1
Downloaded gix-path v0.12.0
Downloaded humantime v2.3.0
Downloaded adler2 v2.0.1
Downloaded anstream v1.0.0
Downloaded vsimd v0.8.0
Downloaded inquire v0.9.4
Downloaded anstyle v1.0.14
Downloaded normalize-line-endings v0.3.0
Downloaded icu_provider v2.1.1
Downloaded cyphernet v0.5.4
Downloaded thiserror v1.0.69
Downloaded writeable v0.6.2
Downloaded anstyle-parse v1.0.0
Downloaded itoa v1.0.17
Downloaded tree-sitter-css v0.23.2
Downloaded zerotrie v0.2.3
Downloaded ssh-agent-lib v0.6.0
Downloaded ascii v1.1.0
Downloaded litemap v0.8.1
Downloaded thiserror v2.0.18
Downloaded opaque-debug v0.3.1
Downloaded value-bag-serde1 v1.12.0
Downloaded gix-url v0.36.0
Downloaded typenum v1.20.1
Downloaded litrs v1.0.0
Downloaded sval v2.17.0
Downloaded tracing-log v0.2.0
Downloaded nonempty v0.9.0
Downloaded arc-swap v1.9.1
Downloaded wait-timeout v0.2.1
Downloaded zmij v1.0.21
Downloaded num-complex v0.4.6
Downloaded toml_writer v1.0.7+spec-1.1.0
Downloaded iana-time-zone v0.1.65
Downloaded tinyvec v1.11.0
Downloaded schemars v1.2.1
Downloaded value-bag v1.12.0
Downloaded thiserror-impl v1.0.69
Downloaded ahash v0.8.12
Downloaded group v0.13.0
Downloaded gix-pack v0.70.0
Downloaded thiserror-impl v2.0.18
Downloaded zeroize v1.9.0
Downloaded amplify_syn v2.0.1
Downloaded bit-set v0.8.0
Downloaded anstyle-query v1.1.5
Downloaded hmac v0.12.1
Downloaded walkdir v2.5.0
Downloaded serde v1.0.228
Downloaded anstyle-parse v0.2.7
Downloaded gix-protocol v0.61.0
Downloaded bcrypt-pbkdf v0.10.0
Downloaded tempfile v3.27.0
Downloaded gix-transport v0.57.0
Downloaded heck v0.5.0
Downloaded prodash v31.0.0
Downloaded ssh-key v0.6.7
Downloaded num-bigint v0.4.6
Downloaded addr2line v0.25.1
Downloaded thread_local v1.1.9
Downloaded p256 v0.13.2
Downloaded heapless v0.8.0
Downloaded parking_lot v0.12.5
Downloaded yansi v1.0.1
Downloaded similar v3.1.1
Downloaded aes v0.8.4
Downloaded jiff-static v0.2.23
Downloaded p521 v0.13.3
Downloaded zerovec v0.11.5
Downloaded icu_collections v2.1.1
Downloaded icu_locale_core v2.1.1
Downloaded url v2.5.8
Downloaded radicle-surf v0.28.0
Downloaded tar v0.4.45
Downloaded tinystr v0.8.2
Downloaded num-traits v0.2.19
Downloaded anstream v0.6.21
Downloaded tree-sitter-go v0.23.4
Downloaded tree-sitter-python v0.23.6
Downloaded amplify v4.9.0
Downloaded bit-vec v0.8.0
Downloaded indexmap v2.13.0
Downloaded chrono v0.4.44
Downloaded git2 v0.21.0
Downloaded num-bigint-dig v0.8.6
Downloaded icu_normalizer v2.1.1
Downloaded uuid v1.22.0
Downloaded backtrace v0.3.76
Downloaded base64 v0.21.7
Downloaded mio v1.1.1
Downloaded base64 v0.22.1
Downloaded aes-gcm v0.10.3
Downloaded libm v0.2.16
Downloaded icu_normalizer_data v2.1.1
Downloaded indicatif v0.18.4
Downloaded zlib-rs v0.6.5
Downloaded p384 v0.13.1
Downloaded gimli v0.32.3
Downloaded jsonschema v0.30.0
Downloaded memchr v2.8.0
Downloaded syn v1.0.109
Downloaded tree-sitter v0.24.7
Downloaded unicode-normalization v0.1.25
Downloaded unicode-segmentation v1.12.0
Downloaded serde_json v1.0.149
Downloaded aho-corasick v1.1.4
Downloaded hashbrown v0.16.1
Downloaded vcpkg v0.2.15
Downloaded tracing-subscriber v0.3.23
Downloaded tree-sitter-c v0.23.4
Downloaded tree-sitter-rust v0.23.3
Downloaded proptest v1.10.0
Downloaded idna v1.1.0
Downloaded tree-sitter-ruby v0.23.1
Downloaded itertools v0.14.0
Downloaded icu_properties_data v2.1.2
Downloaded sysinfo v0.37.2
Downloaded libc v0.2.183
Downloaded tree-sitter-typescript v0.23.2
Downloaded jiff v0.2.23
Downloaded tokio v1.50.0
Downloaded regex-syntax v0.8.10
Downloaded unicode-width v0.2.2
Downloaded zerocopy v0.8.42
Downloaded bloomy v1.2.0
Downloaded syn v2.0.117
Downloaded tree-sitter-md v0.3.2
Downloaded object v0.37.3
Downloaded rustix v1.1.4
Downloaded tree-sitter-bash v0.23.3
Downloaded tracing v0.1.44
Downloaded regex-automata v0.4.14
Downloaded sha1-checked v0.10.0
Downloaded sha3 v0.10.8
Downloaded libz-sys v1.1.25
Downloaded libgit2-sys v0.18.4+1.9.3
Downloaded sqlite3-src v0.7.0
Downloaded linux-raw-sys v0.12.1
Compiling libc v0.2.183
Compiling proc-macro2 v1.0.106
Compiling unicode-ident v1.0.24
Compiling quote v1.0.45
Compiling serde_core v1.0.228
Checking cfg-if v1.0.4
Compiling serde v1.0.228
Compiling version_check v0.9.5
Compiling generic-array v0.14.7
Compiling syn v2.0.117
Checking typenum v1.20.1
Checking getrandom v0.2.17
Checking rand_core v0.6.4
Checking memchr v2.8.0
Compiling jobserver v0.1.34
Compiling find-msvc-tools v0.1.9
Compiling shlex v1.3.0
Compiling cc v1.2.57
Checking subtle v2.6.1
Checking const-oid v0.9.6
Checking regex-syntax v0.8.10
Checking aho-corasick v1.1.4
Checking smallvec v1.15.1
Checking cpufeatures v0.2.17
Compiling thiserror v2.0.18
Compiling parking_lot_core v0.9.12
Checking fastrand v2.3.0
Checking scopeguard v1.2.0
Checking regex-automata v0.4.14
Checking lock_api v0.4.14
Checking stable_deref_trait v1.2.1
Checking parking_lot v0.12.5
Checking bitflags v2.11.0
Compiling typeid v1.0.3
Compiling erased-serde v0.4.10
Checking itoa v1.0.17
Checking gix-trace v0.1.19
Compiling crc32fast v1.5.0
Checking tinyvec_macros v0.1.1
Checking tinyvec v1.11.0
Checking serde_fmt v1.1.0
Checking hashbrown v0.16.1
Checking unicode-normalization v0.1.25
Checking value-bag-serde1 v1.12.0
Checking byteorder v1.5.0
Checking gix-utils v0.3.2
Checking value-bag v1.12.0
Checking same-file v1.0.6
Checking walkdir v2.5.0
Compiling serde_derive v1.0.228
Compiling thiserror-impl v2.0.18
Checking log v0.4.29
Checking bstr v1.12.1
Checking prodash v31.0.0
Checking gix-validate v0.11.1
Checking zlib-rs v0.6.5
Compiling autocfg v1.5.0
Compiling libm v0.2.16
Checking gix-path v0.12.0
Compiling num-traits v0.2.19
Compiling pkg-config v0.3.32
Compiling heapless v0.8.0
Compiling rustix v1.1.4
Checking gix-features v0.48.0
Checking hash32 v0.3.1
Checking linux-raw-sys v0.12.1
Compiling getrandom v0.4.3
Checking faster-hex v0.10.0
Compiling zerocopy v0.8.42
Compiling zmij v1.0.21
Checking equivalent v1.0.2
Checking once_cell v1.21.4
Checking zeroize v1.9.0
Compiling serde_json v1.0.149
Checking indexmap v2.13.0
Checking crypto-common v0.1.7
Checking block-buffer v0.10.4
Checking digest v0.10.7
Checking block-padding v0.3.3
Checking inout v0.1.4
Checking sha2 v0.10.9
Checking sha1 v0.10.6
Checking sha1-checked v0.10.0
Checking cipher v0.4.4
Checking gix-hash v0.25.0
Compiling ref-cast v1.0.25
Checking der v0.7.10
Compiling ref-cast-impl v1.0.25
Checking spin v0.9.8
Checking lazy_static v1.5.0
Compiling vcpkg v0.2.15
Compiling syn v1.0.109
Compiling thiserror v1.0.69
Checking dyn-clone v1.0.20
Checking tempfile v3.27.0
Compiling thiserror-impl v1.0.69
Compiling tree-sitter-language v0.1.7
Compiling libz-sys v1.1.25
Checking ppv-lite86 v0.2.21
Checking hmac v0.12.1
Checking universal-hash v0.5.1
Checking num-integer v0.1.46
Compiling serde_derive_internals v0.29.1
Checking opaque-debug v0.3.1
Checking spki v0.7.3
Checking signature v2.2.0
Compiling libgit2-sys v0.18.4+1.9.3
Checking ff v0.13.1
Checking base16ct v0.2.0
Checking sec1 v0.7.3
Checking group v0.13.0
Checking rand_chacha v0.3.1
Compiling schemars_derive v1.2.1
Checking crypto-bigint v0.5.5
Compiling data-encoding v2.10.0
Checking schemars v1.2.1
Compiling data-encoding-macro-internal v0.1.17
Checking elliptic-curve v0.13.8
Compiling amplify_syn v2.0.1
Checking rand v0.8.5
Checking num-iter v0.1.45
Checking aead v0.5.2
Compiling match-lookup v0.1.2
Checking const-str v0.4.3
Checking signature v1.6.4
Compiling semver v1.0.27
Checking base256emoji v1.0.2
Compiling rustc_version v0.4.1
Checking data-encoding-macro v0.1.19
Checking ed25519 v1.5.3
Compiling amplify_derive v4.0.1
Checking poly1305 v0.8.0
Checking rfc6979 v0.4.0
Checking chacha20 v0.9.1
Checking gix-error v0.2.3
Checking ascii v1.1.0
Checking amplify_num v0.5.3
Checking base-x v0.2.11
Checking multibase v0.9.2
Checking ecdsa v0.16.9
Compiling curve25519-dalek v4.1.3
Checking ec25519 v0.1.0
Checking git-ref-format-core v0.6.0
Checking primeorder v0.13.6
Checking amplify v4.9.0
Checking polyval v0.6.2
Checking base64ct v1.8.3
Compiling num-bigint-dig v0.8.6
Checking ghash v0.5.1
Checking cyphergraphy v0.3.1
Checking pem-rfc7468 v0.7.0
Checking pkcs8 v0.10.2
Checking pbkdf2 v0.12.2
Checking ctr v0.9.2
Checking aes v0.8.4
Compiling sqlite3-src v0.7.0
Compiling curve25519-dalek-derive v0.1.1
Checking keccak v0.1.6
Checking sha3 v0.10.8
Checking aes-gcm v0.10.3
Checking pkcs1 v0.7.5
Checking ssh-encoding v0.2.0
Checking ed25519 v2.2.3
Checking blowfish v0.9.1
Checking cbc v0.1.2
Checking base32 v0.4.0
Checking ssh-cipher v0.2.0
Checking cypheraddr v0.4.1
Checking rsa v0.9.10
Checking bcrypt-pbkdf v0.10.0
Checking ed25519-dalek v2.2.0
Checking p521 v0.13.3
Checking p384 v0.13.1
Checking p256 v0.13.2
Checking chacha20poly1305 v0.10.1
Checking qcheck v1.0.0
Checking ssh-key v0.6.7
Checking noise-framework v0.4.1
Checking socks5-client v0.4.3
Checking secrecy v0.10.3
Compiling synstructure v0.13.2
Checking percent-encoding v2.3.2
Checking ssh-agent-lib v0.6.0
Checking cyphernet v0.5.4
Checking utf8parse v0.2.2
Checking anstyle-query v1.1.5
Checking jiff v0.2.23
Compiling zerofrom-derive v0.1.6
Checking zerofrom v0.1.6
Compiling yoke-derive v0.8.1
Checking radicle-localtime v0.1.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-localtime)
Checking errno v0.3.14
Checking anstyle v1.0.14
Checking is_terminal_polyfill v1.70.2
Checking colorchoice v1.0.5
Checking yoke v0.8.1
Checking gix-hashtable v0.15.0
Checking nonempty v0.9.0
Checking gix-date v0.15.3
Checking siphasher v1.0.2
Checking gix-actor v0.41.0
Checking radicle-git-metadata v0.2.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-git-metadata)
Checking gix-object v0.60.0
Compiling zerovec-derive v0.11.2
Checking radicle-dag v0.10.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-dag)
Checking memmap2 v0.9.10
Checking nonempty v0.12.0
Checking radicle-git-ref-format v0.1.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-git-ref-format)
Checking gix-chunk v0.7.1
Checking zerovec v0.11.5
Compiling displaydoc v0.2.5
Checking base64 v0.21.7
Compiling radicle v0.24.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle)
Checking gix-commitgraph v0.37.0
Checking anstyle-parse v1.0.0
Checking serde-untagged v0.1.9
Checking bytesize v2.3.1
Checking fast-glob v0.3.3
Checking dunce v1.0.5
Checking anstream v1.0.0
Checking gix-revwalk v0.31.0
Checking gix-fs v0.21.1
Checking mio v1.1.1
Checking sem_safe v0.2.1
Checking signals_receipts v0.2.5
Checking gix-tempfile v23.0.0
Checking tinystr v0.8.2
Checking gix-quote v0.7.1
Checking writeable v0.6.2
Checking litemap v0.8.1
Checking zerotrie v0.2.3
Checking icu_locale_core v2.1.1
Checking potential_utf v0.1.4
Checking iana-time-zone v0.1.65
Checking either v1.15.0
Compiling icu_properties_data v2.1.2
Compiling unicode-segmentation v1.12.0
Compiling icu_normalizer_data v2.1.1
Compiling signal-hook v0.3.18
Checking shell-words v1.1.1
Checking gix-command v0.9.0
Compiling convert_case v0.10.0
Checking icu_provider v2.1.1
Checking chrono v0.4.44
Checking icu_collections v2.1.1
Checking radicle-signals v0.11.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-signals)
Checking signal-hook-registry v1.4.8
Checking colored v2.2.0
Compiling object v0.37.3
Compiling rustversion v1.0.22
Compiling derive_more-impl v2.1.1
Checking gix-lock v23.0.0
Checking gix-url v0.36.0
Checking gix-config-value v0.18.0
Checking gix-sec v0.14.0
Compiling litrs v1.0.0
Checking gimli v0.32.3
Checking unicode-width v0.2.2
Compiling portable-atomic v1.13.1
Checking adler2 v2.0.1
Checking miniz_oxide v0.8.9
Compiling document-features v0.2.12
Checking gix-prompt v0.15.0
Checking derive_more v2.1.1
Checking icu_normalizer v2.1.1
Checking addr2line v0.25.1
Checking icu_properties v2.1.2
Checking signal-hook-mio v0.2.5
Checking radicle-log v0.1.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-log)
Checking gix-traverse v0.57.0
Checking gix-revision v0.45.0
Checking gix-diff v0.63.0
Checking anstyle-parse v0.2.7
Checking gix-packetline v0.21.3
Checking gix-glob v0.26.0
Compiling anyhow v1.0.102
Checking rustc-demangle v0.1.27
Checking backtrace v0.3.76
Checking gix-refspec v0.41.0
Checking gix-transport v0.57.0
Checking anstream v0.6.21
Checking gix-pack v0.70.0
Checking idna_adapter v1.2.1
Checking arc-swap v1.9.1
Checking crossterm v0.29.0
Checking gix-credentials v0.38.0
Checking console v0.16.3
Checking gix-shallow v0.12.0
Checking gix-ref v0.63.0
Checking gix-negotiate v0.31.0
Checking regex v1.12.3
Compiling maybe-async v0.2.10
Compiling tree-sitter v0.24.7
Compiling proc-macro-error-attr3 v3.0.2
Checking utf8_iter v1.0.4
Compiling getrandom v0.3.4
Compiling simd-adler32 v0.3.8
Checking unit-prefix v0.5.2
Checking indicatif v0.18.4
Checking idna v1.1.0
Compiling proc-macro-error3 v3.0.2
Checking gix-protocol v0.61.0
Checking inquire v0.9.4
Checking gix-odb v0.80.0
Compiling xattr v1.6.1
Checking unicode-display-width v0.3.0
Compiling filetime v0.2.27
Checking uuid v1.22.0
Checking bytes v1.11.1
Compiling flate2 v1.1.9
Compiling tar v0.4.45
Compiling git-ref-format-macro v0.7.0
Checking snapbox-macros v1.1.0
Checking salsa20 v0.10.2
Checking similar v3.1.1
Checking clap_lex v1.1.0
Compiling heck v0.5.0
Checking siphasher v0.3.11
Checking streaming-iterator v0.1.9
Checking strsim v0.11.1
Checking normalize-line-endings v0.3.0
Compiling crossbeam-utils v0.8.21
Checking snapbox v1.2.2
Checking clap_builder v4.6.0
Checking bloomy v1.2.0
Compiling clap_derive v4.6.0
Checking sqlite3-sys v0.18.0
Checking sqlite v0.37.0
Checking radicle-crypto v0.17.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-crypto)
Checking scrypt v0.11.0
Compiling radicle-surf v0.28.0
Checking git-ref-format v0.7.0
Checking form_urlencoded v1.2.2
Checking systemd-journal-logger v2.2.2
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-python v0.23.6
Compiling tree-sitter-css v0.23.2
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-ruby v0.23.1
Checking toml_datetime v0.7.5+spec-1.1.0
Checking serde_spanned v1.0.4
Checking toml_writer v1.0.7+spec-1.1.0
Checking radicle-std-ext v0.2.0
Checking pin-project-lite v0.2.17
Checking toml v0.9.12+spec-1.1.0
Checking tokio v1.50.0
Checking clap v4.6.0
Checking url v2.5.8
Checking sysinfo v0.37.2
Compiling radicle-node v0.20.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-node)
Compiling radicle-cli v0.21.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-cli)
Checking yansi v1.0.1
Checking diff v0.1.13
Checking human-panic v2.0.6
Checking crossbeam-channel v0.5.15
Checking pretty_assertions v1.4.1
Checking clap_complete v4.6.0
Checking structured-logger v1.0.5
Checking radicle-systemd v0.13.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.7
Checking itertools v0.14.0
Checking socket2 v0.5.10
Checking timeago v0.4.2
Compiling escargot v0.5.15
Checking humantime v2.3.0
Checking lexopt v0.3.2
Checking bit-vec v0.8.0
Checking bit-set v0.8.0
Checking rand_core v0.9.5
Compiling qcheck-macros v1.0.0
Checking num-bigint v0.4.6
Compiling ahash v0.8.12
Checking num-complex v0.4.6
Checking env_filter v1.0.0
Checking borrow-or-share v0.2.4
Checking fluent-uri v0.3.2
Checking env_logger v0.11.9
Checking phf_shared v0.11.3
Compiling test-log-macros v0.2.19
Checking num-rational v0.4.2
Checking wait-timeout v0.2.1
Compiling radicle-remote-helper v0.17.0 (/57c396a0-5542-4bf8-a735-4da25a7267c2/w/crates/radicle-remote-helper)
Checking num v0.4.3
Checking quick-error v1.2.3
Checking outref v0.5.2
Checking fnv v1.0.7
Checking vsimd v0.8.0
Checking rusty-fork v0.3.1
Checking fraction v0.15.3
Checking test-log v0.2.19
Checking phf v0.11.3
Checking referencing v0.30.0
Checking uuid-simd v0.8.0
Checking rand_xorshift v0.4.0
Checking rand_chacha v0.9.0
Checking rand v0.9.2
Checking fancy-regex v0.14.0
Checking email_address v0.2.9
Checking bytecount v0.6.9
Checking base64 v0.22.1
Checking unarray v0.1.4
Checking num-cmp v0.1.0
Checking proptest v1.10.0
Checking emojis v0.6.4
Checking jsonschema v0.30.0
Compiling pastey v0.2.1
error: unused import: `VerifyingKey`
--> crates/radicle-crypto/src/lib.rs:800:29
|
800 | use crate::{SigningKey, VerifyingKey};
| ^^^^^^^^^^^^
|
= note: `-D unused-imports` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(unused_imports)]`
error: could not compile `radicle-crypto` (lib test) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
Exit code: 101
{
"response": "finished",
"result": "failure"
}