From d6fc4921fb3d7ba0be259c7c506867d289d88c57 Mon Sep 17 00:00:00 2001 From: Marc Hartmayer Date: Wed, 24 Jun 2026 16:45:53 +0200 Subject: [PATCH] utils: Read hybrid HKDs * Add `HkdVersion` (classic or hybrid) and `HkdVersionSelection`. * Add `get_verified_hkds_new` that returns a list of verified HKDs read according to the given `HkdVersionSelection`. Co-Developed-by: Timo Keller Signed-off-by: Timo Keller Signed-off-by: Marc Hartmayer Reviewed-by: Steffen Eiden Signed-off-by: Steffen Eiden --- rust/pv/src/error.rs | 6 +++ rust/pv/src/lib.rs | 6 ++- rust/pv/src/utils.rs | 48 +++++++++++++++++ rust/utils/src/cli.rs | 120 ++++++++++++++++++++++++++++++++++++++++-- rust/utils/src/lib.rs | 4 +- 5 files changed, 176 insertions(+), 8 deletions(-) diff --git a/rust/pv/src/error.rs b/rust/pv/src/error.rs index df75c591..c6f7191e 100644 --- a/rust/pv/src/error.rs +++ b/rust/pv/src/error.rs @@ -84,6 +84,9 @@ pub enum Error { #[error("The provided host key document in {0} contains no certificate!")] NoHkdInFile(String), + #[error("The provided host key document in {0} contains the wrong number of keys!")] + WrongNumberOfKeys(String), + #[error("Invalid input size ({0}) for boot hdr")] InvBootHdrSize(usize), @@ -135,6 +138,9 @@ pub enum Error { #[error("Unsupported cipher: {:?}", .0.as_raw())] UnsupportedCipher(Nid), + + #[error("{}", .0)] + InvalidHkd(String), } // used in macros diff --git a/rust/pv/src/lib.rs b/rust/pv/src/lib.rs index 737753eb..98ab383d 100644 --- a/rust/pv/src/lib.rs +++ b/rust/pv/src/lib.rs @@ -79,7 +79,7 @@ pub mod pem { pub mod misc { pub use pv_core::misc::*; - pub use crate::utils::read_certs; + pub use crate::utils::{read_certs, read_hkd}; } pub use error::{Error, Result}; @@ -95,7 +95,9 @@ pub mod request { AeadDecryptionResult, AeadEncryptionResult, Aes256GcmKey, Aes256XtsKey, SymKey, SymKeyType, SHA_512_HASH_LEN, }; - pub use crate::req::{EcPubKeyCoord, Encrypt, HostKey, Keyslot, ReqEncrCtx, Request}; + pub use crate::req::{ + EcPubKeyCoord, Encrypt, HostKey, HybridPKey, Keyslot, ReqEncrCtx, Request, + }; pub use crate::verify::{CertVerifier, HkdVerifier, NoVerifyHkd}; /// Reexports some useful OpenSSL symbols diff --git a/rust/pv/src/utils.rs b/rust/pv/src/utils.rs index 3743c171..e857a1cd 100644 --- a/rust/pv/src/utils.rs +++ b/rust/pv/src/utils.rs @@ -1,9 +1,14 @@ +use std::path::Path; + // SPDX-License-Identifier: MIT // // Copyright IBM Corp. 2023 +use log::{error, info}; use openssl::error::ErrorStack; use openssl::x509::{X509Crl, X509}; +use pv_core::misc::read_file; +use crate::req::{HostKey, HybridPKey}; use crate::{Error, Result}; /// Read all CRLs from the buffer and parse them into a vector. @@ -31,6 +36,49 @@ pub fn read_certs>(buf: T) -> Result, ErrorStack> { .or_else(|_| X509::stack_from_pem(buf.as_ref())) } +/// Read a host-key document from a file. +/// +/// # Errors +/// +/// This function will return an error if: +/// - The file cannot be read +/// - The content is not valid PEM or DER format +/// - The file contains no certificates or more than 2 certificates +/// - The public key cannot be extracted from the certificate(s) +pub fn read_hkd>(path: P) -> Result { + let path = path.as_ref(); + let hk = read_file(path, "host-key document")?; + let certs = read_certs(&hk).map_err(|source| Error::HkdNotPemOrDer { + hkd: path.display().to_string(), + source, + })?; + if certs.is_empty() { + return Err(Error::NoHkdInFile(path.display().to_string())); + } + let c1 = certs.first().unwrap(); + match certs.len() { + 1 => { + info!("Using version 1 of the host-key document format"); + Ok(HostKey::V1(c1.public_key()?)) + } + 2 => { + info!("Using version 2 of the host-key document format"); + let c2 = &certs[1]; + Ok(HostKey::V2(HybridPKey::new( + c1.public_key()?, + c2.public_key()?, + )?)) + } + _ => { + error!( + "Invalid host-key document '{}': it contains more than two certificates, which is not supported by any host-key document format.", + path.display() + ); + Err(Error::WrongNumberOfKeys(path.display().to_string())) + } + } +} + #[cfg(test)] mod tests { use crate::test_utils::*; diff --git a/rust/utils/src/cli.rs b/rust/utils/src/cli.rs index defe5bbf..53d7b1cc 100644 --- a/rust/utils/src/cli.rs +++ b/rust/utils/src/cli.rs @@ -1,7 +1,7 @@ -use std::fmt::Display; // SPDX-License-Identifier: MIT // -// Copyright IBM Corp. 2023, 2024 +// Copyright IBM Corp. +use std::fmt::Display; use std::io::{Read, Write}; use std::marker::PhantomData; use std::path::{Path, PathBuf}; @@ -11,10 +11,12 @@ use std::str::FromStr; use clap::builder::{EnumValueParser, PossibleValue, TypedValueParser}; use clap::{Arg, ArgAction, ArgGroup, Args, Command, ValueEnum, ValueHint}; use log::{info, warn, LevelFilter}; +use openssl::Nid; use pv::misc::{create_file, open_file, read_certs, read_file}; -use pv::request::openssl::pkey::{PKey, Public}; -use pv::request::HkdVerifier; +use pv::request::openssl::pkey::{KeyType, PKey, PKeyRef, Public}; +use pv::request::{openssl, HkdVerifier, HostKey, HybridPKey}; use pv::{Error, Result}; +use utils_macros::{ValueEnumDisplay, ValueEnumFromStr}; /// Generic version selection for CLI #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -131,6 +133,17 @@ where } } +/// Host key document version for CLI +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, ValueEnumDisplay, ValueEnumFromStr)] +pub enum HkdVersion { + /// Version 1 - uses traditional cryptographic keys + Classical, + /// Version 2 - uses hybrid (post-quantum) cryptographic keys + Hybrid, +} + +pub type HkdVersionSelection = AutoOrExplicit; + /// CLI Argument collection for handling host-keys, IBM signing keys, and certificates. #[derive(Args, Debug, Clone, PartialEq, Eq, Default)] #[command( @@ -263,6 +276,105 @@ impl CertificateOptions { } Ok(res) } + + fn is_ec_p521_key(key: &PKeyRef) -> bool { + if key.id() == openssl::pkey::Id::EC { + let ec_key = key.ec_key().unwrap(); + let group = ec_key.group(); + let curve_nid = group.curve_name().unwrap(); + curve_nid == Nid::SECP521R1 + } else { + false + } + } + + fn is_mlkem1024_key(key: &PKeyRef) -> bool { + key.is_a(KeyType::ML_KEM_1024) + } + + /// Read the hybrid host-keys specified and verifies them if required + /// + /// - `protectee`: what you want to create. e.g. add-secret request or SE-image + /// - `version`: requested host-key document version + /// + /// # Error + /// Returns an error if something went wrong during parsing the HKDs, the verification chain + /// could not built, or when the verification + /// failed. + pub fn get_verified_hkds_new( + &self, + protectee: &'static str, + requested_version: HkdVersionSelection, + ) -> Result> { + let hkds = &self.host_key_documents; + let verifier = self.verifier(protectee)?; + + let mut res = Vec::with_capacity(hkds.len()); + for hkd in hkds { + let hk = read_file(hkd, "host-key document")?; + let certs = read_certs(&hk).map_err(|source| Error::HkdNotPemOrDer { + hkd: hkd.display().to_string(), + source, + })?; + if certs.is_empty() { + return Err(Error::NoHkdInFile(hkd.display().to_string())); + } + let required_cert_count = match requested_version { + HkdVersionSelection::Auto => { + info!("Auto-detecting version of the host-key document format"); + match certs.len() { + 1 => 1, + 2 => 2, + _ => { + warn!( + "The host-key document in '{}' contains more than two certificates!", + hkd.display() + ); + return Err(Error::WrongNumberOfKeys(hkd.display().to_string())); + } + } + } + HkdVersionSelection::Explicit(HkdVersion::Classical) => { + info!("Using version 1 of the host-key document format"); + 1 + } + HkdVersionSelection::Explicit(HkdVersion::Hybrid) => { + info!("Using version 2 of the host-key document format"); + 2 + } + }; + if required_cert_count != certs.len() { + return Err(Error::WrongNumberOfKeys(hkd.display().to_string())); + } + let c1 = certs.first().unwrap(); + if !Self::is_ec_p521_key(c1.public_key()?.as_ref()) { + return Err(Error::InvalidHkd( + "First key must be a EC-p521 key".to_string(), + )); + } + verifier.verify(c1)?; + match certs.len() { + 1 => { + res.push(HostKey::V1(c1.public_key()?)); + } + 2 => { + let c2 = &certs[1]; + if !Self::is_mlkem1024_key(c2.public_key()?.as_ref()) { + return Err(Error::InvalidHkd( + "Second key must be a ML-KEM 1024 key".to_string(), + )); + } + verifier.verify(c2)?; + res.push(HostKey::V2(HybridPKey::new( + c1.public_key()?, + c2.public_key()?, + )?)) + } + _ => unreachable!("Already checked"), + } + } + Ok(res) + } } /// stdout diff --git a/rust/utils/src/lib.rs b/rust/utils/src/lib.rs index e00d6d6c..b5113699 100644 --- a/rust/utils/src/lib.rs +++ b/rust/utils/src/lib.rs @@ -19,8 +19,8 @@ pub use utils_macros::{ControlFlag, ValueEnumDisplay, ValueEnumFromStr}; pub use crate::cli::{ combined_path_opt, combined_path_req, get_reader_from_cli_file_arg, get_writer_from_cli_file_arg, print_cli_error, print_error, AutoOrExplicit, - AutoOrExplicitParser, CertificateOptions, DeprecatedVerbosityOptions, VerbosityOptions, STDIN, - STDOUT, + AutoOrExplicitParser, CertificateOptions, DeprecatedVerbosityOptions, HkdVersion, + HkdVersionSelection, VerbosityOptions, STDIN, STDOUT, }; pub use crate::exit_code::{docstring, ExitCodeDoc, ExitCodeTrait, ExitCodeVariantDoc}; pub use crate::file::{AtomicFile, AtomicFileOperation};