Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions integration-tests/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use base64::{
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use openssl::pkey::PKey;
use serde_json::json;
use std::collections::HashMap;
use std::sync::{Arc, Once};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tempfile::TempDir;
Expand Down Expand Up @@ -332,6 +333,7 @@ impl TestHarness {
extra_teekey_paths: vec![],
},
attestation_service: AttestationConfig {
policy_id_map: HashMap::new(),
attestation_service: AttestationServiceConfig::CoCoASBuiltIn(
kbs::attestation::coco::builtin::Config {
rvps_config,
Expand Down
13 changes: 13 additions & 0 deletions kbs/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ Concrete attestation service can be set via `type` field. Supported attestation

Due to different `type` field, properties are different.

`timeout` and `policy_id_map` apply to every type. The latter is a table mapping
each policy-selector a client may select to one or more policy IDs:

```toml
[attestation_service.policy_id_map]
alice = ["alice-strict"]
bob = ["bob-cpu", "bob-gpu"]
```

#### Built-In CoCo AS

When `type` is set to `coco_as_builtin`, the following properties can be set.
Expand All @@ -164,6 +173,7 @@ When `type` is set to `coco_as_builtin`, the following properties can be set.
| Property | Type | Description | Default |
|----------------------------|-----------------------------|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| `timeout` | Integer | The maximum time (in minutes) of the attestation session | 5 |
| `policy_id_map` | Map of String array | AS policies selectable by a client, keyed by policy-selector. See [RCAR `Request`][ps]| `{}` |
| `rvps_config` | [RVPSConfiguration][2] | RVPS configuration | See [RVPSConfiguration][2] |
| `attestation_token_broker` | [AttestationTokenBroker][1] | Attestation result token configuration. | See [AttestationTokenBroker][1] |
| `verifier_config` | Object | Optional verifier specific configuration (for example TPM)| See [Verifier Configuration][vcfg] |
Expand All @@ -173,6 +183,7 @@ When `type` is set to `coco_as_builtin`, the following properties can be set.
[3]: #keyvaluestorage
[4]: #tokensignerconfig
[vcfg]: ../../attestation-service/docs/config.md#verifier-configuration
[ps]: ./kbs_attestation_protocol.md#request

##### AttestationTokenBroker

Expand Down Expand Up @@ -234,6 +245,7 @@ The following properties can be set.
| Property | Type | Description | Default |
|-------------|---------|-------------------------------------------------------------------------------------------------------------------------------|--------------------------|
| `timeout` | Integer | The maximum time (in minutes) between RCAR handshake's `auth` and `attest` requests | 5 |
| `policy_id_map` | Map of String array | AS policies selectable by a client, keyed by policy-selector. See [RCAR `Request`][ps] | `{}` |
| `as_addr` | String | The URL of the remote CoCoAS | `http://127.0.0.1:50004` |
| `pool_size` | Integer | The connections between KBS and CoCoAS are maintained in a conenction pool. This property determines the max size of the pool | `100` |

Expand All @@ -251,6 +263,7 @@ The following properties can be set.
| `api_key` | String | Intel Trust Authority API key. | Yes | - |
| `certs_file` | String | URL to an Intel Trust Authority portal or path to JWKS file used for token verification. | Yes | - |
| `policy_ids` | String array | Quoted and comma-separated list of policy IDs defined in ITA portal. | No | `[]` |
| `policy_id_map` | Map of String array | Policies selectable by a client, keyed by policy-selector. A selected policy-selector replaces `policy_ids`. See [RCAR `Request`][ps] | No | `{}` |
| `allow_unmatched_policy` | Boolean | Whether policy matching is required. If no `policy_ids` are specified, policy matching is not checked. | No | false |

Detailed [documentation](https://docs.trustauthority.intel.com).
Expand Down
22 changes: 22 additions & 0 deletions kbs/docs/kbs_attestation_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ transfer some specific information. For example, some attestations follow the
Diffie–Hellman key exchange protocol to first build a secure channel and
transfer secret messages (Such as AMD SEV(-ES) pre-attestation).

`extra-params` may also carry a `policy-selector`, which selects the Attestation
Service policies that evaluate this session's evidence:

```json
"extra-params": { "policy-selector": "alice" }
```

KBS maps each `policy-selector` to one or more attestation policies, so the
accepted values are specific to a deployment and must be known to the KBC in
advance. An unmapped `policy-selector` is rejected, and omitting the field
selects a default policy.
Note that not all backend attestation services support multiple policies, e.g.
CoCo AS now only support one policy, while ITA supports multiple.

Selecting a `policy-selector` does not by itself entitle the KBC to anything,
since KBS still decides what each appraisal may release. It does change what the
resulting token means, though, so a relying party should not treat every token
alike. The [Attestation Results Token](#attestation-results-token) names the
policy that was actually applied, which is the policy the `policy-selector`
resolved to rather than the `policy-selector` itself, so a resource policy can
require a particular appraisal before releasing a resource.

## `Challenge`

If the KBC does not own any KBS generated HTTP Cookie, or if the Cookie validity
Expand Down
117 changes: 114 additions & 3 deletions kbs/src/attestation/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::prometheus::{
use super::{
config::{AttestationConfig, AttestationServiceConfig},
session::SessionStatus,
Error, Result,
Error, Result, POLICY_SELECTOR_JSON_KEY,
};

const KBS_SESSION_STORAGE_NAMESPACE: &str = "kbs_protocol_session";
Expand Down Expand Up @@ -93,7 +93,15 @@ pub trait Attest: Send + Sync {

/// Verify Attestation Evidence
/// Return Attestation Results Token
async fn verify(&self, evidence_to_verify: Vec<IndependentEvidence>) -> anyhow::Result<String>;
///
/// `policy_ids` are the policies resolved from the policy-selector
/// selected by the client, or `None` to apply the default of this
/// Attestation Service.
async fn verify(
&self,
evidence_to_verify: Vec<IndependentEvidence>,
policy_ids: Option<&[String]>,
) -> anyhow::Result<String>;

/// generate the Challenge to pass to attester based on Tee and nonce
async fn generate_challenge(
Expand Down Expand Up @@ -122,6 +130,32 @@ pub trait Attest: Send + Sync {
}
}

/// Resolve the policy-selector that a client optionally selected in an RCAR
/// `Request` into the Attestation Service policies to evaluate its evidence
/// with.
///
/// Selecting no policy-selector leaves the Attestation Service default in
/// place. An unknown policy-selector is rejected instead of silently falling
/// back, so a client can only reach policies that an administrator declared.
fn resolve_policy_ids(
policy_id_map: &HashMap<String, Vec<String>>,
extra_params: &serde_json::Value,
) -> anyhow::Result<Option<Vec<String>>> {
let Some(policy_selector) = extra_params.get(POLICY_SELECTOR_JSON_KEY) else {
return Ok(None);
};

let policy_selector = policy_selector
.as_str()
.context("policy-selector is not a string")?;

policy_id_map
.get(policy_selector)
.cloned()
.map(Some)
.context("policy-selector is not configured")
}

/// Attestation Service
#[derive(Clone)]
pub struct AttestationService {
Expand All @@ -133,6 +167,9 @@ pub struct AttestationService {

/// Maximum session expiration time.
timeout: i64,

/// Policies a client is allowed to select, keyed by policy-selector
policy_id_map: HashMap<String, Vec<String>>,
}

#[derive(Deserialize, Debug)]
Expand All @@ -148,6 +185,18 @@ impl AttestationService {
storage_backend_config: &StorageBackendConfig,
storage_provider: Arc<dyn StorageProvider>,
) -> Result<Self> {
// A policy-selector without any policy would leave the evidence
// unevaluated, which some Attestation Services accept silently.
for (policy_selector, policy_ids) in &config.policy_id_map {
if policy_ids.is_empty() {
return Err(Error::AttestationServiceInitialization {
source: anyhow!(
"no attestation policy is mapped to policy-selector {policy_selector}"
),
});
}
}

let inner = match config.attestation_service {
#[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))]
AttestationServiceConfig::CoCoASBuiltIn(cfg) => {
Expand Down Expand Up @@ -217,6 +266,7 @@ impl AttestationService {
inner,
timeout: config.timeout,
session_map,
policy_id_map: config.policy_id_map,
})
}

Expand Down Expand Up @@ -256,6 +306,14 @@ impl AttestationService {
);
}

// Resolve eagerly so that an unusable ID is reported here instead of
// surfacing as an attestation failure later on.
if let Some(policy_ids) = resolve_policy_ids(&self.policy_id_map, &request.extra_params)
.inspect_err(|_| AUTH_ERRORS.inc())?
{
debug!("Selected attestation policies: {policy_ids:?}");
}

let challenge = self
.inner
.generate_challenge(request.tee, request.extra_params.clone())
Expand Down Expand Up @@ -403,9 +461,12 @@ impl AttestationService {
.trim_end_matches('"')
.to_owned();

let policy_ids = resolve_policy_ids(&self.policy_id_map, &session.request().extra_params)
.inspect_err(|_| ATTESTATION_ERRORS.inc())?;

let token = self
.inner
.verify(evidence_to_verify)
.verify(evidence_to_verify, policy_ids.as_deref())
.await
.inspect_err(|_| {
ATTESTATION_FAILURES
Expand Down Expand Up @@ -487,6 +548,56 @@ impl AttestationService {
mod tests {
use super::*;

fn policy_id_map() -> HashMap<String, Vec<String>> {
HashMap::from([
("alice".to_string(), vec!["alice-strict".to_string()]),
(
"bob".to_string(),
vec!["bob-cpu".to_string(), "bob-gpu".to_string()],
),
])
}

#[rstest::rstest]
// No policy-selector selected: the Attestation Service default applies.
#[case(json!({}), Some(None))]
#[case(json!(""), Some(None))]
#[case(json!({"selected-hash-algorithm": "sha384"}), Some(None))]
// A declared policy-selector resolves to every policy it is mapped to.
#[case(json!({"policy-selector": "alice"}), Some(Some(vec!["alice-strict"])))]
#[case(json!({"policy-selector": "bob"}), Some(Some(vec!["bob-cpu", "bob-gpu"])))]
// Anything else is rejected rather than downgraded to the default.
#[case(json!({"policy-selector": "alice-strict"}), None)]
#[case(json!({"policy-selector": "../escape"}), None)]
#[case(json!({"policy-selector": ""}), None)]
#[case(json!({"policy-selector": 1}), None)]
fn test_resolve_policy_ids(
#[case] extra_params: serde_json::Value,
#[case] expected: Option<Option<Vec<&str>>>,
) {
let resolved = resolve_policy_ids(&policy_id_map(), &extra_params);

let expected = expected.map(|policy_ids| {
policy_ids
.map(|policy_ids| policy_ids.into_iter().map(String::from).collect::<Vec<_>>())
});

match expected {
Some(policy_ids) => assert_eq!(resolved.unwrap(), policy_ids),
None => assert!(resolved.is_err()),
}
}

#[test]
fn test_resolve_policy_ids_without_map() {
let policy_id_map = HashMap::new();

assert!(resolve_policy_ids(&policy_id_map, &json!({}))
.unwrap()
.is_none());
assert!(resolve_policy_ids(&policy_id_map, &json!({"policy-selector": "alice"})).is_err());
}

#[tokio::test]
async fn test_make_nonce() {
const BITS_PER_BYTE: usize = 8;
Expand Down
15 changes: 12 additions & 3 deletions kbs/src/attestation/coco/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;

use crate::attestation::backend::{make_nonce, Attest, IndependentEvidence};
use crate::attestation::{
backend::{make_nonce, Attest, IndependentEvidence},
coco::DEFAULT_POLICY_ID,
};

#[derive(Clone, Debug, Deserialize, PartialEq, Default)]
pub struct Config {
Expand Down Expand Up @@ -70,7 +73,11 @@ impl Attest for BuiltInCoCoAs {
.await
}

async fn verify(&self, evidence_to_verify: Vec<IndependentEvidence>) -> Result<String> {
async fn verify(
&self,
evidence_to_verify: Vec<IndependentEvidence>,
policy_ids: Option<&[String]>,
) -> Result<String> {
let mut verification_requests = vec![];

for evidence in evidence_to_verify {
Expand All @@ -96,7 +103,9 @@ impl Attest for BuiltInCoCoAs {
verification_requests.push(request);
}

let policy_ids = vec!["default".to_string()];
let policy_ids = policy_ids
.map(<[String]>::to_vec)
.unwrap_or_else(|| vec![DEFAULT_POLICY_ID.to_string()]);
self.inner
.read()
.await
Expand Down
15 changes: 12 additions & 3 deletions kbs/src/attestation/coco/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use std::collections::HashMap;
use tonic::transport::Channel;
use tracing::info;

use crate::attestation::backend::{make_nonce, Attest, IndependentEvidence};
use crate::attestation::{
backend::{make_nonce, Attest, IndependentEvidence},
coco::DEFAULT_POLICY_ID,
};

use self::attestation::{
attestation_service_client::AttestationServiceClient,
Expand Down Expand Up @@ -100,7 +103,11 @@ impl Attest for GrpcClientPool {
Ok(())
}

async fn verify(&self, evidence_to_verify: Vec<IndependentEvidence>) -> Result<String> {
async fn verify(
&self,
evidence_to_verify: Vec<IndependentEvidence>,
policy_ids: Option<&[String]>,
) -> Result<String> {
let mut verification_requests: Vec<IndividualAttestationRequest> = vec![];

for evidence in evidence_to_verify {
Expand Down Expand Up @@ -136,7 +143,9 @@ impl Attest for GrpcClientPool {

let attestation_request = tonic::Request::new(AttestationRequest {
verification_requests,
policy_ids: vec!["default".to_string()],
policy_ids: policy_ids
.map(<[String]>::to_vec)
.unwrap_or_else(|| vec![DEFAULT_POLICY_ID.to_string()]),
});

let mut client = self.pool.get().await?;
Expand Down
4 changes: 4 additions & 0 deletions kbs/src/attestation/coco/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ pub mod grpc;

#[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))]
pub mod builtin;

/// Attestation Service policy applied when a client does not select a
/// policy-selector.
pub const DEFAULT_POLICY_ID: &str = "default";
11 changes: 11 additions & 0 deletions kbs/src/attestation/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0

use serde::Deserialize;
use std::collections::HashMap;

pub const DEFAULT_TIMEOUT: i64 = 5;

Expand All @@ -14,13 +15,23 @@ pub struct AttestationConfig {

#[serde(default = "default_timeout")]
pub timeout: i64,

/// Maps a policy-selector, which a client may select in the RCAR
/// `Request`, to the Attestation Service policies that evaluate its
/// evidence.
///
/// Empty by default, which leaves clients unable to influence policy
/// selection. Only policy-selectors declared here are reachable.
#[serde(default)]
pub policy_id_map: HashMap<String, Vec<String>>,
}

impl Default for AttestationConfig {
fn default() -> Self {
Self {
attestation_service: AttestationServiceConfig::default(),
timeout: DEFAULT_TIMEOUT,
policy_id_map: HashMap::new(),
}
}
}
Expand Down
Loading
Loading