mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
test(connect): isolate offline enrollment e2e root (#6659)
* test(connect): isolate offline enrollment e2e root * test(connect): own e2e issuer key id
This commit is contained in:
@@ -80,6 +80,11 @@ embedded-secrets-check: ## Check no private key material or credential literal i
|
||||
@echo "🔑 Checking embedded secret material guard..."
|
||||
./scripts/check_embedded_secrets.sh
|
||||
|
||||
.PHONY: offline-enrollment-e2e-check
|
||||
offline-enrollment-e2e-check: core-deps ## Build and exercise the dedicated offline enrollment E2E root
|
||||
@echo "🔐 Checking the offline enrollment E2E root boundary..."
|
||||
./scripts/check_offline_enrollment_e2e.sh
|
||||
|
||||
.PHONY: test-wiring-check
|
||||
test-wiring-check: ## Check tests stay registered and selected by their intended runners
|
||||
@echo "🧪 Checking test wiring..."
|
||||
|
||||
@@ -23,7 +23,7 @@ pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-gua
|
||||
@echo "✅ All pre-commit checks passed!"
|
||||
|
||||
.PHONY: pre-pr
|
||||
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check log-analyzer-rules-check clippy-check test ## Run full pre-PR checks with clippy and tests
|
||||
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check log-analyzer-rules-check offline-enrollment-e2e-check clippy-check test ## Run full pre-PR checks with clippy and tests
|
||||
@echo "✅ All pre-PR checks passed!"
|
||||
|
||||
.PHONY: dev-check
|
||||
|
||||
@@ -308,6 +308,9 @@ jobs:
|
||||
} > artifacts/test-and-lint/doctest-diagnostics.txt
|
||||
exit "${status}"
|
||||
|
||||
- name: Check offline enrollment E2E root boundary
|
||||
run: ./scripts/check_offline_enrollment_e2e.sh
|
||||
|
||||
- name: Upload test reports and diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
|
||||
@@ -39,6 +39,11 @@ path = "src/main.rs"
|
||||
name = "rustfs-cli"
|
||||
path = "src/bin/rustfs-cli.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rustfs-cli-e2e"
|
||||
path = "src/bin/rustfs-cli.rs"
|
||||
required-features = ["offline-enrollment-e2e-root"]
|
||||
|
||||
[features]
|
||||
default = ["ftps", "webdav"]
|
||||
metrics-gpu = ["rustfs-obs/gpu"]
|
||||
@@ -52,6 +57,8 @@ tracing-chunk-debug = [] # Enable per-chunk tracing in data plane (high noise,
|
||||
full = ["metrics-gpu", "ftps", "swift", "webdav", "sftp", "pyroscope"]
|
||||
manual-test-runners = []
|
||||
e2e-test-hooks = []
|
||||
# Builds the dedicated rustfs-cli-e2e target with a build-time public enrollment root.
|
||||
offline-enrollment-e2e-root = []
|
||||
rio-v2 = ["rustfs-ecstore/rio-v2"]
|
||||
pyroscope = ["rustfs-obs/pyroscope"]
|
||||
# Tokio runtime telemetry. Requires `--cfg tokio_unstable`; use `make build-profiling`.
|
||||
|
||||
@@ -294,7 +294,12 @@ fn enroll(arguments: &[String]) -> Result<(), String> {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| "the system clock is before the Unix epoch".to_string())?
|
||||
.as_secs() as i64;
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
let now = enrollment_evaluation_time(now)?;
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
let verified = verify_enrollment_challenge(&challenge, now).map_err(|error| error.to_string())?;
|
||||
#[cfg(not(feature = "offline-enrollment-e2e-root"))]
|
||||
let verified = OfflineEnrollment::verify_challenge(&challenge, now).map_err(|error| error.to_string())?;
|
||||
|
||||
// First use mints the key; a retry answers with the one already enrolled,
|
||||
@@ -313,6 +318,32 @@ fn enroll(arguments: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
fn enrollment_evaluation_time(system_now: i64) -> Result<i64, String> {
|
||||
// The public fixture gate needs a compile-time clock; real E2E builds omit it.
|
||||
if env!("CARGO_BIN_NAME") == "rustfs-cli-e2e"
|
||||
&& let Some(value) = option_env!("RUSTFS_E2E_OFFLINE_ENROLLMENT_FIXTURE_TIME")
|
||||
{
|
||||
return value
|
||||
.parse()
|
||||
.map_err(|_| "the compiled E2E enrollment fixture time is invalid".to_owned());
|
||||
}
|
||||
|
||||
Ok(system_now)
|
||||
}
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
fn verify_enrollment_challenge(
|
||||
challenge: &[u8],
|
||||
now: i64,
|
||||
) -> Result<rustfs::connect::offline::VerifiedChallenge, rustfs::connect::offline::EnrollmentError> {
|
||||
if env!("CARGO_BIN_NAME") == "rustfs-cli-e2e" {
|
||||
return OfflineEnrollment::verify_e2e_challenge(challenge, now);
|
||||
}
|
||||
|
||||
OfflineEnrollment::verify_challenge(challenge, now)
|
||||
}
|
||||
|
||||
/// Reads the challenge from a file, or from stdin when the path is `-`.
|
||||
///
|
||||
/// A challenge is not a secret — it is signed, public, and carried in by hand —
|
||||
@@ -420,6 +451,26 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
#[test]
|
||||
fn only_the_dedicated_e2e_target_selects_the_test_root() {
|
||||
let challenge =
|
||||
fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/offline-enrollment-e2e/challenge.json"))
|
||||
.expect("read E2E enrollment challenge");
|
||||
let result = verify_enrollment_challenge(&challenge, 4_070_908_800);
|
||||
|
||||
if env!("CARGO_BIN_NAME") == "rustfs-cli-e2e" {
|
||||
result.expect("the dedicated E2E binary selects the test root");
|
||||
} else {
|
||||
assert_eq!(
|
||||
result
|
||||
.expect_err("every production binary must retain the hosted root")
|
||||
.reason(),
|
||||
"ENROLLMENT_ROOT_UNKNOWN"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_offline_bundle_cli_shutdown_is_bounded_with_a_stuck_blocking_collector() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("test runtime");
|
||||
|
||||
@@ -46,8 +46,22 @@ use crate::connect::identity::DeviceIdentity;
|
||||
/// The hosted enrolment root, compiled in. Both halves are pinned: the
|
||||
/// fingerprint identifies the root, and the point is what actually verifies the
|
||||
/// first link, so a build cannot be pointed at a different key by supplying one.
|
||||
const PINNED_ROOT_KEY_ID: &str = "df22e2806112debbe953672aafa186d699af0e97dd3fd2b09fa8359005fe348f";
|
||||
const PINNED_ROOT_PUBLIC_KEY: &str = "BFfx-K-FfEA5nK_Rz3IHacvRCkJyQ7JOd1geLyU6HKRZDgNezmVuKhvJ22VhemyjV__Gshk8JGGqOBzYPMD0p6s";
|
||||
const HOSTED_ROOT: EnrollmentRoot = EnrollmentRoot {
|
||||
key_id: "df22e2806112debbe953672aafa186d699af0e97dd3fd2b09fa8359005fe348f",
|
||||
public_key: "BFfx-K-FfEA5nK_Rz3IHacvRCkJyQ7JOd1geLyU6HKRZDgNezmVuKhvJ22VhemyjV__Gshk8JGGqOBzYPMD0p6s",
|
||||
};
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
const E2E_ROOT: EnrollmentRoot = EnrollmentRoot {
|
||||
key_id: env!("RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_KEY_ID"),
|
||||
public_key: env!("RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_PUBLIC_KEY"),
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct EnrollmentRoot {
|
||||
key_id: &'static str,
|
||||
public_key: &'static str,
|
||||
}
|
||||
|
||||
/// Domain separation tags. A document that verifies under one of these must not
|
||||
/// be accepted for another artifact type, so the tag is part of the signature
|
||||
@@ -327,6 +341,22 @@ impl OfflineEnrollment {
|
||||
/// `now_unix` is the device's reading of the current time, which the clock
|
||||
/// skew tolerance treats as advisory.
|
||||
pub fn verify_challenge(document: &[u8], now_unix: i64) -> Result<VerifiedChallenge, EnrollmentError> {
|
||||
Self::verify_challenge_with_root(document, now_unix, HOSTED_ROOT)
|
||||
}
|
||||
|
||||
/// Verify a challenge against the fixed root carried only by the dedicated
|
||||
/// E2E CLI build. No caller can supply or replace this root at runtime.
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
#[doc(hidden)]
|
||||
pub fn verify_e2e_challenge(document: &[u8], now_unix: i64) -> Result<VerifiedChallenge, EnrollmentError> {
|
||||
Self::verify_challenge_with_root(document, now_unix, E2E_ROOT)
|
||||
}
|
||||
|
||||
fn verify_challenge_with_root(
|
||||
document: &[u8],
|
||||
now_unix: i64,
|
||||
root: EnrollmentRoot,
|
||||
) -> Result<VerifiedChallenge, EnrollmentError> {
|
||||
let envelope: SignedDocument = serde_json::from_slice(document).map_err(|_| EnrollmentError::MalformedDocument)?;
|
||||
|
||||
// Step 1: the encoding is checked before anything is decoded from it, so
|
||||
@@ -345,7 +375,7 @@ impl OfflineEnrollment {
|
||||
let issued_at = parse_timestamp(&routing.issued_at)?;
|
||||
|
||||
// Steps 3 to 5.
|
||||
let connect_key = verify_trust_chain(&routing.trust_chain, &routing.connect_key_id, issued_at)?;
|
||||
let connect_key = verify_trust_chain(&routing.trust_chain, &routing.connect_key_id, issued_at, root)?;
|
||||
|
||||
// Step 6. The verification key comes from the chain, so `signature.keyId`
|
||||
// is a label rather than an input: a value naming some other key simply
|
||||
@@ -439,24 +469,25 @@ fn verify_trust_chain(
|
||||
chain: &[SignedDocument],
|
||||
connect_key_id: &str,
|
||||
challenge_issued_at: i64,
|
||||
root: EnrollmentRoot,
|
||||
) -> Result<VerifyingKey, EnrollmentError> {
|
||||
// The pinned root gate runs before the chain's shape is examined, so a
|
||||
// chain that is internally consistent under a foreign root — exactly what
|
||||
// trust on first use would have accepted — is refused for its root rather
|
||||
// than for its length.
|
||||
let first = chain.first().ok_or(EnrollmentError::EnrollmentRootUnknown)?;
|
||||
let root = decode_trust_link(first)?;
|
||||
if root.0.issuer_key_id != PINNED_ROOT_KEY_ID {
|
||||
let first_link = decode_trust_link(first)?;
|
||||
if first_link.0.issuer_key_id != root.key_id {
|
||||
return Err(EnrollmentError::EnrollmentRootUnknown);
|
||||
}
|
||||
|
||||
let [_, second] = chain else {
|
||||
return Err(EnrollmentError::TrustChainInvalid);
|
||||
};
|
||||
let links = [root, decode_trust_link(second)?];
|
||||
let links = [first_link, decode_trust_link(second)?];
|
||||
|
||||
let mut issuer_key_id = PINNED_ROOT_KEY_ID.to_owned();
|
||||
let (mut issuer_key, _) = decode_public_key(PINNED_ROOT_PUBLIC_KEY).ok_or(EnrollmentError::EnrollmentRootUnknown)?;
|
||||
let mut issuer_key_id = root.key_id.to_owned();
|
||||
let (mut issuer_key, _) = decode_public_key(root.public_key).ok_or(EnrollmentError::EnrollmentRootUnknown)?;
|
||||
|
||||
for (index, ((link, link_bytes), entry)) in links.iter().zip(chain).enumerate() {
|
||||
if link.format_version != FORMAT_TRUST_LINK
|
||||
|
||||
@@ -54,6 +54,14 @@ fn fixture_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../protocol/agent/v1/fixtures/offline-enrollment")
|
||||
}
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
fn e2e_fixture(name: &str) -> Vec<u8> {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/offline-enrollment-e2e")
|
||||
.join(name);
|
||||
fs::read(&path).unwrap_or_else(|error| panic!("read {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
Sha256::digest(bytes).iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
@@ -268,6 +276,85 @@ fn every_challenge_accept_vector_verifies_and_exposes_the_signed_fields() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
#[test]
|
||||
fn e2e_root_is_fixed_and_disjoint_from_the_hosted_root() {
|
||||
let challenge = e2e_fixture("challenge.json");
|
||||
let now = unix("2099-01-01T00:00:00Z");
|
||||
|
||||
let verified =
|
||||
OfflineEnrollment::verify_e2e_challenge(&challenge, now).expect("the dedicated E2E root verifies its challenge");
|
||||
assert_eq!(verified.challenge_id, "018f7e6d-9d6a-7d93-8f64-8b20b3384712");
|
||||
assert_eq!(
|
||||
OfflineEnrollment::verify_challenge(&challenge, now)
|
||||
.expect_err("the production verifier must not trust the E2E root")
|
||||
.reason(),
|
||||
"ENROLLMENT_ROOT_UNKNOWN"
|
||||
);
|
||||
|
||||
let hosted = accept_vector_named("challenge signed by a chained signing key under the pinned root");
|
||||
let hosted_now = unix(field(&hosted, "evaluationTime"));
|
||||
assert_eq!(
|
||||
OfflineEnrollment::verify_e2e_challenge(&envelope(&hosted["document"]), hosted_now)
|
||||
.expect_err("the E2E verifier must not silently retain the hosted root")
|
||||
.reason(),
|
||||
"ENROLLMENT_ROOT_UNKNOWN"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "offline-enrollment-e2e-root")]
|
||||
#[test]
|
||||
fn e2e_public_chain_matches_the_challenge_and_every_signature_verifies() {
|
||||
use p256::ecdsa::signature::Verifier as _;
|
||||
|
||||
let root: Value = serde_json::from_slice(&e2e_fixture("root.json")).expect("E2E root parses");
|
||||
let chain_bytes = e2e_fixture("trust-chain.json");
|
||||
let chain: Value = serde_json::from_slice(&chain_bytes).expect("E2E chain parses");
|
||||
let challenge_envelope: Value = serde_json::from_slice(&e2e_fixture("challenge.json")).expect("E2E challenge parses");
|
||||
let challenge_bytes = signed_octets(&challenge_envelope);
|
||||
let challenge: Value = serde_json::from_slice(&challenge_bytes).expect("signed E2E challenge parses");
|
||||
|
||||
assert_eq!(challenge["trustChain"], chain, "the independent and embedded chain JSON must match");
|
||||
let compact_chain = chain_bytes
|
||||
.strip_suffix(b"\n")
|
||||
.expect("the chain fixture has one final newline");
|
||||
assert!(
|
||||
challenge_bytes
|
||||
.windows(compact_chain.len())
|
||||
.any(|window| window == compact_chain),
|
||||
"the signed challenge must embed the independent chain byte for byte"
|
||||
);
|
||||
|
||||
let mut issuer = verifying_key(field(&root, "publicKey"));
|
||||
let mut issuer_id = field(&root, "keyId").to_owned();
|
||||
for link in chain.as_array().expect("E2E chain is a list") {
|
||||
assert_eq!(field(&link["signature"], "keyId"), issuer_id.as_str());
|
||||
let signature = BASE64_URL_NO_PAD
|
||||
.decode(field(&link["signature"], "value"))
|
||||
.expect("trust-link signature is base64url");
|
||||
issuer
|
||||
.verify(
|
||||
&signing_input("rustfs-offline-trust-link-v1", &signed_octets(link)),
|
||||
&p256::ecdsa::Signature::from_slice(&signature).expect("trust-link signature parses"),
|
||||
)
|
||||
.expect("trust-link signature verifies");
|
||||
let document = signed_document(link);
|
||||
issuer_id = field(&document, "subjectKeyId").to_owned();
|
||||
issuer = verifying_key(field(&document, "subjectPublicKey"));
|
||||
}
|
||||
|
||||
assert_eq!(field(&challenge, "connectKeyId"), issuer_id.as_str());
|
||||
let signature = BASE64_URL_NO_PAD
|
||||
.decode(field(&challenge_envelope["signature"], "value"))
|
||||
.expect("challenge signature is base64url");
|
||||
issuer
|
||||
.verify(
|
||||
&signing_input("rustfs-offline-enrollment-challenge-v1", &challenge_bytes),
|
||||
&p256::ecdsa::Signature::from_slice(&signature).expect("challenge signature parses"),
|
||||
)
|
||||
.expect("challenge signature verifies");
|
||||
}
|
||||
|
||||
/// Connect's own producer wrote the response accept vectors. Rebuilding them
|
||||
/// from the challenge they answer, with the device nonce and production time
|
||||
/// they used, must reproduce every field that does not depend on which device
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"bytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Qub2ZmbGluZS5lbnJvbGxtZW50Q2hhbGxlbmdlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsImNoYWxsZW5nZUlkIjoiMDE4ZjdlNmQtOWQ2YS03ZDkzLThmNjQtOGIyMGIzMzg0NzEyIiwib3JnYW5pemF0aW9uTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE4ZjdlNmQtOWQ2YS03ZDkzLThmNjQtOGIyMGIzMzg0NzEzIiwiY2x1c3Rlck5hbWUiOiJvcmdhbml6YXRpb25zLzAxOGY3ZTZkLTlkNmEtN2Q5My04ZjY0LThiMjBiMzM4NDcxMy9jbHVzdGVycy8wMThmN2U2ZC05ZDZhLTdkOTMtOGY2NC04YjIwYjMzODQ3MTQiLCJub25jZSI6InBLU2twS1NrcEtTa3BLU2twS1NrcEtTa3BLU2twS1NrcEtTa3BLU2twS1EiLCJpc3N1ZWRBdCI6IjIwOTktMDEtMDFUMDA6MDA6MDBaIiwiZXhwaXJlc0F0IjoiMjA5OS0wMS0wOFQwMDowMDowMFoiLCJjb25uZWN0S2V5SWQiOiIwNjA3NWY0ODRkY2U1YzA3NDVjYTc2NWE4ODk5MzA5YmQ3MTc2MTI5NDdhMzc3MGEwZDI3YjhjY2I4YmY5MDUyIiwidHJ1c3RDaGFpbiI6W3siYnl0ZXMiOiJleUptYjNKdFlYUldaWEp6YVc5dUlqb2ljblZ6ZEdaekxtTnZibTVsWTNRdWIyWm1iR2x1WlM1MGNuVnpkRXhwYm1zdk1TSXNJbkJ5YjNSdlkyOXNWbVZ5YzJsdmJpSTZJbll4SWl3aWMyVnlhV0ZzSWpvaVpUQTBZVEF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKeWIyeGxJam9pYVc1MFpYSnRaV1JwWVhSbElpd2lhWE56ZFdWeVMyVjVTV1FpT2lJNVlUVmpNemcxWVdKa09UZGpOMlppT0RSak5UUXdZalZtTldVME1tUTNPVGt4T1RNd1lUUTBOMk01TURJMk56azVaVGhtWkRSak5ESXpNR0kxWXpOaklpd2ljM1ZpYW1WamRFdGxlVWxrSWpvaVlUazFOVGxpWkRSbE1EazBPV0ZqWXpFeU1tRm1abU0xTldVMlpHSTRaVGt4Wmpoa01EazNaVFUxWldNNE9UbGtZVFl4T1RGbE5EWTVZelE0T0RKaE1pSXNJbk4xWW1wbFkzUlFkV0pzYVdOTFpYa2lPaUpDVG13elZFeDFTazV0Tld4NFRGUmFNRGxUWVdwWVRHMUZSM0ZqZHpGaVQyZzROVWhEYmpWdVVUazJWVlZUYUZVNFZHeEhVVWsxYmxaSldWUnJRWFpLYjFWRVMwWmhVVlp0WVZGVk9WRkhaemsyVDJ0RU1EUWlMQ0p1YjNSQ1pXWnZjbVVpT2lJeU1ESXdMVEF4TFRBeFZEQXdPakF3T2pBd1dpSXNJbTV2ZEVGbWRHVnlJam9pTWpBNU9TMHdNUzB3TVZRd01Eb3dNRG93TUZvaWZRPT0iLCJzaWduYXR1cmUiOnsiYWxnb3JpdGhtIjoiRVMyNTYiLCJrZXlJZCI6IjlhNWMzODVhYmQ5N2M3ZmI4NGM1NDBiNWY1ZTQyZDc5OTE5MzBhNDQ3YzkwMjY3OTllOGZkNGM0MjMwYjVjM2MiLCJ2YWx1ZSI6IklUSThKVGFURjRoRHJ0bF9tSk0zYmZ0eUlhMlJaSjFNNVpPVk1xaEE2cVJSSzFNcEcxQlF0emRRVzB6bHo4eG1RZUJNWl84clViTUlFcnFBel8tSWZRIn19LHsiYnl0ZXMiOiJleUptYjNKdFlYUldaWEp6YVc5dUlqb2ljblZ6ZEdaekxtTnZibTVsWTNRdWIyWm1iR2x1WlM1MGNuVnpkRXhwYm1zdk1TSXNJbkJ5YjNSdlkyOXNWbVZ5YzJsdmJpSTZJbll4SWl3aWMyVnlhV0ZzSWpvaVpUQTBZVEF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNRElpTENKeWIyeGxJam9pYzJsbmJtbHVaeUlzSW1semMzVmxja3RsZVVsa0lqb2lZVGsxTlRsaVpEUmxNRGswT1dGall6RXlNbUZtWm1NMU5XVTJaR0k0WlRreFpqaGtNRGszWlRVMVpXTTRPVGxrWVRZeE9URmxORFk1WXpRNE9ESmhNaUlzSW5OMVltcGxZM1JMWlhsSlpDSTZJakEyTURjMVpqUTROR1JqWlRWak1EYzBOV05oTnpZMVlUZzRPVGt6TURsaVpEY3hOell4TWprME4yRXpOemN3WVRCa01qZGlPR05qWWpoaVpqa3dOVElpTENKemRXSnFaV04wVUhWaWJHbGpTMlY1SWpvaVFrRk9lWFpITFdkR1YwdFRaM0pMWTNkUVpGUmhNRWN3TTBKdlVtRkNSbWhvZG10cWRFUjZlVk5tYm5weFYxcHROa2xLV0U1RWRVZEdlVXBFZVZWSmNYZzJSa1p5TlZSV2NsOXVhbUZCYzFWb2FEbE1VMlJ2SWl3aWJtOTBRbVZtYjNKbElqb2lNakF5TUMwd01TMHdNVlF3TURvd01Eb3dNRm9pTENKdWIzUkJablJsY2lJNklqSXdPVGt0TURFdE1ERlVNREE2TURBNk1EQmFJbjA9Iiwic2lnbmF0dXJlIjp7ImFsZ29yaXRobSI6IkVTMjU2Iiwia2V5SWQiOiJhOTU1OWJkNGUwOTQ5YWNjMTIyYWZmYzU1ZTZkYjhlOTFmOGQwOTdlNTVlYzg5OWRhNjE5MWU0NjljNDg4MmEyIiwidmFsdWUiOiI4MTVrVGNMY1hkZXZORFRia1dBck1KSnpORTVmaEdxbTNUb3hTM3Exdl9ZQkRVcmFieldmTFhjMEc5TjZiVXZFeEd3WEJUMVFpVzJvUDBCM3JKUS0ydyJ9fV19",
|
||||
"signature": {
|
||||
"algorithm": "ES256",
|
||||
"keyId": "06075f484dce5c0745ca765a8899309bd717612947a3770a0d27b8ccb8bf9052",
|
||||
"value": "8i2nvYhNsZbL1UPF82Otr4pcecYfKhdRV1Q7m3Geqcx-TdvYk486Q_cZHSQ7OaqnIhTZwPgMYFYKUuEhwAaYEQ"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"keyId": "9a5c385abd97c7fb84c540b5f5e42d7991930a447c9026799e8fd4c4230b5c3c",
|
||||
"publicKey": "BIPyiA1W2NDuy3ftHLtlY2tO2WNjkjGINZ_9QQvyyN9syzbMb91QYG2SN0AqZPylFsTL-loF4M1tVySZtXJpKhM"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[{"bytes":"eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Qub2ZmbGluZS50cnVzdExpbmsvMSIsInByb3RvY29sVmVyc2lvbiI6InYxIiwic2VyaWFsIjoiZTA0YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJyb2xlIjoiaW50ZXJtZWRpYXRlIiwiaXNzdWVyS2V5SWQiOiI5YTVjMzg1YWJkOTdjN2ZiODRjNTQwYjVmNWU0MmQ3OTkxOTMwYTQ0N2M5MDI2Nzk5ZThmZDRjNDIzMGI1YzNjIiwic3ViamVjdEtleUlkIjoiYTk1NTliZDRlMDk0OWFjYzEyMmFmZmM1NWU2ZGI4ZTkxZjhkMDk3ZTU1ZWM4OTlkYTYxOTFlNDY5YzQ4ODJhMiIsInN1YmplY3RQdWJsaWNLZXkiOiJCTmwzVEx1Sk5tNWx4TFRaMDlTYWpYTG1FR3FjdzFiT2g4NUhDbjVuUTk2VVVTaFU4VGxHUUk1blZJWVRrQXZKb1VES0ZhUVZtYVFVOVFHZzk2T2tEMDQiLCJub3RCZWZvcmUiOiIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsIm5vdEFmdGVyIjoiMjA5OS0wMS0wMVQwMDowMDowMFoifQ==","signature":{"algorithm":"ES256","keyId":"9a5c385abd97c7fb84c540b5f5e42d7991930a447c9026799e8fd4c4230b5c3c","value":"ITI8JTaTF4hDrtl_mJM3bftyIa2RZJ1M5ZOVMqhA6qRRK1MpG1BQtzdQW0zlz8xmQeBMZ_8rUbMIErqAz_-IfQ"}},{"bytes":"eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Qub2ZmbGluZS50cnVzdExpbmsvMSIsInByb3RvY29sVmVyc2lvbiI6InYxIiwic2VyaWFsIjoiZTA0YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDIiLCJyb2xlIjoic2lnbmluZyIsImlzc3VlcktleUlkIjoiYTk1NTliZDRlMDk0OWFjYzEyMmFmZmM1NWU2ZGI4ZTkxZjhkMDk3ZTU1ZWM4OTlkYTYxOTFlNDY5YzQ4ODJhMiIsInN1YmplY3RLZXlJZCI6IjA2MDc1ZjQ4NGRjZTVjMDc0NWNhNzY1YTg4OTkzMDliZDcxNzYxMjk0N2EzNzcwYTBkMjdiOGNjYjhiZjkwNTIiLCJzdWJqZWN0UHVibGljS2V5IjoiQkFOeXZHLWdGV0tTZ3JLY3dQZFRhMEcwM0JvUmFCRmhodmtqdER6eVNmbnpxV1ptNklKWE5EdUdGeUpEeVVJcXg2RkZyNVRWcl9uamFBc1VoaDlMU2RvIiwibm90QmVmb3JlIjoiMjAyMC0wMS0wMVQwMDowMDowMFoiLCJub3RBZnRlciI6IjIwOTktMDEtMDFUMDA6MDA6MDBaIn0=","signature":{"algorithm":"ES256","keyId":"a9559bd4e0949acc122affc55e6db8e91f8d097e55ec899da6191e469c4882a2","value":"815kTcLcXdevNDTbkWArMJJzNE5fhGqm3ToxS3q1v_YBDUrabzWfLXc0G9N6bUvExGwXBT1QiW2oP0B3rJQ-2w"}}]
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
fixture_root="${repo_root}/rustfs/tests/fixtures/offline-enrollment-e2e"
|
||||
root_fixture="${fixture_root}/root.json"
|
||||
challenge_fixture="${fixture_root}/challenge.json"
|
||||
|
||||
for command in cargo python3; do
|
||||
command -v "${command}" >/dev/null 2>&1 || {
|
||||
echo "offline enrollment E2E gate: ${command} is required" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
read_fixture_field()
|
||||
{
|
||||
python3 -c 'import json, sys; value=json.load(open(sys.argv[1], encoding="utf-8"))[sys.argv[2]]; assert isinstance(value, str) and value; print(value)' "$1" "$2"
|
||||
}
|
||||
|
||||
RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_KEY_ID="$(read_fixture_field "${root_fixture}" keyId)"
|
||||
RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_PUBLIC_KEY="$(read_fixture_field "${root_fixture}" publicKey)"
|
||||
RUSTFS_E2E_OFFLINE_ENROLLMENT_FIXTURE_TIME="$(
|
||||
python3 -c 'import base64, datetime, json, sys; envelope=json.load(open(sys.argv[1], encoding="utf-8")); document=json.loads(base64.b64decode(envelope["bytes"], validate=True)); print(int(datetime.datetime.fromisoformat(document["issuedAt"].replace("Z", "+00:00")).timestamp()))' "${challenge_fixture}"
|
||||
)"
|
||||
export RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_KEY_ID
|
||||
export RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_PUBLIC_KEY
|
||||
export RUSTFS_E2E_OFFLINE_ENROLLMENT_FIXTURE_TIME
|
||||
|
||||
[[ "${RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_KEY_ID}" =~ ^[0-9a-f]{64}$ ]] \
|
||||
|| { echo 'offline enrollment E2E gate: fixture root key id is invalid' >&2; exit 1; }
|
||||
[[ "${RUSTFS_E2E_OFFLINE_ENROLLMENT_ROOT_PUBLIC_KEY}" =~ ^[A-Za-z0-9_-]{87}$ ]] \
|
||||
|| { echo 'offline enrollment E2E gate: fixture root public key is invalid' >&2; exit 1; }
|
||||
[[ "${RUSTFS_E2E_OFFLINE_ENROLLMENT_FIXTURE_TIME}" =~ ^[0-9]+$ ]] \
|
||||
|| { echo 'offline enrollment E2E gate: fixture evaluation time is invalid' >&2; exit 1; }
|
||||
|
||||
task_root="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-offline-enrollment-e2e.XXXXXX")"
|
||||
case "${task_root}" in
|
||||
"${TMPDIR:-/tmp}"/rustfs-offline-enrollment-e2e.*) ;;
|
||||
*) echo 'offline enrollment E2E gate: unsafe temporary path' >&2; exit 1 ;;
|
||||
esac
|
||||
cleanup()
|
||||
{
|
||||
rm -rf -- "${task_root}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' HUP INT TERM
|
||||
|
||||
export CARGO_TARGET_DIR="${task_root}/target"
|
||||
feature=offline-enrollment-e2e-root
|
||||
cargo build --locked -p rustfs --bin rustfs-cli-e2e --features "${feature}"
|
||||
cargo build --locked -p rustfs --bin rustfs-cli --features "${feature}"
|
||||
cargo test --locked -p rustfs --test connect_offline_enrollment --features "${feature}" e2e_
|
||||
|
||||
"${CARGO_TARGET_DIR}/debug/rustfs-cli-e2e" connect offline enroll \
|
||||
--challenge "${challenge_fixture}" \
|
||||
--output "${task_root}/e2e-response.json" \
|
||||
--key-dir "${task_root}/e2e-key" \
|
||||
>"${task_root}/e2e.stdout" 2>"${task_root}/e2e.stderr"
|
||||
test -s "${task_root}/e2e-response.json" \
|
||||
|| { echo 'offline enrollment E2E gate: dedicated CLI produced no response' >&2; exit 1; }
|
||||
|
||||
if "${CARGO_TARGET_DIR}/debug/rustfs-cli" connect offline enroll \
|
||||
--challenge "${challenge_fixture}" \
|
||||
--output "${task_root}/production-response.json" \
|
||||
--key-dir "${task_root}/production-key" \
|
||||
>"${task_root}/production.stdout" 2>"${task_root}/production.stderr"
|
||||
then
|
||||
echo 'offline enrollment E2E gate: production CLI accepted the E2E root' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Fq 'the offline enrollment trust chain is not issued by a root pinned in this build' \
|
||||
"${task_root}/production.stderr" \
|
||||
|| { echo 'offline enrollment E2E gate: production CLI failed for the wrong reason' >&2; exit 1; }
|
||||
test ! -e "${task_root}/production-response.json" \
|
||||
|| { echo 'offline enrollment E2E gate: rejected production CLI wrote a response' >&2; exit 1; }
|
||||
|
||||
echo 'Offline enrollment E2E root gate passed.'
|
||||
Reference in New Issue
Block a user