Skip to content

Commit

Permalink
KBS: Support Verify X.509 certificate chain in Attestation Token JWK …
Browse files Browse the repository at this point in the history
…field

Signed-off-by: Jiale Zhang <[email protected]>
  • Loading branch information
jialez0 committed Dec 22, 2023
1 parent ba3c34c commit b218dad
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 42 deletions.
3 changes: 3 additions & 0 deletions kbs/config/kbs-config-grpc.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
insecure_http = true
insecure_api = true

[attestation_token_config]
attestation_token_type = "CoCo"

[grpc_config]
as_addr = "http://127.0.0.1:50004"
3 changes: 3 additions & 0 deletions kbs/config/kbs-config-intel-trust-authority.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
insecure_http = true
insecure_api = true

[attestation_token_config]
attestation_token_type = "CoCo"

[intel_trust_authority_config]
base_url = "https://api.trustauthority.intel.com"
api_key = "tBfd5kKX2x9ahbodKV1..."
Expand Down
2 changes: 2 additions & 0 deletions kbs/config/kbs-config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
insecure_http = true
insecure_api = true

[attestation_token_config]
attestation_token_type = "CoCo"

[repository_config]
Expand Down
3 changes: 3 additions & 0 deletions kbs/config/kubernetes/base/kbs-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ auth_public_key = "/kbs/kbs.pem"
# https://cert-manager.io/docs/configuration/acme/
insecure_http = true

[attestation_token_config]
attestation_token_type = "CoCo"

[as_config]
work_dir = "/opt/confidential-containers/attestation-service"
policy_engine = "opa"
Expand Down
2 changes: 1 addition & 1 deletion kbs/src/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ edition.workspace = true

[features]
default = ["coco-as-builtin", "resource", "opa", "rustls"]
resource = ["rsa", "aes-gcm"]
resource = ["rsa", "dep:openssl", "aes-gcm"]
as = []
policy = []
opa = ["policy"]
Expand Down
11 changes: 3 additions & 8 deletions kbs/src/api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::policy_engine::PolicyEngineConfig;
#[cfg(feature = "resource")]
use crate::resource::RepositoryConfig;
#[cfg(feature = "resource")]
use crate::token::AttestationTokenVerifierType;
use crate::token::AttestationTokenVerifierConfig;
use anyhow::anyhow;
#[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))]
use attestation_service::config::Config as AsConfig;
Expand All @@ -22,7 +22,6 @@ use serde_json::Value;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};

