feat(m1): bascule-shell composes a real SAT anchored on session_leaf
Replaces the opaque BASCULE_ATTESTATION_HASH (a SHA over a
"pcrN:val;ima:hash;" evidence string) with a real proto-canonical
SatBundle composed from the operator's identity + local platform
attestation, anchored on the L4 SessionClaim.
bascule-core::sat (NEW): pure composer module.
- build_session_claim(SessionInputs) -> SessionClaim builds the
L4 leaf from {principal, auth_method, actor_type,
identity_verified, platform_attested, software_verified,
nonce_seed}, computes posture per SAT-SPEC-0002 §7, and
populates the L1/L2/L3 binding fields with zero-padded
placeholders until upstream producers exist.
- compose_local(SessionClaim) -> ComposedSat assembles the proto
SatBundle via SatBundleBuilder. Hot path stays local per ADR D9
(zero network); QM's gRPC ComposeSat is the warm-path surface.
- 7 unit tests cover layer/actor wiring, posture math at each
evidence level, deterministic nonce, sat_hash uniqueness across
principal changes.
bascule-shell: composes the SAT in main() right before execvp
of the inner shell — that's the OSS equivalent of an "Authenticated
-> ShellActive" transition (the OSS Bascule has no russh state
machine; it's a CLI wrapper). Exports the new env var surface:
BASCULE_SAT_HASH hex of proto sat_hash (canonical)
BASCULE_SESSION_CLAIM_HASH hex of L4 leaf hash
BASCULE_SESSION_ID UUID from SessionClaim
BASCULE_POSTURE_LEVEL SAT-SPEC-0002 §7 posture
BASCULE_ATTESTATION_HASH retained as compat alias (gsh /
dashboard consumers); now points at
the proto sat_hash, not the old
evidence-string SHA.
Cross-workspace path dep: substrate-proto via
../substrate-project/substrate/crates/substrate-proto. CI mounts
~/projects as one volume so the path resolves. Switching to a git
dep is post-MVP.
Note: russh-keys pulls `home` which requires Rust 1.88; CI bumps
the docker image accordingly. No code change.
Tested:
cargo build -p bascule-core -p bascule-shell clean
cargo test -p bascule-core --lib sat 7/7
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude Code <claude@guildhouse.dev>
This commit is contained in:
parent
6eb2de5dc0
commit
999c78ef4c
7 changed files with 654 additions and 19 deletions
365
Cargo.lock
generated
365
Cargo.lock
generated
|
|
@ -130,6 +130,28 @@ version = "1.0.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
|
|
@ -153,13 +175,40 @@ 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 = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.7.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core 0.4.5",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit 0.7.3",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower 0.5.3",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core 0.5.6",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -169,7 +218,7 @@ dependencies = [
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"itoa",
|
"itoa",
|
||||||
"matchit",
|
"matchit 0.8.4",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
|
@ -180,12 +229,32 @@ dependencies = [
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower 0.5.3",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
|
|
@ -228,11 +297,14 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"hex",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"russh",
|
"russh",
|
||||||
"russh-keys",
|
"russh-keys",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"substrate-proto",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -266,7 +338,7 @@ name = "bascule-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum 0.8.8",
|
||||||
"bascule-auth-agent-id",
|
"bascule-auth-agent-id",
|
||||||
"bascule-core",
|
"bascule-core",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
@ -282,6 +354,7 @@ name = "bascule-shell"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bascule-core",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|
@ -1222,6 +1295,12 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "elliptic-curve"
|
name = "elliptic-curve"
|
||||||
version = "0.13.8"
|
version = "0.13.8"
|
||||||
|
|
@ -1328,6 +1407,12 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
|
|
@ -1575,6 +1660,25 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap 2.13.1",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
|
|
@ -1586,6 +1690,12 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
|
@ -1719,6 +1829,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
|
@ -1747,6 +1858,19 @@ dependencies = [
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-timeout"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||||
|
dependencies = [
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
|
|
@ -1764,7 +1888,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2 0.6.3",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -1909,6 +2033,16 @@ dependencies = [
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.1"
|
version = "2.13.1"
|
||||||
|
|
@ -1962,6 +2096,15 @@ version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
|
@ -2130,6 +2273,12 @@ dependencies = [
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
|
@ -2194,6 +2343,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multimap"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
|
|
@ -2437,6 +2592,16 @@ version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "petgraph"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"indexmap 2.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.11"
|
version = "1.1.11"
|
||||||
|
|
@ -2615,6 +2780,58 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-build"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"multimap",
|
||||||
|
"once_cell",
|
||||||
|
"petgraph",
|
||||||
|
"prettyplease",
|
||||||
|
"prost",
|
||||||
|
"prost-types",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-types"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||||
|
dependencies = [
|
||||||
|
"prost",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
|
|
@ -2628,7 +2845,7 @@ dependencies = [
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash 2.1.2",
|
"rustc-hash 2.1.2",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2 0.6.3",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2665,7 +2882,7 @@ dependencies = [
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2 0.6.3",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
@ -2770,6 +2987,18 @@ dependencies = [
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
@ -2815,7 +3044,7 @@ dependencies = [
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower",
|
"tower 0.5.3",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
|
|
@ -3483,6 +3712,16 @@ version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
|
@ -3572,6 +3811,36 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "substrate-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"prost",
|
||||||
|
"prost-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic",
|
||||||
|
"tonic-build",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "substrate-proto"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"prost",
|
||||||
|
"prost-types",
|
||||||
|
"substrate-common",
|
||||||
|
"tonic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -3757,7 +4026,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2 0.6.3",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
@ -3835,7 +4104,7 @@ version = "0.22.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.13.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
|
|
@ -3849,6 +4118,70 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream",
|
||||||
|
"async-trait",
|
||||||
|
"axum 0.7.9",
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-timeout",
|
||||||
|
"hyper-util",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"prost",
|
||||||
|
"socket2 0.5.10",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic-build"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
|
||||||
|
dependencies = [
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"prost-build",
|
||||||
|
"prost-types",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"pin-project",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -3887,7 +4220,7 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower 0.5.3",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -4241,7 +4574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"indexmap",
|
"indexmap 2.13.1",
|
||||||
"wasm-encoder",
|
"wasm-encoder",
|
||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
@ -4267,7 +4600,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap",
|
"indexmap 2.13.1",
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4713,7 +5046,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"heck",
|
"heck",
|
||||||
"indexmap",
|
"indexmap 2.13.1",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"syn",
|
"syn",
|
||||||
"wasm-metadata",
|
"wasm-metadata",
|
||||||
|
|
@ -4744,7 +5077,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"indexmap",
|
"indexmap 2.13.1",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
|
@ -4763,7 +5096,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"id-arena",
|
"id-arena",
|
||||||
"indexmap",
|
"indexmap 2.13.1",
|
||||||
"log",
|
"log",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
# M1: depend on the canonical proto-generated SAT types from the substrate
|
||||||
|
# workspace. Path dep across workspaces; CI mounts both checkouts side by
|
||||||
|
# side. substrate-proto is itself a thin facade over substrate-common which
|
||||||
|
# owns the tonic-build invocation, so consumers only need to worry about
|
||||||
|
# the substrate-proto import surface.
|
||||||
|
substrate-proto = { path = "../substrate-project/substrate/crates/substrate-proto" }
|
||||||
russh = "0.46"
|
russh = "0.46"
|
||||||
russh-keys = "0.46"
|
russh-keys = "0.46"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ chrono = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
portable-pty = { workspace = true }
|
portable-pty = { workspace = true }
|
||||||
|
substrate-proto = { workspace = true }
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ pub mod handler;
|
||||||
pub mod hooks;
|
pub mod hooks;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
|
pub mod sat;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
|
||||||
259
crates/bascule-core/src/sat.rs
Normal file
259
crates/bascule-core/src/sat.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
//! SAT composition — turn a Bascule session's identity + platform attestation
|
||||||
|
//! into a real proto-canonical `SatBundle` anchored on the L4 SessionClaim.
|
||||||
|
//!
|
||||||
|
//! M1 milestone: replaces the opaque `BASCULE_ATTESTATION_HASH` composite
|
||||||
|
//! with a real four-layer SAT (only L4 today; L1/L2/L3 join as the upstream
|
||||||
|
//! producers wire up).
|
||||||
|
//!
|
||||||
|
//! ## Why local composition first
|
||||||
|
//!
|
||||||
|
//! Per ADR D9 the hot path is local and zero-network. Bascule composes the
|
||||||
|
//! bundle in-process at session establish time so a single shell launch
|
||||||
|
//! doesn't pay an RTT to QM. The same `SatBundle` *can* be re-derived by
|
||||||
|
//! QM via the `ComposeSat` gRPC if a renegotiation needs the producer's
|
||||||
|
//! authoritative L1/L2/L3 layers (M4 work).
|
||||||
|
//!
|
||||||
|
//! ## Inputs
|
||||||
|
//!
|
||||||
|
//! Bascule itself is identity-agnostic; the `bascule-shell` binary detects
|
||||||
|
//! the operator identity (Entra/AZ-CLI/Kerberos/cached-OIDC/system) and the
|
||||||
|
//! local platform attestation (TPM/IMA/Keylime/Entra-device). Both come in
|
||||||
|
//! here as plain types so this module stays free of OS-detection logic.
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use substrate_proto::v2::builder::SatBundleBuilder;
|
||||||
|
use substrate_proto::v2::hashing::session_claim_hash;
|
||||||
|
use substrate_proto::v2::{
|
||||||
|
ActorContext, PostureEvidence, PostureLevel, SatBundle, SessionClaim,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Inputs to SAT composition. Plain data so the caller can be `bascule-shell`
|
||||||
|
/// or a future server-side handler without dragging in OS detection.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SessionInputs<'a> {
|
||||||
|
/// Operator principal — `tyler@guildhouse.dev`, SPIFFE ID, system user, etc.
|
||||||
|
pub principal: &'a str,
|
||||||
|
/// Bascule's classification of how `principal` was authenticated.
|
||||||
|
/// e.g. `oidc-entra`, `kerberos`, `oidc-cached`, `ssh-key`.
|
||||||
|
pub auth_method: &'a str,
|
||||||
|
/// `human` | `agent` | `system` | `node` per ActorContext schema.
|
||||||
|
pub actor_type: &'a str,
|
||||||
|
/// Did this principal acquire a fresh attestation token alongside the
|
||||||
|
/// shell session (Entra access token, Kerberos TGT)?
|
||||||
|
pub identity_verified: bool,
|
||||||
|
/// Was a TPM quote read at attestation time?
|
||||||
|
pub platform_attested: bool,
|
||||||
|
/// Did the local image hash match the expected/attested value?
|
||||||
|
/// Bascule today doesn't compute this; pass `false` until the loader
|
||||||
|
/// produces a verdict.
|
||||||
|
pub software_verified: bool,
|
||||||
|
/// Optional hex string seed used to derive a deterministic nonce.
|
||||||
|
/// Pass `None` for a random nonce.
|
||||||
|
pub nonce_seed: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outcome of SAT composition.
|
||||||
|
pub struct ComposedSat {
|
||||||
|
/// The full proto bundle (only the L4 SessionClaim is populated today).
|
||||||
|
pub bundle: SatBundle,
|
||||||
|
/// Hex-encoded `bundle.sat_hash`.
|
||||||
|
pub sat_hash_hex: String,
|
||||||
|
/// Hex-encoded `session_claim.claim_hash`.
|
||||||
|
pub session_claim_hash_hex: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `SessionClaim` from session inputs. Pure function; no I/O.
|
||||||
|
pub fn build_session_claim(inputs: &SessionInputs<'_>) -> SessionClaim {
|
||||||
|
let evidence = PostureEvidence {
|
||||||
|
platform_attested: inputs.platform_attested,
|
||||||
|
platform_method: if inputs.platform_attested {
|
||||||
|
"tpm_quote".into()
|
||||||
|
} else {
|
||||||
|
"none".into()
|
||||||
|
},
|
||||||
|
software_verified: inputs.software_verified,
|
||||||
|
software_method: if inputs.software_verified {
|
||||||
|
"hash_match".into()
|
||||||
|
} else {
|
||||||
|
"none".into()
|
||||||
|
},
|
||||||
|
// Bascule does not bind to a Quartermaster-issued accord at session
|
||||||
|
// establish time; M4 will set this when the Accord verdict pipeline
|
||||||
|
// returns a permit.
|
||||||
|
governance_bound: false,
|
||||||
|
governance_method: "none".into(),
|
||||||
|
identity_verified: inputs.identity_verified,
|
||||||
|
identity_method: inputs.auth_method.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let posture_level = compute_posture(&evidence);
|
||||||
|
|
||||||
|
let nonce = match inputs.nonce_seed {
|
||||||
|
Some(seed) => Sha256::digest(seed.as_bytes()).to_vec(),
|
||||||
|
None => {
|
||||||
|
use rand::RngCore;
|
||||||
|
let mut buf = vec![0u8; 16];
|
||||||
|
rand::thread_rng().fill_bytes(&mut buf);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut claim = SessionClaim {
|
||||||
|
layer: 4,
|
||||||
|
session_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
actor: Some(ActorContext {
|
||||||
|
actor_id: inputs.principal.to_string(),
|
||||||
|
actor_type: inputs.actor_type.to_string(),
|
||||||
|
auth_method: inputs.auth_method.to_string(),
|
||||||
|
delegated_by: None,
|
||||||
|
delegation_id: None,
|
||||||
|
}),
|
||||||
|
posture_evidence: Some(evidence),
|
||||||
|
posture_level: posture_level as i32,
|
||||||
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||||
|
nonce,
|
||||||
|
// L1/L2/L3 binding hashes are zero-padded until upstream producers
|
||||||
|
// exist. The L4 leaf hash is still verifiable on its own.
|
||||||
|
platform_claim_hash: vec![0u8; 32],
|
||||||
|
software_claim_hash: vec![0u8; 32],
|
||||||
|
governance_claim_hash: vec![0u8; 32],
|
||||||
|
claim_hash: Vec::new(),
|
||||||
|
};
|
||||||
|
claim.claim_hash = session_claim_hash(&claim).to_vec();
|
||||||
|
claim
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compose a SAT bundle locally from a `SessionClaim`. The other layers are
|
||||||
|
/// `None` until the corresponding producers are wired in. The composite
|
||||||
|
/// `sat_hash` covers all four slots (zero-padded for absent layers), so the
|
||||||
|
/// hash is comparable to one assembled later by QM with full layers.
|
||||||
|
pub fn compose_local(claim: SessionClaim) -> ComposedSat {
|
||||||
|
let session_claim_hash_hex = hex::encode(&claim.claim_hash);
|
||||||
|
let bundle = SatBundleBuilder::new().session(claim).build();
|
||||||
|
let sat_hash_hex = hex::encode(&bundle.sat_hash);
|
||||||
|
ComposedSat {
|
||||||
|
bundle,
|
||||||
|
sat_hash_hex,
|
||||||
|
session_claim_hash_hex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: build_session_claim + compose_local in one call.
|
||||||
|
pub fn compose_from_inputs(inputs: &SessionInputs<'_>) -> ComposedSat {
|
||||||
|
compose_local(build_session_claim(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SAT-SPEC-0002 §7 posture computation, mirrored from substrate-common's
|
||||||
|
/// `compute_posture` so we can call it without dragging the symbol through
|
||||||
|
/// the proto facade. Kept identical on purpose.
|
||||||
|
fn compute_posture(evidence: &PostureEvidence) -> PostureLevel {
|
||||||
|
if evidence.platform_attested
|
||||||
|
&& evidence.software_verified
|
||||||
|
&& evidence.governance_bound
|
||||||
|
&& evidence.identity_verified
|
||||||
|
{
|
||||||
|
PostureLevel::Attested
|
||||||
|
} else if evidence.software_verified
|
||||||
|
&& evidence.governance_bound
|
||||||
|
&& evidence.identity_verified
|
||||||
|
{
|
||||||
|
PostureLevel::Governed
|
||||||
|
} else if evidence.software_verified && evidence.identity_verified {
|
||||||
|
PostureLevel::Verified
|
||||||
|
} else if evidence.identity_verified {
|
||||||
|
PostureLevel::Local
|
||||||
|
} else {
|
||||||
|
PostureLevel::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn inputs() -> SessionInputs<'static> {
|
||||||
|
SessionInputs {
|
||||||
|
principal: "tyler@guildhouse.dev",
|
||||||
|
auth_method: "oidc-entra",
|
||||||
|
actor_type: "human",
|
||||||
|
identity_verified: true,
|
||||||
|
platform_attested: false,
|
||||||
|
software_verified: false,
|
||||||
|
nonce_seed: Some("test-seed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_claim_carries_actor_and_layer4() {
|
||||||
|
let claim = build_session_claim(&inputs());
|
||||||
|
assert_eq!(claim.layer, 4);
|
||||||
|
let actor = claim.actor.as_ref().unwrap();
|
||||||
|
assert_eq!(actor.actor_id, "tyler@guildhouse.dev");
|
||||||
|
assert_eq!(actor.auth_method, "oidc-entra");
|
||||||
|
assert_eq!(actor.actor_type, "human");
|
||||||
|
assert!(!claim.claim_hash.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn posture_identity_only_is_local() {
|
||||||
|
// identity verified but no platform/software/governance evidence
|
||||||
|
let claim = build_session_claim(&inputs());
|
||||||
|
assert_eq!(claim.posture_level, PostureLevel::Local as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn posture_identity_plus_software_is_verified() {
|
||||||
|
let mut i = inputs();
|
||||||
|
i.software_verified = true;
|
||||||
|
let claim = build_session_claim(&i);
|
||||||
|
assert_eq!(claim.posture_level, PostureLevel::Verified as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn posture_full_chain_is_attested_when_governance_bound() {
|
||||||
|
let mut i = inputs();
|
||||||
|
i.platform_attested = true;
|
||||||
|
i.software_verified = true;
|
||||||
|
let mut claim = build_session_claim(&i);
|
||||||
|
// Bascule sets governance_bound=false until M4 — flip it manually
|
||||||
|
// to confirm the math.
|
||||||
|
let mut ev = claim.posture_evidence.take().unwrap();
|
||||||
|
ev.governance_bound = true;
|
||||||
|
ev.governance_method = "qm_live".into();
|
||||||
|
claim.posture_evidence = Some(ev);
|
||||||
|
let bumped = compute_posture(claim.posture_evidence.as_ref().unwrap());
|
||||||
|
assert_eq!(bumped, PostureLevel::Attested);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonce_seed_is_deterministic() {
|
||||||
|
let a = build_session_claim(&inputs());
|
||||||
|
let b = build_session_claim(&inputs());
|
||||||
|
assert_eq!(a.nonce, b.nonce);
|
||||||
|
// session_id and timestamp differ, so the leaf hashes still differ
|
||||||
|
// — that's intentional, the nonce is not the only freshness source.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compose_local_produces_nonempty_sat_hash() {
|
||||||
|
let composed = compose_from_inputs(&inputs());
|
||||||
|
assert!(!composed.bundle.sat_hash.is_empty());
|
||||||
|
assert_eq!(composed.bundle.sat_version, 2);
|
||||||
|
assert!(composed.bundle.session_claim.is_some());
|
||||||
|
assert!(composed.bundle.platform_claim.is_none());
|
||||||
|
assert!(composed.bundle.software_claim.is_none());
|
||||||
|
assert!(composed.bundle.governance_claim.is_none());
|
||||||
|
assert_eq!(composed.sat_hash_hex.len(), 64); // sha256 hex
|
||||||
|
assert_eq!(composed.session_claim_hash_hex.len(), 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sat_hash_changes_when_principal_changes() {
|
||||||
|
let a = compose_from_inputs(&inputs());
|
||||||
|
let mut other = inputs();
|
||||||
|
other.principal = "alice@guildhouse.dev";
|
||||||
|
let b = compose_from_inputs(&other);
|
||||||
|
assert_ne!(a.sat_hash_hex, b.sat_hash_hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ name = "bascule-shell"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bascule-core = { path = "../bascule-core" }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,25 @@ fn main() -> Result<()> {
|
||||||
banner::display(&id, &attest);
|
banner::display(&id, &attest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// M1: compose a real proto SatBundle anchored on the L4 SessionClaim,
|
||||||
|
// built from the operator identity + local platform attestation. The
|
||||||
|
// composer lives in bascule_core::sat so server-side handlers can call
|
||||||
|
// it later without dragging in bascule-shell's OS-detection logic.
|
||||||
|
let nonce_seed = format!("{}|{}", id.principal, attest.composite_hash);
|
||||||
|
let composed = bascule_core::sat::compose_from_inputs(&bascule_core::sat::SessionInputs {
|
||||||
|
principal: &id.principal,
|
||||||
|
auth_method: &id.auth_method,
|
||||||
|
actor_type: "human",
|
||||||
|
identity_verified: id.has_token,
|
||||||
|
platform_attested: attest.tpm_available && !attest.pcr_values.is_empty(),
|
||||||
|
// M2 will flip this once gsh corpus check runs and the loader
|
||||||
|
// produces a software verdict.
|
||||||
|
software_verified: false,
|
||||||
|
nonce_seed: Some(&nonce_seed),
|
||||||
|
});
|
||||||
|
|
||||||
// Set BASCULE_* env vars
|
// Set BASCULE_* env vars
|
||||||
set_env(&id, &attest);
|
set_env(&id, &attest, &composed);
|
||||||
|
|
||||||
// Determine inner shell
|
// Determine inner shell
|
||||||
let shell = cli.shell.unwrap_or_else(|| config.inner_shell.clone());
|
let shell = cli.shell.unwrap_or_else(|| config.inner_shell.clone());
|
||||||
|
|
@ -96,13 +113,28 @@ fn main() -> Result<()> {
|
||||||
anyhow::bail!("Failed to exec inner shell: {}", shell);
|
anyhow::bail!("Failed to exec inner shell: {}", shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_env(id: &identity::Identity, attest: &attestation::Attestation) {
|
fn set_env(
|
||||||
|
id: &identity::Identity,
|
||||||
|
attest: &attestation::Attestation,
|
||||||
|
composed: &bascule_core::sat::ComposedSat,
|
||||||
|
) {
|
||||||
std::env::set_var("BASCULE_PRINCIPAL", &id.principal);
|
std::env::set_var("BASCULE_PRINCIPAL", &id.principal);
|
||||||
std::env::set_var("BASCULE_AUTH_METHOD", &id.auth_method);
|
std::env::set_var("BASCULE_AUTH_METHOD", &id.auth_method);
|
||||||
if let Some(ref domain) = id.domain {
|
if let Some(ref domain) = id.domain {
|
||||||
std::env::set_var("BASCULE_DOMAIN", domain);
|
std::env::set_var("BASCULE_DOMAIN", domain);
|
||||||
}
|
}
|
||||||
std::env::set_var("BASCULE_ATTESTATION_HASH", &attest.composite_hash);
|
// BASCULE_ATTESTATION_HASH was an opaque "evidence string" SHA. M1
|
||||||
|
// replaces it with the proto SAT composite hash. Kept under the same
|
||||||
|
// env var name for backward compatibility with existing gsh consumers
|
||||||
|
// (and a NEW BASCULE_SAT_HASH alias for the renamed surface).
|
||||||
|
std::env::set_var("BASCULE_ATTESTATION_HASH", &composed.sat_hash_hex);
|
||||||
|
std::env::set_var("BASCULE_SAT_HASH", &composed.sat_hash_hex);
|
||||||
|
std::env::set_var("BASCULE_SESSION_CLAIM_HASH", &composed.session_claim_hash_hex);
|
||||||
|
if let Some(ref claim) = composed.bundle.session_claim {
|
||||||
|
std::env::set_var("BASCULE_SESSION_ID", &claim.session_id);
|
||||||
|
std::env::set_var("BASCULE_POSTURE_LEVEL", claim.posture_level.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
std::env::set_var("BASCULE_TPM_AVAILABLE", attest.tpm_available.to_string());
|
std::env::set_var("BASCULE_TPM_AVAILABLE", attest.tpm_available.to_string());
|
||||||
std::env::set_var("BASCULE_PCR_COUNT", attest.pcr_values.len().to_string());
|
std::env::set_var("BASCULE_PCR_COUNT", attest.pcr_values.len().to_string());
|
||||||
if let Some(ref ima_hash) = attest.ima_log_hash {
|
if let Some(ref ima_hash) = attest.ima_log_hash {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue