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
1 change: 1 addition & 0 deletions integration-tests/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ impl TestHarness {
insecure_header_jwk: true,
trusted_jwk_sets: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
attestation_service: AttestationConfig {
attestation_service: AttestationServiceConfig::CoCoASBuiltIn(
Expand Down
5 changes: 5 additions & 0 deletions kbs/docs/admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ This lets you keep identity issuance external while keeping KBS-side authorizati
`[admin.authentication.bearer_jwt]` accepts:

- `identity_providers` (array): list of trusted identity providers
- `insecure_public_key_uri` (optional bool, default `false`): allow fetching verification keys over plaintext `http://`. Applies to configured `public_key_uri` and `jwk_set_uri` values, and to any `jwks_uri` returned by OpenID discovery for remote `jwk_set_uri` entries
- `insecure_header_jwk` (optional bool, default `false`): skip endorsement checks for JWT header-embedded JWK keys (signature is still verified, testing only)

Each `identity_providers` entry:

Expand All @@ -93,8 +95,11 @@ Each entry must provide at least one of `public_key_uri` or `jwk_set_uri`.

- `file://...` or local path (for example `./keys/admin.jwks`): JWKS JSON file, read directly
- `https://...`: remote JWKS URL or OpenID issuer base URL (see note below)
- `http://...`: same as `https://`, only when `insecure_public_key_uri=true`

> [!NOTE]
> For remote `jwk_set_uri` values, KBS first tries to load JWKS from the configured URL
> directly (for example a `/jwks/` endpoint). If that fails, it falls back to OpenID discovery
> at `{uri}/.well-known/openid-configuration` and loads keys from the returned `jwks_uri`.
> Plaintext `http://` is rejected unless `insecure_public_key_uri=true`, including on
> URLs returned by discovery.
30 changes: 22 additions & 8 deletions kbs/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,19 @@ token signing, and deployment examples, see

The following properties can be set under the `[attestation_token]` section.

| Property | Type | Description | Default |
|-----------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------|---------|
| `trusted_jwk_sets` | String Array | Trusted JWKS sources (`file://` or `https://`). Loads JWKS directly when possible, otherwise via OpenID discovery. The keys are used to verify attestation tokens. | Empty |
| `trusted_certs_paths` | String Array | Trusted Certificates file (PEM format) for Attestation Tokens trustworthy verification | Empty |
| `extra_teekey_paths` | String Array | User defined paths to the tee public key in the JWT body | Empty |
| `insecure_header_jwk` | Boolean | Skip `x5c`/`trusted_certs_paths` endorsement for a JWK in the JWT header; signature is still verified | `false` |
| Property | Type | Description | Default |
|--------------------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| `trusted_jwk_sets` | String Array | Trusted JWKS sources. `file://` and local paths: JWKS JSON file. `https://` and `http://` (when allowed): JWKS URL or OpenID issuer base URL; loads JWKS directly when possible, otherwise via OpenID discovery. The keys are used to verify attestation tokens. | Empty |
| `trusted_certs_paths` | String Array | Trusted Certificates file (PEM format) for Attestation Tokens trustworthy verification | Empty |
| `extra_teekey_paths` | String Array | User defined paths to the tee public key in the JWT body | Empty |
| `insecure_header_jwk` | Boolean | Skip `x5c`/`trusted_certs_paths` endorsement for a JWK in the JWT header; signature is still verified. | `false` |
| `insecure_public_key_uri` | Boolean | Allow loading attestation-token verification keys via plaintext `http://` URLs. Applies to configured `trusted_jwk_sets` and any `jwks_uri` returned by OpenID discovery. | `false` |

> [!NOTE]
> `https://`, `file://`, and local paths are always accepted for `trusted_jwk_sets`.
> Plaintext `http://` is rejected unless `insecure_public_key_uri=true`, including on URLs
> returned by OpenID discovery. Enable this flag only in controlled network environments or for
> development, as HTTP is vulnerable to tampering.

Each JWT contains a TEE Public Key. Users can use the `extra_teekey_paths` field to additionally specify the path of
this Key in the JWT.
Expand Down Expand Up @@ -273,18 +280,25 @@ For `authorization_mode = "AuthenticatedAuthorization"`, configure:
| Property | Type | Description | Required | Default |
|----------|------|-------------|----------|---------|
| `identity_providers` | Array | Trusted issuer entries for JWT verification | No | Empty |
| `insecure_public_key_uri` | Boolean | Allow loading admin verification keys via plaintext `http://`. Applies to configured URIs and any `jwks_uri` returned by OpenID discovery for remote `jwk_set_uri` entries. | No | `false` |
| `insecure_header_jwk` | Boolean | Skip endorsement checks for header-embedded JWK keys. Signature is still verified; use only in controlled test environments. | No | `false` |

Each `identity_providers` item:

| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `issuer` | String | Expected JWT `iss` value (leave empty to skip issuer check) | No |
| `audience` | String | Expected JWT `aud` value (leave empty to skip audience check) | No |
| `public_key_uri` | String | PEM public key source (`https://`, `file://`, local path) | No* |
| `jwk_set_uri` | String | JWKS source (`https://`, `file://`, or local path). Remote `https://` URLs load JWKS directly when possible, otherwise via OpenID discovery. | No* |
| `public_key_uri` | String | PEM public key source (`https://`, `file://`, local path, or `http://` when `insecure_public_key_uri=true`) | No* |
| `jwk_set_uri` | String | JWKS source (`https://`, `file://`, local path, or `http://` when `insecure_public_key_uri=true`). Remote URLs load JWKS directly when possible, otherwise via OpenID discovery. | No* |

\* At least one of `public_key_uri` or `jwk_set_uri` is required.

> [!NOTE]
> When `insecure_public_key_uri=true`, KBS may fetch admin verification keys over
> plaintext HTTP. Enable this only in controlled network environments or for development,
> as HTTP is vulnerable to tampering.

JWTs used for admin access **MUST** include a `role` claim.

`regex_acl` properties:
Expand Down
54 changes: 51 additions & 3 deletions kbs/src/admin/authentication/bearer_jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,30 @@ use crate::crypto::jwt::JwtVerifier;
#[serde(default, deny_unknown_fields)]
pub struct BearerJwtConfig {
pub identity_providers: Vec<IssuerConfig>,

/// Allow loading admin verification keys over plaintext HTTP.
///
/// When false, any HTTP fetch for key material is rejected: configured `public_key_uri`
/// and `jwk_set_uri` values, and any `jwks_uri` returned by OpenID discovery for remote
/// `jwk_set_uri` entries. Keep disabled by default and only enable in controlled environments.
pub insecure_public_key_uri: bool,

/// Whether to skip endorsement checks of header-embedded JWK keys.
/// Signature is still verified.
/// This should only be set to true for testing.
///
/// When false, header-embedded `jwk` keys must include an `x5c` certificate chain
/// that can be validated against configured trusted certificates (if any).
pub insecure_header_jwk: bool,
}

/// Issuer config used to verify admin JWT tokens.
///
/// - `public_key_uri`: a PEM file source (`https://`, `file://`, local path)
/// - `jwk_set_uri`: a JWKS source (https://, file:// or local path)
/// - `public_key_uri`: a PEM file source (`https://`, `file://`, local path,
/// or `http://` when `insecure_public_key_uri=true`)
/// - `jwk_set_uri`: trusted JWKS source (`https://`, `file://`, local path,
/// or `http://` when `insecure_public_key_uri=true`). Remote URLs are loaded as JWKS
/// directly when possible, otherwise via OpenID discovery.
/// - `issuer`: the issuer of the JWT token. If given, This field will be checked when a token is verified successfully
/// with given public key or JWKS. If the token's issuer is matched, the token will be verified successfully.
/// - `audience`: the audience of the JWT token. If given, This field will be checked when a token is verified successfully
Expand Down Expand Up @@ -68,7 +86,8 @@ impl BearerJwtTokenVerifier {
&trusted_jwk_set_uris,
&[],
&trusted_pem_public_key_uris,
false,
config.insecure_header_jwk,
config.insecure_public_key_uri,
)
.await
.map_err(|e| Error::InvalidTokenVerifierConfig(e.to_string()))?;
Expand Down Expand Up @@ -171,3 +190,32 @@ fn claims_from_value(value: Value) -> Result<Claims> {

Ok(Claims { role })
}

#[cfg(test)]
mod tests {
use super::BearerJwtConfig;

#[test]
fn deserialize_insecure_header_jwk_true() {
let config: BearerJwtConfig = toml::from_str(
r#"
insecure_public_key_uri = true
insecure_header_jwk = true
identity_providers = [
{ issuer = "admin", public_key_uri = "/etc/kbs-admin.pub" },
]
"#,
)
.expect("valid bearer_jwt config");

assert!(config.insecure_header_jwk);
assert!(config.insecure_public_key_uri);
assert_eq!(config.identity_providers.len(), 1);
}

#[test]
fn default_insecure_header_jwk_false() {
let config = BearerJwtConfig::default();
assert!(!config.insecure_header_jwk);
}
}
1 change: 1 addition & 0 deletions kbs/src/attestation/intel_trust_authority/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ impl IntelTrustAuthority {
&trusted_certs_paths,
&trusted_pem_public_keys,
true,
false,
)
.await
.context("Failed to initialize token verifier")?;
Expand Down
7 changes: 7 additions & 0 deletions kbs/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ mod tests {
insecure_header_jwk: false,
trusted_jwk_sets: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "coco-as-grpc")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down Expand Up @@ -401,6 +402,7 @@ mod tests {
insecure_header_jwk: false,
trusted_jwk_sets: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "coco-as-builtin")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down Expand Up @@ -450,6 +452,7 @@ mod tests {
insecure_header_jwk: false,
trusted_certs_paths: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "intel-trust-authority-as")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down Expand Up @@ -541,6 +544,7 @@ mod tests {
insecure_header_jwk: false,
trusted_jwk_sets: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "coco-as-builtin")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down Expand Up @@ -584,6 +588,7 @@ mod tests {
insecure_header_jwk: false,
trusted_certs_paths: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "intel-trust-authority-as")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down Expand Up @@ -641,6 +646,7 @@ mod tests {
insecure_header_jwk: false,
trusted_certs_paths: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "intel-trust-authority-as")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down Expand Up @@ -671,6 +677,7 @@ mod tests {
insecure_header_jwk: false,
trusted_jwk_sets: vec![],
extra_teekey_paths: vec![],
insecure_public_key_uri: false,
},
#[cfg(feature = "coco-as-builtin")]
attestation_service: crate::attestation::config::AttestationConfig {
Expand Down
26 changes: 20 additions & 6 deletions kbs/src/crypto/jwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub(crate) enum JwksGetError {
#[source]
source: anyhow::Error,
},
#[error("HTTP source is not allowed when `insecure_public_key_uri` is false")]
HttpNotAllowed,
}

#[derive(Deserialize)]
Expand All @@ -32,10 +34,16 @@ pub(crate) struct OpenIDConfig {
/// Load a JWK set from a configured source.
///
/// - `file://` and local paths: JWKS JSON file, read directly.
/// - `https://`: remote source. KBS tries to load JWKS from the configured URL directly; if that
/// - `https://` and `http://`: remote source. KBS tries to load JWKS from the configured URL directly; if that
/// fails or returns no keys, it falls back to OpenID discovery at
/// `{uri}/.well-known/openid-configuration` and loads the returned `jwks_uri`.
pub async fn read_jwk_from_uri(uri: &str) -> Result<JwkSet, JwksGetError> {
///
/// Plaintext `http://` is rejected unless `insecure_public_key_uri` is true. This applies
/// to the configured URL and any `jwks_uri` returned by OpenID discovery.
pub async fn read_jwk_from_uri(
uri: &str,
insecure_public_key_uri: bool,
) -> Result<JwkSet, JwksGetError> {
let url = Url::parse(uri).map_err(|e| JwksGetError::InvalidSourcePath(e.to_string()))?;
match url.scheme() {
"file" => {
Expand All @@ -45,7 +53,10 @@ pub async fn read_jwk_from_uri(uri: &str) -> Result<JwkSet, JwksGetError> {
source: Into::<anyhow::Error>::into(e).context("failed to deserialize JWK set"),
})
}
"https" => {
"https" | "http" => {
if url.scheme() == "http" && !insecure_public_key_uri {
return Err(JwksGetError::HttpNotAllowed);
}
// Try to load a JWK set directly from the configured URL first.
match get(uri)
.await
Expand Down Expand Up @@ -100,7 +111,7 @@ pub async fn read_jwk_from_uri(uri: &str) -> Result<JwkSet, JwksGetError> {
Ok(jwks)
}
scheme => Err(JwksGetError::InvalidSourcePath(format!(
"unsupported scheme {scheme} (must be either file or https)"
"unsupported scheme {scheme} (must be either file or https or http)"
))),
}
}
Expand Down Expand Up @@ -134,7 +145,10 @@ mod tests {
#[case("/does/not/exist/keys.jwks", true)]
#[tokio::test]
async fn test_source_path_validation(#[case] source_path: &str, #[case] expect_error: bool) {
assert_eq!(expect_error, read_jwk_from_uri(source_path).await.is_err())
assert_eq!(
expect_error,
read_jwk_from_uri(source_path, false).await.is_err()
)
}

#[rstest]
Expand All @@ -154,7 +168,7 @@ mod tests {
std::fs::write(&jwks_file, json).expect("to get testdata written to tmpdir");

let p = "file://".to_owned() + jwks_file.to_str().expect("to get path as str");
let jwtks = read_jwk_from_uri(&p).await.expect("to get jwks");
let jwtks = read_jwk_from_uri(&p, false).await.expect("to get jwks");
assert_eq!(jwtks.keys.len(), 1);
assert_eq!(jwtks.keys[0].common.key_algorithm, Some(alg));
}
Expand Down
38 changes: 31 additions & 7 deletions kbs/src/crypto/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ fn path_to_file_uri(path: &str) -> Result<String> {
}

fn normalize_jwk_set_source(source: &str) -> Result<String> {
if source.starts_with("https://") || source.starts_with("file://") {
if source.starts_with("https://")
|| source.starts_with("http://")
|| source.starts_with("file://")
{
return Ok(source.to_string());
}

Expand All @@ -47,12 +50,29 @@ fn normalize_jwk_set_source(source: &str) -> Result<String> {
path_to_file_uri(source)
}

/// Read a PEM public key from a URI (`https://`, `file://`, or local path).
pub(crate) async fn read_pem_public_key_from_uri(uri: &str) -> Result<DecodingKey> {
/// Read a PEM public key from a URI.
///
/// # Arguments
///
/// * `uri` - The URI of the PEM public key.
/// * `allow_insecure_http` - Whether to allow HTTP address as uri.
pub(crate) async fn read_pem_public_key_from_uri(
uri: &str,
allow_insecure_http: bool,
) -> Result<DecodingKey> {
let maybe_url = Url::parse(uri);
let data = if let Ok(url) = maybe_url {
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
match url.scheme() {
"https" => reqwest::get(uri).await?.bytes().await?.to_vec(),
"https" => client.get(uri).send().await?.bytes().await?.to_vec(),
"http" => {
if !allow_insecure_http {
bail!("HTTP source is not allowed when `allow_insecure_http` is false");
}
client.get(uri).send().await?.bytes().await?.to_vec()
}
"file" => std::fs::read(url.path())?,
_ => {
Comment thread
Xynnn007 marked this conversation as resolved.
bail!("unsupported scheme in {uri}");
Expand Down Expand Up @@ -127,16 +147,20 @@ impl JwtVerifier {
/// * `trusted_cert_paths` - The paths of the trusted certificates.
/// * `trusted_pem_public_key_uris` - The URIs of the trusted PEM public keys.
/// * `insecure_public_key_from_jwt` - Whether to verify the endorsement of the public key from JWT header.
/// * `insecure_public_key_uri` - Whether to allow plaintext HTTP when loading key material
/// from `trusted_pem_public_key_uris`, `trusted_jwk_set_uris`, and any `jwks_uri` returned
/// by OpenID discovery for remote JWKS sources.
pub async fn new(
trusted_jwk_set_uris: &[String],
trusted_cert_paths: &[String],
trusted_pem_public_key_uris: &[String],
insecure_public_key_from_jwt: bool,
insecure_public_key_uri: bool,
) -> Result<Self> {
let mut trusted_jwk_sets = JwkSet { keys: Vec::new() };
for uri in trusted_jwk_set_uris {
let uri = normalize_jwk_set_source(&uri[..])?;
let mut jwk_set = read_jwk_from_uri(&uri[..]).await?;
let mut jwk_set = read_jwk_from_uri(&uri[..], insecure_public_key_uri).await?;
trusted_jwk_sets.keys.append(&mut jwk_set.keys);
}

Expand All @@ -156,7 +180,7 @@ impl JwtVerifier {

let mut trusted_pem_public_keys = Vec::new();
for uri in trusted_pem_public_key_uris {
let public_key = read_pem_public_key_from_uri(uri).await?;
let public_key = read_pem_public_key_from_uri(uri, insecure_public_key_uri).await?;
trusted_pem_public_keys.push(public_key);
}

Expand Down Expand Up @@ -356,7 +380,7 @@ mod tests {

use crate::crypto::jwt::JwtVerifier;

let verifier = JwtVerifier::new(&[], &[trusted_pem_path.to_string()], &[], false)
let verifier = JwtVerifier::new(&[], &[trusted_pem_path.to_string()], &[], false, false)
.await
.expect("verifier init");
let jwk_json = std::fs::read_to_string(jwk_json_path).expect("read jwk json");
Expand Down
Loading
Loading