diff --git a/rust-keybroker/keybroker-server/Cargo.toml b/rust-keybroker/keybroker-server/Cargo.toml index a97d871..c77573a 100644 --- a/rust-keybroker/keybroker-server/Cargo.toml +++ b/rust-keybroker/keybroker-server/Cargo.toml @@ -22,3 +22,6 @@ clap = { version = "=4.3.24", features = ["derive", "std"] } keybroker-common = { path = "../keybroker-common" } veraison-apiclient = { git = "https://github.com/veraison/rust-apiclient.git" } ear = { git = "https://github.com/veraison/rust-ear.git" } +regorus = "0.2.5" +serde_json = "1.0.128" +anyhow = "1.0.89" diff --git a/rust-keybroker/keybroker-server/src/error.rs b/rust-keybroker/keybroker-server/src/error.rs index 361c292..f10eb2d 100644 --- a/rust-keybroker/keybroker-server/src/error.rs +++ b/rust-keybroker/keybroker-server/src/error.rs @@ -37,6 +37,14 @@ pub enum Error { /// that are transacted through the API between the client and the server, if the client provides faulty data. #[error(transparent)] Base64Decode(#[from] base64::DecodeError), + + /// Represents errors from the use of the policy evaluation library. + #[error(transparent)] + Policy(#[from] anyhow::Error), + + /// Represents errors from the use of the JSON serialisation and deserialisation library. + #[error(transparent)] + Json(#[from] serde_json::Error), } /// Errors happening within the verification process logic. diff --git a/rust-keybroker/keybroker-server/src/main.rs b/rust-keybroker/keybroker-server/src/main.rs index d2571f8..da2a001 100644 --- a/rust-keybroker/keybroker-server/src/main.rs +++ b/rust-keybroker/keybroker-server/src/main.rs @@ -13,6 +13,7 @@ use keystore::KeyStore; mod challenge; mod error; mod keystore; +pub mod policy; mod verifier; #[post("/key/{keyid}")] @@ -90,6 +91,9 @@ async fn submit_evidence( let verifier_base = data.args.verifier.clone(); + let optional_policy = data.args.policy.clone(); + let rims = data.args.rims.clone(); + // We are in an async context, but the verifier client is synchronous, so spawn // it as a blocking task. let handle = task::spawn_blocking(move || { @@ -102,6 +106,8 @@ async fn submit_evidence( content_type_str, &challenge.challenge_value, &evidence_bytes, + optional_policy, + &rims, ) }); let result = handle.await.unwrap(); @@ -167,6 +173,14 @@ struct Args { /// Set the server verbosity #[arg(short, long, default_value_t = false)] verbose: bool, + + /// Rego file containing the appraisal policy for (EAR) attestation results + #[arg(long, default_value = None)] + policy: Option, + + /// File containing a JSON array with base64-encoded known-good RIM values + #[arg(long, default_value = "rims.json")] + rims: String, } struct ServerState { diff --git a/rust-keybroker/keybroker-server/src/policy.rego b/rust-keybroker/keybroker-server/src/policy.rego new file mode 100644 index 0000000..e22eb36 --- /dev/null +++ b/rust-keybroker/keybroker-server/src/policy.rego @@ -0,0 +1,23 @@ +package arm_cca + +default allow := false + +allow if { + input.eat_profile == "tag:github.com,2023:veraison/ear" + + # platform part + prec := input.submods.CCA_SSD_PLATFORM + prec["ear.status"] == "affirming" + + # realm part + rrec := input.submods.CCA_REALM + rrec["ear.status"] == "warning" + + rtv := rrec["ear.trustworthiness-vector"] + rtv["instance-identity"] == 2 + + # check RIM value against known-good-values + rclaims := rrec["ear.veraison.annotated-evidence"] + rim := rclaims["cca-realm-initial-measurement"] + rim in data["reference-values"] +} \ No newline at end of file diff --git a/rust-keybroker/keybroker-server/src/policy.rs b/rust-keybroker/keybroker-server/src/policy.rs new file mode 100644 index 0000000..4c035d7 --- /dev/null +++ b/rust-keybroker/keybroker-server/src/policy.rs @@ -0,0 +1,75 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +use regorus::{self, Value}; + +use crate::error::Result; + +// Evaluate an EAR claims-set against the appraisal policy and known-good RIM values +pub(crate) fn rego_eval( + custom_policy: Option, + rims: &str, + ear_claims: &str, +) -> Result { + const POLICY: &str = include_str!("policy.rego"); + + // Create engine. + let mut engine = regorus::Engine::new(); + + engine.set_rego_v1(true); + engine.set_strict_builtin_errors(false); + + // Add the appraisal policy + match custom_policy { + None => { + engine.add_policy(String::from("policy.rego"), String::from(POLICY))?; + } + Some(file) => { + engine.add_policy_from_file(file)?; + } + } + + // Load the configured known good RIM values + engine.add_data(Value::from_json_file(rims)?)?; + + // Set the EAR claims-set to be appraised + engine.set_input(Value::from_json_str(ear_claims)?); + + let results = engine.eval_rule("data.arm_cca.allow".to_string())?; + + Ok(results) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rego_eval_ear_default_policy_ok() { + let ear_claims = include_str!("../../../testdata/ear-claims-ok.json"); + let rims = stringify_testdata_path("rims-matching.json"); + + let results = rego_eval(None, &rims, ear_claims).expect("successful eval"); + + assert_eq!(results.to_string(), "true"); + } + + #[test] + fn rego_eval_default_policy_unmatched_rim() { + let ear_claims = include_str!("../../../testdata/ear-claims-ok.json"); + let rims = stringify_testdata_path("rims-not-matching.json"); + + let results = rego_eval(None, &rims, ear_claims).expect("successful eval"); + + assert_eq!(results.to_string(), "false"); + } + + fn stringify_testdata_path(s: &str) -> String { + let mut test_data = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + test_data.push("../../testdata"); + test_data.push(s); + + test_data.into_os_string().into_string().unwrap() + } +} diff --git a/rust-keybroker/keybroker-server/src/verifier.rs b/rust-keybroker/keybroker-server/src/verifier.rs index 3cc48e8..d2a23a4 100644 --- a/rust-keybroker/keybroker-server/src/verifier.rs +++ b/rust-keybroker/keybroker-server/src/verifier.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::Result; -use ear::{Algorithm, Ear, TrustTier}; +use crate::policy; +use ear::{Algorithm, Ear}; use veraison_apiclient::*; pub fn verify_with_veraison_instance( @@ -10,6 +11,8 @@ pub fn verify_with_veraison_instance( media_type: &str, challenge: &[u8], evidence: &[u8], + policy: Option, + rims: &str, ) -> Result { // Get the discovery URL from the base URL let discovery = Discovery::from_base_url(String::from(verifier_base_url))?; @@ -60,14 +63,13 @@ pub fn verify_with_veraison_instance( verification_key_string.as_bytes(), )?; - // The simplest possible appraisal policy: accept if we have an AFFIRMING or WARNING result - // from every submodule. - // TODO: This policy is rather too "relaxed" - the simplest and strictest policy would be - // to require AFFIRMING from every submodule. We have some integration issues with Veraison - // today that prevent this. - let verified = ear.submods.iter().all(|(_module, appraisal)| { - appraisal.status == TrustTier::Affirming || appraisal.status == TrustTier::Warning - }); + let ear_claims = serde_json::to_string(&ear)?; - Ok(verified) + // Appraise the received EAR using the embedded policy (see ./policy.rego) + // unless a custom one has been provided on the command line. The default + // policy also wants to match the RIM value reported by the CCA token with + // the known-good RIM values supplied on the command line. + let results = policy::rego_eval(policy, rims, &ear_claims)?; + + Ok(results.to_string() == "true") } diff --git a/testdata/ear-claims-ok.json b/testdata/ear-claims-ok.json new file mode 100644 index 0000000..932c567 --- /dev/null +++ b/testdata/ear-claims-ok.json @@ -0,0 +1,36 @@ +{ + "eat_nonce": "bobW2XzHE7xt1D285JGmtAMRwCeov4WjnaY-nORMEyqKEZ0pb65qaZnpvz5EcbDOASRdiJQkwx6JeTs7HWsVBA==", + "eat_profile": "tag:github.com,2023:veraison/ear", + "submods": { + "CCA_REALM": { + "ear.status": "warning", + "ear.trustworthiness-vector": { + "configuration": 0, + "executables": 33, + "file-system": 0, + "hardware": 0, + "instance-identity": 2, + "runtime-opaque": 0, + "sourced-data": 0, + "storage-opaque": 0 + }, + "ear.veraison.annotated-evidence": { + "cca-realm-challenge": "bobW2XzHE7xt1D285JGmtAMRwCeov4WjnaY+nORMEyqKEZ0pb65qaZnpvz5EcbDOASRdiJQkwx6JeTs7HWsVBA==", + "cca-realm-extensible-measurements": [ + "JNWwopbMBcvYBoxQZ8W9Rzt3Ddpq4IL+O6MKvj+aarE=", + "eI/AkL/GuO2QMVK6hBTnPa9bjHux55rVAqsGmbZZ7RY=", + "2sRqWEFdw6ANenQYUgCOnK5k9S0DufdtdvSzZE/vxBY=", + "MsavxiflVYXAMVU1nzMaDiJfaEDblH3Zbvq4G+JnGTk=" + ], + "cca-realm-hash-algo-id": "sha-256", + "cca-realm-initial-measurement": "MRMUq3NiA1DPdYg0rlxl2ejC3H/r5ufZZUu+hk4wDUk=", + "cca-realm-personalization-value": "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIDEzIGxhenkgZG9ncy5UaGUgcXVpY2sgYnJvd24gZm94IA==", + "cca-realm-public-key": "BHb5iAkb5YXtQYAa7Pq4WFSMYwV+FrDmdhILvQ0vnCngVsXUGgEw65whUXiZ3CMUayjhsGK9PqSzFf0hnxy7Uoy250ykm+Fnc3NPYaHKYQMbK789kY8vlP/EIo5QkZVErg==", + "cca-realm-public-key-hash-algo-id": "sha-256" + } + }, + "CCA_SSD_PLATFORM": { + "ear.status": "affirming" + } + } +} diff --git a/testdata/rims-matching.json b/testdata/rims-matching.json new file mode 100644 index 0000000..57388fa --- /dev/null +++ b/testdata/rims-matching.json @@ -0,0 +1,6 @@ +{ + "reference-values": [ + "MRMUq3NiA1DPdYg0rlxl2ejC3H/r5ufZZUu+hk4wDUk=", + "q3N/r5ufZZUu+iAg0rlxl2ejC3HMRMUhk4wDUk1DPdY=" + ] +} diff --git a/testdata/rims-not-matching.json b/testdata/rims-not-matching.json new file mode 100644 index 0000000..88f4fdb --- /dev/null +++ b/testdata/rims-not-matching.json @@ -0,0 +1,6 @@ +{ + "reference-values": [ + "XRMUq3NiA1DPdYg0rlxl2ejC3H/r5ufZZUu+hk4wDUk=", + "X3N/r5ufZZUu+iAg0rlxl2ejC3HMRMUhk4wDUk1DPdY=" + ] +}