Add generation-bound TLS control face
Create ephemeral trust anchors, publish an exact-profile Unix control socket, and persist generation-authorized mTLS session registration through the Domain Store.
This commit is contained in:
parent
8886436841
commit
c3a76ec8c4
10 changed files with 2351 additions and 17 deletions
218
Cargo.lock
generated
218
Cargo.lock
generated
|
|
@ -32,6 +32,45 @@ version = "1.0.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs-derive",
|
||||||
|
"asn1-rs-impl",
|
||||||
|
"displaydoc",
|
||||||
|
"nom",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-derive"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-impl"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -80,6 +119,15 @@ version = "1.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
|
|
@ -350,6 +398,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
|
|
@ -360,6 +414,26 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der-parser"
|
||||||
|
version = "10.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"displaydoc",
|
||||||
|
"nom",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_builder"
|
name = "derive_builder"
|
||||||
version = "0.20.2"
|
version = "0.20.2"
|
||||||
|
|
@ -1068,12 +1142,20 @@ dependencies = [
|
||||||
name = "lazaret-host-coordinator"
|
name = "lazaret-host-coordinator"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ed25519-dalek",
|
||||||
|
"getrandom 0.4.3",
|
||||||
"lazaret-core",
|
"lazaret-core",
|
||||||
"lazaret-linux",
|
"lazaret-linux",
|
||||||
"lazaret-state-anchor",
|
"lazaret-state-anchor",
|
||||||
"lazaret-storage-catalog",
|
"lazaret-storage-catalog",
|
||||||
|
"rcgen",
|
||||||
"redb",
|
"redb",
|
||||||
|
"rustix",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"time",
|
||||||
|
"x509-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1187,6 +1269,12 @@ version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -1208,6 +1296,41 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|
@ -1278,6 +1401,15 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oid-registry"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "olpc-cjson"
|
name = "olpc-cjson"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
@ -1350,6 +1482,12 @@ dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
|
|
@ -1466,6 +1604,19 @@ dependencies = [
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcgen"
|
||||||
|
version = "0.14.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"time",
|
||||||
|
"x509-parser",
|
||||||
|
"yasna",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redb"
|
name = "redb"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
|
|
@ -1574,6 +1725,15 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusticata-macros"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -2003,6 +2163,36 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.53"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
@ -2553,6 +2743,24 @@ version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x509-parser"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"aws-lc-rs",
|
||||||
|
"data-encoding",
|
||||||
|
"der-parser",
|
||||||
|
"lazy_static",
|
||||||
|
"nom",
|
||||||
|
"oid-registry",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xattr"
|
name = "xattr"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|
@ -2563,6 +2771,16 @@ dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yasna"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,22 @@ license = "Apache-2.0"
|
||||||
anyhow = "=1.0.103"
|
anyhow = "=1.0.103"
|
||||||
ed25519-dalek = { version = "=2.2.0", default-features = false, features = ["std"] }
|
ed25519-dalek = { version = "=2.2.0", default-features = false, features = ["std"] }
|
||||||
flate2 = "=1.1.9"
|
flate2 = "=1.1.9"
|
||||||
|
getrandom = "=0.4.3"
|
||||||
oci-client = { version = "=0.17.0", default-features = false, features = ["rustls-tls"] }
|
oci-client = { version = "=0.17.0", default-features = false, features = ["rustls-tls"] }
|
||||||
oci-spec = { version = "=0.10.0", default-features = false, features = ["runtime"] }
|
oci-spec = { version = "=0.10.0", default-features = false, features = ["runtime"] }
|
||||||
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
||||||
|
rcgen = { version = "=0.14.8", default-features = false, features = ["aws_lc_rs", "x509-parser"] }
|
||||||
redb = "=4.1.0"
|
redb = "=4.1.0"
|
||||||
|
rustls = { version = "=0.23.42", default-features = false, features = ["aws_lc_rs", "std"] }
|
||||||
serde = { version = "=1.0.228", features = ["derive"] }
|
serde = { version = "=1.0.228", features = ["derive"] }
|
||||||
serde_json = "=1.0.150"
|
serde_json = "=1.0.150"
|
||||||
rustix = { version = "=1.1.4", features = ["event", "fs", "net", "pipe", "process", "thread", "time"] }
|
rustix = { version = "=1.1.4", features = ["event", "fs", "net", "pipe", "process", "thread", "time"] }
|
||||||
sha2 = "=0.11.0"
|
sha2 = "=0.11.0"
|
||||||
tar = "=0.4.46"
|
tar = "=0.4.46"
|
||||||
|
time = "=0.3.53"
|
||||||
tokio = { version = "=1.52.3", features = ["fs", "io-util", "process", "rt"] }
|
tokio = { version = "=1.52.3", features = ["fs", "io-util", "process", "rt"] }
|
||||||
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
|
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
|
||||||
|
x509-parser = "=0.18.1"
|
||||||
zstd = "=0.13.3"
|
zstd = "=0.13.3"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,44 @@ pub struct ProtocolPublicationIdentity {
|
||||||
pub coordinator_tls_certificate_sha256: CertificateDigest,
|
pub coordinator_tls_certificate_sha256: CertificateDigest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct ClientEnrollmentRequest {
|
||||||
|
pub protocol_version: ProtocolVersion,
|
||||||
|
pub generation_identity: GenerationIdentity,
|
||||||
|
pub cli_id: ClientId,
|
||||||
|
pub certificate_signing_request_der: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct ClientEnrollmentResponse {
|
||||||
|
pub protocol_version: ProtocolVersion,
|
||||||
|
pub credential: TlsCredential,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct SessionRegistrationRequest {
|
||||||
|
pub protocol_version: ProtocolVersion,
|
||||||
|
pub session_binding: SessionBinding,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct SessionRegistrationResponse {
|
||||||
|
pub protocol_version: ProtocolVersion,
|
||||||
|
pub session_capability: SessionRequestCapability,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum ControlRequest {
|
||||||
|
Enroll(ClientEnrollmentRequest),
|
||||||
|
Register(SessionRegistrationRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum ControlResponse {
|
||||||
|
Enrolled(ClientEnrollmentResponse),
|
||||||
|
Registered(SessionRegistrationResponse),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum ProtocolCredentialRole {
|
pub enum ProtocolCredentialRole {
|
||||||
SessionCoordinator,
|
SessionCoordinator,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
use super::{
|
use super::{
|
||||||
AddressFamily, AddressFamilyDisposition, ArtifactLease, ArtifactState,
|
AddressFamily, AddressFamilyDisposition, ArtifactLease, ArtifactState, ClientEnrollmentRequest,
|
||||||
ConfigurationFingerprintRecordV1, DefaultAction, DeniedDestinationObservation,
|
ClientEnrollmentResponse, ConfigurationFingerprintRecordV1, ControlRequest, ControlResponse,
|
||||||
DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, DnsObservation, Domain,
|
DefaultAction, DeniedDestinationObservation, DerivedNetworkPolicy, DeveloperDecision,
|
||||||
DomainStore, EnvironmentReference, EscalationOutcome, Generation, GenerationIdentity,
|
DeveloperDecisionKind, DnsObservation, Domain, DomainStore, EnvironmentReference,
|
||||||
GitRoutingOwnership, GuestProcessIdentity, HelperOwnership, HostProcessIdentity,
|
EscalationOutcome, Generation, GenerationIdentity, GitRoutingOwnership, GuestProcessIdentity,
|
||||||
HostProcessOwnershipSlot, Mailbox, MailboxCancellation, MailboxRequest, MailboxResult,
|
HelperOwnership, HostProcessIdentity, HostProcessOwnershipSlot, Mailbox, MailboxCancellation,
|
||||||
MailboxResultAck, MailboxResultChunk, MountIdentity, NamespaceIdentity, NetworkObservation,
|
MailboxRequest, MailboxResult, MailboxResultAck, MailboxResultChunk, MountIdentity,
|
||||||
NetworkOwnership, NetworkReachabilityProbe, NetworkReadinessAttestation,
|
NamespaceIdentity, NetworkObservation, NetworkOwnership, NetworkReachabilityProbe,
|
||||||
NetworkReadinessEvidence, NetworkReadinessIdentity, Participant, ParticipantOwnership,
|
NetworkReadinessAttestation, NetworkReadinessEvidence, NetworkReadinessIdentity, Participant,
|
||||||
ParticipantState, PermittedDestinationObservation, PersistedHostProcessOwnership,
|
ParticipantOwnership, ParticipantState, PermittedDestinationObservation,
|
||||||
PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction, PolicyDelivery, PrefixRule,
|
PersistedHostProcessOwnership, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction,
|
||||||
PrepareOperation, PrepareStageState, PreparedArtifact, PreparedLowerOwnership,
|
PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact,
|
||||||
ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion, ProtocolCredential,
|
PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion,
|
||||||
ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity, PublicationOwnership,
|
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity,
|
||||||
ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream, SandboxCredentialBundle,
|
PublicationOwnership, ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream,
|
||||||
SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit, SessionExitOutcome,
|
SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit,
|
||||||
|
SessionExitOutcome, SessionRegistrationRequest, SessionRegistrationResponse,
|
||||||
SessionRequestCapability, SessionTerminal, SessionTerminalEvent, SessionTerminalMessage,
|
SessionRequestCapability, SessionTerminal, SessionTerminalEvent, SessionTerminalMessage,
|
||||||
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership,
|
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership,
|
||||||
StorageCatalog, StoreMetadata, TerminalInput, TerminalOutput, TerminalResize, TerminalSignal,
|
StorageCatalog, StoreMetadata, TerminalInput, TerminalOutput, TerminalResize, TerminalSignal,
|
||||||
|
|
@ -76,6 +77,12 @@ schema_types!(
|
||||||
"TrustManifest" => TrustManifest,
|
"TrustManifest" => TrustManifest,
|
||||||
"TlsCredential" => TlsCredential,
|
"TlsCredential" => TlsCredential,
|
||||||
"ProtocolPublicationIdentity" => ProtocolPublicationIdentity,
|
"ProtocolPublicationIdentity" => ProtocolPublicationIdentity,
|
||||||
|
"ClientEnrollmentRequest" => ClientEnrollmentRequest,
|
||||||
|
"ClientEnrollmentResponse" => ClientEnrollmentResponse,
|
||||||
|
"SessionRegistrationRequest" => SessionRegistrationRequest,
|
||||||
|
"SessionRegistrationResponse" => SessionRegistrationResponse,
|
||||||
|
"ControlRequest" => ControlRequest,
|
||||||
|
"ControlResponse" => ControlResponse,
|
||||||
"SandboxCredentialBundle" => SandboxCredentialBundle,
|
"SandboxCredentialBundle" => SandboxCredentialBundle,
|
||||||
"TlsRole" => TlsRole,
|
"TlsRole" => TlsRole,
|
||||||
"ProtocolCredential" => ProtocolCredential,
|
"ProtocolCredential" => ProtocolCredential,
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,30 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||||
},
|
},
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
|
let enrollment_request = ClientEnrollmentRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
generation_identity: generation_identity(),
|
||||||
|
cli_id: ClientId::new(opaque(13)),
|
||||||
|
certificate_signing_request_der: vec![56],
|
||||||
|
};
|
||||||
|
let enrollment_response = ClientEnrollmentResponse {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
credential: tls_credential.clone(),
|
||||||
|
};
|
||||||
|
let registration_request = SessionRegistrationRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_binding: session_binding(),
|
||||||
|
};
|
||||||
|
let registration_response = SessionRegistrationResponse {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_capability: session_capability(),
|
||||||
|
};
|
||||||
|
verify_shape(enrollment_request.clone(), &mut seen);
|
||||||
|
verify_shape(enrollment_response.clone(), &mut seen);
|
||||||
|
verify_shape(registration_request.clone(), &mut seen);
|
||||||
|
verify_shape(registration_response.clone(), &mut seen);
|
||||||
|
verify_shape(ControlRequest::Enroll(enrollment_request), &mut seen);
|
||||||
|
verify_shape(ControlResponse::Enrolled(enrollment_response), &mut seen);
|
||||||
verify_shape(
|
verify_shape(
|
||||||
SandboxCredentialBundle::new(
|
SandboxCredentialBundle::new(
|
||||||
generation_identity(),
|
generation_identity(),
|
||||||
|
|
@ -841,3 +865,30 @@ fn unknown_enums_and_protocol_versions_are_rejected() {
|
||||||
Err(CodecError::MalformedPayload(_))
|
Err(CodecError::MalformedPayload(_))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_wire_unknown_protocol_versions_are_rejected() {
|
||||||
|
assert_unknown_protocol_version(&ClientEnrollmentRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
generation_identity: generation_identity(),
|
||||||
|
cli_id: ClientId::new(opaque(4)),
|
||||||
|
certificate_signing_request_der: vec![5],
|
||||||
|
});
|
||||||
|
assert_unknown_protocol_version(&ClientEnrollmentResponse {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
credential: TlsCredential::client_cli(
|
||||||
|
generation_identity(),
|
||||||
|
ClientId::new(opaque(4)),
|
||||||
|
vec![5],
|
||||||
|
CertificateDigest::new(opaque(6)),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert_unknown_protocol_version(&SessionRegistrationRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_binding: session_binding(),
|
||||||
|
});
|
||||||
|
assert_unknown_protocol_version(&SessionRegistrationResponse {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_capability: session_capability(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,22 @@ name = "lazaret-host-coordinator"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ed25519-dalek.workspace = true
|
||||||
|
getrandom.workspace = true
|
||||||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||||
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||||
lazaret-state-anchor = { version = "=0.0.0", path = "../lazaret-state-anchor" }
|
lazaret-state-anchor = { version = "=0.0.0", path = "../lazaret-state-anchor" }
|
||||||
lazaret-storage-catalog = { version = "=0.0.0", path = "../lazaret-storage-catalog" }
|
lazaret-storage-catalog = { version = "=0.0.0", path = "../lazaret-storage-catalog" }
|
||||||
|
rcgen.workspace = true
|
||||||
redb.workspace = true
|
redb.workspace = true
|
||||||
|
rustix.workspace = true
|
||||||
|
rustls.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
time.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde.workspace = true
|
x509-parser.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
1239
crates/lazaret-host-coordinator/src/control.rs
Normal file
1239
crates/lazaret-host-coordinator/src/control.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
||||||
#![doc = "Host-side coordination authority for one Lazaret Domain."]
|
#![doc = "Host-side coordination authority for one Lazaret Domain."]
|
||||||
|
|
||||||
|
pub mod control;
|
||||||
pub mod domain_store;
|
pub mod domain_store;
|
||||||
pub mod state_anchor;
|
pub mod state_anchor;
|
||||||
|
|
|
||||||
719
crates/lazaret-host-coordinator/tests/control.rs
Normal file
719
crates/lazaret-host-coordinator/tests/control.rs
Normal file
|
|
@ -0,0 +1,719 @@
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::thread::{self, JoinHandle};
|
||||||
|
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
|
use lazaret_core::identity::{
|
||||||
|
ClientId, ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, HostPath,
|
||||||
|
NetworkSidecarId, SandboxId, SessionId,
|
||||||
|
};
|
||||||
|
use lazaret_core::records::{
|
||||||
|
ClientEnrollmentResponse, ControlRequest, ControlResponse, Domain, DurableDecision, Generation,
|
||||||
|
GenerationIdentity, ProtocolVersion, SessionBinding, SessionPhase, SessionRegistrationRequest,
|
||||||
|
SessionRegistrationResponse, TlsCredential,
|
||||||
|
};
|
||||||
|
use lazaret_host_coordinator::control::{
|
||||||
|
CONTROL_ALPN, ControlError, PendingClientEnrollment, PublishedControlFace, TrustAnchor,
|
||||||
|
};
|
||||||
|
use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
||||||
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
|
use rustix::process::getuid;
|
||||||
|
use x509_parser::extensions::GeneralName;
|
||||||
|
use x509_parser::parse_x509_certificate;
|
||||||
|
|
||||||
|
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
struct TestDirectory(PathBuf);
|
||||||
|
|
||||||
|
impl TestDirectory {
|
||||||
|
fn new(label: &str) -> Self {
|
||||||
|
let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"lazaret-control-{label}-{}-{sequence}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
std::fs::create_dir(&path).unwrap();
|
||||||
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self, name: &str) -> PathBuf {
|
||||||
|
self.0.join(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestDirectory {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
std::fs::remove_dir_all(&self.0).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(value: u8) -> [u8; 32] {
|
||||||
|
[value; 32]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generation_identity(generation: u64) -> GenerationIdentity {
|
||||||
|
GenerationIdentity {
|
||||||
|
agent_profile: "trusted".into(),
|
||||||
|
domain_id: DomainId::new(bytes(1)),
|
||||||
|
generation_id: GenerationId::new(generation),
|
||||||
|
configuration_fingerprint: ConfigurationFingerprint::new(bytes(2)),
|
||||||
|
coordinator_id: CoordinatorId::new(bytes(u8::try_from(generation + 10).unwrap())),
|
||||||
|
sandbox_id: SandboxId::new(bytes(u8::try_from(generation + 20).unwrap())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain() -> Domain {
|
||||||
|
Domain {
|
||||||
|
domain_id: DomainId::new(bytes(1)),
|
||||||
|
profile_name: "trusted".into(),
|
||||||
|
canonical_workspace: HostPath::new(b"/workspace/project".to_vec()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generation(identity: &GenerationIdentity, decision: DurableDecision) -> Generation {
|
||||||
|
Generation {
|
||||||
|
domain_id: identity.domain_id,
|
||||||
|
generation_id: identity.generation_id,
|
||||||
|
configuration_fingerprint: identity.configuration_fingerprint,
|
||||||
|
coordinator_id: identity.coordinator_id,
|
||||||
|
network_sidecar_id: NetworkSidecarId::new(bytes(4)),
|
||||||
|
sandbox_id: identity.sandbox_id,
|
||||||
|
durable_decision: decision,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_store(
|
||||||
|
directory: &TestDirectory,
|
||||||
|
identity: &GenerationIdentity,
|
||||||
|
) -> Arc<HostCoordinatorDomainStore> {
|
||||||
|
let lock_path = directory.path("domain.lock");
|
||||||
|
let lock = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(lock_path)
|
||||||
|
.unwrap();
|
||||||
|
let lock = try_exclusive_flock(lock).unwrap();
|
||||||
|
let store = Arc::new(
|
||||||
|
HostCoordinatorDomainStore::open(lock, &directory.path("domain.redb"), &domain()).unwrap(),
|
||||||
|
);
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
identity,
|
||||||
|
DurableDecision::None,
|
||||||
|
))])
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
identity,
|
||||||
|
DurableDecision::Preparing,
|
||||||
|
))])
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
identity,
|
||||||
|
DurableDecision::Published,
|
||||||
|
))])
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_preparing_store(
|
||||||
|
directory: &TestDirectory,
|
||||||
|
identity: &GenerationIdentity,
|
||||||
|
) -> Arc<HostCoordinatorDomainStore> {
|
||||||
|
let lock = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(directory.path("domain.lock"))
|
||||||
|
.unwrap();
|
||||||
|
let store = Arc::new(
|
||||||
|
HostCoordinatorDomainStore::open(
|
||||||
|
try_exclusive_flock(lock).unwrap(),
|
||||||
|
&directory.path("domain.redb"),
|
||||||
|
&domain(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
for decision in [DurableDecision::None, DurableDecision::Preparing] {
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
identity, decision,
|
||||||
|
))])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish(
|
||||||
|
directory: &TestDirectory,
|
||||||
|
identity: &GenerationIdentity,
|
||||||
|
) -> (Arc<PublishedControlFace>, Arc<HostCoordinatorDomainStore>) {
|
||||||
|
let store = open_store(directory, identity);
|
||||||
|
let face = PublishedControlFace::publish(
|
||||||
|
&directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity.clone()).unwrap(),
|
||||||
|
Arc::clone(&store),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
(Arc::new(face), store)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server(face: &Arc<PublishedControlFace>) -> JoinHandle<Result<(), ControlError>> {
|
||||||
|
let face = Arc::clone(face);
|
||||||
|
thread::spawn(move || face.serve_next())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnrolledClient {
|
||||||
|
pending: PendingClientEnrollment,
|
||||||
|
credential: TlsCredential,
|
||||||
|
server_certificate: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enroll(face: &Arc<PublishedControlFace>, cli_id: ClientId) -> EnrolledClient {
|
||||||
|
let pending = PendingClientEnrollment::create(
|
||||||
|
face.publication_identity().generation_identity.clone(),
|
||||||
|
cli_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let serving = server(face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(
|
||||||
|
&mut stream,
|
||||||
|
&ControlRequest::Enroll(pending.request().clone()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let response: ControlResponse = PublishedControlFace::receive(&mut stream).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
stream.conn.protocol_version(),
|
||||||
|
Some(rustls::ProtocolVersion::TLSv1_3)
|
||||||
|
);
|
||||||
|
assert_eq!(stream.conn.alpn_protocol(), Some(CONTROL_ALPN));
|
||||||
|
let server_certificate = stream.conn.peer_certificates().unwrap()[0].to_vec();
|
||||||
|
drop(stream);
|
||||||
|
serving.join().unwrap().unwrap();
|
||||||
|
let ControlResponse::Enrolled(ClientEnrollmentResponse { credential, .. }) = response else {
|
||||||
|
panic!("unexpected enrollment response")
|
||||||
|
};
|
||||||
|
EnrolledClient {
|
||||||
|
pending,
|
||||||
|
credential,
|
||||||
|
server_certificate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_certificate_profiles(face: &PublishedControlFace, client: &EnrolledClient) {
|
||||||
|
let (_, ca) =
|
||||||
|
parse_x509_certificate(&face.publication_identity().tls_ca_certificate_der).unwrap();
|
||||||
|
assert!(ca.is_ca());
|
||||||
|
assert!(ca.key_usage().unwrap().unwrap().value.key_cert_sign());
|
||||||
|
assert_eq!(
|
||||||
|
ca.signature_algorithm.algorithm.to_id_string(),
|
||||||
|
"1.3.101.112"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, server_certificate) = parse_x509_certificate(&client.server_certificate).unwrap();
|
||||||
|
let server_usage = server_certificate.extended_key_usage().unwrap().unwrap();
|
||||||
|
assert!(server_usage.value.server_auth);
|
||||||
|
assert!(!server_usage.value.client_auth);
|
||||||
|
let server_names = server_certificate
|
||||||
|
.subject_alternative_name()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
server_names
|
||||||
|
.value
|
||||||
|
.general_names
|
||||||
|
.iter()
|
||||||
|
.any(|name| { matches!(name, GeneralName::DNSName("coordinator.lazaret.invalid")) })
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
server_certificate.validity().not_after.timestamp()
|
||||||
|
- server_certificate.validity().not_before.timestamp()
|
||||||
|
<= 3_660
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, client_certificate) =
|
||||||
|
parse_x509_certificate(&client.credential.certificate_der).unwrap();
|
||||||
|
let client_usage = client_certificate.extended_key_usage().unwrap().unwrap();
|
||||||
|
assert!(client_usage.value.client_auth);
|
||||||
|
assert!(!client_usage.value.server_auth);
|
||||||
|
assert_eq!(
|
||||||
|
client_certificate
|
||||||
|
.signature_algorithm
|
||||||
|
.algorithm
|
||||||
|
.to_id_string(),
|
||||||
|
"1.3.101.112"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
client_certificate.validity().not_after.timestamp()
|
||||||
|
- client_certificate.validity().not_before.timestamp()
|
||||||
|
<= 3_660
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registration_binding(
|
||||||
|
identity: &GenerationIdentity,
|
||||||
|
cli_id: ClientId,
|
||||||
|
session: u8,
|
||||||
|
anchor: u8,
|
||||||
|
) -> SessionBinding {
|
||||||
|
SessionBinding {
|
||||||
|
generation_identity: identity.clone(),
|
||||||
|
cli_id,
|
||||||
|
session_id: SessionId::new(bytes(session)),
|
||||||
|
session_anchor_public_key: SigningKey::from_bytes(&bytes(anchor))
|
||||||
|
.verifying_key()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_registration(
|
||||||
|
face: &Arc<PublishedControlFace>,
|
||||||
|
client: &EnrolledClient,
|
||||||
|
binding: SessionBinding,
|
||||||
|
) -> (
|
||||||
|
rustls::StreamOwned<rustls::ClientConnection, std::os::unix::net::UnixStream>,
|
||||||
|
JoinHandle<Result<(), ControlError>>,
|
||||||
|
SessionRegistrationResponse,
|
||||||
|
) {
|
||||||
|
let serving = server(face);
|
||||||
|
let config = PublishedControlFace::client_config(
|
||||||
|
face.publication_identity(),
|
||||||
|
Some((&client.credential, client.pending.private_key())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(
|
||||||
|
&mut stream,
|
||||||
|
&ControlRequest::Register(SessionRegistrationRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_binding: binding,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let response: ControlResponse = PublishedControlFace::receive(&mut stream).unwrap();
|
||||||
|
let ControlResponse::Registered(response) = response else {
|
||||||
|
panic!("unexpected registration response")
|
||||||
|
};
|
||||||
|
(stream, serving, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refused_registration(
|
||||||
|
face: &Arc<PublishedControlFace>,
|
||||||
|
client: &EnrolledClient,
|
||||||
|
binding: SessionBinding,
|
||||||
|
) -> ControlError {
|
||||||
|
let serving = server(face);
|
||||||
|
let config = PublishedControlFace::client_config(
|
||||||
|
face.publication_identity(),
|
||||||
|
Some((&client.credential, client.pending.private_key())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(
|
||||||
|
&mut stream,
|
||||||
|
&ControlRequest::Register(SessionRegistrationRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_binding: binding,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
||||||
|
drop(stream);
|
||||||
|
serving.join().unwrap().unwrap_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_uds_tls_enrollment_and_registration_preserve_exact_profiles_and_session_state() {
|
||||||
|
let directory = TestDirectory::new("complete");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let (face, store) = publish(&directory, &identity);
|
||||||
|
|
||||||
|
let socket_metadata = std::fs::symlink_metadata(face.socket_path()).unwrap();
|
||||||
|
let identity_metadata = std::fs::symlink_metadata(face.identity_path()).unwrap();
|
||||||
|
assert_eq!(socket_metadata.uid(), getuid().as_raw());
|
||||||
|
assert_eq!(socket_metadata.mode() & 0o7777, 0o600);
|
||||||
|
assert_eq!(identity_metadata.uid(), getuid().as_raw());
|
||||||
|
assert_eq!(identity_metadata.mode() & 0o7777, 0o600);
|
||||||
|
assert_eq!(face.socket_path().file_name().unwrap(), "control.sock");
|
||||||
|
assert_eq!(
|
||||||
|
face.identity_path().file_name().unwrap(),
|
||||||
|
"protocol-identity.postcard"
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = enroll(&face, ClientId::new(bytes(30)));
|
||||||
|
assert_certificate_profiles(&face, &client);
|
||||||
|
|
||||||
|
let mut wrong_leaf_pin = face.publication_identity().clone();
|
||||||
|
wrong_leaf_pin.coordinator_tls_certificate_sha256 =
|
||||||
|
lazaret_core::identity::CertificateDigest::new(bytes(99));
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
assert!(PublishedControlFace::connect(face.socket_path(), config, &wrong_leaf_pin).is_err());
|
||||||
|
assert!(serving.join().unwrap().is_err());
|
||||||
|
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(
|
||||||
|
face.publication_identity(),
|
||||||
|
Some((&client.credential, client.pending.private_key())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(
|
||||||
|
&mut stream,
|
||||||
|
&ControlRequest::Enroll(client.pending.request().clone()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
||||||
|
drop(stream);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::InvalidRequest
|
||||||
|
));
|
||||||
|
|
||||||
|
let binding = registration_binding(&identity, ClientId::new(bytes(30)), 31, 32);
|
||||||
|
let (stream, serving, response) = open_registration(&face, &client, binding.clone());
|
||||||
|
assert_eq!(response.session_capability.session_binding, binding);
|
||||||
|
assert_eq!(face.registry().connected_sessions().unwrap().len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
store.snapshot().unwrap().sessions[0].session_phase,
|
||||||
|
SessionPhase::Attached
|
||||||
|
);
|
||||||
|
drop(stream);
|
||||||
|
serving.join().unwrap().unwrap();
|
||||||
|
assert!(face.registry().connected_sessions().unwrap().is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
store.snapshot().unwrap().sessions[0].session_phase,
|
||||||
|
SessionPhase::Closed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_session_cli_and_anchor_identities_are_refused_while_registered() {
|
||||||
|
let directory = TestDirectory::new("duplicates");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let (face, _) = publish(&directory, &identity);
|
||||||
|
let first = enroll(&face, ClientId::new(bytes(40)));
|
||||||
|
let second = enroll(&face, ClientId::new(bytes(41)));
|
||||||
|
let mismatched_cli = registration_binding(&identity, ClientId::new(bytes(41)), 39, 40);
|
||||||
|
assert!(matches!(
|
||||||
|
refused_registration(&face, &first, mismatched_cli),
|
||||||
|
ControlError::InvalidCertificateProfile
|
||||||
|
));
|
||||||
|
let first_binding = registration_binding(&identity, ClientId::new(bytes(40)), 42, 43);
|
||||||
|
let (first_stream, first_server, _) = open_registration(&face, &first, first_binding.clone());
|
||||||
|
|
||||||
|
let duplicate_session = registration_binding(&identity, ClientId::new(bytes(41)), 42, 44);
|
||||||
|
assert!(matches!(
|
||||||
|
refused_registration(&face, &second, duplicate_session),
|
||||||
|
ControlError::DuplicateIdentity
|
||||||
|
));
|
||||||
|
let duplicate_cli = registration_binding(&identity, ClientId::new(bytes(40)), 45, 46);
|
||||||
|
assert!(matches!(
|
||||||
|
refused_registration(&face, &first, duplicate_cli),
|
||||||
|
ControlError::DuplicateIdentity
|
||||||
|
));
|
||||||
|
let duplicate_anchor = SessionBinding {
|
||||||
|
generation_identity: identity,
|
||||||
|
cli_id: ClientId::new(bytes(41)),
|
||||||
|
session_id: SessionId::new(bytes(47)),
|
||||||
|
session_anchor_public_key: first_binding.session_anchor_public_key,
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
refused_registration(&face, &second, duplicate_anchor),
|
||||||
|
ControlError::DuplicateIdentity
|
||||||
|
));
|
||||||
|
|
||||||
|
drop(first_stream);
|
||||||
|
first_server.join().unwrap().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anonymous_connections_are_enrollment_only_bounded_and_exact_alpn_is_required() {
|
||||||
|
let directory = TestDirectory::new("anonymous");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let (face, _) = publish(&directory, &identity);
|
||||||
|
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(
|
||||||
|
&mut stream,
|
||||||
|
&ControlRequest::Register(SessionRegistrationRequest {
|
||||||
|
protocol_version: ProtocolVersion::V1,
|
||||||
|
session_binding: registration_binding(&identity, ClientId::new(bytes(48)), 49, 50),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
||||||
|
drop(stream);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::InvalidRequest
|
||||||
|
));
|
||||||
|
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
stream.write_all(&8_193_u32.to_be_bytes()).unwrap();
|
||||||
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
||||||
|
drop(stream);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::OversizeRequest {
|
||||||
|
length: 8_193,
|
||||||
|
maximum: 8_192
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut tampered = PendingClientEnrollment::create(identity.clone(), ClientId::new(bytes(51)))
|
||||||
|
.unwrap()
|
||||||
|
.request()
|
||||||
|
.clone();
|
||||||
|
*tampered.certificate_signing_request_der.last_mut().unwrap() ^= 1;
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(&mut stream, &ControlRequest::Enroll(tampered)).unwrap();
|
||||||
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
||||||
|
drop(stream);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::Certificate(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
let serving = server(&face);
|
||||||
|
let mut config =
|
||||||
|
PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
config.alpn_protocols = vec![b"not-lazaret".to_vec()];
|
||||||
|
assert!(
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity(),)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(serving.join().unwrap().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_generation_and_publication_identity_are_refused() {
|
||||||
|
let directory = TestDirectory::new("stale");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let (face, store) = publish(&directory, &identity);
|
||||||
|
|
||||||
|
let stale =
|
||||||
|
PendingClientEnrollment::create(generation_identity(6), ClientId::new(bytes(60))).unwrap();
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
let mut stream =
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.unwrap();
|
||||||
|
PublishedControlFace::send(
|
||||||
|
&mut stream,
|
||||||
|
&ControlRequest::Enroll(stale.request().clone()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
||||||
|
drop(stream);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::StaleGeneration
|
||||||
|
));
|
||||||
|
|
||||||
|
let later = generation_identity(8);
|
||||||
|
for decision in [
|
||||||
|
DurableDecision::None,
|
||||||
|
DurableDecision::Preparing,
|
||||||
|
DurableDecision::Published,
|
||||||
|
] {
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
&later, decision,
|
||||||
|
))])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
assert!(
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity(),)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::StaleGeneration
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_coordinator_key_loss_never_reuses_old_tls_authority() {
|
||||||
|
let directory = TestDirectory::new("key-loss");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let store = open_store(&directory, &identity);
|
||||||
|
let old_face = Arc::new(
|
||||||
|
PublishedControlFace::publish(
|
||||||
|
&directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity.clone()).unwrap(),
|
||||||
|
Arc::clone(&store),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let old_client = enroll(&old_face, ClientId::new(bytes(70)));
|
||||||
|
let old_publication = old_face.publication_identity().clone();
|
||||||
|
drop(old_face);
|
||||||
|
|
||||||
|
let new_face = Arc::new(
|
||||||
|
PublishedControlFace::publish(
|
||||||
|
&directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity).unwrap(),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
new_face.publication_identity().tls_ca_certificate_der,
|
||||||
|
old_publication.tls_ca_certificate_der
|
||||||
|
);
|
||||||
|
let serving = server(&new_face);
|
||||||
|
let old_config = PublishedControlFace::client_config(
|
||||||
|
&old_publication,
|
||||||
|
Some((&old_client.credential, old_client.pending.private_key())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
PublishedControlFace::connect(new_face.socket_path(), old_config, &old_publication,)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(serving.join().unwrap().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publication_refuses_wrong_root_path_mode_and_stale_identity_file() {
|
||||||
|
let mode_directory = TestDirectory::new("mode");
|
||||||
|
std::fs::set_permissions(&mode_directory.0, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let store = open_store(&mode_directory, &identity);
|
||||||
|
assert!(matches!(
|
||||||
|
PublishedControlFace::publish(
|
||||||
|
&mode_directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity.clone()).unwrap(),
|
||||||
|
store,
|
||||||
|
),
|
||||||
|
Err(ControlError::InvalidPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let symlink_parent = TestDirectory::new("symlink-parent");
|
||||||
|
let target = symlink_parent.path("target");
|
||||||
|
std::fs::create_dir(&target).unwrap();
|
||||||
|
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||||
|
let link = symlink_parent.path("link");
|
||||||
|
std::os::unix::fs::symlink(&target, &link).unwrap();
|
||||||
|
let store_directory = TestDirectory::new("symlink-store");
|
||||||
|
let store = open_store(&store_directory, &identity);
|
||||||
|
assert!(matches!(
|
||||||
|
PublishedControlFace::publish(
|
||||||
|
&link,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity.clone()).unwrap(),
|
||||||
|
store,
|
||||||
|
),
|
||||||
|
Err(ControlError::InvalidPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let stale_directory = TestDirectory::new("stale-file");
|
||||||
|
let (face, _) = publish(&stale_directory, &identity);
|
||||||
|
let mut identity_file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(face.identity_path())
|
||||||
|
.unwrap();
|
||||||
|
identity_file.write_all(b"stale publication").unwrap();
|
||||||
|
identity_file.sync_all().unwrap();
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
assert!(
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity(),)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::InvalidPublication(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publication_refuses_preexisting_fixed_control_path() {
|
||||||
|
let directory = TestDirectory::new("existing");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let store = open_store(&directory, &identity);
|
||||||
|
File::create(directory.path("control.sock")).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
PublishedControlFace::publish(
|
||||||
|
&directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity).unwrap(),
|
||||||
|
store,
|
||||||
|
),
|
||||||
|
Err(ControlError::InvalidPublication(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prepared_control_face_admits_only_after_generation_publication() {
|
||||||
|
let directory = TestDirectory::new("publication-gate");
|
||||||
|
let identity = generation_identity(7);
|
||||||
|
let store = open_preparing_store(&directory, &identity);
|
||||||
|
let face = Arc::new(
|
||||||
|
PublishedControlFace::publish(
|
||||||
|
&directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity.clone()).unwrap(),
|
||||||
|
Arc::clone(&store),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let serving = server(&face);
|
||||||
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
||||||
|
assert!(
|
||||||
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
serving.join().unwrap().unwrap_err(),
|
||||||
|
ControlError::StaleGeneration
|
||||||
|
));
|
||||||
|
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
&identity,
|
||||||
|
DurableDecision::Published,
|
||||||
|
))])
|
||||||
|
.unwrap();
|
||||||
|
let client = enroll(&face, ClientId::new(bytes(90)));
|
||||||
|
assert_eq!(client.credential.generation_identity, identity);
|
||||||
|
}
|
||||||
|
|
@ -671,6 +671,46 @@ classDiagram
|
||||||
coordinator_tls_certificate_sha256
|
coordinator_tls_certificate_sha256
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ClientEnrollmentRequest {
|
||||||
|
<<serialized>>
|
||||||
|
protocol_version
|
||||||
|
generation_identity
|
||||||
|
cli_id
|
||||||
|
certificate_signing_request_der
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClientEnrollmentResponse {
|
||||||
|
<<serialized>>
|
||||||
|
protocol_version
|
||||||
|
credential
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionRegistrationRequest {
|
||||||
|
<<serialized>>
|
||||||
|
protocol_version
|
||||||
|
session_binding
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionRegistrationResponse {
|
||||||
|
<<serialized>>
|
||||||
|
protocol_version
|
||||||
|
session_capability
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControlRequest {
|
||||||
|
<<serialized>>
|
||||||
|
<<union>>
|
||||||
|
Enroll
|
||||||
|
Register
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControlResponse {
|
||||||
|
<<serialized>>
|
||||||
|
<<union>>
|
||||||
|
Enrolled
|
||||||
|
Registered
|
||||||
|
}
|
||||||
|
|
||||||
class SandboxCredentialBundle {
|
class SandboxCredentialBundle {
|
||||||
<<serialized>>
|
<<serialized>>
|
||||||
protocol_version
|
protocol_version
|
||||||
|
|
@ -730,6 +770,14 @@ classDiagram
|
||||||
ProtocolPublicationIdentity --> GenerationIdentity
|
ProtocolPublicationIdentity --> GenerationIdentity
|
||||||
ProtocolPublicationIdentity --> TrustManifest
|
ProtocolPublicationIdentity --> TrustManifest
|
||||||
ProtocolPublicationIdentity --> TlsCredential : coordinator TLS leaf
|
ProtocolPublicationIdentity --> TlsCredential : coordinator TLS leaf
|
||||||
|
ClientEnrollmentRequest --> GenerationIdentity
|
||||||
|
ClientEnrollmentResponse --> TlsCredential
|
||||||
|
SessionRegistrationRequest --> SessionBinding
|
||||||
|
SessionRegistrationResponse --> SessionRequestCapability
|
||||||
|
ControlRequest --> ClientEnrollmentRequest : Enroll
|
||||||
|
ControlRequest --> SessionRegistrationRequest : Register
|
||||||
|
ControlResponse --> ClientEnrollmentResponse : Enrolled
|
||||||
|
ControlResponse --> SessionRegistrationResponse : Registered
|
||||||
SandboxCredentialBundle --> GenerationIdentity
|
SandboxCredentialBundle --> GenerationIdentity
|
||||||
SandboxCredentialBundle --> TrustManifest
|
SandboxCredentialBundle --> TrustManifest
|
||||||
SandboxCredentialBundle --> ProtocolCredential : coordinator verifier
|
SandboxCredentialBundle --> ProtocolCredential : coordinator verifier
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue