Compare commits
3 Commits
check-uac-
...
enforcing-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
694c569c08 | ||
|
|
bec82e036e | ||
|
|
763058b014 |
@@ -67,14 +67,18 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
|
|||||||
## Cryptography
|
## Cryptography
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
- **Client protocol:** ML-DSA
|
- **Client protocol:** ed25519
|
||||||
|
|
||||||
### 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:** ML-DSA
|
- **Supported schemes:** RSA, Ed25519, ECDSA (secp256k1)
|
||||||
- **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out
|
- **Why:** the user agent authenticates with keys backed by platform facilities, and those facilities differ by platform
|
||||||
|
- **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**
|
||||||
|
|||||||
262
server/Cargo.lock
generated
262
server/Cargo.lock
generated
@@ -347,7 +347,7 @@ dependencies = [
|
|||||||
"ruint",
|
"ruint",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"sha3 0.10.8",
|
"sha3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -548,7 +548,7 @@ dependencies = [
|
|||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"sha3 0.10.8",
|
"sha3",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
"syn-solidity",
|
"syn-solidity",
|
||||||
]
|
]
|
||||||
@@ -680,9 +680,9 @@ name = "arbiter-client"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
"arbiter-crypto",
|
|
||||||
"arbiter-proto",
|
"arbiter-proto",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"ed25519-dalek",
|
||||||
"http",
|
"http",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
@@ -692,29 +692,6 @@ dependencies = [
|
|||||||
"tonic",
|
"tonic",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arbiter-crypto"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"alloy",
|
|
||||||
"base64",
|
|
||||||
"chrono",
|
|
||||||
"hmac",
|
|
||||||
"memsafe",
|
|
||||||
"ml-dsa",
|
|
||||||
"rand 0.10.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arbiter-macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"arbiter-crypto",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbiter-proto"
|
name = "arbiter-proto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -748,8 +725,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arbiter-crypto",
|
|
||||||
"arbiter-macros",
|
|
||||||
"arbiter-proto",
|
"arbiter-proto",
|
||||||
"arbiter-tokens-registry",
|
"arbiter-tokens-registry",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -767,8 +742,10 @@ dependencies = [
|
|||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
"kameo",
|
"kameo",
|
||||||
"ml-dsa",
|
"macro_rules_attribute",
|
||||||
|
"memsafe",
|
||||||
"mutants",
|
"mutants",
|
||||||
|
"paste",
|
||||||
"pem",
|
"pem",
|
||||||
"proptest",
|
"proptest",
|
||||||
"prost",
|
"prost",
|
||||||
@@ -776,13 +753,14 @@ 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 0.7.3",
|
"spki",
|
||||||
"strum 0.28.0",
|
"strum 0.28.0",
|
||||||
"subtle",
|
"subtle",
|
||||||
"test-log",
|
"test-log",
|
||||||
@@ -1473,12 +1451,6 @@ 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"
|
||||||
@@ -1509,12 +1481,6 @@ 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"
|
||||||
@@ -1636,15 +1602,6 @@ 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"
|
||||||
@@ -1783,17 +1740,8 @@ 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 0.9.6",
|
"const-oid",
|
||||||
"zeroize",
|
"pem-rfc7468",
|
||||||
]
|
|
||||||
|
|
||||||
[[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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1936,7 +1884,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 0.9.6",
|
"const-oid",
|
||||||
"crypto-common 0.1.7",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
@@ -2000,13 +1948,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 0.7.10",
|
"der",
|
||||||
"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 0.7.3",
|
"spki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2015,6 +1963,7 @@ 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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2027,6 +1976,7 @@ 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",
|
||||||
@@ -2065,7 +2015,7 @@ dependencies = [
|
|||||||
"ff",
|
"ff",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"group",
|
"group",
|
||||||
"pkcs8 0.10.2",
|
"pkcs8",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"sec1",
|
"sec1",
|
||||||
"serdect",
|
"serdect",
|
||||||
@@ -2612,7 +2562,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
|
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3010,16 +2959,6 @@ 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"
|
||||||
@@ -3035,6 +2974,9 @@ 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"
|
||||||
@@ -3117,6 +3059,22 @@ 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"
|
||||||
@@ -3233,34 +3191,6 @@ 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"
|
||||||
@@ -3302,6 +3232,23 @@ 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"
|
||||||
@@ -3317,6 +3264,17 @@ 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"
|
||||||
@@ -3486,6 +3444,15 @@ 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"
|
||||||
@@ -3545,24 +3512,25 @@ 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 0.7.10",
|
"der",
|
||||||
"spki 0.7.3",
|
"spki",
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
||||||
@@ -4202,6 +4170,28 @@ 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"
|
||||||
@@ -4441,9 +4431,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct",
|
"base16ct",
|
||||||
"der 0.7.10",
|
"der",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"pkcs8 0.10.2",
|
"pkcs8",
|
||||||
"serdect",
|
"serdect",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -4637,17 +4627,7 @@ 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 0.1.6",
|
"keccak",
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
||||||
@@ -4700,10 +4680,6 @@ 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"
|
||||||
@@ -4763,6 +4739,12 @@ 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"
|
||||||
@@ -4770,17 +4752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"der 0.7.10",
|
"der",
|
||||||
]
|
|
||||||
|
|
||||||
[[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", "logging", "prefer-post-quantum", "std"], default-features = false }
|
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||||
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,6 +45,3 @@ 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"] }
|
|
||||||
base64 = "0.22.1"
|
|
||||||
hmac = "0.12.1"
|
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ evm = ["dep:alloy"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arbiter-proto.path = "../arbiter-proto"
|
arbiter-proto.path = "../arbiter-proto"
|
||||||
arbiter-crypto.path = "../arbiter-crypto"
|
|
||||||
alloy = { workspace = true, optional = true }
|
alloy = { workspace = true, optional = true }
|
||||||
tonic.workspace = true
|
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
|
||||||
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,6 +1,5 @@
|
|||||||
use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge};
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata,
|
ClientMetadata, format_challenge,
|
||||||
proto::{
|
proto::{
|
||||||
client::{
|
client::{
|
||||||
ClientRequest,
|
ClientRequest,
|
||||||
@@ -15,6 +14,7 @@ use arbiter_proto::{
|
|||||||
shared::ClientInfo as ProtoClientInfo,
|
shared::ClientInfo as ProtoClientInfo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use ed25519_dalek::Signer 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: &SigningKey,
|
key: &ed25519_dalek::SigningKey,
|
||||||
) -> 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.public_key().to_bytes(),
|
pubkey: key.verifying_key().to_bytes().to_vec(),
|
||||||
client_info: Some(ProtoClientInfo {
|
client_info: Some(ProtoClientInfo {
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
@@ -95,14 +95,11 @@ async fn receive_auth_challenge(
|
|||||||
|
|
||||||
async fn send_auth_challenge_solution(
|
async fn send_auth_challenge_solution(
|
||||||
transport: &mut ClientTransport,
|
transport: &mut ClientTransport,
|
||||||
key: &SigningKey,
|
key: &ed25519_dalek::SigningKey,
|
||||||
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
|
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
||||||
.sign_message(&challenge_payload, CLIENT_CONTEXT)
|
|
||||||
.map_err(|_| AuthError::UnexpectedAuthResponse)?
|
|
||||||
.to_bytes();
|
|
||||||
|
|
||||||
transport
|
transport
|
||||||
.send(ClientRequest {
|
.send(ClientRequest {
|
||||||
@@ -143,7 +140,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: &SigningKey,
|
key: &ed25519_dalek::SigningKey,
|
||||||
) -> 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,4 +1,3 @@
|
|||||||
use arbiter_crypto::authn::SigningKey;
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
||||||
};
|
};
|
||||||
@@ -61,7 +60,7 @@ impl ArbiterClient {
|
|||||||
pub async fn connect_with_key(
|
pub async fn connect_with_key(
|
||||||
url: ArbiterUrl,
|
url: ArbiterUrl,
|
||||||
metadata: ClientMetadata,
|
metadata: ClientMetadata,
|
||||||
key: SigningKey,
|
key: ed25519_dalek::SigningKey,
|
||||||
) -> 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,3 @@
|
|||||||
use arbiter_crypto::authn::SigningKey;
|
|
||||||
use arbiter_proto::home_path;
|
use arbiter_proto::home_path;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ pub enum StorageError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait SigningKeyStorage {
|
pub trait SigningKeyStorage {
|
||||||
fn load_or_create(&self) -> std::result::Result<SigningKey, StorageError>;
|
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -21,7 +20,7 @@ pub struct FileSigningKeyStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileSigningKeyStorage {
|
impl FileSigningKeyStorage {
|
||||||
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ml_dsa.key";
|
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.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() }
|
||||||
@@ -31,7 +30,7 @@ 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<SigningKey, StorageError> {
|
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
||||||
let bytes = std::fs::read(path)?;
|
let bytes = std::fs::read(path)?;
|
||||||
let raw: [u8; 32] =
|
let raw: [u8; 32] =
|
||||||
bytes
|
bytes
|
||||||
@@ -40,12 +39,12 @@ impl FileSigningKeyStorage {
|
|||||||
expected: 32,
|
expected: 32,
|
||||||
actual: v.len(),
|
actual: v.len(),
|
||||||
})?;
|
})?;
|
||||||
Ok(SigningKey::from_seed(raw))
|
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SigningKeyStorage for FileSigningKeyStorage {
|
impl SigningKeyStorage for FileSigningKeyStorage {
|
||||||
fn load_or_create(&self) -> std::result::Result<SigningKey, StorageError> {
|
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, 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)?;
|
||||||
}
|
}
|
||||||
@@ -54,8 +53,8 @@ impl SigningKeyStorage for FileSigningKeyStorage {
|
|||||||
return Self::read_key(&self.path);
|
return Self::read_key(&self.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = SigningKey::generate();
|
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let raw_key = key.to_seed();
|
let raw_key = key.to_bytes();
|
||||||
|
|
||||||
// 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()
|
||||||
@@ -104,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_seed(), key_b.to_seed());
|
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
|
||||||
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,6 +59,10 @@ 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,
|
||||||
|
|||||||
1
server/crates/arbiter-crypto/.gitignore
vendored
1
server/crates/arbiter-crypto/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/target
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "arbiter-crypto"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
ml-dsa = {workspace = true, optional = true }
|
|
||||||
rand = {workspace = true, optional = true}
|
|
||||||
base64 = {workspace = true, optional = true }
|
|
||||||
memsafe = {version = "0.4.0", optional = true}
|
|
||||||
hmac.workspace = true
|
|
||||||
alloy.workspace = true
|
|
||||||
chrono.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["authn", "safecell"]
|
|
||||||
authn = ["dep:ml-dsa", "dep:rand", "dep:base64"]
|
|
||||||
safecell = ["dep:memsafe"]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod v1;
|
|
||||||
pub use v1::*;
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
|
||||||
use hmac::digest::Digest;
|
|
||||||
use ml_dsa::{
|
|
||||||
EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature,
|
|
||||||
SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
|
|
||||||
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
|
|
||||||
|
|
||||||
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
|
|
||||||
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
|
|
||||||
concat_form.into_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type KeyParams = MlDsa87;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct PublicKey(Box<MlDsaVerifyingKey<KeyParams>>);
|
|
||||||
|
|
||||||
impl crate::hashing::Hashable for PublicKey {
|
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
|
||||||
hasher.update(self.to_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct Signature(Box<MlDsaSignature<KeyParams>>);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SigningKey(Box<MlDsaSigningKey<KeyParams>>);
|
|
||||||
|
|
||||||
impl PublicKey {
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
self.0.encode().0.to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
|
|
||||||
self.0.verify_with_context(
|
|
||||||
&format_challenge(nonce, &self.to_bytes()),
|
|
||||||
context,
|
|
||||||
&signature.0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signature {
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
self.0.encode().0.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SigningKey {
|
|
||||||
pub fn generate() -> Self {
|
|
||||||
Self(Box::new(KeyParams::key_gen(&mut rand::rng())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_seed(seed: [u8; 32]) -> Self {
|
|
||||||
Self(Box::new(KeyParams::from_seed(&Seed::from(seed))))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_seed(&self) -> [u8; 32] {
|
|
||||||
self.0.to_seed().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn public_key(&self) -> PublicKey {
|
|
||||||
self.0.verifying_key().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sign_message(&self, message: &[u8], context: &[u8]) -> Result<Signature, Error> {
|
|
||||||
self.0
|
|
||||||
.signing_key()
|
|
||||||
.sign_deterministic(message, context)
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result<Signature, Error> {
|
|
||||||
self.sign_message(
|
|
||||||
&format_challenge(nonce, &self.public_key().to_bytes()),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 From<MlDsaSigningKey<KeyParams>> for SigningKey {
|
|
||||||
fn from(value: MlDsaSigningKey<KeyParams>) -> Self {
|
|
||||||
Self(Box::new(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Vec<u8>> for PublicKey {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
|
||||||
Self::try_from(value.as_slice())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Vec<u8>> for Signature {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
|
||||||
Self::try_from(value.as_slice())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
|
|
||||||
|
|
||||||
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT};
|
|
||||||
|
|
||||||
#[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 = SigningKey::generate();
|
|
||||||
let signature = key
|
|
||||||
.sign_message(b"challenge", CLIENT_CONTEXT)
|
|
||||||
.expect("signature should be created");
|
|
||||||
|
|
||||||
let decoded =
|
|
||||||
Signature::try_from(signature.to_bytes().as_slice()).expect("signature should decode");
|
|
||||||
|
|
||||||
assert_eq!(decoded, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn challenge_verification_uses_context_and_canonical_key_bytes() {
|
|
||||||
let key = SigningKey::generate();
|
|
||||||
let public_key = key.public_key();
|
|
||||||
let nonce = 17;
|
|
||||||
let signature = key
|
|
||||||
.sign_challenge(nonce, CLIENT_CONTEXT)
|
|
||||||
.expect("signature should be created");
|
|
||||||
|
|
||||||
assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature));
|
|
||||||
assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn signing_key_round_trip_seed_preserves_public_key_and_signing() {
|
|
||||||
let original = SigningKey::generate();
|
|
||||||
let restored = SigningKey::from_seed(original.to_seed());
|
|
||||||
|
|
||||||
assert_eq!(restored.public_key(), original.public_key());
|
|
||||||
|
|
||||||
let signature = restored
|
|
||||||
.sign_challenge(9, CLIENT_CONTEXT)
|
|
||||||
.expect("signature should be created");
|
|
||||||
|
|
||||||
assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#[cfg(feature = "authn")]
|
|
||||||
pub mod authn;
|
|
||||||
pub mod hashing;
|
|
||||||
#[cfg(feature = "safecell")]
|
|
||||||
pub mod safecell;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "arbiter-macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
proc-macro2 = "1.0"
|
|
||||||
quote = "1.0"
|
|
||||||
syn = { version = "2.0", features = ["derive", "fold", "full", "visit-mut"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
arbiter-crypto = { path = "../arbiter-crypto" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
use proc_macro2::{Span, TokenStream, TokenTree};
|
|
||||||
use quote::quote;
|
|
||||||
use syn::parse_quote;
|
|
||||||
use syn::spanned::Spanned;
|
|
||||||
use syn::{DataStruct, DeriveInput, Fields, Generics, Index};
|
|
||||||
|
|
||||||
use crate::utils::{HASHABLE_TRAIT_PATH, HMAC_DIGEST_PATH};
|
|
||||||
|
|
||||||
pub(crate) fn derive(input: &DeriveInput) -> TokenStream {
|
|
||||||
match &input.data {
|
|
||||||
syn::Data::Struct(struct_data) => hashable_struct(input, struct_data),
|
|
||||||
syn::Data::Enum(_) => {
|
|
||||||
syn::Error::new_spanned(input, "Hashable can currently be derived only for structs")
|
|
||||||
.to_compile_error()
|
|
||||||
}
|
|
||||||
syn::Data::Union(_) => {
|
|
||||||
syn::Error::new_spanned(input, "Hashable cannot be derived for unions")
|
|
||||||
.to_compile_error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hashable_struct(input: &DeriveInput, struct_data: &syn::DataStruct) -> TokenStream {
|
|
||||||
let ident = &input.ident;
|
|
||||||
let hashable_trait = HASHABLE_TRAIT_PATH.to_path();
|
|
||||||
let hmac_digest = HMAC_DIGEST_PATH.to_path();
|
|
||||||
let generics = add_hashable_bounds(input.generics.clone(), &hashable_trait);
|
|
||||||
let field_accesses = collect_field_accesses(struct_data);
|
|
||||||
let hash_calls = build_hash_calls(&field_accesses, &hashable_trait);
|
|
||||||
|
|
||||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#[automatically_derived]
|
|
||||||
impl #impl_generics #hashable_trait for #ident #ty_generics #where_clause {
|
|
||||||
fn hash<H: #hmac_digest>(&self, hasher: &mut H) {
|
|
||||||
#(#hash_calls)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_hashable_bounds(mut generics: Generics, hashable_trait: &syn::Path) -> Generics {
|
|
||||||
for type_param in generics.type_params_mut() {
|
|
||||||
type_param.bounds.push(parse_quote!(#hashable_trait));
|
|
||||||
}
|
|
||||||
|
|
||||||
generics
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FieldAccess {
|
|
||||||
access: TokenStream,
|
|
||||||
span: Span,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> {
|
|
||||||
match &struct_data.fields {
|
|
||||||
Fields::Named(fields) => {
|
|
||||||
// Keep deterministic alphabetical order for named fields.
|
|
||||||
// Do not remove this sort, because it keeps hash output stable regardless of source order.
|
|
||||||
let mut named_fields = fields
|
|
||||||
.named
|
|
||||||
.iter()
|
|
||||||
.map(|field| {
|
|
||||||
let name = field
|
|
||||||
.ident
|
|
||||||
.as_ref()
|
|
||||||
.expect("Fields::Named(fields) must have names")
|
|
||||||
.clone();
|
|
||||||
(name.to_string(), name)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
named_fields.sort_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
|
|
||||||
named_fields
|
|
||||||
.into_iter()
|
|
||||||
.map(|(_, name)| FieldAccess {
|
|
||||||
access: quote! { #name },
|
|
||||||
span: name.span(),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
Fields::Unnamed(fields) => fields
|
|
||||||
.unnamed
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, field)| FieldAccess {
|
|
||||||
access: {
|
|
||||||
let index = Index::from(i);
|
|
||||||
quote! { #index }
|
|
||||||
},
|
|
||||||
span: field.ty.span(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
Fields::Unit => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_hash_calls(
|
|
||||||
field_accesses: &[FieldAccess],
|
|
||||||
hashable_trait: &syn::Path,
|
|
||||||
) -> Vec<TokenStream> {
|
|
||||||
field_accesses
|
|
||||||
.iter()
|
|
||||||
.map(|field| {
|
|
||||||
let access = &field.access;
|
|
||||||
let call = quote! {
|
|
||||||
#hashable_trait::hash(&self.#access, hasher);
|
|
||||||
};
|
|
||||||
|
|
||||||
respan(call, field.span)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively set span on all tokens, including interpolated ones.
|
|
||||||
fn respan(tokens: TokenStream, span: Span) -> TokenStream {
|
|
||||||
tokens
|
|
||||||
.into_iter()
|
|
||||||
.map(|tt| match tt {
|
|
||||||
TokenTree::Group(g) => {
|
|
||||||
let mut new = proc_macro2::Group::new(g.delimiter(), respan(g.stream(), span));
|
|
||||||
new.set_span(span);
|
|
||||||
TokenTree::Group(new)
|
|
||||||
}
|
|
||||||
mut other => {
|
|
||||||
other.set_span(span);
|
|
||||||
other
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
use syn::{DeriveInput, parse_macro_input};
|
|
||||||
|
|
||||||
mod hashable;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
#[proc_macro_derive(Hashable)]
|
|
||||||
pub fn derive_hashable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|
||||||
let input = parse_macro_input!(input as DeriveInput);
|
|
||||||
hashable::derive(&input).into()
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
pub struct ToPath(pub &'static str);
|
|
||||||
|
|
||||||
impl ToPath {
|
|
||||||
pub fn to_path(&self) -> syn::Path {
|
|
||||||
syn::parse_str(self.0).expect("Invalid path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! ensure_path {
|
|
||||||
($path:path) => {{
|
|
||||||
#[cfg(test)]
|
|
||||||
#[expect(unused_imports)]
|
|
||||||
use $path as _;
|
|
||||||
ToPath(stringify!($path))
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HASHABLE_TRAIT_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Hashable);
|
|
||||||
pub const HMAC_DIGEST_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Digest);
|
|
||||||
@@ -17,7 +17,7 @@ url = "2.5.8"
|
|||||||
miette.workspace = true
|
miette.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
rustls-pki-types.workspace = true
|
rustls-pki-types.workspace = true
|
||||||
base64.workspace = true
|
base64 = "0.22.1"
|
||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
pub mod transport;
|
pub mod transport;
|
||||||
pub mod url;
|
pub mod url;
|
||||||
|
|
||||||
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||||
|
|
||||||
pub mod proto {
|
pub mod proto {
|
||||||
tonic::include_proto!("arbiter");
|
tonic::include_proto!("arbiter");
|
||||||
|
|
||||||
@@ -82,3 +84,8 @@ pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {
|
|||||||
|
|
||||||
Ok(arbiter_home)
|
Ok(arbiter_home)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
|
||||||
|
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
|
||||||
|
concat_form.into_bytes()
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ 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"
|
||||||
arbiter-crypto.path = "../arbiter-crypto"
|
|
||||||
arbiter-macros.path = "../arbiter-macros"
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tonic.workspace = true
|
tonic.workspace = true
|
||||||
@@ -37,15 +37,21 @@ dashmap = "6.1.0"
|
|||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
rcgen.workspace = true
|
rcgen.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
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.workspace = true
|
hmac = "0.12"
|
||||||
spki.workspace = true
|
spki.workspace = true
|
||||||
alloy.workspace = true
|
alloy.workspace = true
|
||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
@@ -55,10 +61,8 @@ 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"
|
||||||
ml-dsa.workspace = true
|
macro_rules_attribute = "0.2.2"
|
||||||
ed25519-dalek.workspace = true
|
paste = "1.0.15"
|
||||||
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),
|
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
||||||
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,6 +1,5 @@
|
|||||||
use arbiter_crypto::authn::{self, CLIENT_CONTEXT};
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata,
|
ClientMetadata, format_challenge,
|
||||||
transport::{Bi, expect_message},
|
transport::{Bi, expect_message},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -9,6 +8,7 @@ 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 +18,7 @@ use crate::{
|
|||||||
flow_coordinator::{self, RequestClientApproval},
|
flow_coordinator::{self, RequestClientApproval},
|
||||||
keyholder::KeyHolder,
|
keyholder::KeyHolder,
|
||||||
},
|
},
|
||||||
crypto::integrity::{self, AttestationStatus},
|
crypto::integrity::{self, Verified, verified::VerifiedFieldsAccessor},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||||
@@ -62,20 +62,17 @@ pub enum ApproveError {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Inbound {
|
pub enum Inbound {
|
||||||
AuthChallengeRequest {
|
AuthChallengeRequest {
|
||||||
pubkey: authn::PublicKey,
|
pubkey: VerifyingKey,
|
||||||
metadata: ClientMetadata,
|
metadata: ClientMetadata,
|
||||||
},
|
},
|
||||||
AuthChallengeSolution {
|
AuthChallengeSolution {
|
||||||
signature: authn::Signature,
|
signature: Signature,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Outbound {
|
pub enum Outbound {
|
||||||
AuthChallenge {
|
AuthChallenge { pubkey: VerifyingKey, nonce: i32 },
|
||||||
pubkey: authn::PublicKey,
|
|
||||||
nonce: i32,
|
|
||||||
},
|
|
||||||
AuthSuccess,
|
AuthSuccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +80,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: &authn::PublicKey,
|
pubkey: &VerifyingKey,
|
||||||
) -> Result<Option<(i32, i32)>, Error> {
|
) -> Result<Option<(i32, i32)>, Error> {
|
||||||
let pubkey_bytes = pubkey.to_bytes();
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
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
|
||||||
@@ -102,53 +99,14 @@ 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: &authn::PublicKey,
|
pubkey: &VerifyingKey,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let pubkey_bytes = pubkey.to_bytes();
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
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");
|
||||||
@@ -157,7 +115,6 @@ 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))
|
||||||
@@ -170,7 +127,7 @@ async fn create_nonce(
|
|||||||
conn,
|
conn,
|
||||||
&keyholder,
|
&keyholder,
|
||||||
&ClientCredentials {
|
&ClientCredentials {
|
||||||
pubkey: pubkey.clone(),
|
pubkey: *pubkey,
|
||||||
nonce: new_nonce,
|
nonce: new_nonce,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
@@ -179,7 +136,8 @@ 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)
|
||||||
})
|
})
|
||||||
@@ -213,11 +171,10 @@ 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: &authn::PublicKey,
|
pubkey: &VerifyingKey,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<Verified<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| {
|
||||||
@@ -227,7 +184,6 @@ 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;
|
||||||
|
|
||||||
@@ -243,7 +199,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.to_bytes()),
|
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
||||||
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),
|
||||||
))
|
))
|
||||||
@@ -252,11 +208,11 @@ async fn insert_client(
|
|||||||
.get_result::<i32>(conn)
|
.get_result::<i32>(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
integrity::sign_entity(
|
let verified_id = integrity::sign_entity(
|
||||||
conn,
|
conn,
|
||||||
&keyholder,
|
&keyholder,
|
||||||
&ClientCredentials {
|
&ClientCredentials {
|
||||||
pubkey: pubkey.clone(),
|
pubkey: *pubkey,
|
||||||
nonce: NONCE_START,
|
nonce: NONCE_START,
|
||||||
},
|
},
|
||||||
client_id,
|
client_id,
|
||||||
@@ -265,9 +221,10 @@ 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(client_id)
|
Ok(verified_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -346,17 +303,14 @@ async fn sync_client_metadata(
|
|||||||
|
|
||||||
async fn challenge_client<T>(
|
async fn challenge_client<T>(
|
||||||
transport: &mut T,
|
transport: &mut T,
|
||||||
pubkey: authn::PublicKey,
|
pubkey: VerifyingKey,
|
||||||
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 {
|
.send(Ok(Outbound::AuthChallenge { pubkey, nonce }))
|
||||||
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");
|
||||||
@@ -373,15 +327,20 @@ where
|
|||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) {
|
let formatted = format_challenge(nonce, pubkey.as_bytes());
|
||||||
|
|
||||||
|
pubkey.verify_strict(&formatted, &signature).map_err(|_| {
|
||||||
error!("Challenge solution verification failed");
|
error!("Challenge solution verification failed");
|
||||||
return Err(Error::InvalidChallengeSolution);
|
Error::InvalidChallengeSolution
|
||||||
}
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate<T>(props: &mut ClientConnection, transport: &mut T) -> Result<i32, Error>
|
pub async fn authenticate<T>(
|
||||||
|
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,
|
||||||
{
|
{
|
||||||
@@ -389,16 +348,34 @@ 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((id, _)) => {
|
Some((nonce, id)) => {
|
||||||
verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?;
|
let mut db_conn = props.db.get().await.map_err(|e| {
|
||||||
id
|
error!(error = ?e, "Database pool error");
|
||||||
|
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.clone(),
|
pubkey,
|
||||||
metadata: metadata.clone(),
|
metadata: metadata.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -407,7 +384,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?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
use arbiter_crypto::authn;
|
|
||||||
use arbiter_proto::{ClientMetadata, transport::Bi};
|
use arbiter_proto::{ClientMetadata, transport::Bi};
|
||||||
use kameo::actor::Spawn;
|
use kameo::actor::Spawn;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::session::ClientSession},
|
actors::{GlobalActors, client::session::ClientSession},
|
||||||
crypto::integrity::Integrable,
|
crypto::integrity::{Integrable, hashing::Hashable},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClientProfile {
|
pub struct ClientProfile {
|
||||||
pub pubkey: authn::PublicKey,
|
pub pubkey: ed25519_dalek::VerifyingKey,
|
||||||
pub metadata: ClientMetadata,
|
pub metadata: ClientMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(arbiter_macros::Hashable)]
|
|
||||||
pub struct ClientCredentials {
|
pub struct ClientCredentials {
|
||||||
pub pubkey: authn::PublicKey,
|
pub pubkey: ed25519_dalek::VerifyingKey,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +23,13 @@ impl Integrable for ClientCredentials {
|
|||||||
const KIND: &'static str = "client_credentials";
|
const KIND: &'static str = "client_credentials";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hashable for ClientCredentials {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.pubkey.as_bytes());
|
||||||
|
self.nonce.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ClientConnection {
|
pub struct ClientConnection {
|
||||||
pub(crate) db: db::DatabasePool,
|
pub(crate) db: db::DatabasePool,
|
||||||
pub(crate) actors: GlobalActors,
|
pub(crate) actors: GlobalActors,
|
||||||
@@ -43,9 +48,7 @@ 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,
|
||||||
{
|
{
|
||||||
let fut = auth::authenticate(&mut props, transport);
|
match auth::authenticate(&mut props, transport).await {
|
||||||
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,23 +5,25 @@ 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,
|
||||||
},
|
},
|
||||||
db,
|
crypto::integrity::Verified,
|
||||||
evm::VetError,
|
evm::VetError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::{actors::GlobalActors, db};
|
||||||
|
|
||||||
pub struct ClientSession {
|
pub struct ClientSession {
|
||||||
props: ClientConnection,
|
props: ClientConnection,
|
||||||
client_id: i32,
|
client_id: Verified<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
|
pub(crate) fn new(props: ClientConnection, client_id: Verified<i32>) -> Self {
|
||||||
Self { props, client_id }
|
Self { props, client_id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,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,
|
||||||
})
|
})
|
||||||
@@ -92,11 +94,12 @@ 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: 0,
|
client_id: Verified::new_unchecked(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||||
crypto::integrity,
|
crypto::integrity::{self, Integrable, Verified, hashing::Hashable},
|
||||||
db::{
|
db::{
|
||||||
DatabaseError, DatabasePool,
|
DatabaseError, DatabasePool,
|
||||||
models::{self},
|
models::{self},
|
||||||
@@ -21,16 +21,42 @@ use crate::{
|
|||||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
|
|
||||||
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),
|
||||||
|
|
||||||
@@ -45,6 +71,9 @@ 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)]
|
||||||
@@ -88,7 +117,7 @@ impl EvmActor {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl EvmActor {
|
impl EvmActor {
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn generate(&mut self) -> Result<(i32, Address), Error> {
|
pub async fn generate(&mut self) -> Result<(Verified<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()));
|
||||||
@@ -100,7 +129,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 = insert_into(schema::evm_wallet::table)
|
let wallet_id: i32 = 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,
|
||||||
@@ -110,7 +139,17 @@ impl EvmActor {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?;
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
Ok((wallet_id, address))
|
// Sign integrity envelope to bind encrypted key to wallet 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]
|
||||||
@@ -136,7 +175,7 @@ impl EvmActor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: SharedGrantSettings,
|
basic: SharedGrantSettings,
|
||||||
grant: SpecificGrant,
|
grant: SpecificGrant,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<integrity::Verified<i32>, Error> {
|
||||||
match grant {
|
match grant {
|
||||||
SpecificGrant::EtherTransfer(settings) => self
|
SpecificGrant::EtherTransfer(settings) => self
|
||||||
.engine
|
.engine
|
||||||
@@ -207,9 +246,23 @@ 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.id))
|
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.entity_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
|
||||||
@@ -242,9 +295,23 @@ 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.id))
|
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.entity_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
|
||||||
@@ -263,6 +330,12 @@ 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?;
|
||||||
|
|||||||
@@ -9,17 +9,22 @@ use kameo::{Actor, Reply, messages};
|
|||||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::crypto::{
|
use crate::{
|
||||||
KeyCell, derive_key,
|
crypto::{
|
||||||
encryption::v1::{self, Nonce},
|
KeyCell, derive_key,
|
||||||
integrity::v1::HmacSha256,
|
encryption::v1::{self, Nonce},
|
||||||
|
integrity::v1::HmacSha256,
|
||||||
|
},
|
||||||
|
safe_cell::SafeCell,
|
||||||
};
|
};
|
||||||
use crate::db::{
|
use crate::{
|
||||||
self,
|
db::{
|
||||||
models::{self, RootKeyHistory},
|
self,
|
||||||
schema::{self},
|
models::{self, RootKeyHistory},
|
||||||
|
schema::{self},
|
||||||
|
},
|
||||||
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
|
|
||||||
#[derive(Default, EnumDiscriminants)]
|
#[derive(Default, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||||
@@ -395,8 +400,10 @@ mod tests {
|
|||||||
|
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
use crate::db::{self};
|
use crate::{
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
db::{self},
|
||||||
|
safe_cell::SafeCell,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
use arbiter_crypto::authn;
|
|
||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::actors::user_agent::{
|
use crate::actors::user_agent::{
|
||||||
UserAgentConnection,
|
AuthPublicKey, UserAgentConnection,
|
||||||
auth::state::{AuthContext, AuthStateMachine},
|
auth::state::{AuthContext, AuthStateMachine},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::*;
|
use state::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Inbound {
|
pub enum Inbound {
|
||||||
AuthChallengeRequest {
|
AuthChallengeRequest {
|
||||||
pubkey: authn::PublicKey,
|
pubkey: AuthPublicKey,
|
||||||
bootstrap_token: Option<String>,
|
bootstrap_token: Option<String>,
|
||||||
},
|
},
|
||||||
AuthChallengeSolution {
|
AuthChallengeSolution {
|
||||||
@@ -30,17 +30,26 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
fn internal(details: impl Into<String>) -> Self {
|
#[track_caller]
|
||||||
Self::Internal {
|
pub(super) fn internal(details: impl Into<String>, err: &impl std::fmt::Debug) -> Self {
|
||||||
details: details.into(),
|
let 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 {
|
||||||
error!(?e, "Database error");
|
Self::internal("Database error", &e)
|
||||||
Self::internal("Database error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +80,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<authn::PublicKey, Error>
|
) -> Result<AuthPublicKey, Error>
|
||||||
where
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + Send,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
|
|
||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::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};
|
||||||
@@ -10,24 +9,24 @@ use crate::{
|
|||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
keyholder::KeyHolder,
|
keyholder::KeyHolder,
|
||||||
user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||||
},
|
},
|
||||||
crypto::integrity,
|
crypto::integrity,
|
||||||
db::{DatabasePool, schema::useragent_client},
|
db::{DatabasePool, schema::useragent_client},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ChallengeRequest {
|
pub struct ChallengeRequest {
|
||||||
pub pubkey: authn::PublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BootstrapAuthRequest {
|
pub struct BootstrapAuthRequest {
|
||||||
pub pubkey: authn::PublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChallengeContext {
|
pub struct ChallengeContext {
|
||||||
pub challenge_nonce: i32,
|
pub challenge_nonce: i32,
|
||||||
pub key: authn::PublicKey,
|
pub key: AuthPublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChallengeSolution {
|
pub struct ChallengeSolution {
|
||||||
@@ -39,25 +38,26 @@ 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(authn::PublicKey),
|
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey),
|
||||||
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(authn::PublicKey),
|
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 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: &authn::PublicKey,
|
key: &AuthPublicKey,
|
||||||
) -> Result<(i32, i32), Error> {
|
) -> Result<(i32, i32), Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.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_bytes()))
|
.filter(useragent_client::public_key.eq(key.to_stored_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,10 +65,7 @@ async fn get_current_nonce_and_id(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database operation failed", &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
|
||||||
@@ -78,16 +75,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: &authn::PublicKey,
|
pubkey: &AuthPublicKey,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.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 _result = integrity::verify_entity(
|
let attestation_status = integrity::check_entity_attestation(
|
||||||
&mut db_conn,
|
&mut db_conn,
|
||||||
keyholder,
|
keyholder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
@@ -97,36 +94,39 @@ async fn verify_integrity(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Integrity verification failed", &e))?;
|
||||||
error!(?e, "Integrity verification failed");
|
|
||||||
Error::internal("Integrity verification failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
use integrity::AttestationStatus as AS;
|
||||||
|
// 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: &authn::PublicKey,
|
pubkey: &AuthPublicKey,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.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_bytes()))
|
.filter(useragent_client::public_key.eq(pubkey.to_stored_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| {
|
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
integrity::sign_entity(
|
integrity::sign_entity(
|
||||||
conn,
|
conn,
|
||||||
@@ -138,10 +138,8 @@ async fn create_nonce(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database error", &e))?
|
||||||
error!(?e, "Integrity signature update failed");
|
.drop_verification_provenance();
|
||||||
Error::internal("Database error")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Result::<_, Error>::Ok(new_nonce)
|
Result::<_, Error>::Ok(new_nonce)
|
||||||
})
|
})
|
||||||
@@ -153,13 +151,14 @@ async fn create_nonce(
|
|||||||
async fn register_key(
|
async fn register_key(
|
||||||
db: &DatabasePool,
|
db: &DatabasePool,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &authn::PublicKey,
|
pubkey: &AuthPublicKey,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let pubkey_bytes = pubkey.to_bytes();
|
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let key_type = pubkey.key_type();
|
||||||
error!(error = ?e, "Database pool error");
|
let mut conn = db
|
||||||
Error::internal("Database unavailable")
|
.get()
|
||||||
})?;
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
|
|
||||||
conn.transaction(|conn| {
|
conn.transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -169,26 +168,37 @@ 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| {
|
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let entity = UserAgentCredentials {
|
if let Err(e) = integrity::sign_entity(
|
||||||
pubkey: pubkey.clone(),
|
conn,
|
||||||
nonce: NONCE_START,
|
keyholder,
|
||||||
};
|
&UserAgentCredentials {
|
||||||
|
pubkey: pubkey.clone(),
|
||||||
integrity::sign_entity(conn, keyholder, &entity, id)
|
nonce: NONCE_START,
|
||||||
.await
|
},
|
||||||
.map_err(|e| {
|
id,
|
||||||
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
|
)
|
||||||
Error::internal("Failed to register public key")
|
.await
|
||||||
})?;
|
{
|
||||||
|
match e {
|
||||||
|
integrity::Error::Keyholder(
|
||||||
|
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(())
|
||||||
})
|
})
|
||||||
@@ -242,7 +252,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<authn::PublicKey, Self::Error> {
|
) -> Result<AuthPublicKey, Self::Error> {
|
||||||
let token_ok: bool = self
|
let token_ok: bool = self
|
||||||
.conn
|
.conn
|
||||||
.actors
|
.actors
|
||||||
@@ -251,10 +261,7 @@ where
|
|||||||
token: token.clone(),
|
token: token.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Failed to consume bootstrap token", &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");
|
||||||
@@ -290,13 +297,35 @@ where
|
|||||||
key,
|
key,
|
||||||
}: &ChallengeContext,
|
}: &ChallengeContext,
|
||||||
ChallengeSolution { solution }: ChallengeSolution,
|
ChallengeSolution { solution }: ChallengeSolution,
|
||||||
) -> Result<authn::PublicKey, Self::Error> {
|
) -> Result<AuthPublicKey, Self::Error> {
|
||||||
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| {
|
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
|
||||||
error!("Failed to decode signature in challenge solution");
|
|
||||||
Error::InvalidChallengeSolution
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let valid = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature);
|
let valid = match key {
|
||||||
|
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,13 +1,22 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::ClientProfile},
|
actors::{GlobalActors, client::ClientProfile},
|
||||||
crypto::integrity::Integrable,
|
crypto::integrity::Integrable,
|
||||||
db,
|
db::{self, models::KeyType},
|
||||||
};
|
};
|
||||||
use arbiter_crypto::authn;
|
|
||||||
|
|
||||||
#[derive(Debug, arbiter_macros::Hashable)]
|
/// 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)]
|
||||||
pub struct UserAgentCredentials {
|
pub struct UserAgentCredentials {
|
||||||
pub pubkey: authn::PublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,11 +24,67 @@ 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: authn::PublicKey },
|
ClientConnectionCancel { pubkey: ed25519_dalek::VerifyingKey },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserAgentConnection {
|
pub struct UserAgentConnection {
|
||||||
@@ -38,3 +103,18 @@ pub mod session;
|
|||||||
|
|
||||||
pub use auth::authenticate;
|
pub use auth::authenticate;
|
||||||
pub use session::UserAgentSession;
|
pub use session::UserAgentSession;
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for AuthPublicKey {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.to_stored_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for UserAgentCredentials {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.pubkey.hash(hasher);
|
||||||
|
self.nonce.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use arbiter_crypto::authn;
|
|
||||||
|
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
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;
|
||||||
@@ -13,6 +12,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},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ impl Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct PendingClientApproval {
|
pub struct PendingClientApproval {
|
||||||
pubkey: authn::PublicKey,
|
|
||||||
controller: ActorRef<ClientApprovalController>,
|
controller: ActorRef<ClientApprovalController>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ pub struct UserAgentSession {
|
|||||||
state: UserAgentStateMachine<DummyContext>,
|
state: UserAgentStateMachine<DummyContext>,
|
||||||
sender: Box<dyn Sender<OutOfBand>>,
|
sender: Box<dyn Sender<OutOfBand>>,
|
||||||
|
|
||||||
pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>,
|
pending_client_approvals: HashMap<VerifyingKey, PendingClientApproval>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
@@ -119,13 +118,8 @@ impl UserAgentSession {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pending_client_approvals.insert(
|
self.pending_client_approvals
|
||||||
client.pubkey.to_bytes(),
|
.insert(client.pubkey, PendingClientApproval { controller });
|
||||||
PendingClientApproval {
|
|
||||||
pubkey: client.pubkey,
|
|
||||||
controller,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,18 +158,14 @@ 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.clone()));
|
.find_map(|(k, v)| (v.controller.id() == id).then_some(*k));
|
||||||
|
|
||||||
if let Some(pubkey_bytes) = cancelled_pubkey {
|
if let Some(pubkey) = cancelled_pubkey {
|
||||||
let Some(approval) = self.pending_client_approvals.remove(&pubkey_bytes) else {
|
self.pending_client_approvals.remove(&pubkey);
|
||||||
return Ok(std::ops::ControlFlow::Continue(()));
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.sender
|
.sender
|
||||||
.send(OutOfBand::ClientConnectionCancel {
|
.send(OutOfBand::ClientConnectionCancel { pubkey })
|
||||||
pubkey: approval.pubkey,
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
error!(
|
error!(
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||||
use arbiter_crypto::{
|
|
||||||
authn,
|
|
||||||
safecell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
@@ -14,26 +10,89 @@ 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::actors::{
|
|
||||||
evm::{
|
|
||||||
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
|
|
||||||
UseragentCreateGrant, UseragentListGrants,
|
|
||||||
},
|
|
||||||
keyholder::{self, Bootstrap, TryUnseal},
|
|
||||||
user_agent::session::{
|
|
||||||
UserAgentSession,
|
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
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::{
|
||||||
|
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
||||||
|
crypto::integrity::{self, Verified},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
actors::{
|
||||||
|
evm::{
|
||||||
|
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
|
||||||
|
UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||||
|
},
|
||||||
|
keyholder::{self, Bootstrap, TryUnseal},
|
||||||
|
user_agent::session::{
|
||||||
|
UserAgentSession,
|
||||||
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
|
},
|
||||||
|
user_agent::{AuthPublicKey, UserAgentCredentials},
|
||||||
|
},
|
||||||
|
db::schema::useragent_client,
|
||||||
|
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");
|
||||||
@@ -191,6 +250,7 @@ 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(())
|
||||||
@@ -252,6 +312,7 @@ 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(())
|
||||||
@@ -297,7 +358,9 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> {
|
pub(crate) async fn handle_evm_wallet_create(
|
||||||
|
&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!(
|
||||||
@@ -325,12 +388,15 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
pub(crate) async fn handle_grant_list(
|
||||||
|
&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(Error::internal("Failed to list EVM grants"))
|
Err(GrantMutationError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +406,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<i32, GrantMutationError> {
|
) -> Result<Verified<i32>, GrantMutationError> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
@@ -349,6 +415,7 @@ 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)
|
||||||
@@ -361,21 +428,22 @@ impl UserAgentSession {
|
|||||||
&mut self,
|
&mut self,
|
||||||
grant_id: i32,
|
grant_id: i32,
|
||||||
) -> Result<(), GrantMutationError> {
|
) -> Result<(), GrantMutationError> {
|
||||||
// match self
|
match self
|
||||||
// .props
|
.props
|
||||||
// .actors
|
.actors
|
||||||
// .evm
|
.evm
|
||||||
// .ask(UseragentDeleteGrant { grant_id })
|
.ask(UseragentDeleteGrant {
|
||||||
// .await
|
_grant_id: grant_id,
|
||||||
// {
|
})
|
||||||
// Ok(()) => Ok(()),
|
.await
|
||||||
// Err(err) => {
|
{
|
||||||
// error!(?err, "EVM grant delete failed");
|
Ok(()) => Ok(()),
|
||||||
// Err(GrantMutationError::Internal)
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
// }
|
Err(err) => {
|
||||||
// }
|
error!(?err, "EVM grant delete failed");
|
||||||
let _ = grant_id;
|
Err(GrantMutationError::Internal)
|
||||||
todo!()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
@@ -475,10 +543,10 @@ 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: authn::PublicKey,
|
pubkey: ed25519_dalek::VerifyingKey,
|
||||||
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.to_bytes()) {
|
let pending_approval = match self.pending_client_approvals.remove(&pubkey) {
|
||||||
Some(approval) => approval,
|
Some(approval) => approval,
|
||||||
None => {
|
None => {
|
||||||
error!("Received client connection response for unknown client");
|
error!("Received client connection response for unknown client");
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ mod tests {
|
|||||||
use std::ops::Deref as _;
|
use std::ops::Deref as _;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::derive_key;
|
use crate::{
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
crypto::derive_key,
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn derive_seal_key_deterministic() {
|
pub fn derive_seal_key_deterministic() {
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
use crate::actors::keyholder;
|
use crate::actors::keyholder;
|
||||||
use arbiter_crypto::hashing::Hashable;
|
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
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};
|
||||||
use kameo::{actor::ActorRef, error::SendError};
|
use kameo::{actor::ActorRef, error::SendError};
|
||||||
use sha2::Digest as _;
|
use sha2::Digest as _;
|
||||||
|
|
||||||
|
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, NewIntegrityEnvelope},
|
models::{IntegrityEnvelope as IntegrityEnvelopeRow, 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}")]
|
||||||
@@ -45,71 +57,90 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
impl<T: Integrable> Integrable for &T {
|
||||||
let mut hasher = Sha256::new();
|
const KIND: &'static str = T::KIND;
|
||||||
payload.hash(&mut hasher);
|
const VERSION: i32 = T::VERSION;
|
||||||
hasher.finalize().into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
#[derive(Debug, Clone)]
|
||||||
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
pub struct EntityId(Vec<u8>);
|
||||||
out.extend_from_slice(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mac_input(
|
impl Deref for EntityId {
|
||||||
entity_kind: &str,
|
type Target = [u8];
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoId {
|
fn deref(&self) -> &Self::Target {
|
||||||
fn into_id(self) -> Vec<u8>;
|
&self.0
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoId for i32 {
|
|
||||||
fn into_id(self) -> Vec<u8> {
|
|
||||||
self.to_be_bytes().to_vec()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoId for &'_ [u8] {
|
impl From<i32> for EntityId {
|
||||||
fn into_id(self) -> Vec<u8> {
|
fn from(value: i32) -> Self {
|
||||||
self.to_vec()
|
Self(value.to_be_bytes().to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_entity<E: Integrable>(
|
impl From<&'_ [u8]> for EntityId {
|
||||||
|
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,
|
||||||
entity_id: impl IntoId,
|
as_entity_id: Id,
|
||||||
) -> Result<(), Error> {
|
) -> Result<Verified<Id, Nested<E>>, Error> {
|
||||||
let payload_hash = payload_hash(&entity);
|
let payload_hash = payload_hash(entity);
|
||||||
|
|
||||||
let entity_id = entity_id.into_id();
|
let entity_id = as_entity_id.clone().into();
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -124,7 +155,7 @@ pub async fn sign_entity<E: Integrable>(
|
|||||||
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: entity_id.to_vec(),
|
||||||
payload_version: E::VERSION,
|
payload_version: E::VERSION,
|
||||||
key_version,
|
key_version,
|
||||||
mac: mac.to_vec(),
|
mac: mac.to_vec(),
|
||||||
@@ -143,19 +174,19 @@ pub async fn sign_entity<E: Integrable>(
|
|||||||
.await
|
.await
|
||||||
.map_err(db::DatabaseError::from)?;
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(Verified::<Id, Nested<E>>::new(as_entity_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_entity<E: Integrable>(
|
pub async fn check_entity_attestation<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 IntoId,
|
entity_id: impl Into<EntityId>,
|
||||||
) -> Result<AttestationStatus, Error> {
|
) -> Result<AttestationStatus, Error> {
|
||||||
let entity_id = entity_id.into_id();
|
let entity_id = entity_id.into();
|
||||||
let envelope: IntegrityEnvelope = integrity_envelope::table
|
let envelope: IntegrityEnvelopeRow = 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 {
|
||||||
@@ -173,7 +204,7 @@ pub async fn verify_entity<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
|
||||||
@@ -196,126 +227,93 @@ pub async fn verify_entity<E: Integrable>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[derive(Debug, Clone, crate::VerifiedFields!)]
|
||||||
mod tests {
|
#[repr(C)]
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
pub struct Entity<E, Id> {
|
||||||
use diesel_async::RunQueryDsl;
|
pub entity: E,
|
||||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
pub entity_id: Id,
|
||||||
|
}
|
||||||
|
|
||||||
use crate::{
|
impl<E, Id> Deref for Entity<E, Id> {
|
||||||
actors::keyholder::{Bootstrap, KeyHolder},
|
type Target = E;
|
||||||
db::{self, schema},
|
|
||||||
};
|
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
|
|
||||||
use super::{Error, Integrable, sign_entity, verify_entity};
|
fn deref(&self) -> &Self::Target {
|
||||||
#[derive(Clone, arbiter_macros::Hashable)]
|
&self.entity
|
||||||
struct DummyEntity {
|
|
||||||
payload_version: i32,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
}
|
|
||||||
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 { .. }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
|
mod tests;
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
pub use hmac::digest::Digest;
|
use hmac::digest::Digest;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Deterministically hash a value by feeding its fields into the hasher in a consistent order.
|
/// Deterministically hash a value by feeding its fields into the hasher in a consistent order.
|
||||||
#[diagnostic::on_unimplemented(
|
|
||||||
note = "for local types consider adding `#[derive(arbiter_macros::Hashable)]` to your `{Self}` type",
|
|
||||||
note = "for types from other crates check whether the crate offers a `Hashable` implementation"
|
|
||||||
)]
|
|
||||||
pub trait Hashable {
|
pub trait Hashable {
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H);
|
fn hash<H: Digest>(&self, hasher: &mut H);
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_numeric {
|
macro_rules! impl_numeric {
|
||||||
($($t:ty),*) => {
|
($($t:ty),*) => {
|
||||||
$(
|
$(
|
||||||
impl Hashable for $t {
|
impl Hashable for $t {
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(&self.to_be_bytes());
|
hasher.update(&self.to_be_bytes());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)*
|
}
|
||||||
};
|
)*
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_numeric!(u8, u16, u32, u64, i8, i16, i32, i64);
|
impl_numeric!(u8, u16, u32, u64, i8, i16, i32, i64);
|
||||||
298
server/crates/arbiter-server/src/crypto/integrity/v1/tests.rs
Normal file
298
server/crates/arbiter-server/src/crypto/integrity/v1/tests.rs
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
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)
|
||||||
|
));
|
||||||
|
}
|
||||||
593
server/crates/arbiter-server/src/crypto/integrity/v1/verified.rs
Normal file
593
server/crates/arbiter-server/src/crypto/integrity/v1/verified.rs
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use rand::{
|
|||||||
rngs::{StdRng, SysRng},
|
rngs::{StdRng, SysRng},
|
||||||
};
|
};
|
||||||
|
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
pub mod integrity;
|
pub mod integrity;
|
||||||
@@ -141,7 +141,7 @@ mod tests {
|
|||||||
derive_key,
|
derive_key,
|
||||||
encryption::v1::{Nonce, generate_salt},
|
encryption::v1::{Nonce, generate_salt},
|
||||||
};
|
};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn encrypt_decrypt() {
|
pub fn encrypt_decrypt() {
|
||||||
|
|||||||
@@ -72,6 +72,40 @@ 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::*;
|
||||||
|
|
||||||
@@ -210,6 +244,7 @@ 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,
|
crypto::integrity::{self, Verified, VerifiedEntity, verified::VerifiedFieldsAccessor},
|
||||||
db::{
|
db::{
|
||||||
self, DatabaseError,
|
self, DatabaseError,
|
||||||
models::{
|
models::{
|
||||||
@@ -153,12 +153,39 @@ 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 grant = P::try_find_grant(&context, &mut conn)
|
let verified_settings =
|
||||||
|
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)?;
|
||||||
|
|
||||||
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
||||||
|
// (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,
|
||||||
@@ -214,7 +241,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<i32, DatabaseError>
|
) -> Result<Verified<i32>, DatabaseError>
|
||||||
where
|
where
|
||||||
P::Settings: Clone,
|
P::Settings: Clone,
|
||||||
{
|
{
|
||||||
@@ -258,22 +285,23 @@ impl Engine {
|
|||||||
|
|
||||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||||
|
|
||||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
let verified_entity_id =
|
||||||
.await
|
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
.await
|
||||||
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
|
||||||
QueryResult::Ok(basic_grant.id)
|
QueryResult::Ok(verified_entity_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(id)
|
Ok(id.unqualify_origin())
|
||||||
}
|
}
|
||||||
|
|
||||||
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<impl Iterator<Item = Grant<Y>>, ListError>
|
) -> Result<Vec<Grant<Y>>, ListError>
|
||||||
where
|
where
|
||||||
Y: From<Kind::Settings>,
|
Y: From<Kind::Settings>,
|
||||||
{
|
{
|
||||||
@@ -281,16 +309,32 @@ impl Engine {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?;
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
// Verify integrity of all grants before returning any results
|
let mut verified_grants = Vec::with_capacity(all_grants.len());
|
||||||
for grant in &all_grants {
|
|
||||||
integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?;
|
// Verify integrity of all grants before returning any results.
|
||||||
|
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(all_grants.into_iter().map(|g| Grant {
|
Ok(verified_grants)
|
||||||
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> {
|
||||||
|
|||||||
@@ -127,19 +127,19 @@ pub enum SpecificMeaning {
|
|||||||
TokenTransfer(token_transfers::Meaning),
|
TokenTransfer(token_transfers::Meaning),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, arbiter_macros::Hashable)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct TransactionRateLimit {
|
pub struct TransactionRateLimit {
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
pub window: Duration,
|
pub window: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, arbiter_macros::Hashable)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct VolumeRateLimit {
|
pub struct VolumeRateLimit {
|
||||||
pub max_volume: U256,
|
pub max_volume: U256,
|
||||||
pub window: Duration,
|
pub window: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, arbiter_macros::Hashable)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct SharedGrantSettings {
|
pub struct SharedGrantSettings {
|
||||||
pub wallet_access_id: i32,
|
pub wallet_access_id: i32,
|
||||||
pub chain: ChainId,
|
pub chain: ChainId,
|
||||||
@@ -200,7 +200,7 @@ pub enum SpecificGrant {
|
|||||||
TokenTransfer(token_transfers::Settings),
|
TokenTransfer(token_transfers::Settings),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, arbiter_macros::Hashable)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CombinedSettings<PolicyGrant> {
|
pub struct CombinedSettings<PolicyGrant> {
|
||||||
pub shared: SharedGrantSettings,
|
pub shared: SharedGrantSettings,
|
||||||
pub specific: PolicyGrant,
|
pub specific: PolicyGrant,
|
||||||
@@ -219,3 +219,38 @@ impl<P: Integrable> Integrable for CombinedSettings<P> {
|
|||||||
const KIND: &'static str = P::KIND;
|
const KIND: &'static str = P::KIND;
|
||||||
const VERSION: i32 = P::VERSION;
|
const VERSION: i32 = P::VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for TransactionRateLimit {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.count.hash(hasher);
|
||||||
|
self.window.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for VolumeRateLimit {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.max_volume.hash(hasher);
|
||||||
|
self.window.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for SharedGrantSettings {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.wallet_access_id.hash(hasher);
|
||||||
|
self.chain.hash(hasher);
|
||||||
|
self.valid_from.hash(hasher);
|
||||||
|
self.valid_until.hash(hasher);
|
||||||
|
self.max_gas_fee_per_gas.hash(hasher);
|
||||||
|
self.max_priority_fee_per_gas.hash(hasher);
|
||||||
|
self.rate_limit.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Hashable> Hashable for CombinedSettings<P> {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.shared.hash(hasher);
|
||||||
|
self.specific.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ impl From<Meaning> for SpecificMeaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
|
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
|
||||||
#[derive(Debug, Clone, arbiter_macros::Hashable)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub target: Vec<Address>,
|
pub target: Vec<Address>,
|
||||||
pub limit: VolumeRateLimit,
|
pub limit: VolumeRateLimit,
|
||||||
@@ -61,6 +61,15 @@ impl Integrable for Settings {
|
|||||||
const KIND: &'static str = "EtherTransfer";
|
const KIND: &'static str = "EtherTransfer";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for Settings {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.target.hash(hasher);
|
||||||
|
self.limit.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Settings> for SpecificGrant {
|
impl From<Settings> for SpecificGrant {
|
||||||
fn from(val: Settings) -> SpecificGrant {
|
fn from(val: Settings) -> SpecificGrant {
|
||||||
SpecificGrant::EtherTransfer(val)
|
SpecificGrant::EtherTransfer(val)
|
||||||
@@ -101,7 +110,8 @@ 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 = query_relevant_past_transaction(grant.id, window, db).await?;
|
let past_transaction =
|
||||||
|
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
|
||||||
@@ -240,21 +250,20 @@ 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),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ proptest::proptest! {
|
|||||||
) {
|
) {
|
||||||
use rand::{SeedableRng, seq::SliceRandom};
|
use rand::{SeedableRng, seq::SliceRandom};
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use arbiter_crypto::hashing::Hashable;
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect();
|
let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect();
|
||||||
let mut shuffled = addrs.clone();
|
let mut shuffled = addrs.clone();
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl From<Meaning> for SpecificMeaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
|
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
|
||||||
#[derive(Debug, Clone, arbiter_macros::Hashable)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub token_contract: Address,
|
pub token_contract: Address,
|
||||||
pub target: Option<Address>,
|
pub target: Option<Address>,
|
||||||
@@ -72,6 +72,16 @@ impl Integrable for Settings {
|
|||||||
const KIND: &'static str = "TokenTransfer";
|
const KIND: &'static str = "TokenTransfer";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for Settings {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.token_contract.hash(hasher);
|
||||||
|
self.target.hash(hasher);
|
||||||
|
self.volume_limits.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Settings> for SpecificGrant {
|
impl From<Settings> for SpecificGrant {
|
||||||
fn from(val: Settings) -> SpecificGrant {
|
fn from(val: Settings) -> SpecificGrant {
|
||||||
SpecificGrant::TokenTransfer(val)
|
SpecificGrant::TokenTransfer(val)
|
||||||
@@ -276,18 +286,16 @@ 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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ proptest::proptest! {
|
|||||||
) {
|
) {
|
||||||
use rand::{SeedableRng, seq::SliceRandom};
|
use rand::{SeedableRng, seq::SliceRandom};
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use arbiter_crypto::hashing::Hashable;
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
let limits: Vec<VolumeRateLimit> = raw_limits
|
let limits: Vec<VolumeRateLimit> = raw_limits
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
use alloy::{
|
use alloy::{
|
||||||
consensus::SignableTransaction,
|
consensus::SignableTransaction,
|
||||||
network::{TxSigner, TxSignerSync},
|
network::{TxSigner, TxSignerSync},
|
||||||
primitives::{Address, B256, ChainId, Signature},
|
primitives::{Address, B256, ChainId, Signature},
|
||||||
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
|
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
|
||||||
};
|
};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
|
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use arbiter_crypto::authn;
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata,
|
ClientMetadata,
|
||||||
proto::{
|
proto::{
|
||||||
@@ -23,6 +22,7 @@ use tracing::warn;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::client::{self, ClientConnection, auth},
|
actors::client::{self, ClientConnection, auth},
|
||||||
|
crypto::integrity::Verified,
|
||||||
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(),
|
pubkey: pubkey.to_bytes().to_vec(),
|
||||||
nonce,
|
nonce,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,11 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
.await;
|
.await;
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
|
let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) 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;
|
||||||
};
|
};
|
||||||
@@ -171,7 +175,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||||
let Ok(signature) = authn::Signature::try_from(signature.as_slice()) else {
|
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
||||||
let _ = self
|
let _ = self
|
||||||
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
||||||
.await;
|
.await;
|
||||||
@@ -197,7 +201,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<i32, auth::Error> {
|
) -> Result<Verified<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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use arbiter_crypto::authn;
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::user_agent::{
|
proto::user_agent::{
|
||||||
UserAgentRequest, UserAgentResponse,
|
UserAgentRequest, UserAgentResponse,
|
||||||
@@ -6,7 +5,8 @@ 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,
|
||||||
request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload,
|
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
|
||||||
|
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,7 +18,8 @@ use tonic::Status;
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::user_agent::{UserAgentConnection, auth},
|
actors::user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||||
|
db::models::KeyType,
|
||||||
grpc::request_tracker::RequestTracker,
|
grpc::request_tracker::RequestTracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,9 +141,28 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
|
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
|
||||||
pubkey,
|
pubkey,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
key_type: _,
|
key_type,
|
||||||
}) => {
|
}) => {
|
||||||
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
|
let Ok(key_type) = ProtoKeyType::try_from(key_type) 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"
|
||||||
@@ -168,7 +188,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<authn::PublicKey, auth::Error> {
|
) -> Result<AuthPublicKey, 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,
|
id: wallet_id.drop_verification_provenance(),
|
||||||
address: address.to_vec(),
|
address: address.to_vec(),
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -121,6 +121,9 @@ 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())
|
||||||
@@ -147,7 +150,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),
|
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id.drop_verification_provenance()),
|
||||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use arbiter_crypto::authn;
|
|
||||||
use arbiter_proto::proto::{
|
use arbiter_proto::proto::{
|
||||||
shared::ClientInfo as ProtoClientMetadata,
|
shared::ClientInfo as ProtoClientMetadata,
|
||||||
user_agent::{
|
user_agent::{
|
||||||
@@ -42,7 +41,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(),
|
pubkey: profile.pubkey.to_bytes().to_vec(),
|
||||||
info: Some(ProtoClientMetadata {
|
info: Some(ProtoClientMetadata {
|
||||||
name: profile.metadata.name,
|
name: profile.metadata.name,
|
||||||
description: profile.metadata.description,
|
description: profile.metadata.description,
|
||||||
@@ -52,7 +51,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(),
|
pubkey: pubkey.to_bytes().to_vec(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -90,8 +89,10 @@ 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 = authn::PublicKey::try_from(resp.pubkey.as_slice())
|
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
|
||||||
.map_err(|_| Status::invalid_argument("Invalid ML-DSA public key"))?;
|
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
|
||||||
|
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?;
|
||||||
|
|
||||||
actor
|
actor
|
||||||
.ask(HandleNewClientApprove {
|
.ask(HandleNewClientApprove {
|
||||||
@@ -116,7 +117,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.to_vec(),
|
pubkey: client.public_key,
|
||||||
info: Some(ProtoClientMetadata {
|
info: Some(ProtoClientMetadata {
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
#![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;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod evm;
|
pub mod evm;
|
||||||
pub mod grpc;
|
pub mod grpc;
|
||||||
|
pub mod safe_cell;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
|
|||||||
@@ -105,11 +105,6 @@ impl<T> SafeCellHandle<T> for MemSafeCell<T> {
|
|||||||
|
|
||||||
fn abort_memory_breach(action: &str, err: &memsafe::error::MemoryError) -> ! {
|
fn abort_memory_breach(action: &str, err: &memsafe::error::MemoryError) -> ! {
|
||||||
eprintln!("fatal {action}: {err}");
|
eprintln!("fatal {action}: {err}");
|
||||||
// SAFETY: Intentionally cause a segmentation fault to prevent further execution in a compromised state.
|
|
||||||
unsafe {
|
|
||||||
let unsafe_pointer = std::ptr::null_mut::<u8>();
|
|
||||||
std::ptr::write_volatile(unsafe_pointer, 0);
|
|
||||||
}
|
|
||||||
std::process::abort();
|
std::process::abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
struct DeferClosure<F: FnOnce()> {
|
struct DeferClosure<F: FnOnce()> {
|
||||||
f: Option<F>,
|
f: Option<F>,
|
||||||
}
|
}
|
||||||
@@ -14,3 +16,19 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
use arbiter_crypto::{
|
|
||||||
authn::{self, CLIENT_CONTEXT, format_challenge},
|
|
||||||
safecell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
|
||||||
use arbiter_proto::ClientMetadata;
|
use arbiter_proto::ClientMetadata;
|
||||||
use arbiter_proto::transport::{Receiver, Sender};
|
use arbiter_proto::transport::{Receiver, Sender};
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
@@ -12,10 +8,11 @@ use arbiter_server::{
|
|||||||
},
|
},
|
||||||
crypto::integrity,
|
crypto::integrity,
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
|
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 ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair as _};
|
use ed25519_dalek::Signer as _;
|
||||||
|
|
||||||
use super::common::ChannelTransport;
|
use super::common::ChannelTransport;
|
||||||
|
|
||||||
@@ -30,7 +27,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: VerifyingKey<MlDsa87>,
|
pubkey: ed25519_dalek::VerifyingKey,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) {
|
) {
|
||||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
@@ -48,7 +45,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.encode().to_vec()),
|
program_client::public_key.eq(pubkey.to_bytes().to_vec()),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
program_client::metadata_id.eq(metadata_id),
|
||||||
))
|
))
|
||||||
.returning(program_client::id)
|
.returning(program_client::id)
|
||||||
@@ -56,36 +53,21 @@ async fn insert_registered_client(
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
integrity::sign_entity(
|
let _ = integrity::sign_entity(
|
||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&ClientCredentials {
|
&ClientCredentials { pubkey, nonce: 1 },
|
||||||
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 = format_challenge(nonce, &pubkey.to_bytes());
|
|
||||||
key.signing_key()
|
|
||||||
.sign_deterministic(&challenge, 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 = MlDsa87::key_gen(&mut rand::rng())
|
let sentinel_key = ed25519_dalek::SigningKey::generate(&mut rand::rng())
|
||||||
.verifying_key()
|
.verifying_key()
|
||||||
.encode()
|
.to_bytes()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
insert_into(schema::useragent_client::table)
|
insert_into(schema::useragent_client::table)
|
||||||
@@ -125,11 +107,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: new_key.verifying_key(),
|
||||||
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -145,7 +127,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
|
||||||
insert_registered_client(
|
insert_registered_client(
|
||||||
&db,
|
&db,
|
||||||
@@ -165,7 +147,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().into(),
|
pubkey: new_key.verifying_key(),
|
||||||
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -185,7 +167,8 @@ pub async fn test_challenge_auth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Sign the challenge and send solution
|
// Sign the challenge and send solution
|
||||||
let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0);
|
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes());
|
||||||
|
let signature = new_key.sign(&formatted_challenge);
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution { signature })
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
@@ -211,7 +194,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&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;
|
||||||
@@ -226,7 +209,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().into(),
|
pubkey: new_key.verifying_key(),
|
||||||
metadata: requested,
|
metadata: requested,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -237,7 +220,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 = sign_client_challenge(&new_key, nonce, &pubkey);
|
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution { signature })
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
.await
|
.await
|
||||||
@@ -268,7 +251,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
|
||||||
insert_registered_client(
|
insert_registered_client(
|
||||||
&db,
|
&db,
|
||||||
@@ -288,7 +271,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().into(),
|
pubkey: new_key.verifying_key(),
|
||||||
metadata: metadata("client", Some("new"), Some("2.0.0")),
|
metadata: metadata("client", Some("new"), Some("2.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -299,7 +282,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 = sign_client_challenge(&new_key, nonce, &pubkey);
|
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeSolution { signature })
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
.await
|
.await
|
||||||
@@ -356,7 +339,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -374,7 +357,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().encode().to_vec()),
|
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
program_client::metadata_id.eq(metadata_id),
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
@@ -391,7 +374,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().into(),
|
pubkey: new_key.verifying_key(),
|
||||||
metadata: requested,
|
metadata: requested,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
|
use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::KeyHolder,
|
actors::keyholder::KeyHolder,
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use diesel::QueryDsl;
|
use diesel::QueryDsl;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{CreateNew, Error, KeyHolder},
|
actors::keyholder::{CreateNew, Error, KeyHolder},
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
|
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::actor::{ActorRef, Spawn as _};
|
use kameo::actor::{ActorRef, Spawn as _};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, KeyHolder},
|
actors::keyholder::{Error, KeyHolder},
|
||||||
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
|
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use diesel::{QueryDsl, SelectableHelper};
|
use diesel::{QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::Error,
|
actors::keyholder::Error,
|
||||||
crypto::encryption::v1::Nonce,
|
crypto::encryption::v1::Nonce,
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
|
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
use arbiter_crypto::{
|
|
||||||
authn::{self, USERAGENT_CONTEXT, format_challenge},
|
|
||||||
safecell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
|
||||||
|
|
||||||
use arbiter_proto::transport::{Receiver, Sender};
|
use arbiter_proto::transport::{Receiver, Sender};
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
keyholder::Bootstrap,
|
keyholder::Bootstrap,
|
||||||
user_agent::{UserAgentConnection, UserAgentCredentials, auth},
|
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth},
|
||||||
},
|
},
|
||||||
crypto::integrity,
|
crypto::integrity,
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
|
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 ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _};
|
use ed25519_dalek::Signer 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 = format_challenge(nonce, pubkey_bytes);
|
|
||||||
key.signing_key()
|
|
||||||
.sign_deterministic(&challenge, 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() {
|
||||||
@@ -53,10 +37,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: Some(token),
|
bootstrap_token: Some(token),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -79,7 +63,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().encode().to_vec());
|
assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -95,10 +79,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 = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: Some("invalid_token".to_string()),
|
bootstrap_token: Some("invalid_token".to_string()),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -131,8 +115,8 @@ pub async fn test_challenge_auth() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -149,13 +133,14 @@ pub async fn test_challenge_auth() {
|
|||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
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();
|
||||||
@@ -167,7 +152,7 @@ pub async fn test_challenge_auth() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -185,11 +170,12 @@ 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 signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes);
|
let formatted_challenge = arbiter_proto::format_challenge(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(),
|
signature: signature.to_bytes().to_vec(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -220,8 +206,8 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -244,7 +230,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: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -269,8 +255,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -287,13 +273,14 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
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();
|
||||||
@@ -305,7 +292,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: new_key.verifying_key().into(),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -323,11 +310,12 @@ 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 signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes);
|
let wrong_challenge = arbiter_proto::format_challenge(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(),
|
signature: signature.to_bytes().to_vec(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
@@ -9,8 +8,8 @@ use arbiter_server::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
use kameo::actor::Spawn as _;
|
use kameo::actor::Spawn as _;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'dart:convert';
|
|||||||
import 'package:arbiter/features/connection/connection.dart';
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
import 'package:arbiter/features/connection/server_info_storage.dart';
|
import 'package:arbiter/features/connection/server_info_storage.dart';
|
||||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
||||||
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
@@ -46,18 +45,6 @@ class ConnectionException implements Exception {
|
|||||||
String toString() => message;
|
String toString() => message;
|
||||||
}
|
}
|
||||||
|
|
||||||
String certificateFingerprintHex(List<int> derBytes) {
|
|
||||||
return sha256.convert(derBytes).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPinnedServerCertificate({
|
|
||||||
required String expectedFingerprint,
|
|
||||||
required List<int> certificateDer,
|
|
||||||
}) {
|
|
||||||
return certificateFingerprintHex(certificateDer) ==
|
|
||||||
expectedFingerprint.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Connection> connectAndAuthorize(
|
Future<Connection> connectAndAuthorize(
|
||||||
StoredServerInfo serverInfo,
|
StoredServerInfo serverInfo,
|
||||||
KeyHandle key, {
|
KeyHandle key, {
|
||||||
@@ -168,12 +155,7 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
|
|||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 10),
|
||||||
credentials: ChannelCredentials.secure(
|
credentials: ChannelCredentials.secure(
|
||||||
onBadCertificate: (cert, host) {
|
onBadCertificate: (cert, host) {
|
||||||
final isExpectedHost = host == serverInfo.address;
|
return true;
|
||||||
final isPinnedCert = isPinnedServerCertificate(
|
|
||||||
expectedFingerprint: serverInfo.caCertFingerprint,
|
|
||||||
certificateDer: cert.der,
|
|
||||||
);
|
|
||||||
return isExpectedHost && isPinnedCert;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:arbiter/features/connection/auth.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('certificate pinning helpers', () {
|
|
||||||
test('certificateFingerprintHex returns SHA-256 in hex', () {
|
|
||||||
final fingerprint = certificateFingerprintHex('abc'.codeUnits);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
fingerprint,
|
|
||||||
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isPinnedServerCertificate matches expected fingerprint', () {
|
|
||||||
final matches = isPinnedServerCertificate(
|
|
||||||
expectedFingerprint:
|
|
||||||
'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD',
|
|
||||||
certificateDer: 'abc'.codeUnits,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(matches, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isPinnedServerCertificate rejects mismatched fingerprint', () {
|
|
||||||
final matches = isPinnedServerCertificate(
|
|
||||||
expectedFingerprint:
|
|
||||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
||||||
certificateDer: 'abc'.codeUnits,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(matches, isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user