From 35bc6ab39bf9d4ca7a778c67dd65fc8cfde126ed Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Mon, 13 Jul 2026 21:06:05 +0800 Subject: [PATCH 1/4] refactor(as): migrate challenge.rs RS384 to rustcrypto Co-Authored-By: Claude --- attestation-service/src/challenge.rs | 108 +++++++++++++++++++-------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/attestation-service/src/challenge.rs b/attestation-service/src/challenge.rs index bdd469a..1937af5 100644 --- a/attestation-service/src/challenge.rs +++ b/attestation-service/src/challenge.rs @@ -1,12 +1,15 @@ use anyhow::*; use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::Engine; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::rand::rand_bytes; -use openssl::rsa::Rsa; -use openssl::sign::Signer; +use rand::rngs::OsRng; +use rand::RngCore; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs1v15::{Signature, SigningKey, VerifyingKey}; +use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding}; +use rsa::signature::{Signer, Verifier}; +use rsa::{RsaPrivateKey, RsaPublicKey}; use serde_json::{json, Value}; +use sha2::Sha384; use std::fs; use std::path::{Path, PathBuf}; @@ -22,7 +25,7 @@ pub fn default_challenge_key_path() -> PathBuf { Path::new(DEFAULT_KEY_DIR).join(DEFAULT_PRIV_KEY_PEM) } -fn ensure_keypair(key_path: &Path) -> Result> { +fn ensure_keypair(key_path: &Path) -> Result { if let Some(dir) = key_path.parent() { if !dir.as_os_str().is_empty() && !dir.exists() { fs::create_dir_all(dir) @@ -31,30 +34,36 @@ fn ensure_keypair(key_path: &Path) -> Result> { } if key_path.exists() { - let pem = fs::read(key_path).context("read private key pem failed")?; - let rsa = Rsa::private_key_from_pem(&pem).context("parse private key pem failed")?; + let pem = fs::read_to_string(key_path).context("read private key pem failed")?; + // Accept both PKCS#8 (what we write now) and legacy PKCS#1 (what the + // previous implementation's `private_key_to_pem` wrote). + let rsa = RsaPrivateKey::from_pkcs8_pem(&pem) + .or_else(|_| RsaPrivateKey::from_pkcs1_pem(&pem)) + .context("parse private key pem failed")?; return Ok(rsa); } - let rsa = Rsa::generate(RSA_KEY_BITS)?; + let mut rng = OsRng; + let rsa = RsaPrivateKey::new(&mut rng, RSA_KEY_BITS as usize)?; let pem = rsa - .private_key_to_pem() + .to_pkcs8_pem(LineEnding::LF) .context("dump private key to pem failed")?; - fs::write(key_path, pem).context("write private key pem failed")?; + fs::write(key_path, pem.as_bytes()).context("write private key pem failed")?; Ok(rsa) } -fn rs384_sign(rsa: &Rsa, payload: &[u8]) -> Result> { - let rsa_pkey = PKey::from_rsa(rsa.clone())?; - let mut signer = Signer::new(MessageDigest::sha384(), &rsa_pkey)?; - signer.update(payload)?; - Ok(signer.sign_to_vec()?) +fn rs384_sign(rsa: &RsaPrivateKey, payload: &[u8]) -> Result> { + let signing_key = SigningKey::::new(rsa.clone()); + let sig: Signature = signing_key.sign(payload); + Ok(Box::<[u8]>::from(sig).to_vec()) } pub fn generate_common_challenge(key_path: &Path) -> Result { // nonce let mut nonce = [0u8; 32]; - rand_bytes(&mut nonce)?; + OsRng + .try_fill_bytes(&mut nonce) + .context("generate nonce failed")?; let nonce_b64 = STANDARD.encode(nonce); // header @@ -68,7 +77,7 @@ pub fn generate_common_challenge(key_path: &Path) -> Result { // claims with 5-minute expiry let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| anyhow!("time error: {:?}", e))? + .context("time error")? .as_secs(); let exp = now + 5 * 60; let claims_value = json!({ @@ -105,19 +114,17 @@ pub fn verify_challenge_and_extract_nonce_b64url(token: &str, key_path: &Path) - .decode(parts[2]) .context("invalid JWT signature encoding")?; - let pem = fs::read(key_path).context("read nonce token key failed")?; - let rsa = Rsa::private_key_from_pem(&pem).context("parse nonce token key failed")?; - let pkey = PKey::from_rsa(rsa).context("load nonce token key failed")?; - - let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha384(), &pkey) - .context("create verifier failed")?; - verifier - .update(signing_input.as_bytes()) - .context("verifier update failed")?; - let ok = verifier.verify(&sig).context("verify signature failed")?; - if !ok { - bail!("invalid challenge_token signature"); - } + let pem_str = fs::read_to_string(key_path).context("read nonce token key failed")?; + let rsa = RsaPrivateKey::from_pkcs8_pem(&pem_str) + .or_else(|_| RsaPrivateKey::from_pkcs1_pem(&pem_str)) + .context("parse nonce token key failed")?; + let public_key: RsaPublicKey = rsa.to_public_key(); + + let verifying_key = VerifyingKey::::new(public_key); + let sig_obj = Signature::try_from(sig.as_slice()).context("invalid signature bytes")?; + verifying_key + .verify(signing_input.as_bytes(), &sig_obj) + .context("verify signature failed")?; let payload = URL_SAFE_NO_PAD .decode(parts[1]) @@ -147,3 +154,42 @@ pub fn verify_challenge_and_extract_nonce_b64url(token: &str, key_path: &Path) - .context("invalid nonce base64")?; Ok(URL_SAFE_NO_PAD.encode(nonce_bytes)) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Sign a challenge and verify it round-trips. Pins the rustcrypto RS384 + /// implementation against the on-disk key format this module writes. + #[test] + fn challenge_sign_verify_roundtrip() { + // Use a temp key dir so we don't touch /etc/trustee/... + let tmp = tempfile::tempdir().unwrap(); + let key_file = tmp.path().join("key.pem"); + + let challenge_json = generate_common_challenge(&key_file).expect("generate"); + let outer: serde_json::Value = serde_json::from_str(&challenge_json).expect("json"); + let jwt = outer["extra-params"]["jwt"] + .as_str() + .expect("jwt in extra-params") + .to_string(); + + // verify must accept the token we just issued and return the nonce. + let nonce_back = + verify_challenge_and_extract_nonce_b64url(&jwt, &key_file).expect("verify"); + let nonce_original = outer["nonce"].as_str().unwrap().to_string(); + // The challenge JSON uses STANDARD base64 for the nonce, while + // verify_challenge_and_extract_nonce_b64url returns URL_SAFE_NO_PAD. + // Compare the decoded bytes, not the encoded strings. + let nonce_back_bytes = URL_SAFE_NO_PAD + .decode(&nonce_back) + .expect("decode nonce_back"); + let nonce_original_bytes = STANDARD + .decode(&nonce_original) + .expect("decode nonce_original"); + assert_eq!( + nonce_back_bytes, nonce_original_bytes, + "nonce bytes must round-trip" + ); + } +} From e0b738f71a8f4848b5d2112daa99c7e7f556e972 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 14 Jul 2026 16:48:25 +0800 Subject: [PATCH 2/4] refactor(as): migrate simple.rs (RS384 + X509 + JWK) to rustcrypto Add test coverage for cert-chain parse + x5c encode path. Cargo.lock updated separately. Co-Authored-By: Claude --- Cargo.lock | 1 + attestation-service/Cargo.toml | 5 + attestation-service/src/token/simple.rs | 210 +++++++++++++++++++++--- 3 files changed, 190 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fd1d57..625534f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,7 @@ dependencies = [ "tonic-build", "uuid", "verifier", + "x509-cert", ] [[package]] diff --git a/attestation-service/Cargo.toml b/attestation-service/Cargo.toml index b477ea4..58fa049 100644 --- a/attestation-service/Cargo.toml +++ b/attestation-service/Cargo.toml @@ -92,6 +92,11 @@ toml.workspace = true tonic = { workspace = true, optional = true } uuid = { version = "1.1.2", features = ["v4"] } verifier = { path = "../deps/verifier", default-features = false } +# x509-cert is used by `token/simple.rs` for PEM/DER certificate-chain parsing +# (JWK `x5c`). Pulled in as a direct dep so `simple.rs` can call +# `Certificate::from_pem` / `to_der`. The `pem` feature enables +# `der::DecodePem` (used by `from_pem`); `std` is part of the default set. +x509-cert = { version = "0.2.5", default-features = false, features = ["pem", "std"] } [build-dependencies] shadow-rs.workspace = true diff --git a/attestation-service/src/token/simple.rs b/attestation-service/src/token/simple.rs index 4b47a53..e9713a6 100644 --- a/attestation-service/src/token/simple.rs +++ b/attestation-service/src/token/simple.rs @@ -11,22 +11,25 @@ use anyhow::*; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use log::info; -use openssl::rsa::Rsa; -use openssl::sign::Signer; -use openssl::x509::X509; -use openssl::{ - hash::MessageDigest, - pkey::{PKey, Private}, -}; use rand::distributions::Alphanumeric; +use rand::rngs::OsRng; use rand::{thread_rng, Rng}; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs1v15::{Signature, SigningKey}; +use rsa::pkcs8::DecodePrivateKey; +use rsa::signature::Signer; +use rsa::traits::PublicKeyParts; +use rsa::RsaPrivateKey; use serde::Deserialize; use serde_json::{json, Map, Value}; use serde_variant::to_variant_name; +use sha2::Sha384; use shadow_rs::concatcp; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; +use x509_cert::der::{DecodePem, Encode}; +use x509_cert::Certificate; use crate::policy_engine::{PolicyEngine, PolicyEngineType}; use crate::token::{AttestationTokenBroker, DEFAULT_TOKEN_WORK_DIR}; @@ -97,10 +100,10 @@ impl Default for Configuration { } pub struct SimpleAttestationTokenBroker { - private_key: Rsa, + private_key: RsaPrivateKey, config: Configuration, cert_url: Option, - cert_chain: Option>, + cert_chain: Option>, policy_engine: Arc, } @@ -115,8 +118,9 @@ impl SimpleAttestationTokenBroker { if config.signer.is_none() { log::info!("No Token Signer key in config file, create an ephemeral key and without CA pubkey cert"); + let mut rng = OsRng; return Ok(Self { - private_key: Rsa::generate(RSA_KEY_BITS)?, + private_key: RsaPrivateKey::new(&mut rng, RSA_KEY_BITS as usize)?, config, cert_url: None, cert_chain: None, @@ -125,16 +129,18 @@ impl SimpleAttestationTokenBroker { } let signer = config.signer.clone().unwrap(); - let pem_data = std::fs::read(&signer.key_path) - .map_err(|e| anyhow!("Read Token Signer private key failed: {:?}", e))?; - let private_key = Rsa::private_key_from_pem(&pem_data)?; + let pem_data = std::fs::read_to_string(&signer.key_path) + .context("Read Token Signer private key failed")?; + let private_key = RsaPrivateKey::from_pkcs8_pem(&pem_data) + .or_else(|_| RsaPrivateKey::from_pkcs1_pem(&pem_data)) + .context("Parse Token Signer private key failed")?; let cert_chain = signer .cert_path .as_ref() - .map(|cert_path| -> Result> { + .map(|cert_path| -> Result> { let pem_cert_chain = std::fs::read_to_string(cert_path) - .map_err(|e| anyhow!("Read Token Signer cert file failed: {:?}", e))?; + .context("Read Token Signer cert file failed")?; let mut chain = Vec::new(); for pem in pem_cert_chain.split("-----END CERTIFICATE-----") { @@ -142,8 +148,10 @@ impl SimpleAttestationTokenBroker { if !trimmed.starts_with("-----BEGIN CERTIFICATE-----") { continue; } - let cert = X509::from_pem(trimmed.as_bytes()) - .map_err(|_| anyhow!("Invalid PEM certificate chain"))?; + // x509-cert's DecodePem expects a single PEM block; the split + // above already isolates one. Use the Label-aware decoder. + let cert = Certificate::from_pem(trimmed.as_bytes()) + .context("Invalid PEM certificate chain")?; chain.push(cert); } Ok(chain) @@ -162,17 +170,14 @@ impl SimpleAttestationTokenBroker { impl SimpleAttestationTokenBroker { fn rs384_sign(&self, payload: &[u8]) -> Result> { - let rsa_pkey = PKey::from_rsa(self.private_key.clone())?; - let mut signer = Signer::new(MessageDigest::sha384(), &rsa_pkey)?; - signer.update(payload)?; - let signature = signer.sign_to_vec()?; - - Ok(signature) + let signing_key = SigningKey::::new(self.private_key.clone()); + let sig: Signature = signing_key.sign(payload); + Ok(Box::<[u8]>::from(sig).to_vec()) } fn pubkey_jwks(&self) -> Result { - let n = self.private_key.n().to_vec(); - let e = self.private_key.e().to_vec(); + let n = self.private_key.n().to_bytes_be(); + let e = self.private_key.e().to_bytes_be(); let mut jwk = Jwk { kty: "RSA".to_string(), @@ -439,11 +444,16 @@ mod tests { use crate::TeeClaims; use assert_json_diff::assert_json_eq; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + use base64::Engine; use kbs_types::Tee; use serde_json::json; + use tempfile::NamedTempFile; + use x509_cert::der::Decode; + use x509_cert::Certificate; use crate::token::{ - simple::{Configuration, SimpleAttestationTokenBroker}, + simple::{Configuration, SimpleAttestationTokenBroker, TokenSignerConfig}, AttestationTokenBroker, }; @@ -538,4 +548,152 @@ mod tests { }); assert_json_eq!(expected, flatten); } + + // A pre-generated RSA-2048 PKCS#8 private key (PEM). Used together with + // `TEST_CERT_CHAIN_PEM` to exercise the `signer = Some(...)` branch of + // `SimpleAttestationTokenBroker::new`, which parses the PEM cert chain via + // `Certificate::from_pem` and later encodes it into the JWK `x5c` array via + // `Certificate::to_der`. Generated with `openssl genpkey` / `openssl req + // -x509`; embedded as text so no binary fixture is committed. + const TEST_SIGNER_KEY_PEM: &str = "-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdazaeItcE7c8W +cuc3i+KE94fKdLt/aOw2oIr6lVlzW95cwuok35uTYlJwTrvhPd8Qz1xBuerk9qAQ +hMhEtslKX96ZBUHn/St7ajIvLAJahW4VdaOd8hcakS2b9sSaIaw84rtcpaGa/w1k +fQglM5w2zClbfWnhwLYr4Fp0tzhI3hWuqmQs3S7uGc0An1vcb9TZDkgt6hhB6Fpr +LHZcodgKbNHkM/WrNRKqJxFmwgSQc2v0VdA0QQyP7fNpuPdjdqrkSCQH7mMWQh2e +mbifKalRHAj5jzSrmhxfXis+yJ5yU/dij3TwG9JAWuYRR7hbgeKHEq9FMsWoNRCf +0TirPKjJAgMBAAECggEAFnLtrQuG4lsPh0IHmzJFsXSjVsni2z6ZQQkQCMA3q23U +fiIFxhBlXVVOMFnqDSsHnpwTqgPbbZ+GIBTvgm0Ws5aMZgIL7gt6ofT5ByUdiM8y +bbkDBkk55j4B5RYB34Eh0OT8ly+/phztSgFSoguEIYRn+XYfHWSgFg2+mJpwWmOo +KG1xGNSgpaAmjwpaMDWZkvOxeyPUZa7SZ8Qs+IaNd34KdbuCf0Bry5IB2aeNuQX4 +YISpeNa+7ZFk+I9zbHtHMQuUNBYTeTRD6m7nyuKYfTghuooHbJMGDbCCOjHZZ+7+ +wdvtKe3uw8v7bvmYL0YTit46XN45nD+2ZaU7LEPYIQKBgQDOf19oRzRgtMI84XTK +DheTgaZnkH0scBcNTF1InXLrJtD7SaAeDVAKpSjcp9j0ztCfVRqAR86FDJK5N9Sj +y5+F2Za3I6Y84Jv2cEOqlRADDmMZ3T3X1g2AIb4pB53+uqQXSCp7xHc1Y8u/v2tB +kZ/4iYxvdUL1kfgw4chaCDZCrQKBgQDDJ+h/Xkx0wbq2/CKXpwQ5cAX3+//cfmef +cVneWOq4kf7xEfM2es763zLCNEV/R0Jz6k94+Dg/MPRGk7jwYiJQzer3xnjrAlMg +Xmm3NubiCKK4lVbzlHR9GsSNEhOSPzVqvCY4EKjr66ilD5nC97qAugnNx3vvIr8v +P+9Rh7ceDQKBgDpUIE8ETfdDF9q6lJK+iEpSRP7cAX+b6ecHuxHX564kuMNCeMgE +WqenH3O0tcPw510aXPH/VoaelpNbAeWCjvzwCXKRz1NC3sstyu9US8GRPsz/gYiG +HiojXeOZEzfw4IjzCY0MYd/i4Jq5J0LOL7G0qMaTCOb05HZqUH2d9DXBAoGBAMKA +MttGe4LeVh3revqUTcSFHp3CPYZfQR2K1luhWQZtE57mGfVRPpqP+0HM4PryZYur +mlthYIWyX7M7pVWHKNZJ9IXP/FGU9o5LKqecg04B91NqG8gWTGcnV3+V5YWbk7x2 +Gs1D5WeEbodb3g6P4gRL5lt+FsoGYm9QFE+4qEu9AoGBAIYroZ3Zcn/cuZrxd86+ +IfhfMSAQFBcGy3mxwNnFK1oYNsaI2Q58XpmE2Szq3xKCJvxaySoaCYMI0gLnsNIY +cuYg+Fi67/AQ/dSefVb47kn9YnX8xAi2HfTvaX5M3z7bM00W/3aWAL+c785+L15/ +wihjpplQnoBixGHV/2XFex6x +-----END PRIVATE KEY----- +"; + + // A 2-cert PEM chain (leaf + CA). The split-on-`-----END CERTIFICATE-----` + // loop in `SimpleAttestationTokenBroker::new` parses both entries. + const TEST_CERT_CHAIN_PEM: &str = "-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUdzxyN1GLEQxMXM8WxZBC8XyZgLswDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJdGVzdC1sZWFmMB4XDTI2MDcxMzEzMzMyOFoXDTI2MDcx +NDEzMzMyOFowFDESMBAGA1UEAwwJdGVzdC1sZWFmMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAnWs2niLXBO3PFnLnN4vihPeHynS7f2jsNqCK+pVZc1ve +XMLqJN+bk2JScE674T3fEM9cQbnq5PagEITIRLbJSl/emQVB5/0re2oyLywCWoVu +FXWjnfIXGpEtm/bEmiGsPOK7XKWhmv8NZH0IJTOcNswpW31p4cC2K+BadLc4SN4V +rqpkLN0u7hnNAJ9b3G/U2Q5ILeoYQehaayx2XKHYCmzR5DP1qzUSqicRZsIEkHNr +9FXQNEEMj+3zabj3Y3aq5EgkB+5jFkIdnpm4nympURwI+Y80q5ocX14rPsieclP3 +Yo908BvSQFrmEUe4W4HihxKvRTLFqDUQn9E4qzyoyQIDAQABo1MwUTAdBgNVHQ4E +FgQU8wNZeMzYYavyOsdZiCaCRilG0h0wHwYDVR0jBBgwFoAU8wNZeMzYYavyOsdZ +iCaCRilG0h0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQ1EE +2oW6T0isfKL+JDVUJ+SQNj78XrZD/j4Yz1TelUcWisKXy2yLlaS6b5kc76uBt5fq +2k9zIL++DgrJKwLxLQLtzyzjSQfZjUL8droACYpHib68Lrndb5Wcj9a3Wcfiiapj +vvpS0AsewA7vJDbMT20ysk5UXuOhtzBHUGMJ3T8L4SI9DtcPhAHH+xsVj7m/bQSX +lH9fNZ3BtwToqc+EYJGpG73/ywyAIoXPKJz/QzKNgg9AEI+zii+jDp7kmb4/hDzQ +Zec2jczp/Dz57pSUaFnTwESsnxV5MSyYzuS7dtRzznvvUd5zcXmxDM1a9yIb1Odi +IC3JTM1/6bvS5z8fqQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUZXV/g7lAiljh80f59PX8tfvJOpEwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yNjA3MTMxMzMzMjhaFw0yNjA3MTQx +MzMzMjhaMBIxEDAOBgNVBAMMB3Rlc3QtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDETWzpRRPicHUs5jm7jqm3HwuWB4cTHVOyoq87ttlEVVsVYpni +g2DLSUvDu0qXTNf8KaA5IYlqCIcEuDrKYNl0o1vcL79FFT/URFsZVr0Q6R1PjrKf +9nWN7df9sFCvMokJOpIxtBc0t4jFcYJKIS9EGBykP83zmDPv3+9KhLtlOq5cHEvM +89TSLvLn6sCHYwqFvOLlfNVrMGMAbDG656tnIcWXor3AFa/ghlLgsAeJgimfhaDM +I/JjZVZwKK5u3Vm027sKRPcv/U90wtRw7+HL84Cm++HtX71uBPcU8MyeCPV+IziN +ruWeTq9XbfCwMknU8G2/sHplwD4LJbgVUXxXAgMBAAGjUzBRMB0GA1UdDgQWBBSO +UCn2c7P9LIuWx588AgyQ90Y7wjAfBgNVHSMEGDAWgBSOUCn2c7P9LIuWx588AgyQ +90Y7wjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB4UD93ayQK +JhhRqNdusfXGKanIoIxDRbTR6tzCjquJpFl+BuPZtYOQYQr/ZLaF+KlWoKnabTnN +Szdn92fQ0lTrc9T/gGI7MKm7w2iUXN4NYF40NOGl6fwf755+drDZ96uPbbB+ezMW +VLwu9FhLqR+i0ezsjjc4SvKTywsANT2tiheLgOlF//p9WoGSHPfnh57Yrn6TP+NI +6nW8UlqfXfdSjYugZILyU9KM5sMOJJSA8eIFuBApvzIwsxPZlIlfKxOKjaDQlVIE +qSF2UcxxPZ/OQ8T2kTNYWr0OAQ5Xd8niWu7KU6ZX0miQNIv2c7qsYDR14JEroeo0 +frJCGYDUg+8c +-----END CERTIFICATE----- +"; + + #[test] + fn test_simple_signer_cert_chain_x5c() { + // Exercise the `signer = Some(...)` branch of + // `SimpleAttestationTokenBroker::new` with a PEM private key and a + // 2-cert PEM chain. This drives the previously-untested cert-chain + // parse closure (`Certificate::from_pem`) and the `pubkey_jwks()` + // `x5c`-encoding branch (`Certificate::to_der` + base64url). + + let key_file = NamedTempFile::new().expect("create temp key file"); + std::fs::write(&key_file, TEST_SIGNER_KEY_PEM).expect("write temp key file"); + let chain_file = NamedTempFile::new().expect("create temp chain file"); + std::fs::write(&chain_file, TEST_CERT_CHAIN_PEM).expect("write temp chain file"); + + let config = Configuration { + signer: Some(TokenSignerConfig { + key_path: key_file.path().to_string_lossy().to_string(), + cert_url: None, + cert_path: Some(chain_file.path().to_string_lossy().to_string()), + }), + ..Configuration::default() + }; + + let broker = SimpleAttestationTokenBroker::new(config) + .expect("broker construction with signer + cert chain must succeed"); + + let jwks = serde_json::from_str::( + &broker.pubkey_jwks().expect("pubkey_jwks must succeed"), + ) + .expect("pubkey_jwks must return valid JSON"); + + let keys = jwks + .get("keys") + .and_then(|v| v.as_array()) + .expect("JWKS must contain a `keys` array"); + assert_eq!(keys.len(), 1, "exactly one JWK expected"); + + let jwk = &keys[0]; + + // RSA public-key fields derived from the configured private key. + let n = jwk + .get("n") + .and_then(|v| v.as_str()) + .expect("`n` must be present"); + let e = jwk + .get("e") + .and_then(|v| v.as_str()) + .expect("`e` must be present"); + assert!(!n.is_empty(), "`n` must be non-empty"); + assert!(!e.is_empty(), "`e` must be non-empty"); + + // x5c round-trips the `to_der` encode + `from_pem` parse: each entry + // base64url-decodes back to a DER blob that `Certificate::from_der` + // accepts. + let x5c = jwk + .get("x5c") + .and_then(|v| v.as_array()) + .expect("`x5c` must be present when a cert chain is configured"); + assert_eq!(x5c.len(), 2, "x5c must contain exactly 2 cert entries"); + + for entry in x5c { + let b64 = entry + .as_str() + .expect("each x5c entry must be a base64url string"); + let der = URL_SAFE_NO_PAD + .decode(b64) + .expect("each x5c entry must base64url-decode to DER bytes"); + Certificate::from_der(&der).expect("decoded x5c entry must be a valid DER certificate"); + } + } } From d48e17a89350b907adf707dee6f2f7f3d4fc7e06 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 14 Jul 2026 16:48:43 +0800 Subject: [PATCH 3/4] refactor(as): migrate oidc.rs / lib.rs / ear_broker.rs to rustcrypto - oidc.rs: RS256 + X509 + JWK via rustcrypto - lib.rs: get_token_broker_public_key RSA via rustcrypto - ear_broker.rs: P-256 key handling via p256 (jsonwebtoken still signs) Co-Authored-By: Claude --- attestation-service/Cargo.toml | 2 +- attestation-service/src/lib.rs | 16 +- attestation-service/src/token/ear_broker.rs | 83 ++++---- attestation-service/src/token/oidc.rs | 211 +++++++++++++++++--- 4 files changed, 246 insertions(+), 66 deletions(-) diff --git a/attestation-service/Cargo.toml b/attestation-service/Cargo.toml index 58fa049..ad4872c 100644 --- a/attestation-service/Cargo.toml +++ b/attestation-service/Cargo.toml @@ -70,7 +70,7 @@ kbs-types.workspace = true lazy_static = "1.4.0" log.workspace = true openssl = "0.10.55" -p256 = { version = "0.13", features = ["ecdh"] } +p256 = { version = "0.13", features = ["ecdh", "pkcs8", "pem"] } prost = { workspace = true, optional = true } rand = "0.8.5" reqwest = { version = "0.12", features = ["json"] } diff --git a/attestation-service/src/lib.rs b/attestation-service/src/lib.rs index f3aaf60..7d48dd3 100644 --- a/attestation-service/src/lib.rs +++ b/attestation-service/src/lib.rs @@ -18,8 +18,11 @@ use canon_json::CanonicalFormatter; use config::Config; pub use kbs_types::{Attestation, Tee}; use log::{debug, info}; -use openssl::rsa::Rsa; use reqwest::Client; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs8::DecodePrivateKey; +use rsa::traits::PublicKeyParts; +use rsa::RsaPrivateKey; use rvps::{RvpsApi, RvpsError}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -476,10 +479,13 @@ impl AttestationService { token::AttestationTokenConfig::OIDC(cfg) => { if let Some(signer) = &cfg.signer { let pem_data = std::fs::read(&signer.key_path) - .map_err(|e| anyhow!("Read Token Signer private key failed: {:?}", e))?; - let private_key = Rsa::private_key_from_pem(&pem_data)?; - let n = private_key.n().to_vec(); - let e = private_key.e().to_vec(); + .context("Read Token Signer private key failed")?; + let pem_str = + std::str::from_utf8(&pem_data).context("Token Signer key not UTF-8")?; + let private_key = RsaPrivateKey::from_pkcs8_pem(pem_str) + .or_else(|_| RsaPrivateKey::from_pkcs1_pem(pem_str))?; + let n = private_key.n().to_bytes_be(); + let e = private_key.e().to_bytes_be(); let jwk = Jwk { kty: "RSA".to_string(), diff --git a/attestation-service/src/token/ear_broker.rs b/attestation-service/src/token/ear_broker.rs index 597f375..85f74fd 100644 --- a/attestation-service/src/token/ear_broker.rs +++ b/attestation-service/src/token/ear_broker.rs @@ -14,11 +14,10 @@ use ear::{ use jsonwebtoken::{jwk, EncodingKey}; use kbs_types::Tee; use log::{debug, info, warn}; -use openssl::bn::{BigNum, BigNumContext}; -use openssl::ec::{EcGroup, EcKey}; -use openssl::nid::Nid; -use openssl::pkey::{PKey, Private}; -use openssl::x509::X509; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding}; +use p256::SecretKey; +use rand::rngs::OsRng; use serde::Deserialize; use serde_json::{json, Value}; use serde_variant::to_variant_name; @@ -27,6 +26,8 @@ use std::collections::{BTreeMap, HashMap}; use std::path::Path; use std::sync::Arc; use time::{Duration, OffsetDateTime}; +use x509_cert::der::{DecodePem, Encode}; +use x509_cert::Certificate; use crate::policy_engine::{PolicyEngine, PolicyEngineType}; use crate::token::DEFAULT_TOKEN_WORK_DIR; @@ -137,9 +138,9 @@ impl Default for Configuration { pub struct EarAttestationTokenBroker { config: Configuration, - private_key: EcKey, + private_key: SecretKey, cert_url: Option, - cert_chain: Option>, + cert_chain: Option>, policy_engine: Arc, } @@ -164,16 +165,19 @@ impl EarAttestationTokenBroker { } let signer = config.signer.clone().unwrap(); - let pem_data = std::fs::read(&signer.key_path) - .map_err(|e| anyhow!("Read Token Signer private key failed: {:?}", e))?; - let private_key = EcKey::private_key_from_pem(&pem_data)?; + let pem_data = + std::fs::read(&signer.key_path).context("Read Token Signer private key failed")?; + let pem_str = std::str::from_utf8(&pem_data).context("Token Signer key not UTF-8")?; + let private_key = SecretKey::from_sec1_pem(pem_str) + .or_else(|_| SecretKey::from_pkcs8_pem(pem_str)) + .context("Parse Token Signer private key failed")?; let cert_chain = signer .cert_path .as_ref() - .map(|cert_path| -> Result> { + .map(|cert_path| -> Result> { let pem_cert_chain = std::fs::read_to_string(cert_path) - .map_err(|e| anyhow!("Read Token Signer cert file failed: {:?}", e))?; + .context("Read Token Signer cert file failed")?; let mut chain = Vec::new(); for pem in pem_cert_chain.split("-----END CERTIFICATE-----") { @@ -181,8 +185,10 @@ impl EarAttestationTokenBroker { if !trimmed.starts_with("-----BEGIN CERTIFICATE-----") { continue; } - let cert = X509::from_pem(trimmed.as_bytes()) - .map_err(|_| anyhow!("Invalid PEM certificate chain"))?; + // x509-cert's DecodePem expects a single PEM block; the split + // above already isolates one. Use the Label-aware decoder. + let cert = Certificate::from_pem(trimmed.as_bytes()) + .context("Invalid PEM certificate chain")?; chain.push(cert); } Ok(chain) @@ -314,8 +320,11 @@ impl AttestationTokenBroker for EarAttestationTokenBroker { let mut jwt_header = ear::new_jwt_header(&Algorithm::ES256)?; jwt_header.jwk = Some(self.pubkey_jwk()?); - let pkey = PKey::from_ec_key(self.private_key.clone())?; - let private_key_bytes = pkey.private_key_to_pem_pkcs8()?; + let private_key_bytes = self + .private_key + .to_pkcs8_pem(LineEnding::LF) + .context("serialize EC private key to PKCS#8 PEM")?; + let private_key_bytes: &[u8] = private_key_bytes.as_bytes(); let signed_ear = if let Some(transparency) = signer_transparency::load_signer_transparency() { @@ -329,10 +338,10 @@ impl AttestationTokenBroker for EarAttestationTokenBroker { jsonwebtoken::encode( &jwt_header, &Value::Object(ear_claims), - &EncodingKey::from_ec_pem(&private_key_bytes)?, + &EncodingKey::from_ec_pem(private_key_bytes)?, )? } else { - ear.sign_jwt_pem_with_header(&jwt_header, &private_key_bytes)? + ear.sign_jwt_pem_with_header(&jwt_header, private_key_bytes)? }; Ok(signed_ear) @@ -391,18 +400,19 @@ impl EarAttestationTokenBroker { }; let public_key = self.private_key.public_key(); - let group = self.private_key.group(); - - let mut ctx = BigNumContext::new()?; - let mut x = BigNum::new()?; - let mut y = BigNum::new()?; - public_key.affine_coordinates_gfp(group, &mut x, &mut y, &mut ctx)?; + let encoded = public_key.to_encoded_point(false); + let x = encoded + .x() + .ok_or_else(|| anyhow!("EC public key has no x coordinate"))?; + let y = encoded + .y() + .ok_or_else(|| anyhow!("EC public key has no y coordinate"))?; let algorithm = jwk::AlgorithmParameters::EllipticCurve(jwk::EllipticCurveKeyParameters { key_type: jwk::EllipticCurveKeyType::EC, curve: jwk::EllipticCurve::P256, - x: URL_SAFE_NO_PAD.encode(x.to_vec()), - y: URL_SAFE_NO_PAD.encode(y.to_vec()), + x: URL_SAFE_NO_PAD.encode(x.as_slice()), + y: URL_SAFE_NO_PAD.encode(y.as_slice()), }); let jwk = jwk::Jwk { common, algorithm }; @@ -411,15 +421,20 @@ impl EarAttestationTokenBroker { } } -fn generate_ec_keys() -> Result<(EcKey, Vec, Vec)> { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; - let ec_key = EcKey::generate(&group)?; - let pkey = PKey::from_ec_key(ec_key.clone())?; - +fn generate_ec_keys() -> Result<(SecretKey, Vec, Vec)> { + let mut rng = OsRng; + let secret = SecretKey::random(&mut rng); + let priv_pem = secret + .to_pkcs8_pem(LineEnding::LF) + .context("serialize EC private key to PKCS#8 PEM")?; + let pub_pem = secret + .public_key() + .to_public_key_pem(LineEnding::LF) + .context("serialize EC public key to SPKI PEM")?; Ok(( - ec_key, - pkey.private_key_to_pem_pkcs8()?, - pkey.public_key_to_pem()?, + secret, + priv_pem.as_bytes().to_vec(), + pub_pem.as_bytes().to_vec(), )) } diff --git a/attestation-service/src/token/oidc.rs b/attestation-service/src/token/oidc.rs index b76774f..10663de 100755 --- a/attestation-service/src/token/oidc.rs +++ b/attestation-service/src/token/oidc.rs @@ -11,22 +11,25 @@ use anyhow::*; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use log::info; -use openssl::rsa::Rsa; -use openssl::sign::Signer; -use openssl::x509::X509; -use openssl::{ - hash::MessageDigest, - pkey::{PKey, Private}, -}; use rand::distributions::Alphanumeric; +use rand::rngs::OsRng; use rand::{thread_rng, Rng}; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs1v15::{Signature, SigningKey}; +use rsa::pkcs8::DecodePrivateKey; +use rsa::signature::Signer; +use rsa::traits::PublicKeyParts; +use rsa::RsaPrivateKey; use serde::Deserialize; use serde_json::{json, Map, Value}; use serde_variant::to_variant_name; +use sha2::Sha256; use shadow_rs::concatcp; use std::collections::{HashMap, HashSet}; use std::path::Path; use std::sync::Arc; +use x509_cert::der::{DecodePem, Encode}; +use x509_cert::Certificate; use crate::policy_engine::{PolicyEngine, PolicyEngineType}; use crate::token::{AttestationTokenBroker, DEFAULT_TOKEN_WORK_DIR}; @@ -128,10 +131,10 @@ impl Default for Configuration { } pub struct OIDCAttestationTokenBroker { - private_key: Rsa, + private_key: RsaPrivateKey, config: Configuration, cert_url: Option, - cert_chain: Option>, + cert_chain: Option>, policy_engine: Arc, } @@ -146,8 +149,9 @@ impl OIDCAttestationTokenBroker { if config.signer.is_none() { log::info!("No Token Signer key in config file, create an ephemeral key and without CA pubkey cert"); + let mut rng = OsRng; return Ok(Self { - private_key: Rsa::generate(RSA_KEY_BITS)?, + private_key: RsaPrivateKey::new(&mut rng, RSA_KEY_BITS as usize)?, config, cert_url: None, cert_chain: None, @@ -156,16 +160,18 @@ impl OIDCAttestationTokenBroker { } let signer = config.signer.clone().unwrap(); - let pem_data = std::fs::read(&signer.key_path) - .map_err(|e| anyhow!("Read Token Signer private key failed: {:?}", e))?; - let private_key = Rsa::private_key_from_pem(&pem_data)?; + let pem_data = std::fs::read_to_string(&signer.key_path) + .context("Read Token Signer private key failed")?; + let private_key = RsaPrivateKey::from_pkcs8_pem(&pem_data) + .or_else(|_| RsaPrivateKey::from_pkcs1_pem(&pem_data)) + .context("Parse Token Signer private key failed")?; let cert_chain = signer .cert_path .as_ref() - .map(|cert_path| -> Result> { + .map(|cert_path| -> Result> { let pem_cert_chain = std::fs::read_to_string(cert_path) - .map_err(|e| anyhow!("Read Token Signer cert file failed: {:?}", e))?; + .context("Read Token Signer cert file failed")?; let mut chain = Vec::new(); for pem in pem_cert_chain.split("-----END CERTIFICATE-----") { @@ -173,8 +179,10 @@ impl OIDCAttestationTokenBroker { if !trimmed.starts_with("-----BEGIN CERTIFICATE-----") { continue; } - let cert = X509::from_pem(trimmed.as_bytes()) - .map_err(|_| anyhow!("Invalid PEM certificate chain"))?; + // x509-cert's DecodePem expects a single PEM block; the split + // above already isolates one. Use the Label-aware decoder. + let cert = Certificate::from_pem(trimmed.as_bytes()) + .context("Invalid PEM certificate chain")?; chain.push(cert); } Ok(chain) @@ -193,17 +201,14 @@ impl OIDCAttestationTokenBroker { impl OIDCAttestationTokenBroker { fn rs256_sign(&self, payload: &[u8]) -> Result> { - let rsa_pkey = PKey::from_rsa(self.private_key.clone())?; - let mut signer = Signer::new(MessageDigest::sha256(), &rsa_pkey)?; - signer.update(payload)?; - let signature = signer.sign_to_vec()?; - - Ok(signature) + let signing_key = SigningKey::::new(self.private_key.clone()); + let sig: Signature = signing_key.sign(payload); + Ok(Box::<[u8]>::from(sig).to_vec()) } fn pubkey_jwks(&self) -> Result { - let n = self.private_key.n().to_vec(); - let e = self.private_key.e().to_vec(); + let n = self.private_key.n().to_bytes_be(); + let e = self.private_key.e().to_bytes_be(); let mut jwk = Jwk { kty: "RSA".to_string(), @@ -499,11 +504,16 @@ mod tests { use std::collections::HashMap; use crate::TeeClaims; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + use base64::Engine; use kbs_types::Tee; use serde_json::json; + use tempfile::NamedTempFile; + use x509_cert::der::Decode; + use x509_cert::Certificate; use crate::token::{ - oidc::{Configuration, OIDCAttestationTokenBroker}, + oidc::{Configuration, OIDCAttestationTokenBroker, TokenSignerConfig}, AttestationTokenBroker, }; @@ -530,4 +540,153 @@ mod tests { .await .unwrap(); } + + // A pre-generated RSA-2048 PKCS#8 private key (PEM). Used together with + // `TEST_CERT_CHAIN_PEM` to exercise the `signer = Some(...)` branch of + // `OIDCAttestationTokenBroker::new`, which parses the PEM cert chain via + // `Certificate::from_pem` and later encodes it into the JWK `x5c` array via + // `Certificate::to_der`. Generated with `openssl genpkey` / `openssl req + // -x509`; embedded as text so no binary fixture is committed. + const TEST_SIGNER_KEY_PEM: &str = "-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdazaeItcE7c8W +cuc3i+KE94fKdLt/aOw2oIr6lVlzW95cwuok35uTYlJwTrvhPd8Qz1xBuerk9qAQ +hMhEtslKX96ZBUHn/St7ajIvLAJahW4VdaOd8hcakS2b9sSaIaw84rtcpaGa/w1k +fQglM5w2zClbfWnhwLYr4Fp0tzhI3hWuqmQs3S7uGc0An1vcb9TZDkgt6hhB6Fpr +LHZcodgKbNHkM/WrNRKqJxFmwgSQc2v0VdA0QQyP7fNpuPdjdqrkSCQH7mMWQh2e +mbifKalRHAj5jzSrmhxfXis+yJ5yU/dij3TwG9JAWuYRR7hbgeKHEq9FMsWoNRCf +0TirPKjJAgMBAAECggEAFnLtrQuG4lsPh0IHmzJFsXSjVsni2z6ZQQkQCMA3q23U +fiIFxhBlXVVOMFnqDSsHnpwTqgPbbZ+GIBTvgm0Ws5aMZgIL7gt6ofT5ByUdiM8y +bbkDBkk55j4B5RYB34Eh0OT8ly+/phztSgFSoguEIYRn+XYfHWSgFg2+mJpwWmOo +KG1xGNSgpaAmjwpaMDWZkvOxeyPUZa7SZ8Qs+IaNd34KdbuCf0Bry5IB2aeNuQX4 +YISpeNa+7ZFk+I9zbHtHMQuUNBYTeTRD6m7nyuKYfTghuooHbJMGDbCCOjHZZ+7+ +wdvtKe3uw8v7bvmYL0YTit46XN45nD+2ZaU7LEPYIQKBgQDOf19oRzRgtMI84XTK +DheTgaZnkH0scBcNTF1InXLrJtD7SaAeDVAKpSjcp9j0ztCfVRqAR86FDJK5N9Sj +y5+F2Za3I6Y84Jv2cEOqlRADDmMZ3T3X1g2AIb4pB53+uqQXSCp7xHc1Y8u/v2tB +kZ/4iYxvdUL1kfgw4chaCDZCrQKBgQDDJ+h/Xkx0wbq2/CKXpwQ5cAX3+//cfmef +cVneWOq4kf7xEfM2es763zLCNEV/R0Jz6k94+Dg/MPRGk7jwYiJQzer3xnjrAlMg +Xmm3NubiCKK4lVbzlHR9GsSNEhOSPzVqvCY4EKjr66ilD5nC97qAugnNx3vvIr8v +P+9Rh7ceDQKBgDpUIE8ETfdDF9q6lJK+iEpSRP7cAX+b6ecHuxHX564kuMNCeMgE +WqenH3O0tcPw510aXPH/VoaelpNbAeWCjvzwCXKRz1NC3sstyu9US8GRPsz/gYiG +HiojXeOZEzfw4IjzCY0MYd/i4Jq5J0LOL7G0qMaTCOb05HZqUH2d9DXBAoGBAMKA +MttGe4LeVh3revqUTcSFHp3CPYZfQR2K1luhWQZtE57mGfVRPpqP+0HM4PryZYur +mlthYIWyX7M7pVWHKNZJ9IXP/FGU9o5LKqecg04B91NqG8gWTGcnV3+V5YWbk7x2 +Gs1D5WeEbodb3g6P4gRL5lt+FsoGYm9QFE+4qEu9AoGBAIYroZ3Zcn/cuZrxd86+ +IfhfMSAQFBcGy3mxwNnFK1oYNsaI2Q58XpmE2Szq3xKCJvxaySoaCYMI0gLnsNIY +cuYg+Fi67/AQ/dSefVb47kn9YnX8xAi2HfTvaX5M3z7bM00W/3aWAL+c785+L15/ +wihjpplQnoBixGHV/2XFex6x +-----END PRIVATE KEY----- +"; + + // A 2-cert PEM chain (leaf + CA). The split-on-`-----END CERTIFICATE-----` + // loop in `OIDCAttestationTokenBroker::new` parses both entries. + const TEST_CERT_CHAIN_PEM: &str = "-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUdzxyN1GLEQxMXM8WxZBC8XyZgLswDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJdGVzdC1sZWFmMB4XDTI2MDcxMzEzMzMyOFoXDTI2MDcx +NDEzMzMyOFowFDESMBAGA1UEAwwJdGVzdC1sZWFmMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAnWs2niLXBO3PFnLnN4vihPeHynS7f2jsNqCK+pVZc1ve +XMLqJN+bk2JScE674T3fEM9cQbnq5PagEITIRLbJSl/emQVB5/0re2oyLywCWoVu +FXWjnfIXGpEtm/bEmiGsPOK7XKWhmv8NZH0IJTOcNswpW31p4cC2K+BadLc4SN4V +rqpkLN0u7hnNAJ9b3G/U2Q5ILeoYQehaayx2XKHYCmzR5DP1qzUSqicRZsIEkHNr +9FXQNEEMj+3zabj3Y3aq5EgkB+5jFkIdnpm4nympURwI+Y80q5ocX14rPsieclP3 +Yo908BvSQFrmEUe4W4HihxKvRTLFqDUQn9E4qzyoyQIDAQABo1MwUTAdBgNVHQ4E +FgQU8wNZeMzYYavyOsdZiCaCRilG0h0wHwYDVR0jBBgwFoAU8wNZeMzYYavyOsdZ +iCaCRilG0h0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQ1EE +2oW6T0isfKL+JDVUJ+SQNj78XrZD/j4Yz1TelUcWisKXy2yLlaS6b5kc76uBt5fq +2k9zIL++DgrJKwLxLQLtzyzjSQfZjUL8droACYpHib68Lrndb5Wcj9a3Wcfiiapj +vvpS0AsewA7vJDbMT20ysk5UXuOhtzBHUGMJ3T8L4SI9DtcPhAHH+xsVj7m/bQSX +lH9fNZ3BtwToqc+EYJGpG73/ywyAIoXPKJz/QzKNgg9AEI+zii+jDp7kmb4/hDzQ +Zec2jczp/Dz57pSUaFnTwESsnxV5MSyYzuS7dtRzznvvUd5zcXmxDM1a9yIb1Odi +IC3JTM1/6bvS5z8fqQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUZXV/g7lAiljh80f59PX8tfvJOpEwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yNjA3MTMxMzMzMjhaFw0yNjA3MTQx +MzMzMjhaMBIxEDAOBgNVBAMMB3Rlc3QtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDETWzpRRPicHUs5jm7jqm3HwuWB4cTHVOyoq87ttlEVVsVYpni +g2DLSUvDu0qXTNf8KaA5IYlqCIcEuDrKYNl0o1vcL79FFT/URFsZVr0Q6R1PjrKf +9nWN7df9sFCvMokJOpIxtBc0t4jFcYJKIS9EGBykP83zmDPv3+9KhLtlOq5cHEvM +89TSLvLn6sCHYwqFvOLlfNVrMGMAbDG656tnIcWXor3AFa/ghlLgsAeJgimfhaDM +I/JjZVZwKK5u3Vm027sKRPcv/U90wtRw7+HL84Cm++HtX71uBPcU8MyeCPV+IziN +ruWeTq9XbfCwMknU8G2/sHplwD4LJbgVUXxXAgMBAAGjUzBRMB0GA1UdDgQWBBSO +UCn2c7P9LIuWx588AgyQ90Y7wjAfBgNVHSMEGDAWgBSOUCn2c7P9LIuWx588AgyQ +90Y7wjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB4UD93ayQK +JhhRqNdusfXGKanIoIxDRbTR6tzCjquJpFl+BuPZtYOQYQr/ZLaF+KlWoKnabTnN +Szdn92fQ0lTrc9T/gGI7MKm7w2iUXN4NYF40NOGl6fwf755+drDZ96uPbbB+ezMW +VLwu9FhLqR+i0ezsjjc4SvKTywsANT2tiheLgOlF//p9WoGSHPfnh57Yrn6TP+NI +6nW8UlqfXfdSjYugZILyU9KM5sMOJJSA8eIFuBApvzIwsxPZlIlfKxOKjaDQlVIE +qSF2UcxxPZ/OQ8T2kTNYWr0OAQ5Xd8niWu7KU6ZX0miQNIv2c7qsYDR14JEroeo0 +frJCGYDUg+8c +-----END CERTIFICATE----- +"; + + #[test] + fn test_oidc_signer_cert_chain_x5c() { + // Exercise the `signer = Some(...)` branch of + // `OIDCAttestationTokenBroker::new` with a PEM private key and a + // 2-cert PEM chain. This drives the previously-untested cert-chain + // parse closure (`Certificate::from_pem`) and the `pubkey_jwks()` + // `x5c`-encoding branch (`Certificate::to_der` + base64url). + + let key_file = NamedTempFile::new().expect("create temp key file"); + std::fs::write(&key_file, TEST_SIGNER_KEY_PEM).expect("write temp key file"); + let chain_file = NamedTempFile::new().expect("create temp chain file"); + std::fs::write(&chain_file, TEST_CERT_CHAIN_PEM).expect("write temp chain file"); + + let config = Configuration { + signer: Some(TokenSignerConfig { + key_path: key_file.path().to_string_lossy().to_string(), + cert_url: None, + cert_path: Some(chain_file.path().to_string_lossy().to_string()), + }), + oid_config: None, + ..Configuration::default() + }; + + let broker = OIDCAttestationTokenBroker::new(config) + .expect("broker construction with signer + cert chain must succeed"); + + let jwks = serde_json::from_str::( + &broker.pubkey_jwks().expect("pubkey_jwks must succeed"), + ) + .expect("pubkey_jwks must return valid JSON"); + + let keys = jwks + .get("keys") + .and_then(|v| v.as_array()) + .expect("JWKS must contain a `keys` array"); + assert_eq!(keys.len(), 1, "exactly one JWK expected"); + + let jwk = &keys[0]; + + // RSA public-key fields derived from the configured private key. + let n = jwk + .get("n") + .and_then(|v| v.as_str()) + .expect("`n` must be present"); + let e = jwk + .get("e") + .and_then(|v| v.as_str()) + .expect("`e` must be present"); + assert!(!n.is_empty(), "`n` must be non-empty"); + assert!(!e.is_empty(), "`e` must be non-empty"); + + // x5c round-trips the `to_der` encode + `from_pem` parse: each entry + // base64url-decodes back to a DER blob that `Certificate::from_der` + // accepts. + let x5c = jwk + .get("x5c") + .and_then(|v| v.as_array()) + .expect("`x5c` must be present when a cert chain is configured"); + assert_eq!(x5c.len(), 2, "x5c must contain exactly 2 cert entries"); + + for entry in x5c { + let b64 = entry + .as_str() + .expect("each x5c entry must be a base64url string"); + let der = URL_SAFE_NO_PAD + .decode(b64) + .expect("each x5c entry must base64url-decode to DER bytes"); + Certificate::from_der(&der).expect("decoded x5c entry must be a valid DER certificate"); + } + } } From f69f70934779acdcdbcbd712fb4bade3fe84994d Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 14 Jul 2026 19:23:25 +0800 Subject: [PATCH 4/4] build: remove openssl-sys and consolidate TLS deps on rustls Finish the migration away from openssl-sys by switching every HTTP/TLS client to rustls and aligning on a single workspace reqwest (0.12). - as/rvps: disable reqwest default-tls and enable rustls-tls, dropping the openssl-sys transitive dependency - as: make the `openssl` crate optional, gated behind the restful-bin feature (actix-web/openssl still requires it for the restful binary) - verifier: drop the vestigial `openssl` feature from tdx-verifier - as/verifier: depend on `ear` with the `jwt` feature only - workspace: point `ear` at the patched git rev 5cf2251 (split cose/jwt features, default-features disabled), which drops the aws-lc-rs/bindgen dependencies - consolidate attestation-service and rvps on the workspace reqwest 0.12 + rustls-tls, retiring the legacy reqwest 0.11 and its hyper-rustls 0.24 / tokio-rustls 0.24 / rustls 0.21 chain Co-Authored-By: Claude Signed-off-by: Kun Lai --- Cargo.lock | 127 +++++++++++++++++++-------------- Cargo.toml | 5 +- attestation-service/Cargo.toml | 6 +- deps/kms/Cargo.toml | 2 +- deps/verifier/Cargo.toml | 5 +- kbs/Cargo.toml | 2 +- rvds/Cargo.toml | 2 +- rvps/Cargo.toml | 2 +- 8 files changed, 85 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 625534f..bd16883 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,7 +622,7 @@ dependencies = [ "strum 0.25.0", "tempfile", "testing_logger", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", "tokio", "toml 0.8.23", @@ -1050,7 +1050,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -1071,16 +1071,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "cbor-codec" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e083a023562b37c52837e850131a51b1154cceb9d149f41ee3d386737b140f46" -dependencies = [ - "byteorder", - "libc", -] - [[package]] name = "cc" version = "1.2.15" @@ -1376,21 +1366,20 @@ dependencies = [ ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "cose-rust" -version = "0.1.7" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f221b4189b72ce93755b7fa1495d1741cc330bbd9698d3032562811698e3ab84" -dependencies = [ - "cbor-codec", - "openssl", - "rand", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpio" @@ -1572,7 +1561,7 @@ checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" dependencies = [ "curl-sys", "libc", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", "socket2", @@ -1995,16 +1984,13 @@ checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "ear" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1134a8dbb5ad666d26d82da83d12b71703b16f2ed5433d5ba24d8cfea2b66d96" +source = "git+https://github.com/inclavare-containers/rust-ear.git?rev=5cf22512e4b0c446a28d969225fc48abc97c450f#5cf22512e4b0c446a28d969225fc48abc97c450f" dependencies = [ "base64 0.22.1", "ciborium", - "cose-rust", "hex", "jsonwebtoken", "lazy_static", - "openssl", "phf", "serde", "serde_json", @@ -2600,7 +2586,7 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -2951,6 +2937,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls 0.23.23", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -3486,7 +3473,7 @@ dependencies = [ "serde", "serde_json", "superboring", - "thiserror 2.0.12", + "thiserror 2.0.18", "zeroize", ] @@ -3559,7 +3546,7 @@ dependencies = [ "sqlx", "strum 0.25.0", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", "tokio", "toml 0.8.23", @@ -3581,7 +3568,7 @@ dependencies = [ "sha2", "sm3", "strum 0.27.2", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -3614,7 +3601,7 @@ dependencies = [ "serde_json", "sha2", "strum 0.25.0", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tonic", "tonic-build", @@ -3857,9 +3844,9 @@ dependencies = [ [[package]] name = "metrics" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884adb57038347dfbaf2d5065887b6cf4312330dc8e94bc30a1a839bd79d3261" +checksum = "3045b4193fbdc5b5681f32f11070da9be3609f189a79f3390706d42587f46bb5" dependencies = [ "ahash 0.8.11", "portable-atomic", @@ -3933,10 +3920,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -4184,6 +4171,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-src" version = "300.4.2+3.4.1" @@ -4443,7 +4436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.18", "ucd-trie", ] @@ -4904,7 +4897,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls 0.23.23", "socket2", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -4923,7 +4916,7 @@ dependencies = [ "rustls 0.23.23", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -5079,7 +5072,7 @@ dependencies = [ "log", "path-clean", "prost", - "reqwest 0.11.27", + "reqwest 0.12.12", "rpm", "rstest", "serde", @@ -5226,6 +5219,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.23", + "rustls-native-certs", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -5324,7 +5318,7 @@ dependencies = [ "pgp", "sha1", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.18", "xz2", "zstd", ] @@ -5470,6 +5464,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -5556,7 +5562,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "url", "web3", @@ -5827,7 +5833,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -6232,7 +6251,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", ] @@ -6686,7 +6705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -6697,7 +6716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -6776,11 +6795,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -6796,9 +6815,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -7481,7 +7500,7 @@ dependencies = [ "reqwest 0.12.12", "serde", "serde_with 3.14.0", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "url", ] @@ -7533,7 +7552,7 @@ dependencies = [ "shadow-rs", "sm3", "strum 0.25.0", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tonic-build", "tss-esapi", diff --git a/Cargo.toml b/Cargo.toml index a92f511..901900d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,8 @@ cfg-if = "1.0.0" chrono = "0.4.19" clap = { version = "4", features = ["derive"] } config = "0.13.3" -ear = "0.3.0" +# Use the patched ear which carries the split `cose`/`jwt` features +ear = { git = "https://github.com/inclavare-containers/rust-ear.git", rev = "5cf22512e4b0c446a28d969225fc48abc97c450f", default-features = false, features = ["jwt"] } env_logger = "0.10.0" hex = "0.4.3" jwt-simple = { version = "0.12", default-features = false, features = [ @@ -48,7 +49,7 @@ regorus = { version = "0.2.6", default-features = false, features = [ "time", "std", ] } -reqwest = { version = "0.12", default-features = false } +reqwest = { version = "0.12", default-features = false, features = ["charset", "http2", "rustls-tls-native-roots", "json"] } rstest = "0.18.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.89" diff --git a/attestation-service/Cargo.toml b/attestation-service/Cargo.toml index ad4872c..eb11e19 100644 --- a/attestation-service/Cargo.toml +++ b/attestation-service/Cargo.toml @@ -32,7 +32,7 @@ rvps-grpc = [ "prost", "tonic" ] grpc-bin = [ "clap", "env_logger", "prost", "tonic" ] # For restful CoCo-AS binary -restful-bin = [ "actix-cors", "actix-web/openssl", "clap", "env_logger" ] +restful-bin = [ "actix-cors", "actix-web/openssl", "clap", "env_logger", "openssl" ] [[bin]] name = "grpc-as" @@ -69,11 +69,11 @@ jsonwebtoken.workspace = true kbs-types.workspace = true lazy_static = "1.4.0" log.workspace = true -openssl = "0.10.55" +openssl = { version = "0.10.55", optional = true } p256 = { version = "0.13", features = ["ecdh", "pkcs8", "pem"] } prost = { workspace = true, optional = true } rand = "0.8.5" -reqwest = { version = "0.12", features = ["json"] } +reqwest.workspace = true rsa = { version = "0.9.2", features = ["sha2"] } reference-value-provider-service.path = "../rvps" regorus.workspace = true diff --git a/deps/kms/Cargo.toml b/deps/kms/Cargo.toml index 8ec364e..3d0b8d3 100644 --- a/deps/kms/Cargo.toml +++ b/deps/kms/Cargo.toml @@ -17,7 +17,7 @@ prost.workspace = true p12 = "0.6.3" rand = "0.8" ring = "0.17" -reqwest = { workspace = true, default-features = false, features = ["rustls-tls"] } +reqwest.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true diff --git a/deps/verifier/Cargo.toml b/deps/verifier/Cargo.toml index e184a4f..8a734e8 100644 --- a/deps/verifier/Cargo.toml +++ b/deps/verifier/Cargo.toml @@ -39,7 +39,6 @@ all-verifier-rust = [ tdx-verifier = [ "scroll", "gpu-verifier", - "openssl", ] tdx-dcap-ffi = ["tdx-verifier", "intel-tee-quote-verification-rs"] # Backend built on `dcap-qvl` (with its `ring` crypto backend). Removes the @@ -109,7 +108,7 @@ openssl = { version = "0.10.55", optional = true } openssl-sys = { version = "0.9", optional = true } pv = { version = "0.10.0", package = "s390_pv", optional = true } quick-xml = { version = "0.36", optional = true } -reqwest = { workspace = true, features = ["json", "rustls-tls"], optional = true } +reqwest = { workspace = true, optional = true } regex = { version = "1.10", optional = true } scroll = { version = "0.11.0", default-features = false, features = [ "derive", @@ -138,7 +137,7 @@ tss-esapi = { version = "7.4.0", optional = true } uuid = { version = "1.0", features = ["v4"], optional = true } veraison-apiclient = { git = "https://github.com/veraison/rust-apiclient", rev = "fe149cd", optional = true } # ccatoken = { git = "https://github.com/veraison/rust-ccatoken", rev = "dfe9ca2", optional = true } -ear = { version = "0.3.0", optional = true } +ear = { workspace = true, optional = true } x509-parser = { version = "0.14.0", optional = true } bitflags = { version = "2.8.0", features = ["serde"] } diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 88aef03..f5e3120 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -78,7 +78,7 @@ prost = { workspace = true, optional = true } rand = "0.8.5" regex = "1.11.1" regorus.workspace = true -reqwest = { workspace = true, features = ["json", "rustls-tls"] } +reqwest.workspace = true rsa = { version = "0.9.2", features = ["sha2"] } scc = "2" serde_qs.workspace = true diff --git a/rvds/Cargo.toml b/rvds/Cargo.toml index 3924a8b..e018732 100644 --- a/rvds/Cargo.toml +++ b/rvds/Cargo.toml @@ -13,7 +13,7 @@ env_logger = { workspace = true } futures = "0.3" log = { workspace = true } - reqwest = { workspace = true, features = ["json", "rustls-tls"] } + reqwest.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/rvps/Cargo.toml b/rvps/Cargo.toml index c006f58..9554009 100644 --- a/rvps/Cargo.toml +++ b/rvps/Cargo.toml @@ -38,7 +38,7 @@ hex = { version = "0.4", optional = true } log.workspace = true path-clean = { version = "1.0.1", optional = true } prost = { workspace = true, optional = true } -reqwest = { version = "0.11", features = ["blocking", "json"], optional = true } +reqwest = { workspace = true, features = ["blocking"], optional = true } rpm = { version = "0.16", optional = true } serde.workspace = true serde_json.workspace = true