Skip to content

Commit

Permalink
token: implement EAR token generation
Browse files Browse the repository at this point in the history
This commit allows the AS to issue EAR tokens with the
help of the rust-ear crate.

EAR tokens require particular claims. This creates a binding
between the AS policy and the EAR token.
Specifically, the policy engine must return an EAR appraisal.
The policy engine is still generic. Multiple policy engines
could be implemented as long as they create an appraisal.

Token generation is no longer generic.
Since the policy engine, will always return an appraisal,
we must generate an EAR token.
This commit removes the simple token issuer
and replaces the TokenProvider trait with a struct.

The KBS will still be able to validate many different tokens,
but this commit changes the AS to only issue EAR tokens.

There are a few other changes, including that the policy engine
no longer takes multiple policies. For now, we only evaluate
the first policy in the policy list, but future commits will
change this convention so that we only ever think about one
policy for the attestation service (until we introduce support
for validating multiple devices at once).

This commit also slightly changes how we handle init-data by
adding the init_data_claims and runtime_data_claims
to the flattened claims when the init_data and report_data
fields are part of the tcb_claims returned by the verifier.

This surfaces the json init_data and report_data to the AS
and KBS policy engines and includes them in the EAR token.
Note that this will increase the size of the token
and that some complex init_data values might break out
JSON flattening code.

Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
  • Loading branch information
fitzthum committed Sep 28, 2024
1 parent 40f9b6f commit 4f7281e
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 494 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ cfg-if = "1.0.0"
chrono = "0.4.19"
clap = { version = "4", features = ["derive"] }
config = "0.13.3"
#ear = "0.2.0"
ear = { git = "https://github.com/veraison/rust-ear.git", rev = "13962e36225f85dcc3bad8ac7047a306b1350baf" }
env_logger = "0.10.0"
hex = "0.4.3"
jwt-simple = "0.11"
Expand Down
1 change: 1 addition & 0 deletions attestation-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async-trait.workspace = true
base64.workspace = true
cfg-if.workspace = true
clap = { workspace = true, optional = true }
ear.workspace = true
env_logger = { workspace = true, optional = true }
futures = "0.3.17"
hex.workspace = true
Expand Down
9 changes: 1 addition & 8 deletions attestation-service/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::rvps::RvpsConfig;
use crate::token::{AttestationTokenBrokerType, AttestationTokenConfig};
use crate::token::AttestationTokenConfig;

use serde::Deserialize;
use std::fs::File;
Expand All @@ -21,12 +21,6 @@ pub struct Config {
/// Configurations for RVPS.
pub rvps_config: RvpsConfig,

/// The Attestation Result Token Broker type.
///
/// Possible values:
/// * `Simple`
pub attestation_token_broker: AttestationTokenBrokerType,

/// The Attestation Result Token Broker Config
pub attestation_token_config: AttestationTokenConfig,
}
Expand Down Expand Up @@ -54,7 +48,6 @@ impl Default for Config {
work_dir,
policy_engine: "opa".to_string(),
rvps_config: RvpsConfig::default(),
attestation_token_broker: AttestationTokenBrokerType::Simple,
attestation_token_config: AttestationTokenConfig::default(),
}
}
Expand Down
93 changes: 46 additions & 47 deletions attestation-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ pub use kbs_types::{Attestation, Tee};
use log::{debug, info};
use policy_engine::{PolicyEngine, PolicyEngineType};
use rvps::{RvpsApi, RvpsError};
use serde_json::{json, Value};
use serde_json::Value;
use serde_variant::to_variant_name;
use sha2::{Digest, Sha256, Sha384, Sha512};
use std::{collections::HashMap, str::FromStr};
use std::{
collections::{BTreeMap, HashMap},
str::FromStr,
};
use strum::{AsRefStr, Display, EnumString};
use thiserror::Error;
use tokio::fs;
Expand Down Expand Up @@ -96,7 +99,7 @@ pub struct AttestationService {
_config: Config,
policy_engine: Box<dyn PolicyEngine + Send + Sync>,
rvps: Box<dyn RvpsApi + Send + Sync>,
token_broker: Box<dyn AttestationTokenBroker + Send + Sync>,
token_broker: AttestationTokenBroker,
}