const DEFAULT_ATTESTATION_TOKEN_TYPE: &str = "CoCo";
const DEFAULT_INSECURE_API: bool = false;
const DEFAULT_INSECURE_HTTP: bool = false;
const DEFAULT_SOCKET: &str = "127.0.0.1:8080";
Expand All @@ -35,12 +34,9 @@ pub struct KbsConfig {
#[cfg(feature = "resource")]
pub repository_config: Option<RepositoryConfig>,

/// Attestation token result broker type.
///
/// Possible values:
/// * `CoCo`
/// Attestation token result broker config.
#[cfg(feature = "resource")]
pub attestation_token_type: AttestationTokenVerifierType,
pub attestation_token_config: AttestationTokenVerifierConfig,

/// Configuration for the built-in Attestation Service.
#[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))]
Expand Down Expand Up @@ -92,7 +88,6 @@ impl TryFrom<&Path> for KbsConfig {
/// `config` crate. See `KbsConfig` for schema information.
fn try_from(config_path: &Path) -> Result<Self, Self::Error> {
let c = Config::builder()
.set_default("attestation_token_type", DEFAULT_ATTESTATION_TOKEN_TYPE)?
.set_default("insecure_api", DEFAULT_INSECURE_API)?
.set_default("insecure_http", DEFAULT_INSECURE_HTTP)?
.set_default("sockets", vec![DEFAULT_SOCKET])?
Expand Down
11 changes: 6 additions & 5 deletions kbs/src/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use semver::{BuildMetadata, Prerelease, Version, VersionReq};
use std::net::SocketAddr;
use std::path::PathBuf;
#[cfg(feature = "resource")]
use token::AttestationTokenVerifierType;
use token::AttestationTokenVerifierConfig;

#[cfg(feature = "rustls")]
use rustls::ServerConfig;
Expand Down Expand Up @@ -112,7 +112,7 @@ pub struct ApiServer {
#[cfg(feature = "resource")]
repository_config: RepositoryConfig,
#[cfg(feature = "resource")]
attestation_token_type: AttestationTokenVerifierType,
attestation_token_config: AttestationTokenVerifierConfig,
#[cfg(feature = "policy")]
policy_engine_config: PolicyEngineConfig,
}
Expand All @@ -131,7 +131,7 @@ impl ApiServer {
http_timeout: i64,
insecure_api: bool,
#[cfg(feature = "resource")] repository_config: RepositoryConfig,
#[cfg(feature = "resource")] attestation_token_type: AttestationTokenVerifierType,
#[cfg(feature = "resource")] attestation_token_config: AttestationTokenVerifierConfig,
#[cfg(feature = "policy")] policy_engine_config: PolicyEngineConfig,
) -> Result<Self> {
if !insecure && (private_key.is_none() || certificate.is_none()) {
Expand Down Expand Up @@ -159,7 +159,7 @@ impl ApiServer {
#[cfg(feature = "resource")]
repository_config,
#[cfg(feature = "resource")]
attestation_token_type,
attestation_token_config,
#[cfg(feature = "policy")]
policy_engine_config,
})
Expand Down Expand Up @@ -244,7 +244,8 @@ impl ApiServer {
let repository = self.repository_config.initialize()?;

#[cfg(feature = "resource")]
let token_verifier = self.attestation_token_type.to_token_verifier()?;
let token_verifier =
crate::token::create_token_verifier(self.attestation_token_config.clone())?;

#[cfg(feature = "policy")]
let policy_engine = PolicyEngine::new(&self.policy_engine_config).await?;
Expand Down
114 changes: 96 additions & 18 deletions kbs/src/api/src/token/coco.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,42 @@
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use crate::token::AttestationTokenVerifier;
use crate::token::{AttestationTokenVerifier, AttestationTokenVerifierConfig};
use anyhow::*;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use rsa::signature::Verifier;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::sign::Verifier;
use openssl::stack::Stack;
use openssl::x509::store::{X509Store, X509StoreBuilder};
use openssl::x509::{X509StoreContext, X509};
use serde_json::Value;

#[derive(Default)]
pub struct CoCoAttestationTokenVerifier {}
pub struct CoCoAttestationTokenVerifier {
trust_certs: Option<X509Store>,
}

impl CoCoAttestationTokenVerifier {
pub fn new(config: &AttestationTokenVerifierConfig) -> Result<Self> {
let trust_certs = match &config.trust_certs_paths {
Some(paths) => {
let mut store_builder = X509StoreBuilder::new()?;
for path in paths {
let trust_cert_pem = std::fs::read(path)
.map_err(|e| anyhow!("Load trusted certificate failed: {e}"))?;
let trust_cert = X509::from_pem(&trust_cert_pem)?;
store_builder.add_cert(trust_cert.to_owned())?;
}
Some(store_builder.build())
}
None => None,
};

Ok(Self { trust_certs })
}
}

impl AttestationTokenVerifier for CoCoAttestationTokenVerifier {
fn verify(&self, token: String) -> Result<String> {
Expand Down Expand Up @@ -41,12 +68,18 @@ impl AttestationTokenVerifier for CoCoAttestationTokenVerifier {

let jwk_value = claims_value["jwk"].as_object().ok_or_else(|| anyhow!("CoCo Attestation Token Claims must contain public key (JWK format) to verify signature"))?;
let jwk = serde_json::to_string(&jwk_value)?;
let rsa_jwk = serde_json::from_str::<RsaJWK>(&jwk)?;
let payload = format!("{}.{}", &split_token[0], &split_token[1])
.as_bytes()
.to_vec();

match header_value["alg"].as_str() {
Some("RS384") => rs384_verify(&payload, &signature, &jwk)?,
Some("RS384") => {
if rsa_jwk.alg != *"RS384" {
bail!("Unmatched RSA JWK alg");
}
rs384_verify(&payload, &signature, &rsa_jwk)?;
}
None => {
bail!("Miss `alg` in JWT header")
}
Expand All @@ -55,6 +88,51 @@ impl AttestationTokenVerifier for CoCoAttestationTokenVerifier {
}
}

if let Some(trusted_store) = &self.trust_certs {
match rsa_jwk.x5c {
Some(x5c) => {
let mut cert_chain: Vec<X509> = vec![];
for base64_der_cert in x5c {
let der_cert = URL_SAFE_NO_PAD.decode(base64_der_cert)?;
let cert = X509::from_der(&der_cert)?;
cert_chain.push(cert)
}

let mut untrusted_stack = Stack::<X509>::new()?;
for cert in cert_chain.iter().skip(1) {
untrusted_stack.push(cert.clone())?;
}

let mut context = X509StoreContext::new()?;
context.init(trusted_store, &cert_chain[0], &untrusted_stack, |_| {
Result::Ok(())
})?;

// Check certificate is valid and trustworthy
if !context.verify_cert()? {
bail!("Untrusted Certificate in Attestation Token JWK")
}

// Chek the public key in JWK is consistent with the public key in certificate
let n = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&rsa_jwk.n)?)?;
let e = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&rsa_jwk.e)?)?;
let rsa_public_key = Rsa::from_public_components(n, e)?;
let rsa_pkey = PKey::from_rsa(rsa_public_key)?;
let cert_pub_key = cert_chain[0].public_key()?;
if !cert_pub_key.public_eq(&rsa_pkey) {
bail!("Certificate Public Key Mismatched in Attestation Token");
}
}
None => {
if rsa_jwk.x5u.is_some() {
bail!("Now only support \"x5c\" in token JWK");
} else {
bail!("Missing certificate in Attestation Token JWK");
}
}
}
}

Ok(serde_json::to_string(&claims_value)?)
}
}
Expand All @@ -66,22 +144,22 @@ struct RsaJWK {
alg: String,
n: String,
e: String,
x5u: Option<String>,
x5c: Option<Vec<String>>,
}

fn rs384_verify(payload: &[u8], signature: &[u8], jwk: &str) -> Result<()> {
let jwk = serde_json::from_str::<RsaJWK>(jwk)?;
if jwk.alg != *"RS384" {
bail!("Unmatched RSA JWK alg");
}
fn rs384_verify(payload: &[u8], signature: &[u8], jwk: &RsaJWK) -> Result<()> {
let n = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&jwk.n)?)?;
let e = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&jwk.e)?)?;
let rsa_public_key = Rsa::from_public_components(n, e)?;
let rsa_pkey = PKey::from_rsa(rsa_public_key)?;

