Skip to content

Commit d0843f8

Browse files
committed
attestation-service: make challenge signing key path configurable
The RSA private key used to sign and verify attestation challenge (nonce) tokens was hard-coded to `/etc/trustee/attestation-service/nonce_token_issuer/key.pem`, so deployments had no way to relocate it (e.g. onto a mounted secret volume). Add an optional `challenge_key_path` field to the AS `Config`. When it is unset the previous default path is used and the key is still generated on first use, so existing deployments are unaffected. The challenge generation and verification helpers now take the resolved path as a parameter, threaded through from the config by both the RESTful and gRPC front-ends. Signed-off-by: Jiale Zhang <zhangjiale@linux.alibaba.com>
1 parent 98b2c6a commit d0843f8

7 files changed

Lines changed: 59 additions & 24 deletions

File tree

attestation-service/docs/config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ section:
1818
| `work_dir` | String | The location for Attestation Service to store data. | False | Firstly try to read from ENV `AS_WORK_DIR`. If not any, use `/opt/confidential-containers/attestation-service` |
1919
| `rvps_config` | [RVPSConfiguration][2] | RVPS configuration | False | - |
2020
| `attestation_token_broker` | [AttestationTokeBroker][1] | Attestation result token configuration. | False | - |
21+
| `challenge_key_path` | String | Path to the RSA private key (PEM) used to sign and verify attestation challenge (nonce) tokens. The key is generated on first use if the file does not exist. | False | `/etc/trustee/attestation-service/nonce_token_issuer/key.pem` |
2122

2223
[1]: #attestationtokenbroker
2324
[2]: #rvps-configuration

attestation-service/src/bin/attestation-challenge-client/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub fn build_default_config(work_dir: &Path) -> Result<Config> {
4040
work_dir: work_dir.to_path_buf(),
4141
rvps_config,
4242
attestation_token_broker: AttestationTokenConfig::Ear(ear_cfg),
43+
challenge_key_path: None,
4344
})
4445
}
4546

