Compare commits
2 Commits
enforcing-
...
a845181ef6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a845181ef6 | ||
|
|
0d424f3afc |
@@ -67,18 +67,14 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
|
|||||||
## Cryptography
|
## Cryptography
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
- **Client protocol:** ed25519
|
- **Client protocol:** ML-DSA
|
||||||
|
|
||||||
### User-Agent Authentication
|
### User-Agent Authentication
|
||||||
|
|
||||||
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
|
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
|
||||||
|
|
||||||
- **Supported schemes:** RSA, Ed25519, ECDSA (secp256k1)
|
- **Supported schemes:** ML-DSA
|
||||||
- **Why:** the user agent authenticates with keys backed by platform facilities, and those facilities differ by platform
|
- **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out
|
||||||
- **Apple Silicon Secure Enclave / Secure Element:** ECDSA-only in practice
|
|
||||||
- **Windows Hello / TPM 2.0:** currently RSA-backed in our integration
|
|
||||||
|
|
||||||
This is why the user-agent auth protocol carries an explicit `KeyType`, while the SDK client protocol remains fixed to ed25519.
|
|
||||||
|
|
||||||
### Encryption at Rest
|
### Encryption at Rest
|
||||||
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**
|
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**
|
||||||
|
|||||||
236
server/Cargo.lock
generated
236
server/Cargo.lock
generated
@@ -347,7 +347,7 @@ dependencies = [
|
|||||||
"ruint",
|
"ruint",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"sha3",
|
"sha3 0.10.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -548,7 +548,7 @@ dependencies = [
|
|||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"sha3",
|
"sha3 0.10.8",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
"syn-solidity",
|
"syn-solidity",
|
||||||
]
|
]
|
||||||
@@ -682,8 +682,8 @@ dependencies = [
|
|||||||
"alloy",
|
"alloy",
|
||||||
"arbiter-proto",
|
"arbiter-proto",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"ed25519-dalek",
|
|
||||||
"http",
|
"http",
|
||||||
|
"ml-dsa",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -742,10 +742,9 @@ dependencies = [
|
|||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
"kameo",
|
"kameo",
|
||||||
"macro_rules_attribute",
|
|
||||||
"memsafe",
|
"memsafe",
|
||||||
|
"ml-dsa",
|
||||||
"mutants",
|
"mutants",
|
||||||
"paste",
|
|
||||||
"pem",
|
"pem",
|
||||||
"proptest",
|
"proptest",
|
||||||
"prost",
|
"prost",
|
||||||
@@ -753,14 +752,13 @@ dependencies = [
|
|||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"restructed",
|
"restructed",
|
||||||
"rsa",
|
|
||||||
"rstest",
|
"rstest",
|
||||||
"rustls",
|
"rustls",
|
||||||
"secrecy",
|
"secrecy",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"smlang",
|
"smlang",
|
||||||
"spki",
|
"spki 0.7.3",
|
||||||
"strum 0.28.0",
|
"strum 0.28.0",
|
||||||
"subtle",
|
"subtle",
|
||||||
"test-log",
|
"test-log",
|
||||||
@@ -1451,6 +1449,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmov"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.11"
|
version = "0.15.11"
|
||||||
@@ -1481,6 +1485,12 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_format"
|
name = "const_format"
|
||||||
version = "0.2.35"
|
version = "0.2.35"
|
||||||
@@ -1602,6 +1612,15 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctutils"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||||
|
dependencies = [
|
||||||
|
"cmov",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.3"
|
version = "4.1.3"
|
||||||
@@ -1740,8 +1759,17 @@ version = "0.7.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid 0.9.6",
|
||||||
"pem-rfc7468",
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid 0.10.2",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1884,7 +1912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer 0.10.4",
|
||||||
"const-oid",
|
"const-oid 0.9.6",
|
||||||
"crypto-common 0.1.7",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
@@ -1948,13 +1976,13 @@ version = "0.16.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"der",
|
"der 0.7.10",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"rfc6979",
|
"rfc6979",
|
||||||
"serdect",
|
"serdect",
|
||||||
"signature 2.2.0",
|
"signature 2.2.0",
|
||||||
"spki",
|
"spki 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1963,7 +1991,6 @@ version = "3.0.0-rc.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
|
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
|
||||||
"signature 3.0.0-rc.10",
|
"signature 3.0.0-rc.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1976,7 +2003,6 @@ dependencies = [
|
|||||||
"curve25519-dalek 5.0.0-pre.6",
|
"curve25519-dalek 5.0.0-pre.6",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"rand_core 0.10.0",
|
"rand_core 0.10.0",
|
||||||
"serde",
|
|
||||||
"sha2 0.11.0-rc.5",
|
"sha2 0.11.0-rc.5",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -2015,7 +2041,7 @@ dependencies = [
|
|||||||
"ff",
|
"ff",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"group",
|
"group",
|
||||||
"pkcs8",
|
"pkcs8 0.10.2",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"sec1",
|
"sec1",
|
||||||
"serdect",
|
"serdect",
|
||||||
@@ -2562,6 +2588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
|
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2959,6 +2986,16 @@ dependencies = [
|
|||||||
"cpufeatures 0.2.17",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keccak"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keccak-asm"
|
name = "keccak-asm"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -2974,9 +3011,6 @@ name = "lazy_static"
|
|||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
dependencies = [
|
|
||||||
"spin",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
@@ -3059,22 +3093,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "macro_rules_attribute"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
|
|
||||||
dependencies = [
|
|
||||||
"macro_rules_attribute-proc_macro",
|
|
||||||
"paste",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "macro_rules_attribute-proc_macro"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3191,6 +3209,34 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ml-dsa"
|
||||||
|
version = "0.1.0-rc.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f5b2bb0ad6fa2b40396775bd56f51345171490fef993f46f91a876ecdbdaea55"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid 0.10.2",
|
||||||
|
"ctutils",
|
||||||
|
"hybrid-array",
|
||||||
|
"module-lattice",
|
||||||
|
"pkcs8 0.11.0-rc.11",
|
||||||
|
"rand_core 0.10.0",
|
||||||
|
"sha3 0.11.0",
|
||||||
|
"signature 3.0.0-rc.10",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "module-lattice"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
"num-traits",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multimap"
|
name = "multimap"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -3232,23 +3278,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-bigint-dig"
|
|
||||||
version = "0.8.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
"libm",
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-traits",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"serde",
|
|
||||||
"smallvec",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3264,17 +3293,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-iter"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -3444,15 +3462,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pem-rfc7468"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -3512,25 +3521,24 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkcs1"
|
|
||||||
version = "0.7.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
|
||||||
dependencies = [
|
|
||||||
"der",
|
|
||||||
"pkcs8",
|
|
||||||
"spki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"der",
|
"der 0.7.10",
|
||||||
"spki",
|
"spki 0.7.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.11.0-rc.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577"
|
||||||
|
dependencies = [
|
||||||
|
"der 0.8.0",
|
||||||
|
"spki 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4170,28 +4178,6 @@ dependencies = [
|
|||||||
"rustc-hex",
|
"rustc-hex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rsa"
|
|
||||||
version = "0.9.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
|
||||||
dependencies = [
|
|
||||||
"const-oid",
|
|
||||||
"digest 0.10.7",
|
|
||||||
"num-bigint-dig",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"pkcs1",
|
|
||||||
"pkcs8",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"serde",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
"signature 2.2.0",
|
|
||||||
"spki",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsqlite-vfs"
|
name = "rsqlite-vfs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4431,9 +4417,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct",
|
"base16ct",
|
||||||
"der",
|
"der 0.7.10",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"pkcs8",
|
"pkcs8 0.10.2",
|
||||||
"serdect",
|
"serdect",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -4627,7 +4613,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
|
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"keccak",
|
"keccak 0.1.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha3"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.11.2",
|
||||||
|
"keccak 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4680,6 +4676,10 @@ name = "signature"
|
|||||||
version = "3.0.0-rc.10"
|
version = "3.0.0-rc.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3"
|
checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.11.2",
|
||||||
|
"rand_core 0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
@@ -4739,12 +4739,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.9.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spki"
|
name = "spki"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -4752,7 +4746,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"der",
|
"der 0.7.10",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ tokio = { version = "1.50.0", features = ["full"] }
|
|||||||
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
|
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
|
||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
rustls = { version = "0.23.37", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false }
|
||||||
smlang = "0.8.0"
|
smlang = "0.8.0"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
@@ -45,3 +45,4 @@ spki = "0.7"
|
|||||||
prost = "0.14.3"
|
prost = "0.14.3"
|
||||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||||
mutants = "0.0.4"
|
mutants = "0.0.4"
|
||||||
|
ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ tonic.workspace = true
|
|||||||
tonic.features = ["tls-aws-lc"]
|
tonic.features = ["tls-aws-lc"]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-stream.workspace = true
|
tokio-stream.workspace = true
|
||||||
ed25519-dalek.workspace = true
|
ml-dsa.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
http = "1.4.0"
|
http = "1.4.0"
|
||||||
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
|
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, format_challenge,
|
CLIENT_CONTEXT, ClientMetadata, format_challenge,
|
||||||
proto::{
|
proto::{
|
||||||
client::{
|
client::{
|
||||||
ClientRequest,
|
ClientRequest,
|
||||||
@@ -14,7 +14,7 @@ use arbiter_proto::{
|
|||||||
shared::ClientInfo as ProtoClientInfo,
|
shared::ClientInfo as ProtoClientInfo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ed25519_dalek::Signer as _;
|
use ml_dsa::{MlDsa87, SigningKey, signature::Keypair as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
storage::StorageError,
|
storage::StorageError,
|
||||||
@@ -54,14 +54,14 @@ fn map_auth_result(code: i32) -> AuthError {
|
|||||||
async fn send_auth_challenge_request(
|
async fn send_auth_challenge_request(
|
||||||
transport: &mut ClientTransport,
|
transport: &mut ClientTransport,
|
||||||
metadata: ClientMetadata,
|
metadata: ClientMetadata,
|
||||||
key: &ed25519_dalek::SigningKey,
|
key: &SigningKey<MlDsa87>,
|
||||||
) -> std::result::Result<(), AuthError> {
|
) -> std::result::Result<(), AuthError> {
|
||||||
transport
|
transport
|
||||||
.send(ClientRequest {
|
.send(ClientRequest {
|
||||||
request_id: next_request_id(),
|
request_id: next_request_id(),
|
||||||
payload: Some(ClientRequestPayload::Auth(proto_auth::Request {
|
payload: Some(ClientRequestPayload::Auth(proto_auth::Request {
|
||||||
payload: Some(AuthRequestPayload::ChallengeRequest(AuthChallengeRequest {
|
payload: Some(AuthRequestPayload::ChallengeRequest(AuthChallengeRequest {
|
||||||
pubkey: key.verifying_key().to_bytes().to_vec(),
|
pubkey: key.verifying_key().encode().to_vec(),
|
||||||
client_info: Some(ProtoClientInfo {
|
client_info: Some(ProtoClientInfo {
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
@@ -95,11 +95,16 @@ async fn receive_auth_challenge(
|
|||||||
|
|
||||||
async fn send_auth_challenge_solution(
|
async fn send_auth_challenge_solution(
|
||||||
transport: &mut ClientTransport,
|
transport: &mut ClientTransport,
|
||||||
key: &ed25519_dalek::SigningKey,
|
key: &SigningKey<MlDsa87>,
|
||||||
challenge: AuthChallenge,
|
challenge: AuthChallenge,
|
||||||
) -> std::result::Result<(), AuthError> {
|
) -> std::result::Result<(), AuthError> {
|
||||||
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
||||||
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
let signature = key
|
||||||
|
.signing_key()
|
||||||
|
.sign_deterministic(&challenge_payload, CLIENT_CONTEXT)
|
||||||
|
.map_err(|_| AuthError::UnexpectedAuthResponse)?
|
||||||
|
.encode()
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
transport
|
transport
|
||||||
.send(ClientRequest {
|
.send(ClientRequest {
|
||||||
@@ -140,7 +145,7 @@ async fn receive_auth_confirmation(
|
|||||||
pub(crate) async fn authenticate(
|
pub(crate) async fn authenticate(
|
||||||
transport: &mut ClientTransport,
|
transport: &mut ClientTransport,
|
||||||
metadata: ClientMetadata,
|
metadata: ClientMetadata,
|
||||||
key: &ed25519_dalek::SigningKey,
|
key: &SigningKey<MlDsa87>,
|
||||||
) -> std::result::Result<(), AuthError> {
|
) -> std::result::Result<(), AuthError> {
|
||||||
send_auth_challenge_request(transport, metadata, key).await?;
|
send_auth_challenge_request(transport, metadata, key).await?;
|
||||||
let challenge = receive_auth_challenge(transport).await?;
|
let challenge = receive_auth_challenge(transport).await?;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
||||||
};
|
};
|
||||||
|
use ml_dsa::{MlDsa87, SigningKey};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{Mutex, mpsc};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
@@ -60,7 +61,7 @@ impl ArbiterClient {
|
|||||||
pub async fn connect_with_key(
|
pub async fn connect_with_key(
|
||||||
url: ArbiterUrl,
|
url: ArbiterUrl,
|
||||||
metadata: ClientMetadata,
|
metadata: ClientMetadata,
|
||||||
key: ed25519_dalek::SigningKey,
|
key: SigningKey<MlDsa87>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use arbiter_proto::home_path;
|
use arbiter_proto::home_path;
|
||||||
|
use ml_dsa::{KeyGen, MlDsa87, Seed, SigningKey};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -11,7 +12,7 @@ pub enum StorageError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait SigningKeyStorage {
|
pub trait SigningKeyStorage {
|
||||||
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
|
fn load_or_create(&self) -> std::result::Result<SigningKey<MlDsa87>, StorageError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -20,7 +21,7 @@ pub struct FileSigningKeyStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileSigningKeyStorage {
|
impl FileSigningKeyStorage {
|
||||||
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
|
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ml_dsa.key";
|
||||||
|
|
||||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||||
Self { path: path.into() }
|
Self { path: path.into() }
|
||||||
@@ -30,21 +31,20 @@ impl FileSigningKeyStorage {
|
|||||||
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
|
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
fn read_key(path: &Path) -> std::result::Result<SigningKey<MlDsa87>, StorageError> {
|
||||||
let bytes = std::fs::read(path)?;
|
let bytes = std::fs::read(path)?;
|
||||||
let raw: [u8; 32] =
|
let raw: [u8; 32] = bytes
|
||||||
bytes
|
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
|
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
|
||||||
expected: 32,
|
expected: 32,
|
||||||
actual: v.len(),
|
actual: v.len(),
|
||||||
})?;
|
})?;
|
||||||
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
|
Ok(MlDsa87::from_seed(&Seed::from(raw)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SigningKeyStorage for FileSigningKeyStorage {
|
impl SigningKeyStorage for FileSigningKeyStorage {
|
||||||
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
fn load_or_create(&self) -> std::result::Result<SigningKey<MlDsa87>, StorageError> {
|
||||||
if let Some(parent) = self.path.parent() {
|
if let Some(parent) = self.path.parent() {
|
||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,8 @@ impl SigningKeyStorage for FileSigningKeyStorage {
|
|||||||
return Self::read_key(&self.path);
|
return Self::read_key(&self.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
let raw_key = key.to_bytes();
|
let raw_key = key.to_seed();
|
||||||
|
|
||||||
// Use create_new to prevent accidental overwrite if another process creates the key first.
|
// Use create_new to prevent accidental overwrite if another process creates the key first.
|
||||||
match std::fs::OpenOptions::new()
|
match std::fs::OpenOptions::new()
|
||||||
@@ -103,7 +103,7 @@ mod tests {
|
|||||||
.load_or_create()
|
.load_or_create()
|
||||||
.expect("second load_or_create should read same key");
|
.expect("second load_or_create should read same key");
|
||||||
|
|
||||||
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
|
assert_eq!(key_a.to_seed(), key_b.to_seed());
|
||||||
assert!(path.exists());
|
assert!(path.exists());
|
||||||
|
|
||||||
std::fs::remove_file(path).expect("temp key file should be removable");
|
std::fs::remove_file(path).expect("temp key file should be removable");
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ pub struct ArbiterEvmWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ArbiterEvmWallet {
|
impl ArbiterEvmWallet {
|
||||||
#[expect(
|
|
||||||
dead_code,
|
|
||||||
reason = "constructor may be used in future extensions, e.g. to support wallet listing"
|
|
||||||
)]
|
|
||||||
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
||||||
Self {
|
Self {
|
||||||
transport,
|
transport,
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ pub mod proto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
|
||||||
|
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ClientMetadata {
|
pub struct ClientMetadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ diesel-async = { version = "0.8.0", features = [
|
|||||||
"sqlite",
|
"sqlite",
|
||||||
"tokio",
|
"tokio",
|
||||||
] }
|
] }
|
||||||
ed25519-dalek.workspace = true
|
|
||||||
ed25519-dalek.features = ["serde"]
|
|
||||||
arbiter-proto.path = "../arbiter-proto"
|
arbiter-proto.path = "../arbiter-proto"
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
@@ -40,16 +38,11 @@ chrono.workspace = true
|
|||||||
memsafe = "0.4.0"
|
memsafe = "0.4.0"
|
||||||
zeroize = { version = "1.8.2", features = ["std", "simd"] }
|
zeroize = { version = "1.8.2", features = ["std", "simd"] }
|
||||||
kameo.workspace = true
|
kameo.workspace = true
|
||||||
x25519-dalek.workspace = true
|
|
||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
argon2 = { version = "0.5.3", features = ["zeroize"] }
|
argon2 = { version = "0.5.3", features = ["zeroize"] }
|
||||||
restructed = "0.2.2"
|
restructed = "0.2.2"
|
||||||
strum = { version = "0.28.0", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
pem = "3.0.6"
|
pem = "3.0.6"
|
||||||
k256.workspace = true
|
|
||||||
k256.features = ["serde"]
|
|
||||||
rsa.workspace = true
|
|
||||||
rsa.features = ["serde"]
|
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
spki.workspace = true
|
spki.workspace = true
|
||||||
@@ -61,8 +54,10 @@ anyhow = "1.0.102"
|
|||||||
serde_with = "3.18.0"
|
serde_with = "3.18.0"
|
||||||
mutants.workspace = true
|
mutants.workspace = true
|
||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
macro_rules_attribute = "0.2.2"
|
ml-dsa.workspace = true
|
||||||
paste = "1.0.15"
|
ed25519-dalek.workspace = true
|
||||||
|
x25519-dalek.workspace = true
|
||||||
|
k256.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ create table if not exists useragent_client (
|
|||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
nonce integer not null default(1), -- used for auth challenge
|
nonce integer not null default(1), -- used for auth challenge
|
||||||
public_key blob not null,
|
public_key blob not null,
|
||||||
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
key_type integer not null default(1),
|
||||||
created_at integer not null default(unixepoch ('now')),
|
created_at integer not null default(unixepoch ('now')),
|
||||||
updated_at integer not null default(unixepoch ('now'))
|
updated_at integer not null default(unixepoch ('now'))
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, format_challenge,
|
CLIENT_CONTEXT, ClientMetadata,
|
||||||
transport::{Bi, expect_message},
|
transport::{Bi, expect_message},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -8,7 +8,6 @@ use diesel::{
|
|||||||
dsl::insert_into, update,
|
dsl::insert_into, update,
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl as _;
|
use diesel_async::RunQueryDsl as _;
|
||||||
use ed25519_dalek::{Signature, VerifyingKey};
|
|
||||||
use kameo::{actor::ActorRef, error::SendError};
|
use kameo::{actor::ActorRef, error::SendError};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -18,7 +17,8 @@ use crate::{
|
|||||||
flow_coordinator::{self, RequestClientApproval},
|
flow_coordinator::{self, RequestClientApproval},
|
||||||
keyholder::KeyHolder,
|
keyholder::KeyHolder,
|
||||||
},
|
},
|
||||||
crypto::integrity::{self, Verified, verified::VerifiedFieldsAccessor},
|
crypto::authn,
|
||||||
|
crypto::integrity::{self, AttestationStatus},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||||
@@ -26,6 +26,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database pool unavailable")]
|
#[error("Database pool unavailable")]
|
||||||
@@ -62,17 +64,20 @@ pub enum ApproveError {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Inbound {
|
pub enum Inbound {
|
||||||
AuthChallengeRequest {
|
AuthChallengeRequest {
|
||||||
pubkey: VerifyingKey,
|
pubkey: authn::PublicKey,
|
||||||
metadata: ClientMetadata,
|
metadata: ClientMetadata,
|
||||||
},
|
},
|
||||||
AuthChallengeSolution {
|
AuthChallengeSolution {
|
||||||
signature: Signature,
|
signature: authn::Signature,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Outbound {
|
pub enum Outbound {
|
||||||
AuthChallenge { pubkey: VerifyingKey, nonce: i32 },
|
AuthChallenge {
|
||||||
|
pubkey: authn::PublicKey,
|
||||||
|
nonce: i32,
|
||||||
|
},
|
||||||
AuthSuccess,
|
AuthSuccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +85,9 @@ pub enum Outbound {
|
|||||||
/// Returns `None` if the pubkey is not registered.
|
/// Returns `None` if the pubkey is not registered.
|
||||||
async fn get_current_nonce_and_id(
|
async fn get_current_nonce_and_id(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
pubkey: &VerifyingKey,
|
pubkey: &authn::PublicKey,
|
||||||
) -> Result<Option<(i32, i32)>, Error> {
|
) -> Result<Option<(i32, i32)>, Error> {
|
||||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
let pubkey_bytes = pubkey.to_bytes();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::DatabasePoolUnavailable
|
Error::DatabasePoolUnavailable
|
||||||
@@ -99,14 +104,53 @@ async fn get_current_nonce_and_id(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn verify_integrity(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
pubkey: &authn::PublicKey,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database pool error");
|
||||||
|
Error::DatabasePoolUnavailable
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| {
|
||||||
|
error!("Client not found during integrity verification");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let attestation = integrity::verify_entity(
|
||||||
|
&mut db_conn,
|
||||||
|
keyholder,
|
||||||
|
&ClientCredentials {
|
||||||
|
pubkey: pubkey.clone(),
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(?e, "Integrity verification failed");
|
||||||
|
Error::IntegrityCheckFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if attestation != AttestationStatus::Attested {
|
||||||
|
error!("Integrity attestation unavailable for client {id}");
|
||||||
|
return Err(Error::IntegrityCheckFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Atomically increments the nonce and re-signs the integrity envelope.
|
/// Atomically increments the nonce and re-signs the integrity envelope.
|
||||||
/// Returns the new nonce, which is used as the challenge nonce.
|
/// Returns the new nonce, which is used as the challenge nonce.
|
||||||
async fn create_nonce(
|
async fn create_nonce(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &VerifyingKey,
|
pubkey: &authn::PublicKey,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
let pubkey_bytes = pubkey.to_bytes();
|
||||||
|
let pubkey = pubkey.clone();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
@@ -115,6 +159,7 @@ async fn create_nonce(
|
|||||||
|
|
||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let keyholder = keyholder.clone();
|
let keyholder = keyholder.clone();
|
||||||
|
let pubkey = pubkey.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
||||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
@@ -127,7 +172,7 @@ async fn create_nonce(
|
|||||||
conn,
|
conn,
|
||||||
&keyholder,
|
&keyholder,
|
||||||
&ClientCredentials {
|
&ClientCredentials {
|
||||||
pubkey: *pubkey,
|
pubkey: pubkey.clone(),
|
||||||
nonce: new_nonce,
|
nonce: new_nonce,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
@@ -136,8 +181,7 @@ async fn create_nonce(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(?e, "Integrity sign failed after nonce update");
|
error!(?e, "Integrity sign failed after nonce update");
|
||||||
Error::DatabaseOperationFailed
|
Error::DatabaseOperationFailed
|
||||||
})?
|
})?;
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
Ok(new_nonce)
|
Ok(new_nonce)
|
||||||
})
|
})
|
||||||
@@ -171,10 +215,11 @@ async fn approve_new_client(
|
|||||||
async fn insert_client(
|
async fn insert_client(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &VerifyingKey,
|
pubkey: &authn::PublicKey,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) -> Result<Verified<i32>, Error> {
|
) -> Result<i32, Error> {
|
||||||
use crate::db::schema::{client_metadata, program_client};
|
use crate::db::schema::{client_metadata, program_client};
|
||||||
|
let pubkey = pubkey.clone();
|
||||||
let metadata = metadata.clone();
|
let metadata = metadata.clone();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
@@ -184,6 +229,7 @@ async fn insert_client(
|
|||||||
|
|
||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let keyholder = keyholder.clone();
|
let keyholder = keyholder.clone();
|
||||||
|
let pubkey = pubkey.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
const NONCE_START: i32 = 1;
|
const NONCE_START: i32 = 1;
|
||||||
|
|
||||||
@@ -199,7 +245,7 @@ async fn insert_client(
|
|||||||
|
|
||||||
let client_id = insert_into(program_client::table)
|
let client_id = insert_into(program_client::table)
|
||||||
.values((
|
.values((
|
||||||
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
program_client::public_key.eq(pubkey.to_bytes()),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
program_client::metadata_id.eq(metadata_id),
|
||||||
program_client::nonce.eq(NONCE_START),
|
program_client::nonce.eq(NONCE_START),
|
||||||
))
|
))
|
||||||
@@ -208,11 +254,11 @@ async fn insert_client(
|
|||||||
.get_result::<i32>(conn)
|
.get_result::<i32>(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let verified_id = integrity::sign_entity(
|
integrity::sign_entity(
|
||||||
conn,
|
conn,
|
||||||
&keyholder,
|
&keyholder,
|
||||||
&ClientCredentials {
|
&ClientCredentials {
|
||||||
pubkey: *pubkey,
|
pubkey: pubkey.clone(),
|
||||||
nonce: NONCE_START,
|
nonce: NONCE_START,
|
||||||
},
|
},
|
||||||
client_id,
|
client_id,
|
||||||
@@ -221,10 +267,9 @@ async fn insert_client(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Failed to sign integrity tag for new client key");
|
error!(error = ?e, "Failed to sign integrity tag for new client key");
|
||||||
Error::DatabaseOperationFailed
|
Error::DatabaseOperationFailed
|
||||||
})?
|
})?;
|
||||||
.unqualify_origin();
|
|
||||||
|
|
||||||
Ok(verified_id)
|
Ok(client_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -303,14 +348,14 @@ async fn sync_client_metadata(
|
|||||||
|
|
||||||
async fn challenge_client<T>(
|
async fn challenge_client<T>(
|
||||||
transport: &mut T,
|
transport: &mut T,
|
||||||
pubkey: VerifyingKey,
|
pubkey: authn::PublicKey,
|
||||||
nonce: i32,
|
nonce: i32,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
|
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
|
||||||
{
|
{
|
||||||
transport
|
transport
|
||||||
.send(Ok(Outbound::AuthChallenge { pubkey, nonce }))
|
.send(Ok(Outbound::AuthChallenge { pubkey: pubkey.clone(), nonce }))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Failed to send auth challenge");
|
error!(error = ?e, "Failed to send auth challenge");
|
||||||
@@ -327,20 +372,15 @@ where
|
|||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let formatted = format_challenge(nonce, pubkey.as_bytes());
|
if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) {
|
||||||
|
|
||||||
pubkey.verify_strict(&formatted, &signature).map_err(|_| {
|
|
||||||
error!("Challenge solution verification failed");
|
error!("Challenge solution verification failed");
|
||||||
Error::InvalidChallengeSolution
|
return Err(Error::InvalidChallengeSolution);
|
||||||
})?;
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate<T>(
|
pub async fn authenticate<T>(props: &mut ClientConnection, transport: &mut T) -> Result<i32, Error>
|
||||||
props: &mut ClientConnection,
|
|
||||||
transport: &mut T,
|
|
||||||
) -> Result<Verified<i32>, Error>
|
|
||||||
where
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||||
{
|
{
|
||||||
@@ -348,34 +388,16 @@ where
|
|||||||
return Err(Error::Transport);
|
return Err(Error::Transport);
|
||||||
};
|
};
|
||||||
|
|
||||||
// fixme! triage needed: probable regretion since in match->Some get_current_nonce_and_id called only once instead of twice
|
|
||||||
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
|
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
|
||||||
Some((nonce, id)) => {
|
Some((id, _)) => {
|
||||||
let mut db_conn = props.db.get().await.map_err(|e| {
|
verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||||
error!(error = ?e, "Database pool error");
|
id
|
||||||
Error::DatabasePoolUnavailable
|
|
||||||
})?;
|
|
||||||
|
|
||||||
integrity::verify_entity(
|
|
||||||
&mut db_conn,
|
|
||||||
&props.actors.key_holder,
|
|
||||||
ClientCredentials { pubkey, nonce },
|
|
||||||
id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(?e, "Integrity verification failed");
|
|
||||||
Error::IntegrityCheckFailed
|
|
||||||
})?
|
|
||||||
.inherit()
|
|
||||||
.entity_id
|
|
||||||
.unqualify_origin()
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
approve_new_client(
|
approve_new_client(
|
||||||
&props.actors,
|
&props.actors,
|
||||||
ClientProfile {
|
ClientProfile {
|
||||||
pubkey,
|
pubkey: pubkey.clone(),
|
||||||
metadata: metadata.clone(),
|
metadata: metadata.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -384,7 +406,7 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sync_client_metadata(&props.db, *client_id, &metadata).await?;
|
sync_client_metadata(&props.db, client_id, &metadata).await?;
|
||||||
let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?;
|
let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||||
challenge_client(transport, pubkey, challenge_nonce).await?;
|
challenge_client(transport, pubkey, challenge_nonce).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::session::ClientSession},
|
actors::{GlobalActors, client::session::ClientSession},
|
||||||
|
crypto::authn,
|
||||||
crypto::integrity::{Integrable, hashing::Hashable},
|
crypto::integrity::{Integrable, hashing::Hashable},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClientProfile {
|
pub struct ClientProfile {
|
||||||
pub pubkey: ed25519_dalek::VerifyingKey,
|
pub pubkey: authn::PublicKey,
|
||||||
pub metadata: ClientMetadata,
|
pub metadata: ClientMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClientCredentials {
|
pub struct ClientCredentials {
|
||||||
pub pubkey: ed25519_dalek::VerifyingKey,
|
pub pubkey: authn::PublicKey,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ impl Integrable for ClientCredentials {
|
|||||||
|
|
||||||
impl Hashable for ClientCredentials {
|
impl Hashable for ClientCredentials {
|
||||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(self.pubkey.as_bytes());
|
hasher.update(self.pubkey.to_bytes());
|
||||||
self.nonce.hash(hasher);
|
self.nonce.hash(hasher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,12 @@ pub async fn connect_client<T>(mut props: ClientConnection, transport: &mut T)
|
|||||||
where
|
where
|
||||||
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
|
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
|
||||||
{
|
{
|
||||||
match auth::authenticate(&mut props, transport).await {
|
let fut = auth::authenticate(&mut props, transport);
|
||||||
|
println!(
|
||||||
|
"authenticate future size: {}",
|
||||||
|
std::mem::size_of_val(&fut)
|
||||||
|
);
|
||||||
|
match fut.await {
|
||||||
Ok(client_id) => {
|
Ok(client_id) => {
|
||||||
ClientSession::spawn(ClientSession::new(props, client_id));
|
ClientSession::spawn(ClientSession::new(props, client_id));
|
||||||
info!("Client authenticated, session started");
|
info!("Client authenticated, session started");
|
||||||
|
|||||||
@@ -5,25 +5,23 @@ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
|
GlobalActors,
|
||||||
client::ClientConnection,
|
client::ClientConnection,
|
||||||
evm::{ClientSignTransaction, SignTransactionError},
|
evm::{ClientSignTransaction, SignTransactionError},
|
||||||
flow_coordinator::RegisterClient,
|
flow_coordinator::RegisterClient,
|
||||||
keyholder::KeyHolderState,
|
keyholder::KeyHolderState,
|
||||||
},
|
},
|
||||||
crypto::integrity::Verified,
|
db,
|
||||||
evm::VetError,
|
evm::VetError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::{actors::GlobalActors, db};
|
|
||||||
|
|
||||||
pub struct ClientSession {
|
pub struct ClientSession {
|
||||||
props: ClientConnection,
|
props: ClientConnection,
|
||||||
client_id: Verified<i32>,
|
client_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub(crate) fn new(props: ClientConnection, client_id: Verified<i32>) -> Self {
|
pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
|
||||||
Self { props, client_id }
|
Self { props, client_id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +54,7 @@ impl ClientSession {
|
|||||||
.actors
|
.actors
|
||||||
.evm
|
.evm
|
||||||
.ask(ClientSignTransaction {
|
.ask(ClientSignTransaction {
|
||||||
client_id: *self.client_id,
|
client_id: self.client_id,
|
||||||
wallet_address,
|
wallet_address,
|
||||||
transaction,
|
transaction,
|
||||||
})
|
})
|
||||||
@@ -94,12 +92,11 @@ impl Actor for ClientSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
#[cfg(test)]
|
|
||||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
let props = ClientConnection::new(db, actors);
|
let props = ClientConnection::new(db, actors);
|
||||||
Self {
|
Self {
|
||||||
props,
|
props,
|
||||||
client_id: Verified::new_unchecked(0),
|
client_id: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ use kameo::{Actor, actor::ActorRef, messages};
|
|||||||
use rand::{SeedableRng, rng, rngs::StdRng};
|
use rand::{SeedableRng, rng, rngs::StdRng};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState},
|
||||||
crypto::integrity::{self, Integrable, Verified, hashing::Hashable},
|
crypto::integrity,
|
||||||
db::{
|
db::{
|
||||||
DatabaseError, DatabasePool,
|
DatabaseError, DatabasePool,
|
||||||
models::{self},
|
models::{self, SqliteTimestamp},
|
||||||
schema,
|
schema,
|
||||||
},
|
},
|
||||||
evm::{
|
evm::{
|
||||||
@@ -26,37 +26,11 @@ use crate::{
|
|||||||
|
|
||||||
pub use crate::evm::safe_signer;
|
pub use crate::evm::safe_signer;
|
||||||
|
|
||||||
/// Hashable structure for wallet integrity protection.
|
|
||||||
/// Binds the encrypted private key to the wallet address using HMAC.
|
|
||||||
pub struct EvmWalletIntegrity {
|
|
||||||
pub address: Vec<u8>, // 20-byte Ethereum address
|
|
||||||
pub aead_encrypted_id: i32, // Reference to encrypted key material
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hashable for EvmWalletIntegrity {
|
|
||||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
|
||||||
hasher.update(&self.address);
|
|
||||||
hasher.update(self.aead_encrypted_id.to_be_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Integrable for EvmWalletIntegrity {
|
|
||||||
const KIND: &'static str = "evm_wallet";
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SignTransactionError {
|
pub enum SignTransactionError {
|
||||||
#[error("Wallet not found")]
|
#[error("Wallet not found")]
|
||||||
WalletNotFound,
|
WalletNotFound,
|
||||||
|
|
||||||
#[error("Wallet integrity check failed")]
|
|
||||||
WalletIntegrityCheckFailed,
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"Decrypted key does not correspond to wallet address (CRITICAL: possible key substitution attack)"
|
|
||||||
)]
|
|
||||||
KeyAddressMismatch,
|
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
Database(#[from] DatabaseError),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
@@ -71,9 +45,6 @@ pub enum SignTransactionError {
|
|||||||
|
|
||||||
#[error("Policy error: {0}")]
|
#[error("Policy error: {0}")]
|
||||||
Vet(#[from] evm::VetError),
|
Vet(#[from] evm::VetError),
|
||||||
|
|
||||||
#[error("Integrity error: {0}")]
|
|
||||||
Integrity(#[from] integrity::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -117,7 +88,7 @@ impl EvmActor {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl EvmActor {
|
impl EvmActor {
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn generate(&mut self) -> Result<(Verified<i32>, Address), Error> {
|
pub async fn generate(&mut self) -> Result<(i32, Address), Error> {
|
||||||
let (mut key_cell, address) = safe_signer::generate(&mut self.rng);
|
let (mut key_cell, address) = safe_signer::generate(&mut self.rng);
|
||||||
|
|
||||||
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
||||||
@@ -129,7 +100,7 @@ impl EvmActor {
|
|||||||
.map_err(|_| Error::KeyholderSend)?;
|
.map_err(|_| Error::KeyholderSend)?;
|
||||||
|
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
let wallet_id: i32 = insert_into(schema::evm_wallet::table)
|
let wallet_id = insert_into(schema::evm_wallet::table)
|
||||||
.values(&models::NewEvmWallet {
|
.values(&models::NewEvmWallet {
|
||||||
address: address.as_slice().to_vec(),
|
address: address.as_slice().to_vec(),
|
||||||
aead_encrypted_id: aead_id,
|
aead_encrypted_id: aead_id,
|
||||||
@@ -139,17 +110,7 @@ impl EvmActor {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?;
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
// Sign integrity envelope to bind encrypted key to wallet address
|
Ok((wallet_id, address))
|
||||||
let wallet_integrity = EvmWalletIntegrity {
|
|
||||||
address: address.as_slice().to_vec(),
|
|
||||||
aead_encrypted_id: aead_id,
|
|
||||||
};
|
|
||||||
let verified_wallet_id =
|
|
||||||
integrity::sign_entity(&mut conn, &self.keyholder, &wallet_integrity, wallet_id)
|
|
||||||
.await?
|
|
||||||
.unqualify_origin();
|
|
||||||
|
|
||||||
Ok((verified_wallet_id, address))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
@@ -175,7 +136,7 @@ impl EvmActor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: SharedGrantSettings,
|
basic: SharedGrantSettings,
|
||||||
grant: SpecificGrant,
|
grant: SpecificGrant,
|
||||||
) -> Result<integrity::Verified<i32>, Error> {
|
) -> Result<i32, Error> {
|
||||||
match grant {
|
match grant {
|
||||||
SpecificGrant::EtherTransfer(settings) => self
|
SpecificGrant::EtherTransfer(settings) => self
|
||||||
.engine
|
.engine
|
||||||
@@ -197,7 +158,7 @@ impl EvmActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||||
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
// let keyholder = self.keyholder.clone();
|
// let keyholder = self.keyholder.clone();
|
||||||
|
|
||||||
@@ -246,23 +207,9 @@ impl EvmActor {
|
|||||||
.optional()
|
.optional()
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
|
|
||||||
// Verify wallet integrity envelope
|
|
||||||
let wallet = integrity::verify_entity(
|
|
||||||
&mut conn,
|
|
||||||
&self.keyholder,
|
|
||||||
EvmWalletIntegrity {
|
|
||||||
address: wallet.address.clone(),
|
|
||||||
aead_encrypted_id: wallet.aead_encrypted_id,
|
|
||||||
},
|
|
||||||
wallet.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| SignTransactionError::WalletIntegrityCheckFailed)?;
|
|
||||||
|
|
||||||
let wallet_access = schema::evm_wallet_access::table
|
let wallet_access = schema::evm_wallet_access::table
|
||||||
.select(models::EvmWalletAccess::as_select())
|
.select(models::EvmWalletAccess::as_select())
|
||||||
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.entity_id))
|
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.id))
|
||||||
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -295,23 +242,9 @@ impl EvmActor {
|
|||||||
.optional()
|
.optional()
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
|
|
||||||
// Verify wallet integrity envelope to ensure encrypted key is bound to address
|
|
||||||
let wallet = integrity::verify_entity(
|
|
||||||
&mut conn,
|
|
||||||
&self.keyholder,
|
|
||||||
EvmWalletIntegrity {
|
|
||||||
address: wallet.address.clone(),
|
|
||||||
aead_encrypted_id: wallet.aead_encrypted_id,
|
|
||||||
},
|
|
||||||
wallet.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| SignTransactionError::WalletIntegrityCheckFailed)?;
|
|
||||||
|
|
||||||
let wallet_access = schema::evm_wallet_access::table
|
let wallet_access = schema::evm_wallet_access::table
|
||||||
.select(models::EvmWalletAccess::as_select())
|
.select(models::EvmWalletAccess::as_select())
|
||||||
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.entity_id))
|
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.id))
|
||||||
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -330,12 +263,6 @@ impl EvmActor {
|
|||||||
|
|
||||||
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
|
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
|
||||||
|
|
||||||
// Verify that the decrypted key's derived address matches the wallet address
|
|
||||||
// This prevents an attacker from substituting one wallet's key with another's even if they compromised the DB
|
|
||||||
if signer.address() != wallet_address {
|
|
||||||
return Err(SignTransactionError::KeyAddressMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.engine
|
self.engine
|
||||||
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ use arbiter_proto::transport::Bi;
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::actors::user_agent::{
|
use crate::actors::user_agent::{
|
||||||
AuthPublicKey, UserAgentConnection,
|
UserAgentConnection,
|
||||||
auth::state::{AuthContext, AuthStateMachine},
|
auth::state::{AuthContext, AuthStateMachine},
|
||||||
};
|
};
|
||||||
|
use crate::crypto::authn;
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::*;
|
use state::*;
|
||||||
@@ -12,7 +13,7 @@ use state::*;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Inbound {
|
pub enum Inbound {
|
||||||
AuthChallengeRequest {
|
AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey,
|
pubkey: authn::PublicKey,
|
||||||
bootstrap_token: Option<String>,
|
bootstrap_token: Option<String>,
|
||||||
},
|
},
|
||||||
AuthChallengeSolution {
|
AuthChallengeSolution {
|
||||||
@@ -30,26 +31,17 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
#[track_caller]
|
fn internal(details: impl Into<String>) -> Self {
|
||||||
pub(super) fn internal(details: impl Into<String>, err: &impl std::fmt::Debug) -> Self {
|
Self::Internal {
|
||||||
let details = details.into();
|
details: details.into(),
|
||||||
let caller = std::panic::Location::caller();
|
}
|
||||||
error!(
|
|
||||||
caller_file = %caller.file(),
|
|
||||||
caller_line = caller.line(),
|
|
||||||
caller_column = caller.column(),
|
|
||||||
details = %details,
|
|
||||||
error = ?err,
|
|
||||||
"Internal error"
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::Internal { details }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<diesel::result::Error> for Error {
|
impl From<diesel::result::Error> for Error {
|
||||||
fn from(e: diesel::result::Error) -> Self {
|
fn from(e: diesel::result::Error) -> Self {
|
||||||
Self::internal("Database error", &e)
|
error!(?e, "Database error");
|
||||||
|
Self::internal("Database error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +72,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
|
|||||||
pub async fn authenticate<T>(
|
pub async fn authenticate<T>(
|
||||||
props: &mut UserAgentConnection,
|
props: &mut UserAgentConnection,
|
||||||
transport: T,
|
transport: T,
|
||||||
) -> Result<AuthPublicKey, Error>
|
) -> Result<authn::PublicKey, Error>
|
||||||
where
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + Send,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::{USERAGENT_CONTEXT, transport::Bi};
|
||||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::actor::ActorRef;
|
use kameo::actor::ActorRef;
|
||||||
@@ -9,24 +9,25 @@ use crate::{
|
|||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
keyholder::KeyHolder,
|
keyholder::KeyHolder,
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||||
},
|
},
|
||||||
|
crypto::authn,
|
||||||
crypto::integrity,
|
crypto::integrity,
|
||||||
db::{DatabasePool, schema::useragent_client},
|
db::{DatabasePool, schema::useragent_client},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ChallengeRequest {
|
pub struct ChallengeRequest {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: authn::PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BootstrapAuthRequest {
|
pub struct BootstrapAuthRequest {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: authn::PublicKey,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChallengeContext {
|
pub struct ChallengeContext {
|
||||||
pub challenge_nonce: i32,
|
pub challenge_nonce: i32,
|
||||||
pub key: AuthPublicKey,
|
pub key: authn::PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChallengeSolution {
|
pub struct ChallengeSolution {
|
||||||
@@ -38,26 +39,25 @@ smlang::statemachine!(
|
|||||||
custom_error: true,
|
custom_error: true,
|
||||||
transitions: {
|
transitions: {
|
||||||
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
|
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
|
||||||
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey),
|
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(authn::PublicKey),
|
||||||
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey),
|
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(authn::PublicKey),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Returns the current nonce, ready to use for the challenge nonce.
|
/// Returns the current nonce, ready to use for the challenge nonce.
|
||||||
async fn get_current_nonce_and_id(
|
async fn get_current_nonce_and_id(
|
||||||
db: &DatabasePool,
|
db: &DatabasePool,
|
||||||
key: &AuthPublicKey,
|
key: &authn::PublicKey,
|
||||||
) -> Result<(i32, i32), Error> {
|
) -> Result<(i32, i32), Error> {
|
||||||
let mut db_conn = db
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
.get()
|
error!(error = ?e, "Database pool error");
|
||||||
.await
|
Error::internal("Database unavailable")
|
||||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
})?;
|
||||||
db_conn
|
db_conn
|
||||||
.exclusive_transaction(|conn| {
|
.exclusive_transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
useragent_client::table
|
useragent_client::table
|
||||||
.filter(useragent_client::public_key.eq(key.to_stored_bytes()))
|
.filter(useragent_client::public_key.eq(key.to_bytes()))
|
||||||
.filter(useragent_client::key_type.eq(key.key_type()))
|
|
||||||
.select((useragent_client::id, useragent_client::nonce))
|
.select((useragent_client::id, useragent_client::nonce))
|
||||||
.first::<(i32, i32)>(conn)
|
.first::<(i32, i32)>(conn)
|
||||||
.await
|
.await
|
||||||
@@ -65,7 +65,10 @@ async fn get_current_nonce_and_id(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| Error::internal("Database operation failed", &e))?
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database error");
|
||||||
|
Error::internal("Database operation failed")
|
||||||
|
})?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error!(?key, "Public key not found in database");
|
error!(?key, "Public key not found in database");
|
||||||
Error::UnregisteredPublicKey
|
Error::UnregisteredPublicKey
|
||||||
@@ -75,16 +78,16 @@ async fn get_current_nonce_and_id(
|
|||||||
async fn verify_integrity(
|
async fn verify_integrity(
|
||||||
db: &DatabasePool,
|
db: &DatabasePool,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &AuthPublicKey,
|
pubkey: &authn::PublicKey,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut db_conn = db
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
.get()
|
error!(error = ?e, "Database pool error");
|
||||||
.await
|
Error::internal("Database unavailable")
|
||||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
})?;
|
||||||
|
|
||||||
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
||||||
|
|
||||||
let attestation_status = integrity::check_entity_attestation(
|
let _result = integrity::verify_entity(
|
||||||
&mut db_conn,
|
&mut db_conn,
|
||||||
keyholder,
|
keyholder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
@@ -94,39 +97,36 @@ async fn verify_integrity(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::internal("Integrity verification failed", &e))?;
|
.map_err(|e| {
|
||||||
|
error!(?e, "Integrity verification failed");
|
||||||
|
Error::internal("Integrity verification failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
use integrity::AttestationStatus as AS;
|
Ok(())
|
||||||
// SAFETY (policy): challenge auth must work in both vault states.
|
|
||||||
// While sealed, integrity checks can only report `Unavailable` because key material is not
|
|
||||||
// accessible. While unsealed, the same check can report `Attested`.
|
|
||||||
// This path intentionally accepts both outcomes to keep challenge auth available across state
|
|
||||||
// transitions; stricter verification is enforced in sensitive post-auth flows.
|
|
||||||
match attestation_status {
|
|
||||||
AS::Attested | AS::Unavailable => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_nonce(
|
async fn create_nonce(
|
||||||
db: &DatabasePool,
|
db: &DatabasePool,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &AuthPublicKey,
|
pubkey: &authn::PublicKey,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let mut db_conn = db
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
.get()
|
error!(error = ?e, "Database pool error");
|
||||||
.await
|
Error::internal("Database unavailable")
|
||||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
})?;
|
||||||
let new_nonce = db_conn
|
let new_nonce = db_conn
|
||||||
.exclusive_transaction(|conn| {
|
.exclusive_transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
|
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
|
||||||
.filter(useragent_client::public_key.eq(pubkey.to_stored_bytes()))
|
.filter(useragent_client::public_key.eq(pubkey.to_bytes()))
|
||||||
.filter(useragent_client::key_type.eq(pubkey.key_type()))
|
|
||||||
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
|
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
|
||||||
.returning((useragent_client::id, useragent_client::nonce))
|
.returning((useragent_client::id, useragent_client::nonce))
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database error");
|
||||||
|
Error::internal("Database operation failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
integrity::sign_entity(
|
integrity::sign_entity(
|
||||||
conn,
|
conn,
|
||||||
@@ -138,8 +138,10 @@ async fn create_nonce(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::internal("Database error", &e))?
|
.map_err(|e| {
|
||||||
.drop_verification_provenance();
|
error!(?e, "Integrity signature update failed");
|
||||||
|
Error::internal("Database error")
|
||||||
|
})?;
|
||||||
|
|
||||||
Result::<_, Error>::Ok(new_nonce)
|
Result::<_, Error>::Ok(new_nonce)
|
||||||
})
|
})
|
||||||
@@ -151,14 +153,13 @@ async fn create_nonce(
|
|||||||
async fn register_key(
|
async fn register_key(
|
||||||
db: &DatabasePool,
|
db: &DatabasePool,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &AuthPublicKey,
|
pubkey: &authn::PublicKey,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
let pubkey_bytes = pubkey.to_bytes();
|
||||||
let key_type = pubkey.key_type();
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
let mut conn = db
|
error!(error = ?e, "Database pool error");
|
||||||
.get()
|
Error::internal("Database unavailable")
|
||||||
.await
|
})?;
|
||||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
|
||||||
|
|
||||||
conn.transaction(|conn| {
|
conn.transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -168,37 +169,26 @@ async fn register_key(
|
|||||||
.values((
|
.values((
|
||||||
useragent_client::public_key.eq(pubkey_bytes),
|
useragent_client::public_key.eq(pubkey_bytes),
|
||||||
useragent_client::nonce.eq(NONCE_START),
|
useragent_client::nonce.eq(NONCE_START),
|
||||||
useragent_client::key_type.eq(key_type),
|
|
||||||
))
|
))
|
||||||
.returning(useragent_client::id)
|
.returning(useragent_client::id)
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database error");
|
||||||
|
Error::internal("Database operation failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Err(e) = integrity::sign_entity(
|
let entity = UserAgentCredentials {
|
||||||
conn,
|
|
||||||
keyholder,
|
|
||||||
&UserAgentCredentials {
|
|
||||||
pubkey: pubkey.clone(),
|
pubkey: pubkey.clone(),
|
||||||
nonce: NONCE_START,
|
nonce: NONCE_START,
|
||||||
},
|
};
|
||||||
id,
|
|
||||||
)
|
integrity::sign_entity(conn, &keyholder, &entity, id)
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|e| {
|
||||||
match e {
|
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
|
||||||
integrity::Error::Keyholder(
|
Error::internal("Failed to register public key")
|
||||||
crate::actors::keyholder::Error::NotBootstrapped,
|
})?;
|
||||||
) => {
|
|
||||||
// IMPORTANT: bootstrap-token auth must work before the vault has a root key.
|
|
||||||
// We intentionally allow creating the DB row first and backfill envelopes
|
|
||||||
// after bootstrap/unseal to keep the bootstrap flow possible.
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
return Err(Error::internal("Failed to register public key", &other));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result::<_, Error>::Ok(())
|
Result::<_, Error>::Ok(())
|
||||||
})
|
})
|
||||||
@@ -252,7 +242,7 @@ where
|
|||||||
async fn verify_bootstrap_token(
|
async fn verify_bootstrap_token(
|
||||||
&mut self,
|
&mut self,
|
||||||
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
|
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
|
||||||
) -> Result<AuthPublicKey, Self::Error> {
|
) -> Result<authn::PublicKey, Self::Error> {
|
||||||
let token_ok: bool = self
|
let token_ok: bool = self
|
||||||
.conn
|
.conn
|
||||||
.actors
|
.actors
|
||||||
@@ -261,7 +251,10 @@ where
|
|||||||
token: token.clone(),
|
token: token.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::internal("Failed to consume bootstrap token", &e))?;
|
.map_err(|e| {
|
||||||
|
error!(?e, "Failed to consume bootstrap token");
|
||||||
|
Error::internal("Failed to consume bootstrap token")
|
||||||
|
})?;
|
||||||
|
|
||||||
if !token_ok {
|
if !token_ok {
|
||||||
error!("Invalid bootstrap token provided");
|
error!("Invalid bootstrap token provided");
|
||||||
@@ -297,35 +290,13 @@ where
|
|||||||
key,
|
key,
|
||||||
}: &ChallengeContext,
|
}: &ChallengeContext,
|
||||||
ChallengeSolution { solution }: ChallengeSolution,
|
ChallengeSolution { solution }: ChallengeSolution,
|
||||||
) -> Result<AuthPublicKey, Self::Error> {
|
) -> Result<authn::PublicKey, Self::Error> {
|
||||||
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
|
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| {
|
||||||
|
error!("Failed to decode signature in challenge solution");
|
||||||
|
Error::InvalidChallengeSolution
|
||||||
|
})?;
|
||||||
|
|
||||||
let valid = match key {
|
let valid = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature);
|
||||||
AuthPublicKey::Ed25519(vk) => {
|
|
||||||
let sig = solution.as_slice().try_into().map_err(|_| {
|
|
||||||
error!(?solution, "Invalid Ed25519 signature length");
|
|
||||||
Error::InvalidChallengeSolution
|
|
||||||
})?;
|
|
||||||
vk.verify_strict(&formatted, &sig).is_ok()
|
|
||||||
}
|
|
||||||
AuthPublicKey::EcdsaSecp256k1(vk) => {
|
|
||||||
use k256::ecdsa::signature::Verifier as _;
|
|
||||||
let sig = k256::ecdsa::Signature::try_from(solution.as_slice()).map_err(|_| {
|
|
||||||
error!(?solution, "Invalid ECDSA signature bytes");
|
|
||||||
Error::InvalidChallengeSolution
|
|
||||||
})?;
|
|
||||||
vk.verify(&formatted, &sig).is_ok()
|
|
||||||
}
|
|
||||||
AuthPublicKey::Rsa(pk) => {
|
|
||||||
use rsa::signature::Verifier as _;
|
|
||||||
let verifying_key = rsa::pss::VerifyingKey::<sha2::Sha256>::new(pk.clone());
|
|
||||||
let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| {
|
|
||||||
error!(?solution, "Invalid RSA signature bytes");
|
|
||||||
Error::InvalidChallengeSolution
|
|
||||||
})?;
|
|
||||||
verifying_key.verify(&formatted, &sig).is_ok()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match valid {
|
match valid {
|
||||||
true => {
|
true => {
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::ClientProfile},
|
actors::{GlobalActors, client::ClientProfile},
|
||||||
|
crypto::authn,
|
||||||
crypto::integrity::Integrable,
|
crypto::integrity::Integrable,
|
||||||
db::{self, models::KeyType},
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum AuthPublicKey {
|
|
||||||
Ed25519(ed25519_dalek::VerifyingKey),
|
|
||||||
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
|
|
||||||
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
|
|
||||||
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
|
|
||||||
Rsa(rsa::RsaPublicKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UserAgentCredentials {
|
pub struct UserAgentCredentials {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: authn::PublicKey,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,67 +15,11 @@ impl Integrable for UserAgentCredentials {
|
|||||||
const KIND: &'static str = "useragent_credentials";
|
const KIND: &'static str = "useragent_credentials";
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthPublicKey {
|
|
||||||
/// Canonical bytes stored in DB and echoed back in the challenge.
|
|
||||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
|
||||||
pub fn to_stored_bytes(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
|
|
||||||
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
|
|
||||||
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
|
|
||||||
AuthPublicKey::Rsa(k) => {
|
|
||||||
use rsa::pkcs8::EncodePublicKey as _;
|
|
||||||
#[allow(clippy::expect_used)]
|
|
||||||
k.to_public_key_der()
|
|
||||||
.expect("rsa SPKI encoding is infallible")
|
|
||||||
.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn key_type(&self) -> KeyType {
|
|
||||||
match self {
|
|
||||||
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
|
|
||||||
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
|
|
||||||
AuthPublicKey::Rsa(_) => KeyType::Rsa,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<(KeyType, Vec<u8>)> for AuthPublicKey {
|
|
||||||
type Error = &'static str;
|
|
||||||
|
|
||||||
fn try_from(value: (KeyType, Vec<u8>)) -> Result<Self, Self::Error> {
|
|
||||||
let (key_type, bytes) = value;
|
|
||||||
match key_type {
|
|
||||||
KeyType::Ed25519 => {
|
|
||||||
let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?;
|
|
||||||
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
|
|
||||||
.map_err(|_e| "invalid Ed25519 key")?;
|
|
||||||
Ok(AuthPublicKey::Ed25519(key))
|
|
||||||
}
|
|
||||||
KeyType::EcdsaSecp256k1 => {
|
|
||||||
let point =
|
|
||||||
k256::EncodedPoint::from_bytes(&bytes).map_err(|_e| "invalid ECDSA key")?;
|
|
||||||
let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point)
|
|
||||||
.map_err(|_e| "invalid ECDSA key")?;
|
|
||||||
Ok(AuthPublicKey::EcdsaSecp256k1(key))
|
|
||||||
}
|
|
||||||
KeyType::Rsa => {
|
|
||||||
use rsa::pkcs8::DecodePublicKey as _;
|
|
||||||
let key = rsa::RsaPublicKey::from_public_key_der(&bytes)
|
|
||||||
.map_err(|_e| "invalid RSA key")?;
|
|
||||||
Ok(AuthPublicKey::Rsa(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Messages, sent by user agent to connection client without having a request
|
// Messages, sent by user agent to connection client without having a request
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutOfBand {
|
pub enum OutOfBand {
|
||||||
ClientConnectionRequest { profile: ClientProfile },
|
ClientConnectionRequest { profile: ClientProfile },
|
||||||
ClientConnectionCancel { pubkey: ed25519_dalek::VerifyingKey },
|
ClientConnectionCancel { pubkey: authn::PublicKey },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserAgentConnection {
|
pub struct UserAgentConnection {
|
||||||
@@ -106,9 +41,9 @@ pub use session::UserAgentSession;
|
|||||||
|
|
||||||
use crate::crypto::integrity::hashing::Hashable;
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
impl Hashable for AuthPublicKey {
|
impl Hashable for authn::PublicKey {
|
||||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(self.to_stored_bytes());
|
hasher.update(self.to_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::{borrow::Cow, collections::HashMap};
|
|||||||
|
|
||||||
use arbiter_proto::transport::Sender;
|
use arbiter_proto::transport::Sender;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use ed25519_dalek::VerifyingKey;
|
|
||||||
use kameo::{Actor, actor::ActorRef, messages};
|
use kameo::{Actor, actor::ActorRef, messages};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -12,6 +11,7 @@ use crate::actors::{
|
|||||||
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
|
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
|
||||||
user_agent::{OutOfBand, UserAgentConnection},
|
user_agent::{OutOfBand, UserAgentConnection},
|
||||||
};
|
};
|
||||||
|
use crate::crypto::authn;
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
||||||
@@ -47,6 +47,7 @@ impl Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct PendingClientApproval {
|
pub struct PendingClientApproval {
|
||||||
|
pubkey: authn::PublicKey,
|
||||||
controller: ActorRef<ClientApprovalController>,
|
controller: ActorRef<ClientApprovalController>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ pub struct UserAgentSession {
|
|||||||
state: UserAgentStateMachine<DummyContext>,
|
state: UserAgentStateMachine<DummyContext>,
|
||||||
sender: Box<dyn Sender<OutOfBand>>,
|
sender: Box<dyn Sender<OutOfBand>>,
|
||||||
|
|
||||||
pending_client_approvals: HashMap<VerifyingKey, PendingClientApproval>,
|
pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
@@ -119,7 +120,13 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.pending_client_approvals
|
self.pending_client_approvals
|
||||||
.insert(client.pubkey, PendingClientApproval { controller });
|
.insert(
|
||||||
|
client.pubkey.to_bytes(),
|
||||||
|
PendingClientApproval {
|
||||||
|
pubkey: client.pubkey,
|
||||||
|
controller,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,14 +165,18 @@ impl Actor for UserAgentSession {
|
|||||||
let cancelled_pubkey = self
|
let cancelled_pubkey = self
|
||||||
.pending_client_approvals
|
.pending_client_approvals
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(k, v)| (v.controller.id() == id).then_some(*k));
|
.find_map(|(k, v)| (v.controller.id() == id).then_some(k.clone()));
|
||||||
|
|
||||||
if let Some(pubkey) = cancelled_pubkey {
|
if let Some(pubkey_bytes) = cancelled_pubkey {
|
||||||
self.pending_client_approvals.remove(&pubkey);
|
let Some(approval) = self.pending_client_approvals.remove(&pubkey_bytes) else {
|
||||||
|
return Ok(std::ops::ControlFlow::Continue(()));
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.sender
|
.sender
|
||||||
.send(OutOfBand::ClientConnectionCancel { pubkey })
|
.send(OutOfBand::ClientConnectionCancel {
|
||||||
|
pubkey: approval.pubkey,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
error!(
|
error!(
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ use kameo::prelude::Context;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
||||||
use crate::actors::keyholder::KeyHolderState;
|
use crate::actors::keyholder::KeyHolderState;
|
||||||
use crate::actors::user_agent::session::Error;
|
use crate::actors::user_agent::session::Error;
|
||||||
|
use crate::crypto::authn;
|
||||||
use crate::db::models::{
|
use crate::db::models::{
|
||||||
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
||||||
};
|
};
|
||||||
use crate::evm::policies::{Grant, SpecificGrant};
|
use crate::evm::policies::{Grant, SpecificGrant};
|
||||||
use crate::safe_cell::SafeCell;
|
use crate::safe_cell::SafeCell;
|
||||||
use crate::{
|
|
||||||
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
|
||||||
crypto::integrity::{self, Verified},
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
evm::{
|
evm::{
|
||||||
@@ -32,67 +30,11 @@ use crate::{
|
|||||||
UserAgentSession,
|
UserAgentSession,
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
},
|
},
|
||||||
user_agent::{AuthPublicKey, UserAgentCredentials},
|
|
||||||
},
|
},
|
||||||
db::schema::useragent_client,
|
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn is_vault_sealed_from_evm<M>(err: &SendError<M, crate::actors::evm::Error>) -> bool {
|
|
||||||
matches!(
|
|
||||||
err,
|
|
||||||
SendError::HandlerError(crate::actors::evm::Error::Keyholder(
|
|
||||||
keyholder::Error::NotBootstrapped
|
|
||||||
)) | SendError::HandlerError(crate::actors::evm::Error::Integrity(
|
|
||||||
crate::crypto::integrity::Error::Keyholder(keyholder::Error::NotBootstrapped)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn backfill_useragent_integrity(&self) -> Result<(), Error> {
|
|
||||||
let mut conn = self.props.db.get().await?;
|
|
||||||
let keyholder = self.props.actors.key_holder.clone();
|
|
||||||
|
|
||||||
conn.transaction(|conn| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let rows: Vec<(i32, i32, Vec<u8>, crate::db::models::KeyType)> =
|
|
||||||
useragent_client::table
|
|
||||||
.select((
|
|
||||||
useragent_client::id,
|
|
||||||
useragent_client::nonce,
|
|
||||||
useragent_client::public_key,
|
|
||||||
useragent_client::key_type,
|
|
||||||
))
|
|
||||||
.load(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for (id, nonce, public_key, key_type) in rows {
|
|
||||||
let pubkey = AuthPublicKey::try_from((key_type, public_key)).map_err(|e| {
|
|
||||||
Error::internal(format!("Invalid user-agent key in db: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
integrity::sign_entity(
|
|
||||||
conn,
|
|
||||||
&keyholder,
|
|
||||||
&UserAgentCredentials { pubkey, nonce },
|
|
||||||
id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
|
||||||
})?
|
|
||||||
.drop_verification_provenance();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result::<_, Error>::Ok(())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||||
error!("Received encrypted key in invalid state");
|
error!("Received encrypted key in invalid state");
|
||||||
@@ -250,7 +192,6 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.backfill_useragent_integrity().await?;
|
|
||||||
info!("Successfully unsealed key with client-provided key");
|
info!("Successfully unsealed key with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -312,7 +253,6 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.backfill_useragent_integrity().await?;
|
|
||||||
info!("Successfully bootstrapped vault with client-provided key");
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -358,9 +298,7 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_evm_wallet_create(
|
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> {
|
||||||
&mut self,
|
|
||||||
) -> Result<(Verified<i32>, Address), Error> {
|
|
||||||
match self.props.actors.evm.ask(Generate {}).await {
|
match self.props.actors.evm.ask(Generate {}).await {
|
||||||
Ok(address) => Ok(address),
|
Ok(address) => Ok(address),
|
||||||
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
|
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
|
||||||
@@ -388,15 +326,12 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_grant_list(
|
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||||
&mut self,
|
|
||||||
) -> Result<Vec<Grant<SpecificGrant>>, GrantMutationError> {
|
|
||||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||||
Ok(grants) => Ok(grants),
|
Ok(grants) => Ok(grants),
|
||||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant list failed");
|
error!(?err, "EVM grant list failed");
|
||||||
Err(GrantMutationError::Internal)
|
Err(Error::internal("Failed to list EVM grants"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +341,7 @@ impl UserAgentSession {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: crate::evm::policies::SharedGrantSettings,
|
basic: crate::evm::policies::SharedGrantSettings,
|
||||||
grant: crate::evm::policies::SpecificGrant,
|
grant: crate::evm::policies::SpecificGrant,
|
||||||
) -> Result<Verified<i32>, GrantMutationError> {
|
) -> Result<i32, GrantMutationError> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
@@ -415,7 +350,6 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(grant_id) => Ok(grant_id),
|
Ok(grant_id) => Ok(grant_id),
|
||||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant create failed");
|
error!(?err, "EVM grant create failed");
|
||||||
Err(GrantMutationError::Internal)
|
Err(GrantMutationError::Internal)
|
||||||
@@ -432,13 +366,10 @@ impl UserAgentSession {
|
|||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
.evm
|
.evm
|
||||||
.ask(UseragentDeleteGrant {
|
.ask(UseragentDeleteGrant { grant_id })
|
||||||
_grant_id: grant_id,
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant delete failed");
|
error!(?err, "EVM grant delete failed");
|
||||||
Err(GrantMutationError::Internal)
|
Err(GrantMutationError::Internal)
|
||||||
@@ -543,10 +474,13 @@ impl UserAgentSession {
|
|||||||
pub(crate) async fn handle_new_client_approve(
|
pub(crate) async fn handle_new_client_approve(
|
||||||
&mut self,
|
&mut self,
|
||||||
approved: bool,
|
approved: bool,
|
||||||
pubkey: ed25519_dalek::VerifyingKey,
|
pubkey: authn::PublicKey,
|
||||||
ctx: &mut Context<Self, Result<(), Error>>,
|
ctx: &mut Context<Self, Result<(), Error>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let pending_approval = match self.pending_client_approvals.remove(&pubkey) {
|
let pending_approval = match self
|
||||||
|
.pending_client_approvals
|
||||||
|
.remove(&pubkey.to_bytes())
|
||||||
|
{
|
||||||
Some(approval) => approval,
|
Some(approval) => approval,
|
||||||
None => {
|
None => {
|
||||||
error!("Received client connection response for unknown client");
|
error!("Received client connection response for unknown client");
|
||||||
|
|||||||
3
server/crates/arbiter-server/src/crypto/authn/mod.rs
Normal file
3
server/crates/arbiter-server/src/crypto/authn/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod v1;
|
||||||
|
|
||||||
|
pub use v1::*;
|
||||||
110
server/crates/arbiter-server/src/crypto/authn/v1/mod.rs
Normal file
110
server/crates/arbiter-server/src/crypto/authn/v1/mod.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
use ml_dsa::{
|
||||||
|
EncodedVerifyingKey, MlDsa87, Signature as MlDsaSignature, VerifyingKey as MlDsaVerifyingKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type KeyParams = MlDsa87;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PublicKey(Box<MlDsaVerifyingKey<KeyParams>>);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Signature(Box<MlDsaSignature<KeyParams>>);
|
||||||
|
|
||||||
|
impl PublicKey {
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
self.0.encode().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
|
||||||
|
self.0.verify_with_context(&format_challenge(nonce, self), context, &signature.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Signature {
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
self.0.encode().to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MlDsaVerifyingKey<KeyParams>> for PublicKey {
|
||||||
|
fn from(value: MlDsaVerifyingKey<KeyParams>) -> Self {
|
||||||
|
Self(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MlDsaSignature<KeyParams>> for Signature {
|
||||||
|
fn from(value: MlDsaSignature<KeyParams>) -> Self {
|
||||||
|
Self(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&'_ [u8]> for PublicKey {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
let encoded = EncodedVerifyingKey::<KeyParams>::try_from(value).map_err(|_| ())?;
|
||||||
|
Ok(Self(Box::new(MlDsaVerifyingKey::decode(&encoded))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&'_ [u8]> for Signature {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
MlDsaSignature::try_from(value)
|
||||||
|
.map(|sig| Self(Box::new(sig)))
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_challenge(nonce: i32, pubkey: &PublicKey) -> Vec<u8> {
|
||||||
|
arbiter_proto::format_challenge(nonce, &pubkey.to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
|
||||||
|
|
||||||
|
use super::{PublicKey, Signature};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_key_round_trip_decodes() {
|
||||||
|
let key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
|
let encoded = PublicKey::from(key.verifying_key()).to_bytes();
|
||||||
|
|
||||||
|
let decoded = PublicKey::try_from(encoded.as_slice()).expect("public key should decode");
|
||||||
|
|
||||||
|
assert_eq!(decoded, PublicKey::from(key.verifying_key()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signature_round_trip_decodes() {
|
||||||
|
let key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
|
let challenge = b"challenge";
|
||||||
|
let signature = key
|
||||||
|
.signing_key()
|
||||||
|
.sign_deterministic(challenge, arbiter_proto::CLIENT_CONTEXT)
|
||||||
|
.expect("signature should be created");
|
||||||
|
|
||||||
|
let decoded = Signature::try_from(signature.encode().to_vec().as_slice()).expect("signature should decode");
|
||||||
|
|
||||||
|
assert_eq!(decoded, Signature::from(signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_verification_uses_context_and_canonical_key_bytes() {
|
||||||
|
let key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
|
let public_key = PublicKey::from(key.verifying_key());
|
||||||
|
let nonce = 17;
|
||||||
|
let challenge =
|
||||||
|
arbiter_proto::format_challenge(nonce, &public_key.to_bytes());
|
||||||
|
let signature = key
|
||||||
|
.signing_key()
|
||||||
|
.sign_deterministic(&challenge, arbiter_proto::CLIENT_CONTEXT)
|
||||||
|
.expect("signature should be created")
|
||||||
|
.into();
|
||||||
|
|
||||||
|
assert!(public_key.verify(nonce, arbiter_proto::CLIENT_CONTEXT, &signature));
|
||||||
|
assert!(!public_key.verify(nonce, arbiter_proto::USERAGENT_CONTEXT, &signature));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::actors::keyholder;
|
use crate::{
|
||||||
use hmac::Hmac;
|
actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _,
|
||||||
|
};
|
||||||
|
use hmac::{Hmac, Mac as _};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::future::Future;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
@@ -11,24 +10,16 @@ use kameo::{actor::ActorRef, error::SendError};
|
|||||||
use sha2::Digest as _;
|
use sha2::Digest as _;
|
||||||
|
|
||||||
pub mod hashing;
|
pub mod hashing;
|
||||||
pub mod verified;
|
|
||||||
use self::hashing::Hashable;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{IntegrityEnvelope as IntegrityEnvelopeRow, NewIntegrityEnvelope},
|
models::{IntegrityEnvelope, NewIntegrityEnvelope},
|
||||||
schema::integrity_envelope,
|
schema::integrity_envelope,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
|
||||||
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
|
||||||
|
|
||||||
pub type HmacSha256 = Hmac<Sha256>;
|
|
||||||
pub use self::verified::{Nested, Root, VerificationOrigin, Verified};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
@@ -57,90 +48,71 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[must_use]
|
|
||||||
pub enum AttestationStatus {
|
pub enum AttestationStatus {
|
||||||
Attested,
|
Attested,
|
||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
||||||
|
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||||
|
|
||||||
|
pub type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
pub trait Integrable: Hashable {
|
pub trait Integrable: Hashable {
|
||||||
const KIND: &'static str;
|
const KIND: &'static str;
|
||||||
const VERSION: i32 = 1;
|
const VERSION: i32 = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Integrable> Integrable for &T {
|
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||||
const KIND: &'static str = T::KIND;
|
let mut hasher = Sha256::new();
|
||||||
const VERSION: i32 = T::VERSION;
|
payload.hash(&mut hasher);
|
||||||
|
hasher.finalize().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||||
pub struct EntityId(Vec<u8>);
|
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for EntityId {
|
fn build_mac_input(
|
||||||
type Target = [u8];
|
entity_kind: &str,
|
||||||
|
entity_id: &[u8],
|
||||||
|
payload_version: i32,
|
||||||
|
payload_hash: &[u8; 32],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(8 + entity_kind.len() + entity_id.len() + 32);
|
||||||
|
push_len_prefixed(&mut out, entity_kind.as_bytes());
|
||||||
|
push_len_prefixed(&mut out, entity_id);
|
||||||
|
out.extend_from_slice(&payload_version.to_be_bytes());
|
||||||
|
out.extend_from_slice(payload_hash);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
pub trait IntoId {
|
||||||
&self.0
|
fn into_id(self) -> Vec<u8>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoId for i32 {
|
||||||
|
fn into_id(self) -> Vec<u8> {
|
||||||
|
self.to_be_bytes().to_vec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<i32> for EntityId {
|
impl IntoId for &'_ [u8] {
|
||||||
fn from(value: i32) -> Self {
|
fn into_id(self) -> Vec<u8> {
|
||||||
Self(value.to_be_bytes().to_vec())
|
self.to_vec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'_ [u8]> for EntityId {
|
pub async fn sign_entity<E: Integrable>(
|
||||||
fn from(bytes: &'_ [u8]) -> Self {
|
|
||||||
Self(bytes.to_vec())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn lookup_verified<E, Id, C, F, Fut>(
|
|
||||||
conn: &mut C,
|
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
|
||||||
entity_id: Id,
|
|
||||||
load: F,
|
|
||||||
) -> Result<Verified<Entity<E, Id>, Nested<E>>, Error>
|
|
||||||
where
|
|
||||||
C: AsyncConnection<Backend = Sqlite>,
|
|
||||||
E: Integrable,
|
|
||||||
Id: Into<EntityId> + Clone,
|
|
||||||
F: FnOnce(&mut C) -> Fut,
|
|
||||||
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
|
||||||
{
|
|
||||||
let entity = load(conn).await?;
|
|
||||||
verify_entity(conn, keyholder, entity, entity_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
|
||||||
conn: &mut C,
|
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
|
||||||
load: F,
|
|
||||||
) -> Result<Verified<Entity<E, Id>, Nested<E>>, Error>
|
|
||||||
where
|
|
||||||
C: AsyncConnection<Backend = Sqlite> + Send,
|
|
||||||
E: Integrable,
|
|
||||||
Id: Into<EntityId> + Clone,
|
|
||||||
F: for<'a> FnOnce(
|
|
||||||
&'a mut C,
|
|
||||||
) -> Pin<
|
|
||||||
Box<dyn Future<Output = Result<(Id, E), db::DatabaseError>> + Send + 'a>,
|
|
||||||
>,
|
|
||||||
{
|
|
||||||
let (entity_id, entity) = load(conn).await?;
|
|
||||||
verify_entity(conn, keyholder, entity, entity_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
as_entity_id: Id,
|
entity_id: impl IntoId,
|
||||||
) -> Result<Verified<Id, Nested<E>>, Error> {
|
) -> Result<(), Error> {
|
||||||
let payload_hash = payload_hash(entity);
|
let payload_hash = payload_hash(&entity);
|
||||||
|
|
||||||
let entity_id = as_entity_id.clone().into();
|
let entity_id = entity_id.into_id();
|
||||||
|
|
||||||
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
||||||
|
|
||||||
@@ -155,7 +127,7 @@ pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
|||||||
insert_into(integrity_envelope::table)
|
insert_into(integrity_envelope::table)
|
||||||
.values(NewIntegrityEnvelope {
|
.values(NewIntegrityEnvelope {
|
||||||
entity_kind: E::KIND.to_owned(),
|
entity_kind: E::KIND.to_owned(),
|
||||||
entity_id: entity_id.to_vec(),
|
entity_id: entity_id,
|
||||||
payload_version: E::VERSION,
|
payload_version: E::VERSION,
|
||||||
key_version,
|
key_version,
|
||||||
mac: mac.to_vec(),
|
mac: mac.to_vec(),
|
||||||
@@ -174,19 +146,19 @@ pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
|||||||
.await
|
.await
|
||||||
.map_err(db::DatabaseError::from)?;
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(Verified::<Id, Nested<E>>::new(as_entity_id))
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_entity_attestation<E: Integrable>(
|
pub async fn verify_entity<E: Integrable>(
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
entity_id: impl Into<EntityId>,
|
entity_id: impl IntoId,
|
||||||
) -> Result<AttestationStatus, Error> {
|
) -> Result<AttestationStatus, Error> {
|
||||||
let entity_id = entity_id.into();
|
let entity_id = entity_id.into_id();
|
||||||
let envelope: IntegrityEnvelopeRow = integrity_envelope::table
|
let envelope: IntegrityEnvelope = integrity_envelope::table
|
||||||
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||||
.filter(integrity_envelope::entity_id.eq(&*entity_id))
|
.filter(integrity_envelope::entity_id.eq(&entity_id))
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| match err {
|
.map_err(|err| match err {
|
||||||
@@ -204,7 +176,7 @@ pub async fn check_entity_attestation<E: Integrable>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload_hash = payload_hash(entity);
|
let payload_hash = payload_hash(&entity);
|
||||||
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
||||||
|
|
||||||
let result = keyholder
|
let result = keyholder
|
||||||
@@ -227,93 +199,139 @@ pub async fn check_entity_attestation<E: Integrable>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, crate::VerifiedFields!)]
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct Entity<E, Id> {
|
|
||||||
pub entity: E,
|
|
||||||
pub entity_id: Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E, Id> Deref for Entity<E, Id> {
|
|
||||||
type Target = E;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.entity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn verify_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
|
||||||
entity: E,
|
|
||||||
entity_id: Id,
|
|
||||||
) -> Result<Verified<Entity<E, Id>, Nested<E>>, Error> {
|
|
||||||
match check_entity_attestation(conn, keyholder, &entity, entity_id.clone()).await? {
|
|
||||||
AttestationStatus::Attested => Ok(Verified::<Entity<E, Id>, Nested<E>>::new(Entity {
|
|
||||||
entity,
|
|
||||||
entity_id,
|
|
||||||
})),
|
|
||||||
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn verify_entity_ref<'e, E: Integrable, Id: Into<EntityId> + Clone>(
|
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
|
||||||
entity: &'e E,
|
|
||||||
entity_id: Id,
|
|
||||||
) -> Result<Verified<Entity<&'e E, Id>, Nested<E>>, Error> {
|
|
||||||
match check_entity_attestation(conn, keyholder, entity, entity_id.clone()).await? {
|
|
||||||
AttestationStatus::Attested => Ok(Verified::<Entity<&'e E, Id>, Nested<E>>::new(Entity {
|
|
||||||
entity,
|
|
||||||
entity_id,
|
|
||||||
})),
|
|
||||||
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_envelope<E: Integrable>(
|
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
|
||||||
entity_id: impl Into<EntityId>,
|
|
||||||
) -> Result<usize, Error> {
|
|
||||||
let entity_id = entity_id.into();
|
|
||||||
|
|
||||||
let affected = diesel::delete(
|
|
||||||
integrity_envelope::table
|
|
||||||
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
|
||||||
.filter(integrity_envelope::entity_id.eq(&*entity_id)),
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.await
|
|
||||||
.map_err(db::DatabaseError::from)?;
|
|
||||||
|
|
||||||
Ok(affected)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
payload.hash(&mut hasher);
|
|
||||||
hasher.finalize().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mac_input(
|
|
||||||
entity_kind: &str,
|
|
||||||
entity_id: &[u8],
|
|
||||||
payload_version: i32,
|
|
||||||
payload_hash: &[u8; 32],
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(8 + entity_kind.len() + entity_id.len() + 32);
|
|
||||||
push_len_prefixed(&mut out, entity_kind.as_bytes());
|
|
||||||
push_len_prefixed(&mut out, entity_id);
|
|
||||||
out.extend_from_slice(&payload_version.to_be_bytes());
|
|
||||||
out.extend_from_slice(payload_hash);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
|
||||||
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
|
||||||
out.extend_from_slice(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests {
|
||||||
|
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::keyholder::{Bootstrap, KeyHolder},
|
||||||
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Error, Integrable, sign_entity, verify_entity};
|
||||||
|
use super::{hashing::Hashable, payload_hash};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct DummyEntity {
|
||||||
|
payload_version: i32,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for DummyEntity {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
self.payload_version.hash(hasher);
|
||||||
|
self.payload.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Integrable for DummyEntity {
|
||||||
|
const KIND: &'static str = "dummy_entity";
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
||||||
|
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
actor
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
actor
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sign_writes_envelope_and_verify_passes() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-7";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let count: i64 = schema::integrity_envelope::table
|
||||||
|
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||||
|
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(count, 1, "envelope row must be created exactly once");
|
||||||
|
verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tampered_mac_fails_verification() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-11";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
diesel::update(schema::integrity_envelope::table)
|
||||||
|
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||||
|
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||||
|
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn changed_payload_fails_verification() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-21";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tampered = DummyEntity {
|
||||||
|
payload: b"payload-v1-but-tampered".to_vec(),
|
||||||
|
..entity
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = verify_entity(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ impl<T: Hashable> Hashable for Option<T> {
|
|||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
match self {
|
match self {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
hasher.update([1]);
|
hasher.update(&[1]);
|
||||||
value.hash(hasher);
|
value.hash(hasher);
|
||||||
}
|
}
|
||||||
None => hasher.update([0]),
|
None => hasher.update(&[0]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,12 +96,12 @@ impl Hashable for alloy::primitives::U256 {
|
|||||||
|
|
||||||
impl Hashable for chrono::Duration {
|
impl Hashable for chrono::Duration {
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(self.num_seconds().to_be_bytes());
|
hasher.update(&self.num_seconds().to_be_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hashable for chrono::DateTime<chrono::Utc> {
|
impl Hashable for chrono::DateTime<chrono::Utc> {
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(self.timestamp_millis().to_be_bytes());
|
hasher.update(&self.timestamp_millis().to_be_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,298 +0,0 @@
|
|||||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
|
||||||
use diesel_async::RunQueryDsl;
|
|
||||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
|
||||||
use sha2::Digest;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
actors::keyholder::{Bootstrap, KeyHolder},
|
|
||||||
db::{self, schema},
|
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::hashing::Hashable;
|
|
||||||
use super::{
|
|
||||||
Error, Integrable, check_entity_attestation, lookup_verified, lookup_verified_from_query,
|
|
||||||
sign_entity, verify_entity,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct DummyEntity {
|
|
||||||
payload_version: i32,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hashable for DummyEntity {
|
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
|
||||||
self.payload_version.hash(hasher);
|
|
||||||
self.payload.hash(hasher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Integrable for DummyEntity {
|
|
||||||
const KIND: &'static str = "dummy_entity";
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
|
||||||
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
|
||||||
actor
|
|
||||||
.ask(Bootstrap {
|
|
||||||
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
actor
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sign_writes_envelope_and_verify_passes() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-7";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
let count: i64 = schema::integrity_envelope::table
|
|
||||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
|
||||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
|
||||||
.count()
|
|
||||||
.get_result(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(count, 1, "envelope row must be created exactly once");
|
|
||||||
let _ = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tampered_mac_fails_verification() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-11";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
diesel::update(schema::integrity_envelope::table)
|
|
||||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
|
||||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
|
||||||
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let err = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn changed_payload_fails_verification() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-21";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
let tampered = DummyEntity {
|
|
||||||
payload: b"payload-v1-but-tampered".to_vec(),
|
|
||||||
..entity
|
|
||||||
};
|
|
||||||
|
|
||||||
let err = check_entity_attestation(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn strict_verify_fails_closed_while_sealed() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-41";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
drop(keyholder);
|
|
||||||
|
|
||||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
|
||||||
|
|
||||||
let err = verify_entity(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
|
||||||
));
|
|
||||||
|
|
||||||
let err = lookup_verified(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
|
||||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn lookup_verified_supports_loaded_aggregate() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: i32 = 77;
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| async {
|
|
||||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn extension_trait_lookup_verified_required_works() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: i32 = 79;
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| {
|
|
||||||
Box::pin(async {
|
|
||||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn lookup_verified_from_query_helpers_work() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: i32 = 80;
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
let verified = lookup_verified_from_query(&mut conn, &keyholder, |_| {
|
|
||||||
Box::pin(async {
|
|
||||||
Ok::<_, db::DatabaseError>((
|
|
||||||
ENTITY_ID,
|
|
||||||
DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
|
||||||
|
|
||||||
drop(keyholder);
|
|
||||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
|
||||||
|
|
||||||
let err = lookup_verified_from_query(&mut conn, &sealed_keyholder, |_| {
|
|
||||||
Box::pin(async {
|
|
||||||
Ok::<_, db::DatabaseError>((
|
|
||||||
ENTITY_ID,
|
|
||||||
DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@@ -1,593 +0,0 @@
|
|||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use super::Integrable;
|
|
||||||
|
|
||||||
mod private {
|
|
||||||
pub trait Sealed {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marker trait for type-level verification provenance.
|
|
||||||
///
|
|
||||||
/// This trait is intentionally sealed so external code cannot invent arbitrary
|
|
||||||
/// provenance tags and bypass the intended type-level guarantees.
|
|
||||||
pub trait VerificationOrigin: private::Sealed {
|
|
||||||
type Origin: VerificationOrigin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Root provenance marker for values directly produced by integrity APIs.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
||||||
pub struct Root;
|
|
||||||
|
|
||||||
/// Nested provenance marker carrying the source integrable type and previous
|
|
||||||
/// provenance marker in the chain.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Nested<From, P: VerificationOrigin = Root>(core::marker::PhantomData<(From, P)>);
|
|
||||||
|
|
||||||
impl private::Sealed for Root {}
|
|
||||||
impl VerificationOrigin for Root {
|
|
||||||
type Origin = Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, P: VerificationOrigin> private::Sealed for Nested<T, P> {}
|
|
||||||
impl<T, P: VerificationOrigin> VerificationOrigin for Nested<T, P> {
|
|
||||||
type Origin = P;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[must_use = "Verified<T> is a proof-bearing wrapper; use self.drop_verification_provenance() to explicitly discard integrity provenance when needed"]
|
|
||||||
pub struct Verified<T, O: VerificationOrigin = Root> {
|
|
||||||
inner: T,
|
|
||||||
origin: core::marker::PhantomData<O>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, O: VerificationOrigin> AsRef<Verified<T, O>> for Verified<&T, O> {
|
|
||||||
fn as_ref(&self) -> &Verified<T, O> {
|
|
||||||
// SAFETY: `Verified<T>` is `#[repr(transparent)]` over `T`, so `&T`
|
|
||||||
// and `&Verified<T>` have identical layout.
|
|
||||||
unsafe { reinterpret_layout_ref::<T, Verified<T, O>>(self.inner) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, U: Integrable, O: VerificationOrigin> Deref for Verified<T, Nested<U, O>> {
|
|
||||||
type Target = Verified<T, O::Origin>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
// SAFETY: `Verified<T, Nested<U, O>>` is `#[repr(transparent)]` over `T`, so `&Verified<T, Nested<U, O>>`
|
|
||||||
// and `&Nested<U, O>` have identical layout.
|
|
||||||
unsafe { reinterpret_layout_ref::<Self, Verified<T, O::Origin>>(self) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Deref for Verified<T, Root> {
|
|
||||||
type Target = T;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, O: VerificationOrigin> Verified<T, O> {
|
|
||||||
/// Unwraps the verified value, discarding the integrity provenance.
|
|
||||||
///
|
|
||||||
/// The name is intentionally verbose — call sites where provenance is
|
|
||||||
/// dropped should be easy to find and audit.
|
|
||||||
pub fn drop_verification_provenance(self) -> T {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Downgrades the origin provenance to any lower nestedness level,
|
|
||||||
/// e.g. `Verified<T, Nested<Other>>` to `Verified<T, Root>`.
|
|
||||||
pub fn unqualify_origin<Target: VerificationOrigin>(self) -> Verified<T, Target>
|
|
||||||
where
|
|
||||||
O: VerificationOrigin<Origin = Target>,
|
|
||||||
{
|
|
||||||
Verified {
|
|
||||||
inner: self.inner,
|
|
||||||
origin: core::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a `Verified<T>` by wrapping a `T`.
|
|
||||||
pub(super) fn new(value: T) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: value,
|
|
||||||
origin: core::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a `Verified<T>` from a raw value without performing any
|
|
||||||
/// integrity check. Only available in test builds; use the integrity
|
|
||||||
/// module's functions to obtain a `Verified<T>` in production code.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn new_unchecked(value: T) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: value,
|
|
||||||
origin: core::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reinterprets `&T` as `&Verified<T>`.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(super) fn from_ref(from: &T) -> &Self {
|
|
||||||
// SAFETY: `Self` is `#[repr(transparent)]` over `T`.
|
|
||||||
unsafe { reinterpret_layout_ref::<T, Self>(from) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bit-copies `value: From` into a `To`, suppressing the source destructor so
|
|
||||||
/// the destination owns the bytes.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// The caller must guarantee that `From` and `To` have identical in-memory
|
|
||||||
/// layout — the raw bytes that encode a valid `From` must also encode a valid
|
|
||||||
/// `To`.
|
|
||||||
///
|
|
||||||
/// A `union` is used instead of [`std::mem::transmute`] because `transmute`
|
|
||||||
/// rejects generic source/destination types at the call site even when their
|
|
||||||
/// sizes are provably equal at monomorphization time.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[inline]
|
|
||||||
pub const unsafe fn reinterpret_layout<From, To>(value: From) -> To {
|
|
||||||
const {
|
|
||||||
assert!(
|
|
||||||
::std::mem::size_of::<From>() == ::std::mem::size_of::<To>(),
|
|
||||||
"reinterpret_layout: source and destination must have identical size"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
::std::mem::align_of::<From>() == ::std::mem::align_of::<To>(),
|
|
||||||
"reinterpret_layout: source and destination must have identical alignment"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
union Reinterpret<A, B> {
|
|
||||||
from: ::std::mem::ManuallyDrop<A>,
|
|
||||||
to: ::std::mem::ManuallyDrop<B>,
|
|
||||||
}
|
|
||||||
// SAFETY: caller guarantees layout equivalence (see fn docs). The union
|
|
||||||
// write-read copies the raw bytes of `value` into a `To` slot, and
|
|
||||||
// `ManuallyDrop` on the source side suppresses its destructor so the
|
|
||||||
// destination owns the bytes unambiguously — no double-drop is possible.
|
|
||||||
unsafe {
|
|
||||||
::std::mem::ManuallyDrop::into_inner(
|
|
||||||
Reinterpret {
|
|
||||||
from: ::std::mem::ManuallyDrop::new(value),
|
|
||||||
}
|
|
||||||
.to,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reinterprets `&From` as `&To` via a layout-preserving pointer cast.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// Same invariants as [`reinterpret_layout`].
|
|
||||||
#[inline]
|
|
||||||
pub const unsafe fn reinterpret_layout_ref<From, To>(value: &From) -> &To {
|
|
||||||
const {
|
|
||||||
assert!(
|
|
||||||
::std::mem::size_of::<From>() == ::std::mem::size_of::<To>(),
|
|
||||||
"reinterpret_layout_ref: source and destination must have identical size"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
::std::mem::align_of::<From>() == ::std::mem::align_of::<To>(),
|
|
||||||
"reinterpret_layout_ref: source and destination must have identical alignment"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// SAFETY: caller guarantees layout equivalence (see fn docs). A reference
|
|
||||||
// cast between identically-laid-out types produces a reference with the
|
|
||||||
// same address and lifetime, which is sound.
|
|
||||||
unsafe { &*(value as *const From as *const To) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implemented on `Verified<T>` by [`VerifiedFields!`], exposing the field-wise counterpart.
|
|
||||||
///
|
|
||||||
/// ## Disclaimer
|
|
||||||
/// Do not implement this trait manually. It is intended to be implemented only
|
|
||||||
/// by the `VerifiedFields!` macro, which generates the necessary layout
|
|
||||||
/// guarantees for sound pointer casts.
|
|
||||||
///
|
|
||||||
/// ## Soundness
|
|
||||||
/// When [`verify_entity`][crate::crypto::integrity::verify_entity] attests an
|
|
||||||
/// entity, it returns `Verified<T>` — an aggregate proof over the whole value.
|
|
||||||
/// This trait converts that wrapper into `Counterpart` (e.g.
|
|
||||||
/// `VerifiedMyStruct`), where every field is individually wrapped in
|
|
||||||
/// [`Verified`], allowing verified data to flow into functions that require
|
|
||||||
/// `Verified<FieldType>` without re-verifying.
|
|
||||||
///
|
|
||||||
/// ## Safety
|
|
||||||
/// The conversion is a zero-cost reinterpretation — no copying (beyond a
|
|
||||||
/// bitwise move in the owned variant) or HMAC work occurs. Soundness rests on
|
|
||||||
/// identical memory layout between `Verified<T>` and `Counterpart`:
|
|
||||||
///
|
|
||||||
/// - `T` carries `#[repr(C)]` (enforced by `@require_repr` in the macro).
|
|
||||||
/// - `T` does **not** carry `packed` (enforced by `@reject_packed`).
|
|
||||||
/// - `Counterpart` also carries `#[repr(C)]`, with the same fields in the same
|
|
||||||
/// order.
|
|
||||||
/// - Each `Verified<F>` field is `#[repr(transparent)]` over `F`, so its size
|
|
||||||
/// and alignment match `F` exactly.
|
|
||||||
/// - `Verified<T>` itself is `#[repr(transparent)]` over `T`.
|
|
||||||
///
|
|
||||||
/// As an additional machine-checked guard, [`reinterpret_layout`] and
|
|
||||||
/// [`reinterpret_layout_ref`] assert size/align equality of the two types at
|
|
||||||
/// monomorphization time.
|
|
||||||
///
|
|
||||||
/// The trait is implemented directly on `Verified<T>` (not on `T`), so no
|
|
||||||
/// `Deref`-coercion or auto-ref stripping is needed at call sites — the impl
|
|
||||||
/// is unambiguous.
|
|
||||||
pub trait VerifiedFieldsAccessor {
|
|
||||||
/// The field-wise verified counterpart, e.g. `VerifiedMyStruct`.
|
|
||||||
type Counterpart;
|
|
||||||
|
|
||||||
/// Reinterprets `&self` as `&Counterpart` via a layout-preserving pointer cast.
|
|
||||||
///
|
|
||||||
/// No data is copied and no re-verification occurs. The returned reference
|
|
||||||
/// borrows from `self` and has the same lifetime.
|
|
||||||
fn inherit_ref(&self) -> &Self::Counterpart;
|
|
||||||
|
|
||||||
/// Consumes `self` and returns `Counterpart` via a layout-preserving
|
|
||||||
/// bitwise move.
|
|
||||||
///
|
|
||||||
/// The original `Verified<T>` is moved without running its destructor
|
|
||||||
/// (there is none — `Verified` is a transparent wrapper with no heap
|
|
||||||
/// allocation), and the returned counterpart owns the original bytes. No
|
|
||||||
/// re-verification occurs.
|
|
||||||
fn inherit(self) -> Self::Counterpart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo! rewrite macro_rules to derive crate
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! VerifiedFields {
|
|
||||||
// --- Entry point (no source generics) ---
|
|
||||||
(
|
|
||||||
$(#$attr:tt)*
|
|
||||||
$vis:vis struct $name:ident
|
|
||||||
{
|
|
||||||
$(
|
|
||||||
$field_vis:vis $field_name:ident : $field_ty:ty
|
|
||||||
),* $(,)?
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
// Attribute-list checks run in isolation — they only receive the attrs,
|
|
||||||
// not the struct body.
|
|
||||||
$crate::VerifiedFields!(@require_repr [$(#$attr)*]);
|
|
||||||
$crate::VerifiedFields!(@reject_packed [$(#$attr)*]);
|
|
||||||
|
|
||||||
paste::paste! {
|
|
||||||
#[doc = concat!(
|
|
||||||
"Field-wise verified counterpart of [`", stringify!($name), "`]."
|
|
||||||
)]
|
|
||||||
//
|
|
||||||
// `#[repr(C)]` is required for the pointer casts in `inherit_ref`
|
|
||||||
// and `inherit` to be sound. Both the source struct (enforced by
|
|
||||||
// `@require_repr`) and this counterpart carry `#[repr(C)]`, which
|
|
||||||
// guarantees matching field offsets. Combined with each
|
|
||||||
// `Verified<F>` being `#[repr(transparent)]` over `F`, the two
|
|
||||||
// structs have identical memory layout.
|
|
||||||
//
|
|
||||||
// `#[repr(transparent)]` is not usable here because it only permits
|
|
||||||
// a single non-ZST field; multi-field structs would fail to compile.
|
|
||||||
#[repr(C)]
|
|
||||||
$vis struct [<Verified $name>]<P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
|
||||||
{
|
|
||||||
$(
|
|
||||||
$field_vis $field_name : $crate::crypto::integrity::Verified<$field_ty, P>
|
|
||||||
),*
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
|
||||||
$crate::crypto::integrity::v1::verified::VerifiedFieldsAccessor
|
|
||||||
for $crate::crypto::integrity::Verified<$name, P>
|
|
||||||
{
|
|
||||||
type Counterpart = [<Verified $name>]<P>;
|
|
||||||
|
|
||||||
fn inherit_ref(&self) -> &Self::Counterpart {
|
|
||||||
// SAFETY: `Self` is `Verified<T>` (transparent over
|
|
||||||
// `T #[repr(C)]`) and `Self::Counterpart` is `#[repr(C)]`
|
|
||||||
// with the same fields in the same order, each wrapped in
|
|
||||||
// a `#[repr(transparent)]` `Verified<F>`. The two types
|
|
||||||
// therefore have identical memory layout, which
|
|
||||||
// `reinterpret_layout_ref` re-checks as size/align
|
|
||||||
// equality at monomorphization.
|
|
||||||
unsafe {
|
|
||||||
$crate::crypto::integrity::v1::verified::reinterpret_layout_ref::<
|
|
||||||
Self,
|
|
||||||
Self::Counterpart,
|
|
||||||
>(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inherit(self) -> Self::Counterpart {
|
|
||||||
// SAFETY: identical layout — see `inherit_ref`. The owned
|
|
||||||
// helper additionally suppresses the source destructor so
|
|
||||||
// the returned counterpart owns the original bytes (no
|
|
||||||
// double-drop is possible).
|
|
||||||
unsafe {
|
|
||||||
$crate::crypto::integrity::v1::verified::reinterpret_layout::<
|
|
||||||
Self,
|
|
||||||
Self::Counterpart,
|
|
||||||
>(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Entry point (source has generics) ---
|
|
||||||
(
|
|
||||||
$(#$attr:tt)*
|
|
||||||
$vis:vis struct $name:ident <$($gen:tt),*>
|
|
||||||
{
|
|
||||||
$(
|
|
||||||
$field_vis:vis $field_name:ident : $field_ty:ty
|
|
||||||
),* $(,)?
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
// Attribute-list checks run in isolation — they only receive the attrs,
|
|
||||||
// not the struct body.
|
|
||||||
$crate::VerifiedFields!(@require_repr [$(#$attr)*]);
|
|
||||||
$crate::VerifiedFields!(@reject_packed [$(#$attr)*]);
|
|
||||||
|
|
||||||
paste::paste! {
|
|
||||||
#[doc = concat!(
|
|
||||||
"Field-wise verified counterpart of [`", stringify!($name), "`]."
|
|
||||||
)]
|
|
||||||
//
|
|
||||||
// `#[repr(C)]` is required for the pointer casts in `inherit_ref`
|
|
||||||
// and `inherit` to be sound. Both the source struct (enforced by
|
|
||||||
// `@require_repr`) and this counterpart carry `#[repr(C)]`, which
|
|
||||||
// guarantees matching field offsets. Combined with each
|
|
||||||
// `Verified<F>` being `#[repr(transparent)]` over `F`, the two
|
|
||||||
// structs have identical memory layout.
|
|
||||||
//
|
|
||||||
// `#[repr(transparent)]` is not usable here because it only permits
|
|
||||||
// a single non-ZST field; multi-field structs would fail to compile.
|
|
||||||
#[repr(C)]
|
|
||||||
$vis struct [<Verified $name>]<$($gen),*, P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
|
||||||
{
|
|
||||||
$(
|
|
||||||
$field_vis $field_name : $crate::crypto::integrity::Verified<$field_ty, P>
|
|
||||||
),*
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<$($gen),*, P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
|
||||||
$crate::crypto::integrity::v1::verified::VerifiedFieldsAccessor
|
|
||||||
for $crate::crypto::integrity::Verified<$name<$($gen),*>, P>
|
|
||||||
{
|
|
||||||
type Counterpart = [<Verified $name>]<$($gen),*, P>;
|
|
||||||
|
|
||||||
fn inherit_ref(&self) -> &Self::Counterpart {
|
|
||||||
// SAFETY: `Self` is `Verified<T>` (transparent over
|
|
||||||
// `T #[repr(C)]`) and `Self::Counterpart` is `#[repr(C)]`
|
|
||||||
// with the same fields in the same order, each wrapped in
|
|
||||||
// a `#[repr(transparent)]` `Verified<F>`. The two types
|
|
||||||
// therefore have identical memory layout, which
|
|
||||||
// `reinterpret_layout_ref` re-checks as size/align
|
|
||||||
// equality at monomorphization.
|
|
||||||
unsafe {
|
|
||||||
$crate::crypto::integrity::v1::verified::reinterpret_layout_ref::<
|
|
||||||
Self,
|
|
||||||
Self::Counterpart,
|
|
||||||
>(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inherit(self) -> Self::Counterpart {
|
|
||||||
// SAFETY: identical layout — see `inherit_ref`. The owned
|
|
||||||
// helper additionally suppresses the source destructor so
|
|
||||||
// the returned counterpart owns the original bytes (no
|
|
||||||
// double-drop is possible).
|
|
||||||
unsafe {
|
|
||||||
$crate::crypto::integrity::v1::verified::reinterpret_layout::<
|
|
||||||
Self,
|
|
||||||
Self::Counterpart,
|
|
||||||
>(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- @require_repr: ensure `#[repr(C)]` appears in the attribute list ---
|
|
||||||
(@require_repr [#[repr(C)] $($rest:tt)*]) => {};
|
|
||||||
(@require_repr [#$other:tt $($rest:tt)*]) => {
|
|
||||||
$crate::VerifiedFields!(@require_repr [$($rest)*]);
|
|
||||||
};
|
|
||||||
(@require_repr []) => {
|
|
||||||
::std::compile_error!(
|
|
||||||
"VerifiedFields requires `#[repr(C)]` on the struct to guarantee field layout"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- @reject_packed: walk attrs and reject any `#[repr(..., packed, ...)]`.
|
|
||||||
//
|
|
||||||
// Without this, a packed struct would still fail at monomorphization via
|
|
||||||
// the const assertions inside the `reinterpret_layout*` helpers, but the
|
|
||||||
// diagnostic would be much harder to read. `align(N)` is *not* rejected
|
|
||||||
// here because const assertions catch alignment mismatches cleanly, and
|
|
||||||
// forbidding it would be unnecessarily restrictive.
|
|
||||||
(@reject_packed [#[repr($($inner:tt)*)] $($rest:tt)*]) => {
|
|
||||||
$crate::VerifiedFields!(@reject_packed_inner [$($inner)*]);
|
|
||||||
$crate::VerifiedFields!(@reject_packed [$($rest)*]);
|
|
||||||
};
|
|
||||||
(@reject_packed [#$other:tt $($rest:tt)*]) => {
|
|
||||||
$crate::VerifiedFields!(@reject_packed [$($rest)*]);
|
|
||||||
};
|
|
||||||
(@reject_packed []) => {};
|
|
||||||
|
|
||||||
(@reject_packed_inner [packed $($rest:tt)*]) => {
|
|
||||||
::std::compile_error!(
|
|
||||||
"VerifiedFields does not support packed layouts; the generated \
|
|
||||||
counterpart would not share layout with the source struct"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
(@reject_packed_inner [$first:tt $($rest:tt)*]) => {
|
|
||||||
$crate::VerifiedFields!(@reject_packed_inner [$($rest)*]);
|
|
||||||
};
|
|
||||||
(@reject_packed_inner []) => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(VerifiedFields!)]
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub struct MyStruct<T> {
|
|
||||||
pub field1: String,
|
|
||||||
pub field2: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify<T>(t: T) -> Verified<T> {
|
|
||||||
Verified {
|
|
||||||
inner: t,
|
|
||||||
origin: core::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- inherit_ref ---
|
|
||||||
|
|
||||||
// Verifies that `inherit_ref` returns a reference to the same memory
|
|
||||||
// address, confirming that no copy is made and the cast is purely a
|
|
||||||
// reinterpretation.
|
|
||||||
#[test]
|
|
||||||
fn inherit_ref_is_same_address() {
|
|
||||||
let v = verify(MyStruct {
|
|
||||||
field1: "hello".into(),
|
|
||||||
field2: 42u32,
|
|
||||||
});
|
|
||||||
let fields = v.inherit_ref();
|
|
||||||
assert_eq!(
|
|
||||||
&v as *const _ as *const u8, fields as *const _ as *const u8,
|
|
||||||
"inherit_ref must return a pointer to the same memory, not a copy"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifies that field values are correctly accessible after `inherit_ref`.
|
|
||||||
#[test]
|
|
||||||
fn inherit_ref_field_values() {
|
|
||||||
let v = verify(MyStruct {
|
|
||||||
field1: "hello".into(),
|
|
||||||
field2: 99u32,
|
|
||||||
});
|
|
||||||
let fields = v.inherit_ref();
|
|
||||||
assert_eq!(*fields.field1, "hello");
|
|
||||||
assert_eq!(*fields.field2, 99u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifies that casting the counterpart back to `Verified<T>` via a raw
|
|
||||||
// pointer lands on the original address — confirms the round-trip is a
|
|
||||||
// pure reinterpretation.
|
|
||||||
#[test]
|
|
||||||
fn inherit_ref_cast_roundtrip() {
|
|
||||||
let v = verify(MyStruct {
|
|
||||||
field1: "x".into(),
|
|
||||||
field2: 7u32,
|
|
||||||
});
|
|
||||||
let fields: &VerifiedMyStruct<u32, Root> = v.inherit_ref();
|
|
||||||
let back_ptr =
|
|
||||||
fields as *const VerifiedMyStruct<u32, Root> as *const Verified<MyStruct<u32>>;
|
|
||||||
assert_eq!(
|
|
||||||
back_ptr as *const u8, &v as *const _ as *const u8,
|
|
||||||
"cast of counterpart must point back to the same Verified<T>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZST fields must still produce a counterpart with identical layout — the
|
|
||||||
// const asserts in `reinterpret_layout_ref` guard this at monomorphization.
|
|
||||||
#[test]
|
|
||||||
fn inherit_ref_with_zst_field() {
|
|
||||||
#[derive(VerifiedFields!)]
|
|
||||||
#[repr(C)]
|
|
||||||
struct WithZst {
|
|
||||||
pub unit: (),
|
|
||||||
pub val: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
let v = Verified::<WithZst>::new_unchecked(WithZst { unit: (), val: 777 });
|
|
||||||
let fields = v.inherit_ref();
|
|
||||||
assert_eq!(*fields.val, 777);
|
|
||||||
assert_eq!(*fields.unit, ());
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- inherit ---
|
|
||||||
|
|
||||||
// Verifies that `inherit` preserves field values in the owned counterpart.
|
|
||||||
#[test]
|
|
||||||
fn inherit_field_values() {
|
|
||||||
let v = verify(MyStruct {
|
|
||||||
field1: "world".into(),
|
|
||||||
field2: 1234u64,
|
|
||||||
});
|
|
||||||
let VerifiedMyStruct { field1, field2 } = v.inherit();
|
|
||||||
assert_eq!(*field1, "world");
|
|
||||||
assert_eq!(*field2, 1234u64);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifies that `inherit` does not double-drop the inner value.
|
|
||||||
// If `ManuallyDrop` handling is wrong, running under Miri or with a drop
|
|
||||||
// counter catches a double-free.
|
|
||||||
#[test]
|
|
||||||
fn inherit_no_double_drop() {
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
|
|
||||||
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
|
|
||||||
struct DropCounter;
|
|
||||||
impl Drop for DropCounter {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(VerifiedFields!)]
|
|
||||||
#[repr(C)]
|
|
||||||
struct WithDrop {
|
|
||||||
pub val: DropCounter,
|
|
||||||
}
|
|
||||||
|
|
||||||
DROP_COUNT.store(0, Ordering::Relaxed);
|
|
||||||
{
|
|
||||||
let v = Verified::<WithDrop>::new_unchecked(WithDrop { val: DropCounter });
|
|
||||||
let _ = v.inherit();
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
DROP_COUNT.load(Ordering::Relaxed),
|
|
||||||
1,
|
|
||||||
"DropCounter must be dropped exactly once"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Verified::from_ref ---
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_ref_is_same_address() {
|
|
||||||
let val = 42u32;
|
|
||||||
let verified: &Verified<u32> = Verified::from_ref(&val);
|
|
||||||
assert_eq!(
|
|
||||||
&val as *const u32 as *const u8, verified as *const _ as *const u8,
|
|
||||||
"from_ref must alias the original reference, not copy the value"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_ref_value_preserved() {
|
|
||||||
let val = String::from("test");
|
|
||||||
let verified: &Verified<String> = Verified::from_ref(&val);
|
|
||||||
assert_eq!(**verified, "test");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- AsRef<Verified<T>> for Verified<&T> ---
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn verified_ref_as_ref_is_same_address() {
|
|
||||||
let val = 99u32;
|
|
||||||
let vref: Verified<&u32> = Verified::new_unchecked(&val);
|
|
||||||
let v: &Verified<u32> = vref.as_ref();
|
|
||||||
assert_eq!(
|
|
||||||
&val as *const u32 as *const u8, v as *const _ as *const u8,
|
|
||||||
"AsRef<Verified<T>> for Verified<&T> must alias the referent, not copy it"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ use rand::{
|
|||||||
|
|
||||||
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
|
pub mod authn;
|
||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
pub mod integrity;
|
pub mod integrity;
|
||||||
|
|
||||||
|
|||||||
@@ -72,40 +72,6 @@ pub mod types {
|
|||||||
Ok(SqliteTimestamp(datetime))
|
Ok(SqliteTimestamp(datetime))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Key algorithm stored in the `useragent_client.key_type` column.
|
|
||||||
/// Values must stay stable — they are persisted in the database.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, strum::FromRepr)]
|
|
||||||
#[diesel(sql_type = Integer)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum KeyType {
|
|
||||||
Ed25519 = 1,
|
|
||||||
EcdsaSecp256k1 = 2,
|
|
||||||
Rsa = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToSql<Integer, Sqlite> for KeyType {
|
|
||||||
fn to_sql<'b>(
|
|
||||||
&'b self,
|
|
||||||
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
|
|
||||||
) -> diesel::serialize::Result {
|
|
||||||
out.set_value(*self as i32);
|
|
||||||
Ok(IsNull::No)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql<Integer, Sqlite> for KeyType {
|
|
||||||
fn from_sql(
|
|
||||||
mut bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
|
|
||||||
) -> diesel::deserialize::Result<Self> {
|
|
||||||
let Some(SqliteType::Long) = bytes.value_type() else {
|
|
||||||
return Err("Expected Integer for KeyType".into());
|
|
||||||
};
|
|
||||||
let discriminant = bytes.read_long();
|
|
||||||
KeyType::from_repr(discriminant as i32)
|
|
||||||
.ok_or_else(|| format!("Unknown KeyType discriminant: {discriminant}").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
@@ -244,7 +210,6 @@ pub struct UseragentClient {
|
|||||||
pub public_key: Vec<u8>,
|
pub public_key: Vec<u8>,
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
pub updated_at: SqliteTimestamp,
|
pub updated_at: SqliteTimestamp,
|
||||||
pub key_type: KeyType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
|
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use kameo::actor::ActorRef;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::KeyHolder,
|
actors::keyholder::KeyHolder,
|
||||||
crypto::integrity::{self, Verified, VerifiedEntity, verified::VerifiedFieldsAccessor},
|
crypto::integrity,
|
||||||
db::{
|
db::{
|
||||||
self, DatabaseError,
|
self, DatabaseError,
|
||||||
models::{
|
models::{
|
||||||
@@ -153,39 +153,12 @@ impl Engine {
|
|||||||
{
|
{
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let verified_settings =
|
let grant = P::try_find_grant(&context, &mut conn)
|
||||||
match integrity::lookup_verified_from_query(&mut conn, &self.keyholder, |conn| {
|
|
||||||
let context = context.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let grant = P::try_find_grant(&context, conn)
|
|
||||||
.await
|
|
||||||
.map_err(DatabaseError::from)?
|
|
||||||
.ok_or_else(|| DatabaseError::from(diesel::result::Error::NotFound))?;
|
|
||||||
|
|
||||||
Ok::<_, DatabaseError>((grant.common_settings_id, grant.settings))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(verified) => verified,
|
|
||||||
Err(integrity::Error::Database(DatabaseError::Connection(
|
|
||||||
diesel::result::Error::NotFound,
|
|
||||||
))) => return Err(PolicyError::NoMatchingGrant),
|
|
||||||
Err(err) => return Err(PolicyError::Integrity(err)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut grant = P::try_find_grant(&context, &mut conn)
|
|
||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||||
|
|
||||||
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||||
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
|
||||||
// integrity check over canonicalized settings.
|
|
||||||
grant.settings = verified_settings
|
|
||||||
.inherit()
|
|
||||||
.entity
|
|
||||||
.drop_verification_provenance();
|
|
||||||
|
|
||||||
let mut violations = check_shared_constraints(
|
let mut violations = check_shared_constraints(
|
||||||
&context,
|
&context,
|
||||||
@@ -241,7 +214,7 @@ impl Engine {
|
|||||||
pub async fn create_grant<P: Policy>(
|
pub async fn create_grant<P: Policy>(
|
||||||
&self,
|
&self,
|
||||||
full_grant: CombinedSettings<P::Settings>,
|
full_grant: CombinedSettings<P::Settings>,
|
||||||
) -> Result<Verified<i32>, DatabaseError>
|
) -> Result<i32, DatabaseError>
|
||||||
where
|
where
|
||||||
P::Settings: Clone,
|
P::Settings: Clone,
|
||||||
{
|
{
|
||||||
@@ -285,23 +258,22 @@ impl Engine {
|
|||||||
|
|
||||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||||
|
|
||||||
let verified_entity_id =
|
|
||||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
|
||||||
QueryResult::Ok(verified_entity_id)
|
QueryResult::Ok(basic_grant.id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(id.unqualify_origin())
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_one_kind<Kind: Policy, Y>(
|
async fn list_one_kind<Kind: Policy, Y>(
|
||||||
&self,
|
&self,
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
) -> Result<Vec<Grant<Y>>, ListError>
|
) -> Result<impl Iterator<Item = Grant<Y>>, ListError>
|
||||||
where
|
where
|
||||||
Y: From<Kind::Settings>,
|
Y: From<Kind::Settings>,
|
||||||
{
|
{
|
||||||
@@ -309,32 +281,16 @@ impl Engine {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?;
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let mut verified_grants = Vec::with_capacity(all_grants.len());
|
// Verify integrity of all grants before returning any results
|
||||||
|
for grant in &all_grants {
|
||||||
// Verify integrity of all grants before returning any results.
|
integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||||
for grant in all_grants {
|
|
||||||
let VerifiedEntity {
|
|
||||||
entity: verified_settings,
|
|
||||||
entity_id: _,
|
|
||||||
} = integrity::verify_entity(
|
|
||||||
conn,
|
|
||||||
&self.keyholder,
|
|
||||||
grant.settings,
|
|
||||||
grant.common_settings_id,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.inherit();
|
|
||||||
|
|
||||||
verified_grants.push(Grant {
|
|
||||||
id: grant.id,
|
|
||||||
common_settings_id: grant.common_settings_id,
|
|
||||||
settings: verified_settings
|
|
||||||
.drop_verification_provenance()
|
|
||||||
.generalize(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(verified_grants)
|
Ok(all_grants.into_iter().map(|g| Grant {
|
||||||
|
id: g.id,
|
||||||
|
common_settings_id: g.common_settings_id,
|
||||||
|
settings: g.settings.generalize(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ pub enum SpecificGrant {
|
|||||||
TokenTransfer(token_transfers::Settings),
|
TokenTransfer(token_transfers::Settings),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct CombinedSettings<PolicyGrant> {
|
pub struct CombinedSettings<PolicyGrant> {
|
||||||
pub shared: SharedGrantSettings,
|
pub shared: SharedGrantSettings,
|
||||||
pub specific: PolicyGrant,
|
pub specific: PolicyGrant,
|
||||||
|
|||||||
@@ -110,8 +110,7 @@ async fn check_rate_limits(
|
|||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
let window = grant.settings.specific.limit.window;
|
let window = grant.settings.specific.limit.window;
|
||||||
|
|
||||||
let past_transaction =
|
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
||||||
query_relevant_past_transaction(grant.common_settings_id, window, db).await?;
|
|
||||||
|
|
||||||
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||||
let prospective_cumulative_volume: U256 = past_transaction
|
let prospective_cumulative_volume: U256 = past_transaction
|
||||||
@@ -250,20 +249,21 @@ impl Policy for EtherTransfer {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let settings = Settings {
|
||||||
|
target: targets,
|
||||||
|
limit: VolumeRateLimit {
|
||||||
|
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
|
||||||
|
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
|
||||||
|
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Some(Grant {
|
Ok(Some(Grant {
|
||||||
id: grant.id,
|
id: grant.id,
|
||||||
common_settings_id: grant.basic_grant_id,
|
common_settings_id: grant.basic_grant_id,
|
||||||
settings: CombinedSettings {
|
settings: CombinedSettings {
|
||||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||||
specific: Settings {
|
specific: settings,
|
||||||
target: targets,
|
|
||||||
limit: VolumeRateLimit {
|
|
||||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(|err| {
|
|
||||||
diesel::result::Error::DeserializationError(Box::new(err))
|
|
||||||
})?,
|
|
||||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,16 +286,18 @@ impl Policy for TokenTransfer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let settings = Settings {
|
||||||
|
token_contract: Address::from(token_contract),
|
||||||
|
target,
|
||||||
|
volume_limits,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Some(Grant {
|
Ok(Some(Grant {
|
||||||
id: token_grant.id,
|
id: token_grant.id,
|
||||||
common_settings_id: token_grant.basic_grant_id,
|
common_settings_id: token_grant.basic_grant_id,
|
||||||
settings: CombinedSettings {
|
settings: CombinedSettings {
|
||||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||||
specific: Settings {
|
specific: settings,
|
||||||
token_contract: Address::from(token_contract),
|
|
||||||
target,
|
|
||||||
volume_limits,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use tracing::warn;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::client::{self, ClientConnection, auth},
|
actors::client::{self, ClientConnection, auth},
|
||||||
crypto::integrity::Verified,
|
crypto::authn,
|
||||||
grpc::request_tracker::RequestTracker,
|
grpc::request_tracker::RequestTracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ impl<'a> AuthTransportAdapter<'a> {
|
|||||||
match response {
|
match response {
|
||||||
auth::Outbound::AuthChallenge { pubkey, nonce } => {
|
auth::Outbound::AuthChallenge { pubkey, nonce } => {
|
||||||
AuthResponsePayload::Challenge(ProtoAuthChallenge {
|
AuthResponsePayload::Challenge(ProtoAuthChallenge {
|
||||||
pubkey: pubkey.to_bytes().to_vec(),
|
pubkey: pubkey.to_bytes(),
|
||||||
nonce,
|
nonce,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -161,11 +161,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
.await;
|
.await;
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else {
|
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
|
||||||
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let Ok(pubkey) = ed25519_dalek::VerifyingKey::from_bytes(&pubkey) else {
|
|
||||||
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
|
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@@ -175,7 +171,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||||
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
let Ok(signature) = authn::Signature::try_from(signature.as_slice()) else {
|
||||||
let _ = self
|
let _ = self
|
||||||
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
||||||
.await;
|
.await;
|
||||||
@@ -201,7 +197,7 @@ pub async fn start(
|
|||||||
conn: &mut ClientConnection,
|
conn: &mut ClientConnection,
|
||||||
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
||||||
request_tracker: &mut RequestTracker,
|
request_tracker: &mut RequestTracker,
|
||||||
) -> Result<Verified<i32>, auth::Error> {
|
) -> Result<i32, auth::Error> {
|
||||||
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||||
client::auth::authenticate(conn, &mut transport).await
|
client::auth::authenticate(conn, &mut transport).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ use arbiter_proto::{
|
|||||||
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
||||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||||
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
|
request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload,
|
||||||
response::Payload as AuthResponsePayload,
|
|
||||||
},
|
},
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
@@ -18,8 +17,8 @@ use tonic::Status;
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
actors::user_agent::{UserAgentConnection, auth},
|
||||||
db::models::KeyType,
|
crypto::authn,
|
||||||
grpc::request_tracker::RequestTracker,
|
grpc::request_tracker::RequestTracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,28 +140,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
|
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
|
||||||
pubkey,
|
pubkey,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
key_type,
|
key_type: _,
|
||||||
}) => {
|
}) => {
|
||||||
let Ok(key_type) = ProtoKeyType::try_from(key_type) else {
|
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
|
||||||
warn!(
|
|
||||||
event = "received request with invalid key type",
|
|
||||||
"grpc.useragent.auth_adapter"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let key_type = match key_type {
|
|
||||||
ProtoKeyType::Ed25519 => KeyType::Ed25519,
|
|
||||||
ProtoKeyType::EcdsaSecp256k1 => KeyType::EcdsaSecp256k1,
|
|
||||||
ProtoKeyType::Rsa => KeyType::Rsa,
|
|
||||||
ProtoKeyType::Unspecified => {
|
|
||||||
warn!(
|
|
||||||
event = "received request with unspecified key type",
|
|
||||||
"grpc.useragent.auth_adapter"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let Ok(pubkey) = AuthPublicKey::try_from((key_type, pubkey)) else {
|
|
||||||
warn!(
|
warn!(
|
||||||
event = "received request with invalid public key",
|
event = "received request with invalid public key",
|
||||||
"grpc.useragent.auth_adapter"
|
"grpc.useragent.auth_adapter"
|
||||||
@@ -188,7 +168,7 @@ pub async fn start(
|
|||||||
conn: &mut UserAgentConnection,
|
conn: &mut UserAgentConnection,
|
||||||
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
|
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
|
||||||
request_tracker: &mut RequestTracker,
|
request_tracker: &mut RequestTracker,
|
||||||
) -> Result<AuthPublicKey, auth::Error> {
|
) -> Result<authn::PublicKey, auth::Error> {
|
||||||
let transport = AuthTransportAdapter::new(bi, request_tracker);
|
let transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||||
auth::authenticate(conn, transport).await
|
auth::authenticate(conn, transport).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async fn handle_wallet_create(
|
|||||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
||||||
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
||||||
id: wallet_id.drop_verification_provenance(),
|
id: wallet_id,
|
||||||
address: address.to_vec(),
|
address: address.to_vec(),
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -121,9 +121,6 @@ async fn handle_grant_list(
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
}),
|
||||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
|
||||||
EvmGrantListResult::Error(ProtoEvmError::VaultSealed.into())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = ?err, "Failed to list EVM grants");
|
warn!(error = ?err, "Failed to list EVM grants");
|
||||||
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
||||||
@@ -150,7 +147,7 @@ async fn handle_grant_create(
|
|||||||
.try_convert()?;
|
.try_convert()?;
|
||||||
|
|
||||||
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
||||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id.drop_verification_provenance()),
|
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
||||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use crate::{
|
|||||||
HandleRevokeEvmWalletAccess, HandleSdkClientList,
|
HandleRevokeEvmWalletAccess, HandleSdkClientList,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
crypto::authn,
|
||||||
db::models::NewEvmWalletAccess,
|
db::models::NewEvmWalletAccess,
|
||||||
grpc::Convert,
|
grpc::Convert,
|
||||||
};
|
};
|
||||||
@@ -41,7 +42,7 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
|
|||||||
match oob {
|
match oob {
|
||||||
OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response(
|
OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response(
|
||||||
SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest {
|
SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest {
|
||||||
pubkey: profile.pubkey.to_bytes().to_vec(),
|
pubkey: profile.pubkey.to_bytes(),
|
||||||
info: Some(ProtoClientMetadata {
|
info: Some(ProtoClientMetadata {
|
||||||
name: profile.metadata.name,
|
name: profile.metadata.name,
|
||||||
description: profile.metadata.description,
|
description: profile.metadata.description,
|
||||||
@@ -51,7 +52,7 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
|
|||||||
),
|
),
|
||||||
OutOfBand::ClientConnectionCancel { pubkey } => wrap_sdk_client_response(
|
OutOfBand::ClientConnectionCancel { pubkey } => wrap_sdk_client_response(
|
||||||
SdkClientResponsePayload::ConnectionCancel(ProtoSdkClientConnectionCancel {
|
SdkClientResponsePayload::ConnectionCancel(ProtoSdkClientConnectionCancel {
|
||||||
pubkey: pubkey.to_bytes().to_vec(),
|
pubkey: pubkey.to_bytes(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -89,10 +90,8 @@ async fn handle_connection_response(
|
|||||||
actor: &ActorRef<UserAgentSession>,
|
actor: &ActorRef<UserAgentSession>,
|
||||||
resp: ProtoSdkClientConnectionResponse,
|
resp: ProtoSdkClientConnectionResponse,
|
||||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
|
let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice())
|
||||||
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
|
.map_err(|_| Status::invalid_argument("Invalid ML-DSA public key"))?;
|
||||||
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
|
|
||||||
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?;
|
|
||||||
|
|
||||||
actor
|
actor
|
||||||
.ask(HandleNewClientApprove {
|
.ask(HandleNewClientApprove {
|
||||||
@@ -117,7 +116,7 @@ async fn handle_list(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(client, metadata)| ProtoSdkClientEntry {
|
.map(|(client, metadata)| ProtoSdkClientEntry {
|
||||||
id: client.id,
|
id: client.id,
|
||||||
pubkey: client.public_key,
|
pubkey: client.public_key.to_vec(),
|
||||||
info: Some(ProtoClientMetadata {
|
info: Some(ProtoClientMetadata {
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
|
#![forbid(unsafe_code)]
|
||||||
use crate::context::ServerContext;
|
use crate::context::ServerContext;
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate macro_rules_attribute;
|
|
||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
struct DeferClosure<F: FnOnce()> {
|
struct DeferClosure<F: FnOnce()> {
|
||||||
f: Option<F>,
|
f: Option<F>,
|
||||||
}
|
}
|
||||||
@@ -16,19 +14,3 @@ impl<F: FnOnce()> Drop for DeferClosure<F> {
|
|||||||
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
|
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
|
||||||
DeferClosure { f: Some(f) }
|
DeferClosure { f: Some(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for casting between two transparently wrapped types with identical memory layouts.
|
|
||||||
///
|
|
||||||
/// [`ReinterpretWrapper`] enables zero-cost conversions between two types (`Self` and `Counterpart`)
|
|
||||||
/// that wrap the same underlying data but differ in how that data is presented. Both types must
|
|
||||||
/// transparently wrap the same "deref target" and provide bidirectional `AsRef` conversions.
|
|
||||||
pub trait ReinterpretWrapper<Counterpart>
|
|
||||||
where
|
|
||||||
Self: Deref<Target = Self::Inner> + AsRef<Counterpart>,
|
|
||||||
Counterpart: Deref<Target = Self::Inner> + AsRef<Self>,
|
|
||||||
{
|
|
||||||
/// The shared target type that both `Self` and `Counterpart` transparently wrap.
|
|
||||||
type Inner;
|
|
||||||
/// Reinterprets `Self` as `Counterpart`.
|
|
||||||
fn reinterpret(self) -> Counterpart;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ use arbiter_server::{
|
|||||||
client::{ClientConnection, ClientCredentials, auth, connect_client},
|
client::{ClientConnection, ClientCredentials, auth, connect_client},
|
||||||
keyholder::Bootstrap,
|
keyholder::Bootstrap,
|
||||||
},
|
},
|
||||||
|
crypto::authn,
|
||||||
crypto::integrity,
|
crypto::integrity,
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
|
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use ed25519_dalek::Signer as _;
|
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair as _};
|
||||||
|
|
||||||
use super::common::ChannelTransport;
|
use super::common::ChannelTransport;
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> Cli
|
|||||||
async fn insert_registered_client(
|
async fn insert_registered_client(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
actors: &GlobalActors,
|
actors: &GlobalActors,
|
||||||
pubkey: ed25519_dalek::VerifyingKey,
|
pubkey: VerifyingKey<MlDsa87>,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) {
|
) {
|
||||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
@@ -45,7 +46,7 @@ async fn insert_registered_client(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let client_id: i32 = insert_into(program_client::table)
|
let client_id: i32 = insert_into(program_client::table)
|
||||||
.values((
|
.values((
|
||||||
program_client::public_key.eq(pubkey.to_bytes().to_vec()),
|
program_client::public_key.eq(pubkey.encode().to_vec()),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
program_client::metadata_id.eq(metadata_id),
|
||||||
))
|
))
|
||||||
.returning(program_client::id)
|
.returning(program_client::id)
|
||||||
@@ -53,21 +54,36 @@ async fn insert_registered_client(
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let _ = integrity::sign_entity(
|
integrity::sign_entity(
|
||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&ClientCredentials { pubkey, nonce: 1 },
|
&ClientCredentials {
|
||||||
|
pubkey: pubkey.into(),
|
||||||
|
nonce: 1,
|
||||||
|
},
|
||||||
client_id,
|
client_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_client_challenge(
|
||||||
|
key: &SigningKey<MlDsa87>,
|
||||||
|
nonce: i32,
|
||||||
|
pubkey: &authn::PublicKey,
|
||||||
|
) -> authn::Signature {
|
||||||
|
let challenge = arbiter_proto::format_challenge(nonce, &pubkey.to_bytes());
|
||||||
|
key.signing_key()
|
||||||
|
.sign_deterministic(&challenge, arbiter_proto::CLIENT_CONTEXT)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
|
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
let sentinel_key = ed25519_dalek::SigningKey::generate(&mut rand::rng())
|
let sentinel_key = MlDsa87::key_gen(&mut rand::rng())
|
||||||
.verifying_key()
|
.verifying_key()
|
||||||
.to_bytes()
|
.encode()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
insert_into(schema::useragent_client::table)
|
insert_into(schema::useragent_client::table)
|
||||||
@@ -107,11 +123,11 @@ pub async fn test_unregistered_pubkey_rejected() {
|
|||||||
connect_client(props, &mut server_transport).await;
|
connect_client(props, &mut server_transport).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key(),
|
pubkey: new_key.verifying_key().into(),
|
||||||
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -127,7 +143,7 @@ pub async fn test_challenge_auth() {
|
|||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = spawn_test_actors(&db).await;
|
let actors = spawn_test_actors(&db).await;
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
|
|
||||||
insert_registered_client(
|
insert_registered_client(
|
||||||
&db,
|
&db,
|
||||||
@@ -147,7 +163,7 @@ pub async fn test_challenge_auth() {
|
|||||||
// Send challenge request
|
// Send challenge request
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key(),
|
pubkey: new_key.verifying_key().into(),
|
||||||
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -167,8 +183,7 @@ pub async fn test_challenge_auth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Sign the challenge and send solution
|
// Sign the challenge and send solution
|
||||||
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes());
|
let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0);
|
||||||
let signature = new_key.sign(&formatted_challenge);
|
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution { signature })
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
@@ -194,7 +209,7 @@ pub async fn test_challenge_auth() {
|
|||||||
pub async fn test_metadata_unchanged_does_not_append_history() {
|
pub async fn test_metadata_unchanged_does_not_append_history() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = spawn_test_actors(&db).await;
|
let actors = spawn_test_actors(&db).await;
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||||
|
|
||||||
insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await;
|
insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await;
|
||||||
@@ -209,7 +224,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key(),
|
pubkey: new_key.verifying_key().into(),
|
||||||
metadata: requested,
|
metadata: requested,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -220,7 +235,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
|
|||||||
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
||||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
};
|
};
|
||||||
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
|
let signature = sign_client_challenge(&new_key, nonce, &pubkey);
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution { signature })
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
.await
|
.await
|
||||||
@@ -251,7 +266,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
|
|||||||
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = spawn_test_actors(&db).await;
|
let actors = spawn_test_actors(&db).await;
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
|
|
||||||
insert_registered_client(
|
insert_registered_client(
|
||||||
&db,
|
&db,
|
||||||
@@ -271,7 +286,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key(),
|
pubkey: new_key.verifying_key().into(),
|
||||||
metadata: metadata("client", Some("new"), Some("2.0.0")),
|
metadata: metadata("client", Some("new"), Some("2.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -282,7 +297,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
|||||||
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
||||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
};
|
};
|
||||||
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
|
let signature = sign_client_challenge(&new_key, nonce, &pubkey);
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution { signature })
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
.await
|
.await
|
||||||
@@ -339,7 +354,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
|
|||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = spawn_test_actors(&db).await;
|
let actors = spawn_test_actors(&db).await;
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -357,7 +372,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
insert_into(program_client::table)
|
insert_into(program_client::table)
|
||||||
.values((
|
.values((
|
||||||
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
program_client::public_key.eq(new_key.verifying_key().encode().to_vec()),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
program_client::metadata_id.eq(metadata_id),
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
@@ -374,7 +389,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key(),
|
pubkey: new_key.verifying_key().into(),
|
||||||
metadata: requested,
|
metadata: requested,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -4,18 +4,31 @@ use arbiter_server::{
|
|||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
keyholder::Bootstrap,
|
keyholder::Bootstrap,
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth},
|
user_agent::{UserAgentConnection, UserAgentCredentials, auth},
|
||||||
},
|
},
|
||||||
|
crypto::authn,
|
||||||
crypto::integrity,
|
crypto::integrity,
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use ed25519_dalek::Signer as _;
|
use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _};
|
||||||
|
|
||||||
use super::common::ChannelTransport;
|
use super::common::ChannelTransport;
|
||||||
|
|
||||||
|
fn sign_useragent_challenge(
|
||||||
|
key: &SigningKey<MlDsa87>,
|
||||||
|
nonce: i32,
|
||||||
|
pubkey_bytes: &[u8],
|
||||||
|
) -> authn::Signature {
|
||||||
|
let challenge = arbiter_proto::format_challenge(nonce, pubkey_bytes);
|
||||||
|
key.signing_key()
|
||||||
|
.sign_deterministic(&challenge, arbiter_proto::USERAGENT_CONTEXT)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_bootstrap_token_auth() {
|
pub async fn test_bootstrap_token_auth() {
|
||||||
@@ -37,10 +50,10 @@ pub async fn test_bootstrap_token_auth() {
|
|||||||
auth::authenticate(&mut props, server_transport).await
|
auth::authenticate(&mut props, server_transport).await
|
||||||
});
|
});
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
bootstrap_token: Some(token),
|
bootstrap_token: Some(token),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -63,7 +76,7 @@ pub async fn test_bootstrap_token_auth() {
|
|||||||
.first::<Vec<u8>>(&mut conn)
|
.first::<Vec<u8>>(&mut conn)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec());
|
assert_eq!(stored_pubkey, new_key.verifying_key().encode().to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -79,10 +92,10 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
|||||||
auth::authenticate(&mut props, server_transport).await
|
auth::authenticate(&mut props, server_transport).await
|
||||||
});
|
});
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
bootstrap_token: Some("invalid_token".to_string()),
|
bootstrap_token: Some("invalid_token".to_string()),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -115,8 +128,8 @@ pub async fn test_challenge_auth() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -133,14 +146,13 @@ pub async fn test_challenge_auth() {
|
|||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
nonce: 1,
|
nonce: 1,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.drop_verification_provenance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
@@ -152,7 +164,7 @@ pub async fn test_challenge_auth() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -170,12 +182,11 @@ pub async fn test_challenge_auth() {
|
|||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatted_challenge = arbiter_proto::format_challenge(challenge, &pubkey_bytes);
|
let signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes);
|
||||||
let signature = new_key.sign(&formatted_challenge);
|
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution {
|
.send(auth::Inbound::AuthChallengeSolution {
|
||||||
signature: signature.to_bytes().to_vec(),
|
signature: signature.to_bytes(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -206,8 +217,8 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -230,7 +241,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -255,8 +266,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -273,14 +284,13 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
nonce: 1,
|
nonce: 1,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.drop_verification_provenance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
@@ -292,7 +302,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: new_key.verifying_key().into(),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -310,12 +320,11 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
|
let signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes);
|
||||||
let signature = new_key.sign(&wrong_challenge);
|
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution {
|
.send(auth::Inbound::AuthChallengeSolution {
|
||||||
signature: signature.to_bytes().to_vec(),
|
signature: signature.to_bytes(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ use arbiter_server::{
|
|||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
|
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::actor::Spawn as _;
|
use kameo::actor::Spawn as _;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user