let pkcs1v15_signature = rsa::pkcs1v15::Signature::try_from(signature)?;
let mut verifier = Verifier::new(MessageDigest::sha384(), &rsa_pkey)?;
verifier.update(payload)?;

let n = rsa::BigUint::from_bytes_be(&URL_SAFE_NO_PAD.decode(&jwk.n)?);
let e = rsa::BigUint::from_bytes_be(&URL_SAFE_NO_PAD.decode(&jwk.e)?);
let rsa_public_key = rsa::RsaPublicKey::new(n, e)?;
let verify_key = rsa::pkcs1v15::VerifyingKey::<rsa::sha2::Sha384>::new(rsa_public_key);
if !verifier.verify(signature)? {
bail!("RS384 verify failed")
}

verify_key
.verify(payload, &pkcs1v15_signature)
.map_err(|e| anyhow!("RS384 verify failed: {e}"))
Ok(())
}
33 changes: 24 additions & 9 deletions kbs/src/api/src/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,34 @@ pub enum AttestationTokenVerifierType {
CoCo,
}

impl AttestationTokenVerifierType {
pub fn to_token_verifier(
&self,
) -> Result<Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>> {
match self {
AttestationTokenVerifierType::CoCo => Ok(Arc::new(RwLock::new(
coco::CoCoAttestationTokenVerifier::default(),
))
as Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>),
#[derive(Deserialize, Debug, Clone)]
pub struct AttestationTokenVerifierConfig {
pub attestation_token_type: AttestationTokenVerifierType,

// Trusted Certificates file (PEM format) path to verify Attestation Token Signature.3
pub trust_certs_paths: Option<Vec<String>>,
}

impl Default for AttestationTokenVerifierConfig {
fn default() -> Self {
Self {
attestation_token_type: AttestationTokenVerifierType::CoCo,
trust_certs_paths: None,
}
}
}

pub fn create_token_verifier(
config: AttestationTokenVerifierConfig,
) -> Result<Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>> {
match config.attestation_token_type {
AttestationTokenVerifierType::CoCo => Ok(Arc::new(RwLock::new(
coco::CoCoAttestationTokenVerifier::new(&config)?,
))
as Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>),
}
}

impl fmt::Display for AttestationTokenVerifierType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
Expand Down
2 changes: 1 addition & 1 deletion kbs/src/kbs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async fn main() -> Result<()> {
#[cfg(feature = "resource")]
kbs_config.repository_config.unwrap_or_default(),
#[cfg(feature = "resource")]
kbs_config.attestation_token_type,
kbs_config.attestation_token_config,
#[cfg(feature = "opa")]
kbs_config.policy_engine_config.unwrap_or_default(),
)?;
Expand Down
3 changes: 3 additions & 0 deletions kbs/test/data/e2e/kbs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ sockets = ["127.0.0.1:8080"]
auth_public_key = "./kbs.pem"
insecure_http = true

[attestation_token_config]
attestation_token_type = "CoCo"

[repository_config]
type = "LocalFs"
dir_path = "./data/repository"
Expand Down
3 changes: 3 additions & 0 deletions kbs/test/data/e2e/resource-kbs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ sockets = ["127.0.0.1:50002"]
auth_public_key = "./kbs.pem"
insecure_http = true

[attestation_token_config]
attestation_token_type = "CoCo"

[repository_config]
type = "LocalFs"
dir_path = "./data/repository"
Expand Down

0 comments on commit b218dad

Please sign in to comment.