attestation-service/src/bin/grpc/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ impl AttestationService for Arc<RwLock<AttestationServer>> {
142142
.map_err(|e| Status::aborted(format!(
143143
"parse structured runtime data: {e}")))?;
144144
if let Some(jwt) = structured.get("challenge_token").and_then(|x| x.as_str()) {
145-
verify_challenge_and_extract_nonce_b64url(jwt)
145+
let challenge_key_path =
146+
self.read().await.attestation_service.challenge_key_path();
147+
verify_challenge_and_extract_nonce_b64url(jwt, &challenge_key_path)
146148
.map_err(|e| Status::aborted(format!(
147149
"verify challenge_token failed: {e}")))?;
148150
}

attestation-service/src/bin/restful/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,11 @@ pub async fn attestation(
187187
Some(RuntimeData::Structured(v)) => {
188188
if let Some(jwt) = v.get("challenge_token").and_then(|x| x.as_str()) {
189189
// 验证 token,但不修改 runtime_data 内容
190-
let _ = verify_challenge_and_extract_nonce_b64url(jwt).map_err(|e| {
191-
Error::Unauthorized(anyhow!("verify challenge_token failed: {e}"))
192-
})?;
190+
let challenge_key_path = cocoas.read().await.challenge_key_path();
191+
let _ = verify_challenge_and_extract_nonce_b64url(jwt, &challenge_key_path)
192+
.map_err(|e| {
193+
Error::Unauthorized(anyhow!("verify challenge_token failed: {e}"))
194+
})?;
193195
}
194196
Some(parse_runtime_data(RuntimeData::Structured(v))?)
195197
}

attestation-service/src/challenge.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,30 @@ use openssl::rsa::Rsa;
88
use openssl::sign::Signer;
99
use serde_json::{json, Value};
1010
use std::fs;
11-
use std::path::Path;
11+
use std::path::{Path, PathBuf};
1212

1313
const RSA_KEY_BITS: u32 = 2048;
1414
const TOKEN_ALG: &str = "RS384";
15-
const KEY_DIR: &str = "/etc/trustee/attestation-service/nonce_token_issuer";
16-
const PRIV_KEY_PEM: &str = "key.pem";
15+
const DEFAULT_KEY_DIR: &str = "/etc/trustee/attestation-service/nonce_token_issuer";
16+
const DEFAULT_PRIV_KEY_PEM: &str = "key.pem";
17+
18+
/// Default filesystem path of the RSA private key used to sign/verify
19+
/// attestation challenge (nonce) tokens. Used when no explicit path is
20+
/// configured in the Attestation Service config.
21+
pub fn default_challenge_key_path() -> PathBuf {
22+
Path::new(DEFAULT_KEY_DIR).join(DEFAULT_PRIV_KEY_PEM)
23+
}
1724

18-
fn ensure_keypair() -> Result<Rsa<Private>> {
19-
let dir = Path::new(KEY_DIR);
20-
if !dir.exists() {
21-
fs::create_dir_all(dir).with_context(|| format!("create dir {} failed", KEY_DIR))?;
25+
fn ensure_keypair(key_path: &Path) -> Result<Rsa<Private>> {
26+
if let Some(dir) = key_path.parent() {
27+
if !dir.as_os_str().is_empty() && !dir.exists() {
28+
fs::create_dir_all(dir)
29+
.with_context(|| format!("create dir {} failed", dir.display()))?;
30+
}
2231
}
2332

24-
let key_path = dir.join(PRIV_KEY_PEM);
2533
if key_path.exists() {
26-
let pem = fs::read(&key_path).context("read private key pem failed")?;
34+
let pem = fs::read(key_path).context("read private key pem failed")?;
2735
let rsa = Rsa::private_key_from_pem(&pem).context("parse private key pem failed")?;
2836
return Ok(rsa);
2937
}
@@ -32,7 +40,7 @@ fn ensure_keypair() -> Result<Rsa<Private>> {
3240
let pem = rsa
3341
.private_key_to_pem()
3442
.context("dump private key to pem failed")?;
35-
fs::write(&key_path, pem).context("write private key pem failed")?;
43+
fs::write(key_path, pem).context("write private key pem failed")?;
3644
Ok(rsa)
3745
}
3846

@@ -43,7 +51,7 @@ fn rs384_sign(rsa: &Rsa<Private>, payload: &[u8]) -> Result<Vec<u8>> {
4351
Ok(signer.sign_to_vec()?)
4452
}
4553

46-
pub fn generate_common_challenge() -> Result<String> {
54+
pub fn generate_common_challenge(key_path: &Path) -> Result<String> {
4755
// nonce
4856
let mut nonce = [0u8; 32];
4957
rand_bytes(&mut nonce)?;
@@ -73,7 +81,7 @@ pub fn generate_common_challenge() -> Result<String> {
7381

7482
// sign
7583
let signing_input = format!("{}.{}", header_b64, claims_b64);
76-
let rsa = ensure_keypair()?;
84+
let rsa = ensure_keypair(key_path)?;
7785
let signature = rs384_sign(&rsa, signing_input.as_bytes())?;
7886
let signature_b64 = URL_SAFE_NO_PAD.encode(signature);
7987
let jwt = format!("{}.{}", signing_input, signature_b64);
@@ -86,7 +94,7 @@ pub fn generate_common_challenge() -> Result<String> {
8694
Ok(serde_json::to_string(&output)?)
8795
}
8896

89-
pub fn verify_challenge_and_extract_nonce_b64url(token: &str) -> Result<String> {
97+
pub fn verify_challenge_and_extract_nonce_b64url(token: &str, key_path: &Path) -> Result<String> {
9098
let parts: Vec<&str> = token.split('.').collect();
9199
if parts.len() != 3 {
92100
bail!("invalid JWT format in challenge_token");
@@ -97,8 +105,7 @@ pub fn verify_challenge_and_extract_nonce_b64url(token: &str) -> Result<String>
97105
.decode(parts[2])
98106
.context("invalid JWT signature encoding")?;
99107

100-
let pem =
101-
fs::read(Path::new(KEY_DIR).join(PRIV_KEY_PEM)).context("read nonce token key failed")?;
108+
let pem = fs::read(key_path).context("read nonce token key failed")?;
102109
let rsa = Rsa::private_key_from_pem(&pem).context("parse nonce token key failed")?;
103110
let pkey = PKey::from_rsa(rsa).context("load nonce token key failed")?;
104111

attestation-service/src/config.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ pub struct Config {
2323
/// The Attestation Result Token Broker Config
2424
#[serde(default)]
2525
pub attestation_token_broker: AttestationTokenConfig,
26+
27+
/// Optional path to the RSA private key used to sign and verify
28+
/// attestation challenge (nonce) tokens. When unset, a built-in default
29+
/// path (`/etc/trustee/attestation-service/nonce_token_issuer/key.pem`)
30+
/// is used, and the key is generated on first use if it does not exist.
31+
#[serde(default)]
32+
pub challenge_key_path: Option<PathBuf>,
2633
}
2734

2835
fn default_work_dir() -> PathBuf {
@@ -48,6 +55,7 @@ impl Default for Config {
4855
work_dir: default_work_dir(),
4956
rvps_config: RvpsConfig::default(),
5057
attestation_token_broker: AttestationTokenConfig::default(),
58+
challenge_key_path: None,
5159
}
5260
}
5361
}
@@ -99,7 +107,8 @@ mod tests {
99107
issuer_name: "test".into(),
100108
signer: None,
101109
policy_dir: "/var/lib/attestation-service/policies".into(),
102-
})
110+
}),
111+
challenge_key_path: None,
103112
})]
104113
#[case("./tests/configs/example2.json", Config {
105114
work_dir: PathBuf::from("/var/lib/attestation-service/"),
@@ -115,7 +124,8 @@ mod tests {
115124
cert_url: Some("https://example.io".into()),
116125
cert_path: Some("/etc/cert.pem".into())
117126
})
118-
})
127+
}),
128+
challenge_key_path: None,
119129
})]
120130
#[case("./tests/configs/example3.json", Config {
121131
work_dir: PathBuf::from("/var/lib/attestation-service/"),
@@ -130,7 +140,8 @@ mod tests {
130140
developer_name: "someone".into(),
131141
build_name: "0.1.0".into(),
132142
profile_name: "tag:github.com,2024:confidential-containers/Trustee".into()
133-
})
143+
}),
144+
challenge_key_path: None,
134145
})]
135146
#[case("./tests/configs/example4.json", Config {
136147
work_dir: PathBuf::from("/var/lib/attestation-service/"),
@@ -149,7 +160,8 @@ mod tests {
149160
cert_url: Some("https://example.io".into()),
150161
cert_path: Some("/etc/cert.pem".into())
151162
})
152-
})
163+
}),
164+
challenge_key_path: None,
153165
})]
154166
fn read_config(#[case] config: &str, #[case] expected: Config) {
155167
let config = std::fs::read_to_string(config).unwrap();

attestation-service/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,13 +373,23 @@ impl AttestationService {
373373
.await
374374
}
375375

376+
/// Filesystem path of the RSA private key used to sign and verify
377+
/// attestation challenge (nonce) tokens. Falls back to the built-in
378+
/// default when not set in the config.
379+
pub fn challenge_key_path(&self) -> std::path::PathBuf {
380+
self._config
381+
.challenge_key_path
382+
.clone()
383+
.unwrap_or_else(challenge::default_challenge_key_path)
384+
}
385+
376386
pub async fn generate_challenge(
377387
&self,
378388
tee: Option<Tee>,
379389
tee_parameters: Option<String>,
380390
) -> Result<String> {
381391
match tee {
382-
None => challenge::generate_common_challenge(),
392+
None => challenge::generate_common_challenge(&self.challenge_key_path()),
383393
Some(t) => {
384394
self.generate_supplemental_challenge(t, tee_parameters.unwrap_or_default())
385395
.await

0 commit comments

Comments
 (0)