impl AttestationService {
Expand All @@ -116,9 +119,7 @@ impl AttestationService {
.await
.map_err(ServiceError::Rvps)?;

let token_broker = config
.attestation_token_broker
.to_token_broker(config.attestation_token_config.clone())?;
let token_broker = AttestationTokenBroker::new(config.attestation_token_config.clone())?;

Ok(Self {
_config: config,
Expand Down Expand Up @@ -164,9 +165,8 @@ impl AttestationService {
/// will not be performed.
/// - `hash_algorithm`: The hash algorithm that is used to calculate the digest of `runtime_data` and
/// `init_data`.
/// - `policy_ids`: The policy ids that used to check this evidence. Any check fails against a policy will
/// not cause this function to return error. The result check against every policy will be included inside
/// the finally Token returned by CoCo-AS.
/// - `policy_id`: The id of the policy that will be used to evaluate the claims.
/// The hash of the policy will be returned as part of the attestation token.
#[allow(clippy::too_many_arguments)]
pub async fn evaluate(
&self,
Expand Down Expand Up @@ -196,62 +196,61 @@ impl AttestationService {
None => InitDataHash::NotProvided,
};

let claims_from_tee_evidence = verifier
let mut claims_from_tee_evidence = verifier
.evaluate(&evidence, &report_data, &init_data_hash)
.await
.map_err(|e| anyhow!("Verifier evaluate failed: {e:?}"))?;
info!("{:?} Verifier/endorsement check passed.", tee);

let flattened_claims = flatten_claims(tee, &claims_from_tee_evidence)?;
debug!("flattened_claims: {:#?}", flattened_claims);
// If the verifier produces an init_data claim (meaning that
// it has validated the init_data hash), add the JSON init_data_claims,
// to the claims map. Do the same for the report data.
//
// These claims will be flattened and provided to the policy engine.
// They will also end up in the EAR token as part of the annotated evidence.
if let Some(claims_map) = claims_from_tee_evidence.as_object_mut() {
if claims_map.get("init_data").is_some() {
claims_map.insert("init_data_claims".to_string(), init_data_claims);
}

if claims_map.get("report_data").is_some() {
claims_map.insert("report_data_claims".to_string(), runtime_data_claims);
}
}

let mut flattened_claims = BTreeMap::new();
flatten_claims(
&mut flattened_claims,
&claims_from_tee_evidence,
to_variant_name(&tee)?.to_string(),
)?;

let tcb_json = serde_json::to_string(&flattened_claims)?;
debug!("flattened_claims: {:#?}", flattened_claims);

let reference_data_map = self
.get_reference_data(flattened_claims.keys())
.await
.map_err(|e| anyhow!("Generate reference data failed: {:?}", e))?;
debug!("reference_data_map: {:#?}", reference_data_map);

let evaluation_report = self
let appraisal = self
.policy_engine
.evaluate(reference_data_map.clone(), tcb_json, policy_ids.clone())
.evaluate(
reference_data_map.clone(),
flattened_claims,
policy_ids[0].clone(),
)
.await
.map_err(|e| anyhow!("Policy Engine evaluation failed: {e}"))?;

info!("Policy check passed.");
let policies: Vec<_> = evaluation_report
.into_iter()
.map(|(k, v)| {
json!({
"policy-id": k,
"policy-hash": v,
})
})
.collect();

let reference_data_map: HashMap<String, Vec<String>> = reference_data_map
.into_iter()
.filter(|it| !it.1.is_empty())
.collect();

let token_claims = json!({
"tee": to_variant_name(&tee)?,
"evaluation-reports": policies,
"tcb-status": flattened_claims,
"reference-data": reference_data_map,
"customized_claims": {
"init_data": init_data_claims,
"runtime_data": runtime_data_claims,
},
});

let attestation_results_token = self.token_broker.issue(token_claims)?;
info!(
"Attestation Token ({}) generated.",
self._config.attestation_token_broker
);
info!("TCB Appraisal Generated Successfully");

// For now, create only one submod, called `cpu`.
// We can create more when we support attesting multiple devices at once.
let mut submods = BTreeMap::new();
submods.insert("cpu".to_string(), appraisal);

let attestation_results_token = self.token_broker.issue_ear(submods)?;
Ok(attestation_results_token)
}

Expand Down
19 changes: 9 additions & 10 deletions attestation-service/src/policy_engine/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use anyhow::Result;
use async_trait::async_trait;
use ear::{Appraisal, RawValue};
use serde::Deserialize;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::io;
use std::path::Path;
use strum::EnumString;
Expand Down Expand Up @@ -41,6 +42,8 @@ pub enum PolicyError {
EvalPolicyFailed(#[source] anyhow::Error),
#[error("json serialization failed: {0}")]
JsonSerializationFailed(#[source] anyhow::Error),
#[error("Policy claim value not valid (must be between -127 and 127)")]
InvalidClaimValue,
}

#[derive(Debug, EnumString, Deserialize)]
Expand All @@ -62,18 +65,14 @@ type PolicyDigest = String;

#[async_trait]
pub trait PolicyEngine {
/// Verify an input body against a set of ref values and a list of policies
/// return a list of policy ids with their sha384 at eval time
/// abort early on first failed validation and any errors.
/// The result is a key-value map.
/// - `key`: the policy id
/// - `value`: the digest of the policy (using **Sha384**).
/// Verify an input body against a set of ref values and a policy id
/// return an EAR Appraisal
async fn evaluate(
&self,
reference_data_map: HashMap<String, Vec<String>>,
input: String,
policy_ids: Vec<String>,
) -> Result<HashMap<String, PolicyDigest>, PolicyError>;
tcb_claims: BTreeMap<String, RawValue>,
policy_id: String,
) -> Result<Appraisal, PolicyError>;

async fn set_policy(&mut self, policy_id: String, policy: String) -> Result<(), PolicyError>;

Expand Down
104 changes: 21 additions & 83 deletions attestation-service/src/policy_engine/opa/default_policy.rego
Original file line number Diff line number Diff line change
@@ -1,93 +1,31 @@
# Attestation Service Default Policy
#
# The function of this policy is to adopt the default policy when no custom policy
# is provided in the attestation request of Attestation Service.
#
# - The input data required by this default policy is a set of key value pairs:
#
# {
# "sample1": "112233",
# "sample2": "332211",
# ...
# }
#
# - The format of reference data required by this default policy is defined as follows:
#
# {
# "reference": {
# "sample1": ["112233", "223311"],
# "sample2": "332211",
# "sample3": [],
# ...
# }
# }
#
# If the default policy is used for verification, the reference meeting the above format
# needs to be provided in the attestation request, otherwise the Attestation Service will
# automatically generate a reference data meeting the above format.
package policy
import rego.v1

import future.keywords.every
# For the `executables` trust claim, the value 33 stands for
# "Runtime memory includes executables, scripts, files, and/or
# objects which are not recognized."
default executables := 33

default allow = false
# For the `hardware` trust claim, the value 97 stands for
# "A Verifier does not recognize an Attester's hardware or
# firmware, but it should be recognized."
default hardware := 97

allow {
every k, v in input {
# `judge_field`: Traverse each key value pair in the input and make policy judgments on it.
#
# For each key value pair:
# * If there isn't a corresponding key in the reference:
# It is considered that the current key value pair has passed the verification.
# * If there is a corresponding key in the reference:
# Call `match_value` to further judge the value in input with the value in reference.
judge_field(k, v)
}
}

judge_field(input_key, input_value) {
has_key(data.reference, input_key)
reference_value := data.reference[input_key]

# `match_value`: judge the value in input with the value in reference.
#
# * If the type of reference value is not array:
# Judge whether input value and reference value are equal。
# * If the type of reference value is array:
# Call `array_include` to further judge the input value with the values in the array.
match_value(reference_value, input_value)
}

judge_field(input_key, input_value) {
not has_key(data.reference, input_key)
}
# For the `executables` trust claim, the value 3 stands for
# "Only a recognized genuine set of approved executables have
# been loaded during the boot process."
executables := 3 if {
input.launch_digest in data.reference.launch_digest

match_value(reference_value, input_value) {
not is_array(reference_value)
input_value == reference_value
}

match_value(reference_value, input_value) {
is_array(reference_value)
# For the `hardware` trust claim, the value 2 stands for
# "An Attester has passed its hardware and/or firmware
# verifications needed to demonstrate that these are genuine/
# supported.
hardware := 2 if {
input.productId in data.reference.productId

# `array_include`: judge the input value with the values in the array.
#
# * If the reference value array is empty:
# It is considered that the current input value has passed the verification.
# * If the reference value array is not empty:
# Judge whether there is a value equal to input value in the reference value array.
array_include(reference_value, input_value)
}

array_include(reference_value_array, input_value) {
reference_value_array == []
}

array_include(reference_value_array, input_value) {
reference_value_array != []
some i
reference_value_array[i] == input_value
}
input.svn in data.reference.svn

has_key(m, k) {
_ = m[k]
}
Loading

0 comments on commit 4f7281e

Please sign in to comment.