From ec404f557736c32e691501183c50bb7553538f72 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 19:42:44 +0100 Subject: [PATCH 01/12] Import 'poc-kmip-crypto-impl' PR This copies the 'src/crypto/kmip.rs' file from the 'poc-kmip-crypto-impl' pull request on Domain: . The file has not been modified at all, to serve as a good base for reviewing changes. There are dozens of compilation failures that will be addressed in the following commits. --- src/lib.rs | 1671 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1671 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 35dd44a..69fb953 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,1674 @@ //! KMIP HSM signing support for [`domain`]. +use core::{fmt, str::FromStr}; + +use std::{ + string::{String, ToString}, + vec::Vec, +}; + +use bcder::{BitString, ConstOid, Oid, decode::SliceSource}; +use kmip::{ + client::pool::SyncConnPool, + types::{ + common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, + response::ManagedObject, + }, +}; +use tracing::{debug, error}; +use url::Url; + +use crate::{ + base::iana::SecurityAlgorithm, + crypto::{common::rsa_encode, sign::SignError}, + rdata::Dnskey, + utils::base16, +}; + +pub use kmip::client::{ClientCertificate, ConnectionSettings}; + pub use domain; + +//------------ Constants ----------------------------------------------------- + +/// [RFC 4055](https://tools.ietf.org/html/rfc4055) `rsaEncryption` +/// +/// Identifies an RSA public key with no limitation to either RSASSA-PSS or +/// RSAES-OEAP. +pub const RSA_ENCRYPTION_OID: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `ecPublicKey`. +/// +/// Identifies public keys for elliptic curve cryptography. +pub const EC_PUBLIC_KEY_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 2, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `secp256r1`. +/// +/// Identifies the P-256 curve for elliptic curve cryptography. +pub const SECP256R1_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 3, 1, 7]); + +//------------ KeyUrl -------------------------------------------------------- + +/// A URL that represents a key stored in a KMIP compatible HSM. +/// +/// The URL structure is: +/// +/// ```text +/// kmip:///keys/?algorithm=&flags= +/// ```` +/// +/// The algorithm and flags must be stored in the URL because they are DNSSEC +/// specific and not properties of the key itself and thus not known to or +/// stored by the HSM. +/// +/// While algorithm may seem to be something known to and stored by the HSM, +/// DNSSEC complicates that by aliasing multiple algorithm numbers to the +/// same cryptographic algorithm, and we need to know when using the key which +/// _DNSSEC_ algorithm number to use. +/// +/// The `server_id` could be the actual address of the target, but does not have +/// to be. There are multiple reasons for this: +/// +/// - In a highly available clustered deployment across multiple subnets +/// it could be that the clustered HSM is available to the clustered +/// application via different names/IP addresses in different subnets of +/// the deployment. Using an abstract server_id which is mapped via local +/// configuration in the subnet to the correct hostname/FQDN/IP address +/// for that subnet allows the correct target address to be determined at +/// the point of access. +/// - Using the actual hostname/FQDN/IP address may make it confusing for +/// an operator trying to understand where the key is actually stored. +/// This can happen for example if the product name for the HSM is say +/// Fortanix DSM, while the domain name used to access the HSM might be +/// eu.smartkey.io, which having no mention of the name Fortanix in the +/// FQDN is not immediately obvious that it has any relationship with +/// Fortanix. +/// - If the same HSM is used for different use cases via use of HSM +/// partitions, referring to the HSM by its address may not make it clear +/// which partition is being used, so using a more meaningful name like +/// 'testing' or such could make it clearer where the key is actually +/// being stored. +/// - Storing the username and password in the key URL will cause many +/// copies of those credentials to be stored, one per key, which is harder +/// to secure than if they are only in a single location and looked up on +/// actual access. +/// - Storing the username and password in the key URL would cause the URL +/// to become unusable if the credentials were rotated even though the +/// location at which the key is stored has not changed. +/// - Even if the FQDN, port number, username and password are all correct, +/// there may need to be more settings specified in order to connect to +/// the HSM some of which would not fit easily into a URL such as TLS +/// client certficate details and whether or not to require the server +/// TLS certificate to be valid (which can be inconvenient in test setups +/// using self-signed certificates). +/// +/// Thus an abstract `server_id` is stored in the key URL and it is the +/// responsibility of the user of the key URL to map the server id to the full +/// set of settings required to successfully connect to the HSM to make use of +/// the key. +pub struct KeyUrl { + /// The original URL from which this KeyUrl was parsed. + url: Url, + + /// The KMIP server ID. Produced by the application. + server_id: String, + + /// The KMIP key ID. Produced by the KMIP server. + key_id: String, + + /// The DNSSEC algorithm this key is to be used for. + algorithm: SecurityAlgorithm, + + /// The DNSSEC flags that apply to this key. + flags: u16, +} + +//--- Accessors + +impl KeyUrl { + /// The KMIP server ID. + pub fn server_id(&self) -> &str { + &self.server_id + } + + /// The KMIP key ID. + pub fn key_id(&self) -> &str { + &self.key_id + } + + /// The DNSSEC algorithm identifier for the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// The DNSSEC flags for the key. + pub fn flags(&self) -> u16 { + self.flags + } +} + +//--- impl Into + +// Disablow the Clippy lint as it is safe to go from a KeyURL to a URL but +// not vice-versa, so we implement Into but not From. +#[allow(clippy::from_over_into)] +impl Into for KeyUrl { + fn into(self) -> Url { + self.url + } +} + +//--- impl Deref + +impl std::ops::Deref for KeyUrl { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.url + } +} + +//--- Conversions + +impl TryFrom for KeyUrl { + type Error = String; + + fn try_from(url: Url) -> Result { + let server_id = url + .host_str() + .ok_or(format!("Key URL lacks hostname component: {url}"))? + .to_string(); + + let url_path = url.path().to_string(); + let key_id = url_path + .strip_prefix("/keys/") + .ok_or(format!("Key URL lacks /keys/ path component: {url}"))?; + + let key_id = key_id.to_string(); + let mut flags = None; + let mut algorithm = None; + for (k, v) in url.query_pairs() { + match &*k { + "flags" => { + flags = Some( + v.parse::() + .map_err(|err| format!("Key URL flags value is invalid: {err}"))?, + ) + } + "algorithm" => { + algorithm = Some( + SecurityAlgorithm::from_str(&v) + .map_err(|err| format!("Key URL algorithm value is invalid: {err}"))?, + ) + } + unknown => Err(format!( + "Key URL contains unknown query parameter: {unknown}" + ))?, + } + } + let algorithm = + algorithm.ok_or(format!("Key URL lacks algorithm query parameter: {url}"))?; + let flags = flags.ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + + Ok(Self { + url, + server_id, + key_id, + algorithm, + flags, + }) + } +} + +//--- impl Display + +impl std::fmt::Display for KeyUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.url.fmt(f) + } +} + +//------------ PublicKey ----------------------------------------------------- + +/// A public key retrieved from a KMIP server. +pub struct PublicKey { + /// The DNSSEC algorithm for use with this public key. + algorithm: SecurityAlgorithm, + + /// The public key octets. + public_key: Vec, +} + +impl PublicKey { + /// Create a public key from a key stored on a KMIP server. + /// + /// The public key details will be retrieved from the KMIP server. + /// + /// The DNSSEC algorithm is needed in order for [`Self::dnskey()`] to + /// generate a [`Dnskey`] and must match the cryptographic algorithm of + /// the key stored on the KMIP server. + /// + /// Note: This function will block while awaiting the response from the + /// KMIP server. + /// + /// If the KMIP operation fails an error or the response cannot be parsed + /// an error will be returned. + /// + /// If the cryptographic algorithm of the retrieved key does not match + /// the given DNSSEC algorithm an error will be returned. + pub fn for_key_id_and_dnssec_algorithm( + public_key_id: &str, + algorithm: SecurityAlgorithm, + conn_pool: SyncConnPool, + ) -> Result { + let public_key = Self::fetch_public_key(public_key_id, algorithm, &conn_pool)?; + + Ok(Self { + algorithm, + public_key, + }) + } + + /// Create a public key from a key stored on a KMIP server. + /// + /// This is a thin wrapper around + /// [`Self::for_key_id_and_dnssec_algorithm`]. + pub fn for_key_url( + public_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + Self::for_key_id_and_dnssec_algorithm( + public_key_url.key_id(), + public_key_url.algorithm(), + conn_pool, + ) + } + + /// The DNSSEC algorithm of the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// Generate a DNSKEY RR or this public key. + pub fn dnskey(&self, flags: u16) -> Dnskey> { + // SAFETY: The key came from a KMIP server and was validated to have + // the expected length when the KMIP server response was parsed by + // fetch_public_key(). + Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()).unwrap() + } +} + +impl PublicKey { + /// Query the KMIP server for the bytes of the specified public key. + /// + /// Verifies that the cryptographic algorithm of the key is compatible + /// with the specified DNSSEC algorithm. + fn fetch_public_key( + public_key_id: &str, + expected_algorithm: SecurityAlgorithm, + conn_pool: &SyncConnPool, + ) -> Result, PublicKeyError> { + // https://datatracker.ietf.org/doc/html/rfc5702#section-2 + // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource + // Records for DNSSEC + // + // 2. DNSKEY Resource Records + // "The format of the DNSKEY RR can be found in [RFC4034]. [RFC3110] + // describes the use of RSA/SHA-1 for DNSSEC signatures." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.4 + // Resource Records for the DNS Security Extensions + // 2. The DNSKEY Resource Record + // 2.1.4. The Public Key Field + // "The Public Key Field holds the public key material. The + // format depends on the algorithm of the key being stored and is + // described in separate documents." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc3110#section-2 + // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) + // 2. RSA Public KEY Resource Records + // "... The structure of the algorithm specific portion of the RDATA + // part of such RRs is as shown below. + // + // Field Size + // ----- ---- + // exponent length 1 or 3 octets (see text) + // exponent as specified by length field + // modulus remaining space + // + // For interoperability, the exponent and modulus are each limited to + // 4096 bits in length. The public key exponent is a variable length + // unsigned integer. Its length in octets is represented as one octet + // if it is in the range of 1 to 255 and by a zero octet followed by + // a two octet unsigned length if it is longer than 255 bytes. The + // public key modulus field is a multiprecision unsigned integer. The + // length of the modulus can be determined from the RDLENGTH and the + // preceding RDATA fields including the exponent. Leading zero octets + // are prohibited in the exponent and modulus. + + let client = conn_pool + .get() + .inspect_err(|err| error!("{err}")) + .map_err(|err| { + kmip::client::Error::ServerError(format!( + "Error while attempting to acquire KMIP connection from pool: {err}" + )) + })?; + + // Note: OpenDNSSEC queries the public key ID, _unless_ it was + // configured not the public key in the HSM (by setting CKA_TOKEN + // false) in which case there is no public key and so it uses the + // private key object handle instead. + let res = client + .get_key(public_key_id) + .inspect_err(|err| error!("{err}"))?; + let ManagedObject::PublicKey(public_key) = res.cryptographic_object else { + return Err(kmip::client::Error::DeserializeError(format!( + "Fetched KMIP object was expected to be a PublicKey but was instead: {}", + res.cryptographic_object + )))?; + }; + + // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 + // "“Raw” key format is intended to be applied to symmetric keys + // and not asymmetric keys" + // + // As we deal in asymmetric keys (RSA, ECDSA), not symmetric keys, + // we should not encounter public_key.key_block.key_format_type + // == KeyFormatType::Raw. However, Fortanix DSM returns + // KeyFormatType::Raw when fetching key data for an ECDSA public key. + + let octets = match public_key.key_block.key_value.key_material { + KeyMaterial::Bytes(bytes) => { + debug!( + "Cryptographic Algorithm: {:?}", + public_key.key_block.cryptographic_algorithm + ); + debug!( + "Cryptographic Length: {:?}", + public_key.key_block.cryptographic_length + ); + debug!( + "Key Format Type: {:?}", + public_key.key_block.key_format_type + ); + debug!( + "Key Compression Type: {:?}", + public_key.key_block.key_compression_type + ); + debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); + + match (expected_algorithm, public_key.key_block.key_format_type) { + (SecurityAlgorithm::RSASHA1, KeyFormatType::PKCS1) + | (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::PKCS1) + | (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) + | (SecurityAlgorithm::RSASHA512, KeyFormatType::PKCS1) => { + // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public + // key data like so: + // RSAPublicKey::=SEQUENCE{ + // modulus INTEGER, -- n + // publicExponent INTEGER -- e } + let source = SliceSource::new(&bytes); + let mut modulus = None; + let mut public_exponent = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + modulus = Some(bcder::Unsigned::take_from(cons)?); + public_exponent = Some(bcder::Unsigned::take_from(cons)?); + Ok(()) + }) + }) + .map_err(|err| { + kmip::client::Error::DeserializeError(format!( + "Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}" + )) + })?; + + let Some(modulus) = modulus else { + return Err(kmip::client::Error::DeserializeError( + "Unable to parse DER encoded PKCS#1 RSAPublicKey: missing modulus" + .into(), + ))?; + }; + + let Some(public_exponent) = public_exponent else { + return Err(kmip::client::Error::DeserializeError("Unable to parse DER encoded PKCS#1 RSAPublicKey: missing public exponent".into()))?; + }; + + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + crate::crypto::common::rsa_encode(e, n) + } + + (SecurityAlgorithm::RSASHA1, KeyFormatType::Raw) + | (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::Raw) + | (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) + | (SecurityAlgorithm::RSASHA512, KeyFormatType::Raw) => { + // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE (2 elem) + // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) + // parameter ANY NULL + // subjectPublicKey BIT STRING (2160 bit) 001100001000001000000001000010100000001010000010000000010000000100000… + // SEQUENCE (2 elem) + // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… + // INTEGER 65537 + let source = SliceSource::new(&bytes); + let mut modulus = None; + let mut public_exponent = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != RSA_ENCRYPTION_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); + } + // Ignore the parameters. + Ok(()) + })?; + cons.take_sequence(|cons| { + modulus = Some(bcder::Unsigned::take_from(cons)?); + public_exponent = Some(bcder::Unsigned::take_from(cons)?); + Ok(()) + }) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(modulus) = modulus else { + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing modulus".into()))?; + }; + + let Some(public_exponent) = public_exponent else { + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing public exponent".into()))?; + }; + + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + crate::crypto::common::rsa_encode(e, n) + } + + (SecurityAlgorithm::ECDSAP256SHA256, KeyFormatType::Raw) => { + // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + // + // From: https://www.rfc-editor.org/rfc/rfc5480.html#section-2.1.1 + // The parameter for id-ecPublicKey is as follows and MUST always be + // present: + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. + // -- Details for SpecifiedECDomain can be found in [X9.62]. + // -- Any future additions to this CHOICE should be coordinated + // -- with ANSI X9. + let source = SliceSource::new(&bytes); + let mut bits = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != EC_PUBLIC_KEY_OID { + Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) + } else { + let named_curve = Oid::take_from(cons)?; + if named_curve != SECP256R1_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); + } + Ok(()) + } + })?; + bits = Some(BitString::take_from(cons)?); + Ok(()) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(bits) = bits else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; + }; + + // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 + // "The subjectPublicKey from SubjectPublicKeyInfo + // is the ECC public key. ECC public keys have the + // following syntax: + // + // ECPoint ::= OCTET STRING + // ... + // The first octet of the OCTET STRING indicates + // whether the key is compressed or uncompressed. + // The uncompressed form is indicated by 0x04 and + // the compressed form is indicated by either 0x02 + // or 0x03 (see 2.3.3 in [SEC1]). The public key + // MUST be rejected if any other value is included + // in the first octet." + let Some(octets) = bits.octet_slice() else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; + }; + + // Expect octet string to be [, + // <32-byte X value>, <32-byte Y value>]. + if octets.len() != 65 { + return Err(kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", + base16::encode_display(octets), + octets.len() + )))?; + } + + // Note: OpenDNSSEC doesn't support the compressed + // form either. + let compression_flag = octets[0]; + if compression_flag != 0x04 { + return Err(kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}" + )))?; + } + + // Expect octet string to be X | Y (| denotes + // concatenation) where X and Y are each 32 bytes + // (because P-256 uses 256 bit values and 256 bits are + // 32 bytes). Skip the compression flag. + octets[1..].to_vec() + } + + (expected, key_format_type) => { + let alg = public_key + .key_block + .cryptographic_algorithm + .map(|a| a.to_string()) + .unwrap_or("unknown algorithm".to_string()); + let len = public_key + .key_block + .cryptographic_length + .map(|l| l.to_string()) + .unwrap_or("unknown length".to_string()); + let actual = format!("{alg} ({len}) as {key_format_type}"); + return Err(PublicKeyError::AlgorithmMismatch { expected, actual }); + } + } + } + + KeyMaterial::TransparentRSAPublicKey( + // Nameshed-HSM-Relay + TransparentRSAPublicKey { + modulus, + public_exponent, + }, + ) => rsa_encode(&public_exponent, &modulus), + + mat => { + return Err(kmip::client::Error::DeserializeError(format!( + "Fetched KMIP object has unsupported key material type: {mat}" + )))?; + } + }; + + Ok(octets) + } +} + +//============ sign ========================================================== + +#[cfg(feature = "unstable-crypto-sign")] +/// Submodule for private keys and signing. +pub mod sign { + use std::boxed::Box; + use std::string::{String, ToString}; + use std::time::SystemTime; + use std::vec::Vec; + + use kmip::client::pool::SyncConnPool; + use kmip::types::common::{ + CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, + DigitalSignatureAlgorithm, HashingAlgorithm, PaddingMethod, UniqueBatchItemID, + UniqueIdentifier, + }; + use kmip::types::request::{ + self, BatchItem, CommonTemplateAttribute, PrivateKeyTemplateAttribute, + PublicKeyTemplateAttribute, RequestPayload, + }; + use kmip::types::response::{CreateKeyPairResponsePayload, ResponsePayload}; + use log::trace; + use openssl::ecdsa::EcdsaSig; + use tracing::{debug, error}; + use url::Url; + use uuid::Uuid; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::common::DigestType; + use crate::crypto::kmip::{DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey}; + use crate::crypto::sign::{GenerateParams, SignError, SignRaw, Signature}; + use crate::rdata::Dnskey; + use crate::utils::base16; + + //----------- KeyPair ---------------------------------------------------- + + /// A reference to a key pair stored in an [OASIS KMIP] compliant HSM + /// server. + /// + /// Allows operations to be performed on and using the key pair. + /// + /// Operations common to key pairs irrespective of the underlying crypto + /// backend are offered via the [`SignRaw`] trait impl. + /// + /// Operations specifc to KMIP key pairs are offered via methods specific + /// to this type, e.g. batching support via [`Self::sign_raw_enqueue()`] + /// and [`Self::sign_raw_submit_queue()`]. + /// + /// See [`Self::from_metadata()`] and [`Self::from_urls()`] to construct + /// a [`KeyPair`] from individual public and private KMIP keys. + /// + /// To generate a KMIP key pair see [`generate()`]. + /// + /// To destroy individual KMIP keys see [`destroy()`]. + /// + /// [OASIS KMIP]: https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip + #[derive(Clone, Debug)] + pub struct KeyPair { + /// The algorithm used by the key. + algorithm: SecurityAlgorithm, + + /// The KMIP ID of the private key. + private_key_id: String, + + /// The KMIP ID of the public key. + public_key_id: String, + + /// The connection pool for connecting to the KMIP server. + // TODO: Should this be T that impl's a Connection trait, why should + // it know that it's a pool rather than a single connection? + conn_pool: SyncConnPool, + + /// Cached DNSKEY RR for the public key. + dnskey: Dnskey>, + + /// Flags from [`Dnskey`]. + flags: u16, + } + + //--- Constructors + + impl KeyPair { + /// Construct a reference to a KMIP HSM held key pair using key + /// metadata. + pub fn from_metadata( + algorithm: SecurityAlgorithm, + flags: u16, + private_key_id: &str, + public_key_id: &str, + conn_pool: SyncConnPool, + ) -> Result { + let dnskey = PublicKey::for_key_id_and_dnssec_algorithm( + public_key_id, + algorithm, + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))? + .dnskey(flags); + + Ok(Self { + algorithm, + private_key_id: private_key_id.to_string(), + public_key_id: public_key_id.to_string(), + conn_pool, + flags, + dnskey, + }) + } + + /// Construct a reference to a KMIP HSM held key pair using key URLs. + pub fn from_urls( + priv_key_url: KeyUrl, + pub_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + if priv_key_url.algorithm() != pub_key_url.algorithm() { + Err(GenerateError::Kmip(format!( + "Private and public key URLs have different algorithms: {} vs {}", + priv_key_url.algorithm(), + pub_key_url.algorithm() + ))) + } else if priv_key_url.flags() != pub_key_url.flags() { + Err(GenerateError::Kmip(format!( + "Private and public key URLs have different flags: {} vs {}", + priv_key_url.flags(), + pub_key_url.flags() + ))) + } else if priv_key_url.server_id() != pub_key_url.server_id() { + Err(GenerateError::Kmip(format!( + "Private and public key URLs have different server IDs: {} vs {}", + priv_key_url.server_id(), + pub_key_url.server_id() + ))) + } else if priv_key_url.server_id() != conn_pool.server_id() { + Err(GenerateError::Kmip(format!( + "Key URLs have different server ID to the KMIP connection pool: {} vs {}", + priv_key_url.server_id(), + conn_pool.server_id() + ))) + } else { + Self::from_metadata( + priv_key_url.algorithm(), + priv_key_url.flags(), + priv_key_url.key_id(), + pub_key_url.key_id(), + conn_pool, + ) + } + } + } + + //--- Accessors + + impl KeyPair { + /// Get the KMIP HSM ID for the private half of this key pair. + pub fn private_key_id(&self) -> &str { + &self.private_key_id + } + + /// Get the KMIP HSM ID for the public half of this key pair. + pub fn public_key_id(&self) -> &str { + &self.public_key_id + } + + /// Get a KMIP URL for the private half of this key pair. + pub fn private_key_url(&self) -> Url { + // + self.mk_key_url(&self.private_key_id).unwrap() + } + + /// Get a KMIP URL for the public half of this key pair. + pub fn public_key_url(&self) -> Url { + self.mk_key_url(&self.public_key_id).unwrap() + } + + /// Get a reference to the KMIP HSM connection pool for this key pair. + pub fn conn_pool(&self) -> &SyncConnPool { + &self.conn_pool + } + } + + //--- Operations + + impl KeyPair { + /// Enqueue a KMIP signing operation using this key pair on the given + /// data. + /// + /// Like [`SignRaw::sign_raw()`] but deferred until + /// [`KeyPair::sign_raw_submit_queue()`] is called. + pub fn sign_raw_enqueue( + &self, + queue: &mut SignQueue, + data: &[u8], + ) -> Result, SignError> { + let request = self.sign_pre(data)?; + let operation = request.operation(); + let batch_item_id = UniqueBatchItemID(Uuid::new_v4().into_bytes().to_vec()); + let batch_item = BatchItem(operation, Some(batch_item_id), request); + queue.0.push(batch_item); + Ok(None) + } + + /// Submit the given signing queue as a batch to the KMIP HSM. + // + // TODO: Should the queue store the KMIP connection pool reference and + // should submit() be a method on the queue? + // TODO: What happens if the same queue is used with + // sign_raw_enqueue() but with keys that are held by different KMIP + // HSMs and thus have different KMIP connection pools? + pub fn sign_raw_submit_queue( + &self, + queue: &mut SignQueue, + ) -> Result, SignError> { + // Execute the request and capture the response. + let client = self + .conn_pool + .get() + .map_err(|err| format!("Error while obtaining KMIP pool connection: {err}"))?; + + // Drain the queue. + let q_size = queue.0.capacity(); + let mut empty = Vec::with_capacity(q_size); + std::mem::swap(&mut queue.0, &mut empty); + let queue = empty; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client + .do_requests(queue) + .map_err(|err| format!("Error while sending KMIP request: {err}"))?; + + let mut sigs = Vec::with_capacity(q_size); + for res in res { + let res = res?; + let sig = self.sign_post(res.payload.unwrap())?; + sigs.push(sig); + } + + Ok(sigs) + } + } + + //--- Internal details + + impl KeyPair { + /// Make a KMIP URL for this key using the given KMIP ID. + fn mk_key_url(&self, key_id: &str) -> Result { + // We have to store the algorithm in the URL because the DNSSEC + // algorithm (e.g. 5 and 7) don't necessarily correspond to the + // cryptographic algorithm of the key known to the HSM. And we + // have to store the flags in the URL because these are not known + // to the HSM, they say someting about the use to which the key + // will be put of which the HSM is unaware. + let url = format!( + "kmip://{}/keys/{}?algorithm={}&flags={}", + self.conn_pool.server_id(), + key_id, + self.algorithm, + self.flags + ); + + let url = Url::parse(&url) + .map_err(|err| KeyUrlParseError(format!("unable to parse {url} as URL: {err}")))?; + + Ok(url) + } + + /// Prepare a KMIP signing operation request to sign the given data + /// using this key pair. + fn sign_pre(&self, data: &[u8]) -> Result { + let (crypto_alg, hashing_alg, _digest_type) = match self.algorithm { + SecurityAlgorithm::RSASHA256 => ( + CryptographicAlgorithm::RSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + SecurityAlgorithm::ECDSAP256SHA256 => ( + CryptographicAlgorithm::ECDSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + alg => { + return Err(format!("Algorithm not supported for KMIP signing: {alg}").into()); + } + }; + let mut cryptographic_parameters = CryptographicParameters::default() + .with_hashing_algorithm(hashing_alg) + .with_cryptographic_algorithm(crypto_alg); + if self.algorithm == SecurityAlgorithm::RSASHA256 { + cryptographic_parameters = + cryptographic_parameters.with_padding_method(PaddingMethod::PKCS1_v1_5); + } + let request = RequestPayload::Sign( + Some(UniqueIdentifier(self.private_key_id.clone())), + Some(cryptographic_parameters), + Data(data.as_ref().to_vec()), + ); + Ok(request) + } + + /// Process a KMIP HSM signing operation response for this key pair. + fn sign_post(&self, res: ResponsePayload) -> Result { + tracing::trace!("Checking sign payload"); + let ResponsePayload::Sign(signed) = res else { + unreachable!(); + }; + + trace!( + "Algorithm: {}, Signature Data: {}", + self.algorithm, + base16::encode_display(&signed.signature_data) + ); + match (self.algorithm, signed.signature_data.len()) { + (SecurityAlgorithm::RSASHA256, _) => Ok(Signature::RsaSha256( + signed.signature_data.into_boxed_slice(), + )), + + (SecurityAlgorithm::ECDSAP256SHA256, _) => { + // ECDSA signature received from Fortanix DSM, decoded + // using this command: + // + // $ echo '' | xxd -r -p | dumpasn1 - + // 0 69: SEQUENCE { + // 2 33: INTEGER + // : 00 C6 A7 D1 2E A1 0C B4 96 BD D9 A5 48 2C 9B F4 + // : 0C EC 9F FC EF 1A 0D 59 BB B9 24 F3 FE DA DC F8 + // : 9E + // 37 32: INTEGER + // : 4B A7 22 69 F2 F8 65 88 63 D0 25 D3 A9 D5 92 4F + // : A2 21 BD 59 CD 27 60 6D 16 C3 79 EF B4 0A CA 33 + // : } + // + // Where the two integer values are known as 'r' and 's'. + let signature = EcdsaSig::from_der(&signed.signature_data).unwrap(); + let mut r = signature.r().to_vec_padded(32).unwrap(); + let mut s = signature.s().to_vec_padded(32).unwrap(); + r.append(&mut s); + Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( + r.try_into().unwrap(), + ))) + } + + // TODO + //(SecurityAlgorithm::ECDSAP384SHA384, 96) => {}, + //(SecurityAlgorithm::ED25519, 64) => {}, + //(SecurityAlgorithm::ED448, 114) => {}, + (alg, sig_len) => Err(format!( + "KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", + base16::encode_display(&signed.signature_data) + ))?, + } + } + } + + //----------- SignQueue -------------------------------------------------- + + /// A queue of KMIP signing operations pending batch submission. + #[derive(Debug, Default)] + pub struct SignQueue(Vec); + + impl SignQueue { + /// Constructs a new empty signing queue. + pub fn new() -> Self { + Self(vec![]) + } + } + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn flags(&self) -> u16 { + self.flags + } + + fn dnskey(&self) -> Dnskey> { + self.dnskey.clone() + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let request = self.sign_pre(data)?; + + // Execute the request and capture the response. + let client = self + .conn_pool + .get() + .map_err(|err| format!("Error while obtaining KMIP pool connection: {err}"))?; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client + .do_request(request) + .map_err(|err| format!("Error while sending KMIP request: {err}"))?; + + self.sign_post(res) + } + } + + //----------- generate() ------------------------------------------------- + + /// Generate a new key pair for a given algorithm using a specified HSM. + pub fn generate( + public_key_name: String, + private_key_name: String, + params: GenerateParams, + // TODO: Is this enough? Or do we need to take SecurityAlgorithm as + // input instead of GenerateParams to ensure we don't lose distinctions + // like 5 vs 7 which are both RSASHA1? + flags: u16, + conn_pool: SyncConnPool, + ) -> Result { + let algorithm = params.algorithm(); + + let client = conn_pool.get().map_err(|err| { + GenerateError::Kmip(format!( + "Key generation failed: Cannot connect to KMIP server {}: {err}", + conn_pool.server_id() + )) + })?; + + // TODO: Determine this on first use of the HSM? + // PyKMIP doesn't support ActivationDate. + // Fortanix DSM does support it and creates the key in an activated + // state but still returns a (harmless?) error: + // Server error: Operation CreateKeyPair failed: Input field `state` + // is not coherent with provided activation/deactivation dates + let activate_on_create = false; + + let use_cryptographic_params = false; + + // Note: Strictly speaking KMIP requires that each key, including + // public and private "halves" of the same key "pair", have a unique + // name within the HSM namespace. We don't enforce that here, e.g. + // maybe you know that your backend is actually a KMIP to PKCS#11 + // gateway and PKCS#11 doesn't have the same restriction and you + // want keys to be named as you are used to with your PKCS#11 HSM. We + // also don't intefere with names by making them unique as that would + // change any max name length calculations performed by the caller + // to avoid known issues with backend name limitations for their + // particular HSM (the PKCS#11 and KMIP specifications are silent on + // name limits but implementations definitely have limits, and not all + // the same). + + let mut common_attrs = vec![]; + let priv_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private + // key. + request::Attribute::Name(private_key_name), + request::Attribute::CryptographicUsageMask(CryptographicUsageMask::Sign), + ]; + let pub_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private + // key. + request::Attribute::Name(public_key_name), + // Krill does verification, do we need to? ODS doesn't. + // Note: PyKMIP requires a Cryptographic Usage Mask for the public + // key. + request::Attribute::CryptographicUsageMask(CryptographicUsageMask::Verify), + ]; + + // PyKMIP doesn't support CryptographicParameters so we cannot supply + // HashingAlgorithm. It also doesn't support the Hash operation. + // How do we specify SHA256 hashing? Do we have to do it ourselves + // post-signing? Can we just specify the hashing to do when invoking + // the Sign operation? + // Fortanix DSM also doesn't support Cryptographic Parameters: + // Server error: Operation CreateKeyPair failed: Don't have handling + // for attribute Cryptographic Parameters + + // PyKMIP doesn't support Attribute::ActivationDate. For HSMs that + // don't support it we have to do a separate Activate operation after + // creating the key pair. + // Fortanix DSM does support ActivationDate. + + match params { + GenerateParams::RsaSha256 { bits } => { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For RSA, Cryptographic Length corresponds to the bit + // length of the Modulus" + + // https://www.rfc-editor.org/rfc/rfc5702.html#section-2.1 + // 2.1. RSA/SHA-256 DNSKEY Resource Records + // "For interoperability, as in [RFC3110], the key size of + // RSA/SHA-256 keys MUST NOT be less than 512 bits and MUST + // NOT be more than 4096 bits." + if !(512..=4096).contains(&bits) { + return Err(GenerateError::UnsupportedAlgorithm); + } + + if use_cryptographic_params { + common_attrs.push(request::Attribute::CryptographicParameters( + CryptographicParameters::default().with_digital_signature_algorithm( + DigitalSignatureAlgorithm::SHA256WithRSAEncryption_PKCS1_v1_5, + ), + )) + } else { + common_attrs.push(request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::RSA, + )); + common_attrs.push(request::Attribute::CryptographicLength( + bits.try_into().unwrap(), + )); + } + } + GenerateParams::RsaSha512 { .. } => { + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::EcdsaP256Sha256 => { + // PyKMIP doesn't support ECDSA: + // "Operation CreateKeyPair failed: The cryptographic + // algorithm (CryptographicAlgorithm.ECDSA) is not a + // supported asymmetric key algorithm." + + if use_cryptographic_params { + common_attrs.push(request::Attribute::CryptographicParameters( + CryptographicParameters::default().with_digital_signature_algorithm( + DigitalSignatureAlgorithm::ECDSAWithSHA256, + ), + )) + } else { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For ECDSA, ECDH, and ECMQV algorithms, Cryptographic + // Length corresponds to the bit length of parameter + // Q." + common_attrs.push(request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::ECDSA, + )); + // ODS doesn't tell PKCS#11 a Q length. I have no idea + // what value we should put here, but as Q length is + // optional let's try not passing it. + // Note: PyKMIP requires a length: use 256 from P-256? + // Note: Fortanix also requires a length and gives error + // "missing required field `elliptic_curve` in request + // body" if cryptographic length is not specified, and + // a value of 256 works fine while a value of 255 causes + // error "Unsupported length for ECC key". When using 256 + // the Fortanix UI shows the key as type EC with curve + // NistP256 so that seems good. + common_attrs.push(request::Attribute::CryptographicLength(256)); + } + } + GenerateParams::EcdsaP384Sha384 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::Ed25519 => { + // RFC 8624 3.1 DNSSEC Signing: RECOMMENDED + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::Ed448 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + }; + + if activate_on_create { + // https://docs.oasis-open.org/kmip/testcases/v1.1/kmip-testcases-v1.1.html + // shows an example including an Activation Date value of 2 noted + // as meaning Thu Jan 01 01:00:02 CET 1970. i.e. the activation + // date should be a UNIX epoch timestamp. + let time_now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + common_attrs.push(request::Attribute::ActivationDate(time_now)); + } + + let request = RequestPayload::CreateKeyPair( + Some(CommonTemplateAttribute::new(common_attrs)), + Some(PrivateKeyTemplateAttribute::new(priv_key_attrs)), + Some(PublicKeyTemplateAttribute::new(pub_key_attrs)), + ); + + // Execute the request and capture the response + let response = client.do_request(request).map_err(|err| { + error!("KMIP request failed: {err}"); + debug!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + debug!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + tracing::trace!("Key generation operation complete"); + + // Drop the KMIP client so that it will be returned to the pool and + // thus be available below when KeyPair::new() is invoked and tries to + // fetch the details needed to determine the DNSKEY RR. + drop(client); + + // Process the successful response + let ResponsePayload::CreateKeyPair(payload) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip( + "Unable to parse KMIP response: payload should be CreateKeyPair".to_string(), + )); + }; + + let CreateKeyPairResponsePayload { + private_key_unique_identifier, + public_key_unique_identifier, + } = payload; + + tracing::trace!("Creating KeyPair with DNSKEY"); + + let key_pair = KeyPair::from_metadata( + algorithm, + flags, + private_key_unique_identifier.as_str(), + public_key_unique_identifier.as_str(), + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))?; + + // Activate the key if not already, otherwise it cannot be used for + // signing. + if !activate_on_create { + let client = conn_pool.get().map_err(|err| { + GenerateError::Kmip(format!( + "Key generation failed: Cannot connect to KMIP server {}: {err}", + conn_pool.server_id() + )) + })?; + let request = RequestPayload::Activate(Some(private_key_unique_identifier)); + + // Execute the request and capture the response + tracing::trace!("Activating KMIP key..."); + let response = client.do_request(request).map_err(|err| { + eprintln!("KMIP activate private key request failed: {err}"); + eprintln!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + eprintln!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + tracing::trace!("Activate operation complete"); + + // Process the successful response + let ResponsePayload::Activate(_) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip( + "Unable to parse KMIP response: payload should be Activate".to_string(), + )); + }; + } + + Ok(key_pair) + } + + //----------- destroy() -------------------------------------------------- + + /// Destroy a KMIP key by ID using a given KMIP server connection pool. + /// + /// As a KMIP key cannot be destroyed if it is active, this function first + /// attempts to revoke the key and then destroy it. + pub fn destroy(key_id: &str, conn_pool: SyncConnPool) -> Result<(), DestroyError> { + let client = conn_pool.get().map_err(|err| { + DestroyError::Kmip(format!( + "Key destruction failed: Cannot connect to KMIP server {}: {err}", + conn_pool.server_id() + )) + })?; + + client + .revoke_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string()))?; + client + .destroy_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string())) + } +} + +//============ Error Types =================================================== + +//--- Conversion + +impl From for SignError { + fn from(err: kmip::client::Error) -> Self { + err.to_string().into() + } +} + +//----------- GenerateError -------------------------------------------------- + +/// An error occurred while generating a key pair with a KMIP server. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm is not supported. + UnsupportedAlgorithm, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedAlgorithm => { + write!(f, "algorithm not supported") + } + Self::Kmip(err) => { + write!( + f, + "a problem occurred while communicating with the KMIP server: {err}" + ) + } + } + } +} + +//--- impl Error + +impl std::error::Error for GenerateError {} + +//------------ DestroyError -------------------------------------------------- + +/// An error occurred while destroying a key using KMIP. +#[derive(Clone, Debug)] +pub enum DestroyError { + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for DestroyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kmip(err) => { + write!( + f, + "a problem occurred while communicating with the KMIP server: {err}" + ) + } + } + } +} + +//--- Error + +impl std::error::Error for DestroyError {} + +//------------ KeyUrlError --------------------------------------------------- + +/// An error occurred while parsing a KMIP key URL. +#[derive(Clone, Debug)] +pub struct KeyUrlParseError(String); + +//--- Formatting + +impl fmt::Display for KeyUrlParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid key URL: {}", self.0) + } +} + +//--- impl Error + +impl std::error::Error for KeyUrlParseError {} + +//--- Conversions + +impl From for KeyUrlParseError { + fn from(err: String) -> Self { + KeyUrlParseError(err) + } +} + +//------------ PublicKeyError ------------------------------------------------ + +/// An error occurred while retrieving a KMIP public key. +#[derive(Clone, Debug)] +pub enum PublicKeyError { + /// The cryptographic algorithm of the KMIP key does not match the + /// specified DNSSEC algorithm. + AlgorithmMismatch { + /// The DNSSEC algorithm that was expected. + expected: SecurityAlgorithm, + + /// The type of key data received from the KMIP server. + actual: String, + }, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for PublicKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AlgorithmMismatch { expected, actual } => { + write!( + f, + "algorithm mismatch: expected {expected} but found {actual}" + ) + } + Self::Kmip(err) => { + write!( + f, + "a problem occurred while communicating with the KMIP server: {err}" + ) + } + } + } +} + +//--- impl Error + +impl std::error::Error for PublicKeyError {} + +//--- Conversions + +impl From for PublicKeyError { + fn from(err: kmip::client::Error) -> Self { + PublicKeyError::Kmip(err.to_string()) + } +} + +//============ Testing ======================================================= + +#[cfg(test)] +mod tests { + use core::time::Duration; + + use std::fs::File; + use std::io::{BufReader, Read}; + use std::string::ToString; + use std::time::SystemTime; + use std::vec::Vec; + + use kmip::client::ConnectionSettings; + use kmip::client::pool::ConnectionManager; + + use crate::crypto::kmip::sign::generate; + use crate::crypto::sign::SignRaw; + use crate::logging::init_logging; + + #[test] + #[ignore = "Requires running PyKMIP"] + fn pykmip_connect() { + init_logging(); + let mut cert_bytes = Vec::new(); + let file = File::open("/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.crt").unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut cert_bytes).unwrap(); + + let mut key_bytes = Vec::new(); + let file = File::open("/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.key").unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut key_bytes).unwrap(); + + let conn_settings = ConnectionSettings { + host: "localhost".to_string(), + port: 5696, + insecure: true, + client_cert: Some(kmip::client::ClientCertificate::SeparatePem { + cert_bytes, + key_bytes, + }), + ..Default::default() + }; + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), + conn_settings.into(), + 16384, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + let res = client.query(); + dbg!(&res); + res.unwrap(); + + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + pub_key_name, + pri_key_name, + crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + dbg!(&res); + let key = res.unwrap(); + + eprintln!("DNSKEY: {}", key.dnskey()); + } + + #[test] + #[ignore = "Requires Fortanix credentials"] + fn fortanix_dsm_test() { + // Note: keyls fails against Fortanix DSM for some reason with error: + // Error: Server error: Operation Locate failed: expected + // AttributeValue, got ObjectType, Diagnostics: + // req: 78[77[69[6Ai6Bi]0C[23[24e1:25[99tA1t]]]0Di]0F[5Ce8:79[08[0At57e4:]]]], + // resp: 7B[7A[69[6Ai6Bi]92d0Di]0F[5Ce8:7Fe1:7Ee100:7Dt]] + + init_logging(); + + // conn_settings.host = "eu.smartkey.io".to_string(); + // conn_settings.port = 5696; + // conn_settings.username = Some(env!("FORTANIX_USER").to_string()); + // conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); + + let conn_settings = ConnectionSettings { + host: "127.0.0.1".to_string(), + port: 5696, + insecure: true, + connect_timeout: Some(Duration::from_secs(3)), + read_timeout: Some(Duration::from_secs(30)), + write_timeout: Some(Duration::from_secs(3)), + ..Default::default() + }; + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), + conn_settings.into(), + 16384, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + // let res = client.query(); + // dbg!(&res); + // res.unwrap(); + + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + pub_key_name, + pri_key_name, + crate::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + let key = res.unwrap(); + eprintln!("Generated public key with id: {}", key.public_key_id()); + eprintln!("Generated private key with id: {}", key.private_key_id()); + + // sleep(Duration::from_secs(5)); + + eprintln!("DNSKEY: {}", key.dnskey()); + + client.activate_key(key.public_key_id()).unwrap(); + + // Fortanix: Activating the public key also activates the private key. + // Attempting to then activate the private key fails as it is already + // active. Yet signing fails with "Object is not yet active"... + // client.activate_key(key.private_key_id()).unwrap(); + + // // This works round the not yet active yet error. + // sleep(Duration::from_secs(5)); + + // let request = RequestPayload::Sign( + // Some(UniqueIdentifier(key.private_key_id().to_string())), + // // While the KMIP 1.2 spec says crypto parameters are optional and + // // if not specified those of the key will be used, Fortanix + // // complains about "No cryptographic parameters specified" if this + // // is None, and "Must specicify HashingAlgorithm" if that is not + // // specified. + // Some( + // CryptographicParameters::default() + // // .with_padding_method(PaddingMethod::) + // .with_hashing_algorithm(HashingAlgorithm::SHA256) + // .with_cryptographic_algorithm( + // CryptographicAlgorithm::RSA, + // //CryptographicAlgorithm::ECDSA, + // ), + // ), + // Data("Message for ECDSA signing".as_bytes().to_vec()), + // ); + + // // Execute the request and capture the response + // let res = client.do_request(request).unwrap(); + + // dbg!(&res); + + // let ResponsePayload::Sign(signed) = res else { + // unreachable!(); + // }; + + // // let signature = + // // openssl::ecdsa::EcdsaSig::from_der(&signed.signature_data) + // // .unwrap(); + + // // dbg!(signature.r().to_vec_padded(32)); + // // dbg!(signature.s().to_vec_padded(32)); + + // // dbg!(response); + } +} From f4fd76673107c51bfc1497abc15890e1277886f7 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 19:46:51 +0100 Subject: [PATCH 02/12] Resolve imports and add temporary dependencies Some of the dependencies, such as 'bcder', could be replaced with simple manual implementations. Importantly, the 'openssl' dependency has not been resolved; it will be replaced with use of 'bcder'. --- Cargo.lock | 713 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 44 ++++ src/lib.rs | 55 +++-- 3 files changed, 789 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04da1b6..f43ba6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,37 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -26,6 +51,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -41,6 +76,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "domain" version = "0.11.1" @@ -52,6 +98,8 @@ dependencies = [ "hashbrown", "octseq", "rand", + "ring", + "secrecy", "time", ] @@ -59,8 +107,13 @@ dependencies = [ name = "domain-kmip" version = "0.0.1" dependencies = [ + "bcder", "domain", "kmip-protocol", + "tracing", + "tracing-subscriber", + "url", + "uuid", ] [[package]] @@ -95,6 +148,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -106,6 +174,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -121,6 +201,118 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "kmip-protocol" version = "0.5.0" @@ -135,11 +327,14 @@ dependencies = [ "maybe-async", "r2d2", "rustc_version", + "rustls", + "rustls-pemfile", "serde", "serde_bytes", "serde_derive", "tracing", "trait-set", + "webpki-roots", ] [[package]] @@ -156,12 +351,24 @@ dependencies = [ "trait-set", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -177,6 +384,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "maybe-async" version = "0.2.10" @@ -188,6 +404,21 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -232,12 +463,27 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -271,6 +517,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r2d2" version = "0.8.10" @@ -309,7 +561,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -321,6 +573,37 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -330,6 +613,56 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -345,6 +678,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.27" @@ -358,6 +700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -390,12 +733,39 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -418,6 +788,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -437,6 +827,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tracing" version = "0.1.41" @@ -467,6 +867,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -486,18 +916,239 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.30" @@ -517,3 +1168,63 @@ dependencies = [ "quote", "syn 2.0.111", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] diff --git a/Cargo.toml b/Cargo.toml index 62e6d63..ad1921d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ repository = "https://github.com/nlnetlabs/domain-kmip/" # TODO: Convert to a regular 'version' spec. git = "https://github.com/NLnetLabs/domain.git" branch = "main" +# TODO: Find a way to enable the 'crypto' module without Ring or OpenSSL. +features = ["ring", "unstable-crypto-sign"] # This crate needs some way to interact with KMIP servers. This requires # connecting to them using an appropriate transport protocol and understanding @@ -35,3 +37,45 @@ package = "kmip-protocol" # TODO: Convert to a regular 'version' spec. git = "https://github.com/NLnetLabs/kmip-protocol.git" branch = "next" +# TODO: Support asynchronous operation? +features = ["tls-with-rustls"] + +# KMIP specifies the format for cryptographic data, often relying on DER +# based formats. DNS sometimes uses different formats, thereby requiring the +# re-encoding of these signatures. +# +# - 'bcder' provides a simple DER encoder and decoder. It is maintained by us +# (NLnet Labs). +# +# TODO: Perhaps try hard-coding the relevant DER decoding? +# TODO: Should 'kmip-protocol' provide this decoding support for us? +[dependencies.bcder] +version = "0.7.0" + +# This crate expresses KMIP key information as a URL for simplicity. It needs +# some way to conformantly serialize and deserialize URLs. +# +# - 'url' is a popular Rust library for handling URLs. +# +# TODO: Maintenance status? +[dependencies.url] +version = "2.5.4" + +# TODO: Does this crate really need tracing? Or is it an artefact of the +# need for quick debugging? +[dependencies.tracing] +version = "0.1.40" + +# TODO: Rely on 'kmip-protocol' to help us generate unique batch item IDs. +# Each KMIP connection could hold a 64-bit counter to assure uniqueness. +[dependencies.uuid] +version = "1.18.0" +features = ["v4"] + + +[dev-dependencies] + +# TODO: See '[dependencies.tracing]'. +[dev-dependencies.tracing-subscriber] +version = "0.3.19" +features = ["env-filter"] diff --git a/src/lib.rs b/src/lib.rs index 69fb953..ec378fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ use kmip::{ use tracing::{debug, error}; use url::Url; -use crate::{ +use domain::{ base::iana::SecurityAlgorithm, crypto::{common::rsa_encode, sign::SignError}, rdata::Dnskey, @@ -442,7 +442,7 @@ impl PublicKey { let n = modulus.as_slice(); let e = public_exponent.as_slice(); - crate::crypto::common::rsa_encode(e, n) + domain::crypto::common::rsa_encode(e, n) } (SecurityAlgorithm::RSASHA1, KeyFormatType::Raw) @@ -490,7 +490,7 @@ impl PublicKey { let n = modulus.as_slice(); let e = public_exponent.as_slice(); - crate::crypto::common::rsa_encode(e, n) + domain::crypto::common::rsa_encode(e, n) } (SecurityAlgorithm::ECDSAP256SHA256, KeyFormatType::Raw) => { @@ -622,7 +622,6 @@ impl PublicKey { //============ sign ========================================================== -#[cfg(feature = "unstable-crypto-sign")] /// Submodule for private keys and signing. pub mod sign { use std::boxed::Box; @@ -641,18 +640,18 @@ pub mod sign { PublicKeyTemplateAttribute, RequestPayload, }; use kmip::types::response::{CreateKeyPairResponsePayload, ResponsePayload}; - use log::trace; use openssl::ecdsa::EcdsaSig; - use tracing::{debug, error}; + use tracing::{debug, error, trace}; use url::Url; use uuid::Uuid; - use crate::base::iana::SecurityAlgorithm; - use crate::crypto::common::DigestType; - use crate::crypto::kmip::{DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey}; - use crate::crypto::sign::{GenerateParams, SignError, SignRaw, Signature}; - use crate::rdata::Dnskey; - use crate::utils::base16; + use domain::base::iana::SecurityAlgorithm; + use domain::crypto::common::DigestType; + use domain::crypto::sign::{GenerateParams, SignError, SignRaw, Signature}; + use domain::rdata::Dnskey; + use domain::utils::base16; + + use super::{DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey}; //----------- KeyPair ---------------------------------------------------- @@ -923,7 +922,7 @@ pub mod sign { /// Process a KMIP HSM signing operation response for this key pair. fn sign_post(&self, res: ResponsePayload) -> Result { - tracing::trace!("Checking sign payload"); + trace!("Checking sign payload"); let ResponsePayload::Sign(signed) = res else { unreachable!(); }; @@ -1217,7 +1216,7 @@ pub mod sign { ); GenerateError::Kmip(err.to_string()) })?; - tracing::trace!("Key generation operation complete"); + trace!("Key generation operation complete"); // Drop the KMIP client so that it will be returned to the pool and // thus be available below when KeyPair::new() is invoked and tries to @@ -1237,7 +1236,7 @@ pub mod sign { public_key_unique_identifier, } = payload; - tracing::trace!("Creating KeyPair with DNSKEY"); + trace!("Creating KeyPair with DNSKEY"); let key_pair = KeyPair::from_metadata( algorithm, @@ -1260,7 +1259,7 @@ pub mod sign { let request = RequestPayload::Activate(Some(private_key_unique_identifier)); // Execute the request and capture the response - tracing::trace!("Activating KMIP key..."); + trace!("Activating KMIP key..."); let response = client.do_request(request).map_err(|err| { eprintln!("KMIP activate private key request failed: {err}"); eprintln!( @@ -1273,7 +1272,7 @@ pub mod sign { ); GenerateError::Kmip(err.to_string()) })?; - tracing::trace!("Activate operation complete"); + trace!("Activate operation complete"); // Process the successful response let ResponsePayload::Activate(_) = response else { @@ -1475,9 +1474,21 @@ mod tests { use kmip::client::ConnectionSettings; use kmip::client::pool::ConnectionManager; - use crate::crypto::kmip::sign::generate; - use crate::crypto::sign::SignRaw; - use crate::logging::init_logging; + use domain::crypto::sign::SignRaw; + + use super::sign::generate; + + fn init_logging() { + use tracing_subscriber::EnvFilter; + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_thread_ids(true) + .without_time() + // Useful sometimes: + // .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NEW) + .init(); + } #[test] #[ignore = "Requires running PyKMIP"] @@ -1539,7 +1550,7 @@ mod tests { let res = generate( pub_key_name, pri_key_name, - crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + domain::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, pool, @@ -1611,7 +1622,7 @@ mod tests { let res = generate( pub_key_name, pri_key_name, - crate::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, + domain::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, pool, From 00d80b76a071feeb45ed7ad2ea886e72c707fa54 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 20:03:22 +0100 Subject: [PATCH 03/12] Use 'bcder' to decode ECDSA signatures This replaces the dependency on OpenSSL. --- src/lib.rs | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ec378fe..59d7b81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -640,7 +640,6 @@ pub mod sign { PublicKeyTemplateAttribute, RequestPayload, }; use kmip::types::response::{CreateKeyPairResponsePayload, ResponsePayload}; - use openssl::ecdsa::EcdsaSig; use tracing::{debug, error, trace}; use url::Url; use uuid::Uuid; @@ -953,13 +952,43 @@ pub mod sign { // : } // // Where the two integer values are known as 'r' and 's'. - let signature = EcdsaSig::from_der(&signed.signature_data).unwrap(); - let mut r = signature.r().to_vec_padded(32).unwrap(); - let mut s = signature.s().to_vec_padded(32).unwrap(); - r.append(&mut s); - Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( - r.try_into().unwrap(), - ))) + let (r, s) = bcder::Mode::Der + .decode(&*signed.signature_data, |cons| { + cons.take_sequence(|cons| { + let r = bcder::Unsigned::take_from(cons)?; + let s = bcder::Unsigned::take_from(cons)?; + Ok((r, s)) + }) + }) + .map_err(|err| { + format!("Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}") + })?; + let (mut r, mut s) = (r.as_slice(), s.as_slice()); + + // In DER, there can be at most one leading zero byte, + // because the high bit might be set and that would + // otherwise indicate a negative integer. Strip it. + for x in [&mut r, &mut s] { + *x = match *x { + [0, 0x80..=0xFF, ..] => &x[1..], + // Badly formatted signature. + [0, _, ..] => { + error!("Leading zeros in ECDSA signature integer"); + return Err(SignError); + } + x => x, + }; + + if x.len() > 32 { + error!("Overly long ECDSA signature integer"); + return Err(SignError); + } + } + + let mut signature = Box::new([0u8; 64]); + signature[..32 - r.len()].copy_from_slice(r); + signature[..64 - r.len()].copy_from_slice(s); + Ok(Signature::EcdsaP256Sha256(signature)) } // TODO From e3640f21acc49f0f794689a747110344a8e57403 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 20:09:18 +0100 Subject: [PATCH 04/12] Adjust to 'domain's current API 'domain' uses 'SignError' for representing on-disk cryptography failures. This is not appropriate for KMIP, but currently we implement the same 'SignRaw' trait anyway. This means we can't stuff enough error information in the 'SignError' type. We should stop implementing 'SignRaw', use a custom error type, and provide an asynchronous API. --- src/lib.rs | 69 +++++++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 59d7b81..d365309 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,10 +19,7 @@ use tracing::{debug, error}; use url::Url; use domain::{ - base::iana::SecurityAlgorithm, - crypto::{common::rsa_encode, sign::SignError}, - rdata::Dnskey, - utils::base16, + base::iana::SecurityAlgorithm, crypto::common::rsa_encode, rdata::Dnskey, utils::base16, }; pub use kmip::client::{ClientCertificate, ConnectionSettings}; @@ -832,10 +829,10 @@ pub mod sign { queue: &mut SignQueue, ) -> Result, SignError> { // Execute the request and capture the response. - let client = self - .conn_pool - .get() - .map_err(|err| format!("Error while obtaining KMIP pool connection: {err}"))?; + let client = self.conn_pool.get().map_err(|err| { + error!("Error while obtaining KMIP pool connection: {err}"); + SignError + })?; // Drain the queue. let q_size = queue.0.capacity(); @@ -846,13 +843,17 @@ pub mod sign { // This will block which could be problematic if executed from an // async task handler thread as it will block execution of other // tasks while waiting for the remote KMIP server to respond. - let res = client - .do_requests(queue) - .map_err(|err| format!("Error while sending KMIP request: {err}"))?; + let res = client.do_requests(queue).map_err(|err| { + error!("Error while sending KMIP request: {err}"); + SignError + })?; let mut sigs = Vec::with_capacity(q_size); for res in res { - let res = res?; + let res = res.map_err(|err| { + error!("{err}"); + SignError + })?; let sig = self.sign_post(res.payload.unwrap())?; sigs.push(sig); } @@ -901,7 +902,8 @@ pub mod sign { DigestType::Sha256, ), alg => { - return Err(format!("Algorithm not supported for KMIP signing: {alg}").into()); + error!("Algorithm not supported for KMIP signing: {alg}"); + return Err(SignError); } }; let mut cryptographic_parameters = CryptographicParameters::default() @@ -961,7 +963,8 @@ pub mod sign { }) }) .map_err(|err| { - format!("Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}") + error!("Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}"); + SignError })?; let (mut r, mut s) = (r.as_slice(), s.as_slice()); @@ -995,10 +998,13 @@ pub mod sign { //(SecurityAlgorithm::ECDSAP384SHA384, 96) => {}, //(SecurityAlgorithm::ED25519, 64) => {}, //(SecurityAlgorithm::ED448, 114) => {}, - (alg, sig_len) => Err(format!( - "KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", - base16::encode_display(&signed.signature_data) - ))?, + (alg, sig_len) => { + error!( + "KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", + base16::encode_display(&signed.signature_data) + ); + Err(SignError) + } } } } @@ -1021,10 +1027,6 @@ pub mod sign { self.algorithm } - fn flags(&self) -> u16 { - self.flags - } - fn dnskey(&self) -> Dnskey> { self.dnskey.clone() } @@ -1033,17 +1035,18 @@ pub mod sign { let request = self.sign_pre(data)?; // Execute the request and capture the response. - let client = self - .conn_pool - .get() - .map_err(|err| format!("Error while obtaining KMIP pool connection: {err}"))?; + let client = self.conn_pool.get().map_err(|err| { + error!("Error while obtaining KMIP pool connection: {err}"); + SignError + })?; // This will block which could be problematic if executed from an // async task handler thread as it will block execution of other // tasks while waiting for the remote KMIP server to respond. - let res = client - .do_request(request) - .map_err(|err| format!("Error while sending KMIP request: {err}"))?; + let res = client.do_request(request).map_err(|err| { + error!("Error while sending KMIP request: {err}"); + SignError + })?; self.sign_post(res) } @@ -1340,14 +1343,6 @@ pub mod sign { //============ Error Types =================================================== -//--- Conversion - -impl From for SignError { - fn from(err: kmip::client::Error) -> Self { - err.to_string().into() - } -} - //----------- GenerateError -------------------------------------------------- /// An error occurred while generating a key pair with a KMIP server. From 64c290a23ad358439c2b69d73659ed198768bbf5 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 20:16:29 +0100 Subject: [PATCH 05/12] Inherit 'domain's CI --- .github/workflows/ci.yml | 248 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4ceb8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,248 @@ +# ====================================================== +# Continuous Integration: making sure the codebase works +# ====================================================== +# +# This workflow tests modifications to 'domain-kmip', ensuring that +# 'domain-kmip' can be used by others successfully. It verifies certain aspects +# of the codebase, such as the formatting and feature flag combinations, and +# runs the full test suite. It runs on Ubuntu, Mac OS, and Windows. + +name: CI + +# When the workflow runs +# --------------------- +on: + # Execute when a pull request is (re-) opened or its head changes (e.g. new + # commits are added or the commit history is rewritten) ... but only if + # build-related files change. + pull_request: + paths: + - '**.rs' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ci.yml' + + # If a pull request is merged, at least one commit is added to the target + # branch. If the target is another pull request, it will be caught by the + # above event. We miss PRs that merge to a non-PR branch, except for the + # 'main' branch. + + # Execute when a commit is pushed to 'main' (including merged PRs) or to a + # release tag ... but only if build-related files change. + push: + branches: + - 'main' + - 'releases/**' + paths: + - '**.rs' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ci.yml' + + # Rebuild 'main' every week. This will account for changes to dependencies + # and to Rust, either of which can trigger new failures. Rust releases are + # every 6 weeks, on a Thursday; this event runs every Friday. + schedule: + - cron: '0 10 * * FRI' + +# Jobs +# ---------------------------------------------------------------------------- +jobs: + + # Check Formatting + # ---------------- + # + # NOTE: This job is run even if no '.rs' files have changed. Inserting such + # a check would require using a separate workflow file or using third-party + # actions. Most commits do change '.rs' files, and 'cargo-fmt' is pretty + # fast, so optimizing this is not necessary. + check-fmt: + name: Check formatting + runs-on: ubuntu-latest + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt + cache: false + + # Do the actual formatting check. + - name: Check formatting + run: cargo fmt --all -- --check + + # Determine MSRV + # -------------- + # + # The MSRV needs to be determined as we will test 'domain-kmip' against the + # Rust compiler at that version. + determine-msrv: + name: Determine MSRV + runs-on: ubuntu-latest + outputs: + msrv: ${{ steps.determine-msrv.outputs.msrv }} + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Determine the MSRV. + - name: Determine MSRV + id: determine-msrv + run: | + msrv=`cargo metadata --no-deps --format-version 1 | jq -r '.packages[]|select(.name=="domain-kmip")|.rust_version'` + echo "msrv=$msrv" >> "$GITHUB_OUTPUT" + + # Check Feature Flags + # ------------------- + # + # Rust does not provide any way to check that all possible feature flag + # combinations will succeed, so we need to try them manually here. We will + # assume this choice is not influenced by the OS or Rust version. + check-feature-flags: + name: Check feature flags + runs-on: ubuntu-latest + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: false + + # Do the actual feature flag checks. + - name: Check empty feature set + run: cargo check --all-targets --no-default-features + + # Check the required feature flags for every example. + - name: Check required features of examples + run: | + # Scrape crate metadata and construct the right 'check' commands. + # Cargo deosn't have an option to select the right features for us. + # See: https://github.com/rust-lang/cargo/issues/4663 + cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[].targets[]|select(.kind|any(.=="example"))|{name,features:(.["required-features"]|join(","))}|"\(.name) \(.features)"' \ + | while read -r name features; do + cargo check --example=$name --no-default-features --features=$features + done + + # Check Minimal Versions + # ---------------------- + # + # Ensure that 'domain-kmip' compiles with the oldest compatible versions of all + # packages, even those 'domain-kmip' depends upon indirectly. + check-minimal-versions: + name: Check minimal versions + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust nightly + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + cache: false + + # Lock all dependencies to their minimal versions. + - name: Lock dependencies to minimal versions + run: cargo +nightly update -Z minimal-versions + + # Check that 'domain-kmip' compiles. + - name: Check + run: cargo check --all-targets --all-features --locked + + # Clippy + # ------ + # + # We run Clippy separately, and only on nightly Rust because it offers a + # superset of the lints. + # + # 'cargo clippy' and 'cargo build' can share some state for fast execution, + # but it's faster to execute them in parallel than to establish an ordering + # between them. + clippy: + name: Clippy + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust nightly + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + components: clippy + cache: false + + # Do the actually Clippy run. + - name: Check Clippy + run: cargo +nightly clippy --all-targets --all-features + + # Test + # ---- + # + # Ensure that 'domain-kmip' compiles and its test suite passes, on a large + # number of operating systems and Rust versions. + test: + name: Test + needs: determine-msrv + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + rust: ["${{ needs.determine-msrv.outputs.msrv }}", stable, nightly] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: "-D warnings" + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Prepare the environment on Windows + - name: Prepare Windows environment + if: matrix.os == 'windows-latest' + shell: bash + run: | + # See + echo "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER=rust-lld" >> "$GITHUB_ENV" + + # Set up the Rust toolchain. + - name: Set up Rust ${{ matrix.rust }} + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + cache: false + + # Build and run the test suite. + - name: Test + run: cargo test --all-targets --all-features + +# TODO: Use 'cargo-semver-checks' on releases. From 4dd016017c4c9eae187edb037e4ee37288a0ffaa Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 20:17:28 +0100 Subject: [PATCH 06/12] Add 'rustfmt.toml' --- rustfmt.toml | 1 + src/lib.rs | 182 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..df99c69 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/src/lib.rs b/src/lib.rs index d365309..52a7346 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,8 @@ use tracing::{debug, error}; use url::Url; use domain::{ - base::iana::SecurityAlgorithm, crypto::common::rsa_encode, rdata::Dnskey, utils::base16, + base::iana::SecurityAlgorithm, crypto::common::rsa_encode, rdata::Dnskey, + utils::base16, }; pub use kmip::client::{ClientCertificate, ConnectionSettings}; @@ -32,7 +33,8 @@ pub use domain; /// /// Identifies an RSA public key with no limitation to either RSASSA-PSS or /// RSAES-OEAP. -pub const RSA_ENCRYPTION_OID: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); +pub const RSA_ENCRYPTION_OID: ConstOid = + Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); /// [RFC 5480](https://tools.ietf.org/html/rfc5480) `ecPublicKey`. /// @@ -187,25 +189,26 @@ impl TryFrom for KeyUrl { for (k, v) in url.query_pairs() { match &*k { "flags" => { - flags = Some( - v.parse::() - .map_err(|err| format!("Key URL flags value is invalid: {err}"))?, - ) + flags = Some(v.parse::().map_err(|err| { + format!("Key URL flags value is invalid: {err}") + })?) } "algorithm" => { - algorithm = Some( - SecurityAlgorithm::from_str(&v) - .map_err(|err| format!("Key URL algorithm value is invalid: {err}"))?, - ) + algorithm = Some(SecurityAlgorithm::from_str(&v).map_err( + |err| { + format!("Key URL algorithm value is invalid: {err}") + }, + )?) } unknown => Err(format!( "Key URL contains unknown query parameter: {unknown}" ))?, } } - let algorithm = - algorithm.ok_or(format!("Key URL lacks algorithm query parameter: {url}"))?; - let flags = flags.ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + let algorithm = algorithm + .ok_or(format!("Key URL lacks algorithm query parameter: {url}"))?; + let flags = flags + .ok_or(format!("Key URL lacks flags query parameter: {url}"))?; Ok(Self { url, @@ -258,7 +261,8 @@ impl PublicKey { algorithm: SecurityAlgorithm, conn_pool: SyncConnPool, ) -> Result { - let public_key = Self::fetch_public_key(public_key_id, algorithm, &conn_pool)?; + let public_key = + Self::fetch_public_key(public_key_id, algorithm, &conn_pool)?; Ok(Self { algorithm, @@ -363,7 +367,8 @@ impl PublicKey { let res = client .get_key(public_key_id) .inspect_err(|err| error!("{err}"))?; - let ManagedObject::PublicKey(public_key) = res.cryptographic_object else { + let ManagedObject::PublicKey(public_key) = res.cryptographic_object + else { return Err(kmip::client::Error::DeserializeError(format!( "Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object @@ -399,9 +404,13 @@ impl PublicKey { ); debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); - match (expected_algorithm, public_key.key_block.key_format_type) { + match (expected_algorithm, public_key.key_block.key_format_type) + { (SecurityAlgorithm::RSASHA1, KeyFormatType::PKCS1) - | (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::PKCS1) + | ( + SecurityAlgorithm::RSASHA1_NSEC3_SHA1, + KeyFormatType::PKCS1, + ) | (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) | (SecurityAlgorithm::RSASHA512, KeyFormatType::PKCS1) => { // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public @@ -443,7 +452,10 @@ impl PublicKey { } (SecurityAlgorithm::RSASHA1, KeyFormatType::Raw) - | (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::Raw) + | ( + SecurityAlgorithm::RSASHA1_NSEC3_SHA1, + KeyFormatType::Raw, + ) | (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) | (SecurityAlgorithm::RSASHA512, KeyFormatType::Raw) => { // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) @@ -490,7 +502,10 @@ impl PublicKey { domain::crypto::common::rsa_encode(e, n) } - (SecurityAlgorithm::ECDSAP256SHA256, KeyFormatType::Raw) => { + ( + SecurityAlgorithm::ECDSAP256SHA256, + KeyFormatType::Raw, + ) => { // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) @@ -558,20 +573,24 @@ impl PublicKey { // Expect octet string to be [, // <32-byte X value>, <32-byte Y value>]. if octets.len() != 65 { - return Err(kmip::client::Error::DeserializeError(format!( - "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", - base16::encode_display(octets), - octets.len() - )))?; + return Err( + kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", + base16::encode_display(octets), + octets.len() + )), + )?; } // Note: OpenDNSSEC doesn't support the compressed // form either. let compression_flag = octets[0]; if compression_flag != 0x04 { - return Err(kmip::client::Error::DeserializeError(format!( - "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}" - )))?; + return Err( + kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}" + )), + )?; } // Expect octet string to be X | Y (| denotes @@ -592,8 +611,12 @@ impl PublicKey { .cryptographic_length .map(|l| l.to_string()) .unwrap_or("unknown length".to_string()); - let actual = format!("{alg} ({len}) as {key_format_type}"); - return Err(PublicKeyError::AlgorithmMismatch { expected, actual }); + let actual = + format!("{alg} ({len}) as {key_format_type}"); + return Err(PublicKeyError::AlgorithmMismatch { + expected, + actual, + }); } } } @@ -628,15 +651,17 @@ pub mod sign { use kmip::client::pool::SyncConnPool; use kmip::types::common::{ - CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, - DigitalSignatureAlgorithm, HashingAlgorithm, PaddingMethod, UniqueBatchItemID, - UniqueIdentifier, + CryptographicAlgorithm, CryptographicParameters, + CryptographicUsageMask, Data, DigitalSignatureAlgorithm, + HashingAlgorithm, PaddingMethod, UniqueBatchItemID, UniqueIdentifier, }; use kmip::types::request::{ self, BatchItem, CommonTemplateAttribute, PrivateKeyTemplateAttribute, PublicKeyTemplateAttribute, RequestPayload, }; - use kmip::types::response::{CreateKeyPairResponsePayload, ResponsePayload}; + use kmip::types::response::{ + CreateKeyPairResponsePayload, ResponsePayload, + }; use tracing::{debug, error, trace}; use url::Url; use uuid::Uuid; @@ -647,7 +672,9 @@ pub mod sign { use domain::rdata::Dnskey; use domain::utils::base16; - use super::{DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey}; + use super::{ + DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey, + }; //----------- KeyPair ---------------------------------------------------- @@ -811,7 +838,8 @@ pub mod sign { ) -> Result, SignError> { let request = self.sign_pre(data)?; let operation = request.operation(); - let batch_item_id = UniqueBatchItemID(Uuid::new_v4().into_bytes().to_vec()); + let batch_item_id = + UniqueBatchItemID(Uuid::new_v4().into_bytes().to_vec()); let batch_item = BatchItem(operation, Some(batch_item_id), request); queue.0.push(batch_item); Ok(None) @@ -881,8 +909,9 @@ pub mod sign { self.flags ); - let url = Url::parse(&url) - .map_err(|err| KeyUrlParseError(format!("unable to parse {url} as URL: {err}")))?; + let url = Url::parse(&url).map_err(|err| { + KeyUrlParseError(format!("unable to parse {url} as URL: {err}")) + })?; Ok(url) } @@ -906,12 +935,13 @@ pub mod sign { return Err(SignError); } }; - let mut cryptographic_parameters = CryptographicParameters::default() - .with_hashing_algorithm(hashing_alg) - .with_cryptographic_algorithm(crypto_alg); + let mut cryptographic_parameters = + CryptographicParameters::default() + .with_hashing_algorithm(hashing_alg) + .with_cryptographic_algorithm(crypto_alg); if self.algorithm == SecurityAlgorithm::RSASHA256 { - cryptographic_parameters = - cryptographic_parameters.with_padding_method(PaddingMethod::PKCS1_v1_5); + cryptographic_parameters = cryptographic_parameters + .with_padding_method(PaddingMethod::PKCS1_v1_5); } let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), @@ -922,7 +952,10 @@ pub mod sign { } /// Process a KMIP HSM signing operation response for this key pair. - fn sign_post(&self, res: ResponsePayload) -> Result { + fn sign_post( + &self, + res: ResponsePayload, + ) -> Result { trace!("Checking sign payload"); let ResponsePayload::Sign(signed) = res else { unreachable!(); @@ -976,7 +1009,9 @@ pub mod sign { [0, 0x80..=0xFF, ..] => &x[1..], // Badly formatted signature. [0, _, ..] => { - error!("Leading zeros in ECDSA signature integer"); + error!( + "Leading zeros in ECDSA signature integer" + ); return Err(SignError); } x => x, @@ -1103,7 +1138,9 @@ pub mod sign { // Note: Fortanix DSM requires a name for at least the private // key. request::Attribute::Name(private_key_name), - request::Attribute::CryptographicUsageMask(CryptographicUsageMask::Sign), + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Sign, + ), ]; let pub_key_attrs = vec![ // Krill supplies a name at creation time. Do we need to? @@ -1113,7 +1150,9 @@ pub mod sign { // Krill does verification, do we need to? ODS doesn't. // Note: PyKMIP requires a Cryptographic Usage Mask for the public // key. - request::Attribute::CryptographicUsageMask(CryptographicUsageMask::Verify), + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Verify, + ), ]; // PyKMIP doesn't support CryptographicParameters so we cannot supply @@ -1153,9 +1192,11 @@ pub mod sign { ), )) } else { - common_attrs.push(request::Attribute::CryptographicAlgorithm( - CryptographicAlgorithm::RSA, - )); + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::RSA, + ), + ); common_attrs.push(request::Attribute::CryptographicLength( bits.try_into().unwrap(), )); @@ -1171,20 +1212,25 @@ pub mod sign { // supported asymmetric key algorithm." if use_cryptographic_params { - common_attrs.push(request::Attribute::CryptographicParameters( - CryptographicParameters::default().with_digital_signature_algorithm( - DigitalSignatureAlgorithm::ECDSAWithSHA256, + common_attrs.push( + request::Attribute::CryptographicParameters( + CryptographicParameters::default() + .with_digital_signature_algorithm( + DigitalSignatureAlgorithm::ECDSAWithSHA256, + ), ), - )) + ) } else { // RFC 8624 3.1 DNSSEC Signing: MUST // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 // "For ECDSA, ECDH, and ECMQV algorithms, Cryptographic // Length corresponds to the bit length of parameter // Q." - common_attrs.push(request::Attribute::CryptographicAlgorithm( - CryptographicAlgorithm::ECDSA, - )); + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::ECDSA, + ), + ); // ODS doesn't tell PKCS#11 a Q length. I have no idea // what value we should put here, but as Q length is // optional let's try not passing it. @@ -1196,7 +1242,8 @@ pub mod sign { // error "Unsupported length for ECC key". When using 256 // the Fortanix UI shows the key as type EC with curve // NistP256 so that seems good. - common_attrs.push(request::Attribute::CryptographicLength(256)); + common_attrs + .push(request::Attribute::CryptographicLength(256)); } } GenerateParams::EcdsaP384Sha384 => { @@ -1288,7 +1335,8 @@ pub mod sign { conn_pool.server_id() )) })?; - let request = RequestPayload::Activate(Some(private_key_unique_identifier)); + let request = + RequestPayload::Activate(Some(private_key_unique_identifier)); // Execute the request and capture the response trace!("Activating KMIP key..."); @@ -1310,7 +1358,8 @@ pub mod sign { let ResponsePayload::Activate(_) = response else { error!("KMIP request failed: Wrong response type received!"); return Err(GenerateError::Kmip( - "Unable to parse KMIP response: payload should be Activate".to_string(), + "Unable to parse KMIP response: payload should be Activate" + .to_string(), )); }; } @@ -1324,7 +1373,10 @@ pub mod sign { /// /// As a KMIP key cannot be destroyed if it is active, this function first /// attempts to revoke the key and then destroy it. - pub fn destroy(key_id: &str, conn_pool: SyncConnPool) -> Result<(), DestroyError> { + pub fn destroy( + key_id: &str, + conn_pool: SyncConnPool, + ) -> Result<(), DestroyError> { let client = conn_pool.get().map_err(|err| { DestroyError::Kmip(format!( "Key destruction failed: Cannot connect to KMIP server {}: {err}", @@ -1519,12 +1571,18 @@ mod tests { fn pykmip_connect() { init_logging(); let mut cert_bytes = Vec::new(); - let file = File::open("/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.crt").unwrap(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.crt", + ) + .unwrap(); let mut reader = BufReader::new(file); reader.read_to_end(&mut cert_bytes).unwrap(); let mut key_bytes = Vec::new(); - let file = File::open("/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.key").unwrap(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.key", + ) + .unwrap(); let mut reader = BufReader::new(file); reader.read_to_end(&mut key_bytes).unwrap(); From db48c4d0e0c4577714f3edb9ba5d700a632472e1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 21:31:07 +0100 Subject: [PATCH 07/12] Refactor and test public key parsing --- src/lib.rs | 604 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 344 insertions(+), 260 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 52a7346..6563376 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use std::{ vec::Vec, }; -use bcder::{BitString, ConstOid, Oid, decode::SliceSource}; +use bcder::{BitString, ConstOid, Oid}; use kmip::{ client::pool::SyncConnPool, types::{ @@ -146,17 +146,6 @@ impl KeyUrl { } } -//--- impl Into - -// Disablow the Clippy lint as it is safe to go from a KeyURL to a URL but -// not vice-versa, so we implement Into but not From. -#[allow(clippy::from_over_into)] -impl Into for KeyUrl { - fn into(self) -> Url { - self.url - } -} - //--- impl Deref impl std::ops::Deref for KeyUrl { @@ -169,6 +158,12 @@ impl std::ops::Deref for KeyUrl { //--- Conversions +impl From for Url { + fn from(key_url: KeyUrl) -> Self { + key_url.url + } +} + impl TryFrom for KeyUrl { type Error = String; @@ -222,7 +217,7 @@ impl TryFrom for KeyUrl { //--- impl Display -impl std::fmt::Display for KeyUrl { +impl fmt::Display for KeyUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.url.fmt(f) } @@ -261,54 +256,6 @@ impl PublicKey { algorithm: SecurityAlgorithm, conn_pool: SyncConnPool, ) -> Result { - let public_key = - Self::fetch_public_key(public_key_id, algorithm, &conn_pool)?; - - Ok(Self { - algorithm, - public_key, - }) - } - - /// Create a public key from a key stored on a KMIP server. - /// - /// This is a thin wrapper around - /// [`Self::for_key_id_and_dnssec_algorithm`]. - pub fn for_key_url( - public_key_url: KeyUrl, - conn_pool: SyncConnPool, - ) -> Result { - Self::for_key_id_and_dnssec_algorithm( - public_key_url.key_id(), - public_key_url.algorithm(), - conn_pool, - ) - } - - /// The DNSSEC algorithm of the key. - pub fn algorithm(&self) -> SecurityAlgorithm { - self.algorithm - } - - /// Generate a DNSKEY RR or this public key. - pub fn dnskey(&self, flags: u16) -> Dnskey> { - // SAFETY: The key came from a KMIP server and was validated to have - // the expected length when the KMIP server response was parsed by - // fetch_public_key(). - Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()).unwrap() - } -} - -impl PublicKey { - /// Query the KMIP server for the bytes of the specified public key. - /// - /// Verifies that the cryptographic algorithm of the key is compatible - /// with the specified DNSSEC algorithm. - fn fetch_public_key( - public_key_id: &str, - expected_algorithm: SecurityAlgorithm, - conn_pool: &SyncConnPool, - ) -> Result, PublicKeyError> { // https://datatracker.ietf.org/doc/html/rfc5702#section-2 // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource // Records for DNSSEC @@ -384,7 +331,7 @@ impl PublicKey { // == KeyFormatType::Raw. However, Fortanix DSM returns // KeyFormatType::Raw when fetching key data for an ECDSA public key. - let octets = match public_key.key_block.key_value.key_material { + match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { debug!( "Cryptographic Algorithm: {:?}", @@ -404,201 +351,29 @@ impl PublicKey { ); debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); - match (expected_algorithm, public_key.key_block.key_format_type) - { - (SecurityAlgorithm::RSASHA1, KeyFormatType::PKCS1) - | ( - SecurityAlgorithm::RSASHA1_NSEC3_SHA1, + match (algorithm, public_key.key_block.key_format_type) { + ( + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512, KeyFormatType::PKCS1, - ) - | (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) - | (SecurityAlgorithm::RSASHA512, KeyFormatType::PKCS1) => { - // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public - // key data like so: - // RSAPublicKey::=SEQUENCE{ - // modulus INTEGER, -- n - // publicExponent INTEGER -- e } - let source = SliceSource::new(&bytes); - let mut modulus = None; - let mut public_exponent = None; - bcder::Mode::Der - .decode(source, |cons| { - cons.take_sequence(|cons| { - modulus = Some(bcder::Unsigned::take_from(cons)?); - public_exponent = Some(bcder::Unsigned::take_from(cons)?); - Ok(()) - }) - }) - .map_err(|err| { - kmip::client::Error::DeserializeError(format!( - "Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}" - )) - })?; - - let Some(modulus) = modulus else { - return Err(kmip::client::Error::DeserializeError( - "Unable to parse DER encoded PKCS#1 RSAPublicKey: missing modulus" - .into(), - ))?; - }; - - let Some(public_exponent) = public_exponent else { - return Err(kmip::client::Error::DeserializeError("Unable to parse DER encoded PKCS#1 RSAPublicKey: missing public exponent".into()))?; - }; + ) => Self::parse_rsa_from_pkcs1(algorithm, &bytes), - let n = modulus.as_slice(); - let e = public_exponent.as_slice(); - domain::crypto::common::rsa_encode(e, n) - } - - (SecurityAlgorithm::RSASHA1, KeyFormatType::Raw) - | ( - SecurityAlgorithm::RSASHA1_NSEC3_SHA1, + ( + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512, KeyFormatType::Raw, - ) - | (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) - | (SecurityAlgorithm::RSASHA512, KeyFormatType::Raw) => { - // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) - // SubjectPublicKeyInfo SEQUENCE (2 elem) - // algorithm AlgorithmIdentifier SEQUENCE (2 elem) - // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) - // parameter ANY NULL - // subjectPublicKey BIT STRING (2160 bit) 001100001000001000000001000010100000001010000010000000010000000100000… - // SEQUENCE (2 elem) - // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… - // INTEGER 65537 - let source = SliceSource::new(&bytes); - let mut modulus = None; - let mut public_exponent = None; - bcder::Mode::Der - .decode(source, |cons| { - cons.take_sequence(|cons| { - cons.take_sequence(|cons| { - let algorithm = Oid::take_from(cons)?; - if algorithm != RSA_ENCRYPTION_OID { - return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); - } - // Ignore the parameters. - Ok(()) - })?; - cons.take_sequence(|cons| { - modulus = Some(bcder::Unsigned::take_from(cons)?); - public_exponent = Some(bcder::Unsigned::take_from(cons)?); - Ok(()) - }) - }) - }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}")))?; - - let Some(modulus) = modulus else { - return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing modulus".into()))?; - }; - - let Some(public_exponent) = public_exponent else { - return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing public exponent".into()))?; - }; - - let n = modulus.as_slice(); - let e = public_exponent.as_slice(); - domain::crypto::common::rsa_encode(e, n) - } + ) => Self::parse_rsa_from_raw(algorithm, &bytes), ( SecurityAlgorithm::ECDSAP256SHA256, KeyFormatType::Raw, - ) => { - // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) - // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) - // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) - // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type - // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve - // subjectPublicKey BIT_STRING @23+66: (520 bit) - // - // From: https://www.rfc-editor.org/rfc/rfc5480.html#section-2.1.1 - // The parameter for id-ecPublicKey is as follows and MUST always be - // present: - // - // ECParameters ::= CHOICE { - // namedCurve OBJECT IDENTIFIER - // -- implicitCurve NULL - // -- specifiedCurve SpecifiedECDomain - // } - // -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. - // -- Details for SpecifiedECDomain can be found in [X9.62]. - // -- Any future additions to this CHOICE should be coordinated - // -- with ANSI X9. - let source = SliceSource::new(&bytes); - let mut bits = None; - bcder::Mode::Der - .decode(source, |cons| { - cons.take_sequence(|cons| { - cons.take_sequence(|cons| { - let algorithm = Oid::take_from(cons)?; - if algorithm != EC_PUBLIC_KEY_OID { - Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) - } else { - let named_curve = Oid::take_from(cons)?; - if named_curve != SECP256R1_OID { - return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); - } - Ok(()) - } - })?; - bits = Some(BitString::take_from(cons)?); - Ok(()) - }) - }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; - - let Some(bits) = bits else { - return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; - }; - - // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 - // "The subjectPublicKey from SubjectPublicKeyInfo - // is the ECC public key. ECC public keys have the - // following syntax: - // - // ECPoint ::= OCTET STRING - // ... - // The first octet of the OCTET STRING indicates - // whether the key is compressed or uncompressed. - // The uncompressed form is indicated by 0x04 and - // the compressed form is indicated by either 0x02 - // or 0x03 (see 2.3.3 in [SEC1]). The public key - // MUST be rejected if any other value is included - // in the first octet." - let Some(octets) = bits.octet_slice() else { - return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; - }; - - // Expect octet string to be [, - // <32-byte X value>, <32-byte Y value>]. - if octets.len() != 65 { - return Err( - kmip::client::Error::DeserializeError(format!( - "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", - base16::encode_display(octets), - octets.len() - )), - )?; - } - - // Note: OpenDNSSEC doesn't support the compressed - // form either. - let compression_flag = octets[0]; - if compression_flag != 0x04 { - return Err( - kmip::client::Error::DeserializeError(format!( - "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}" - )), - )?; - } - - // Expect octet string to be X | Y (| denotes - // concatenation) where X and Y are each 32 bytes - // (because P-256 uses 256 bit values and 256 bits are - // 32 bytes). Skip the compression flag. - octets[1..].to_vec() - } + ) => Self::parse_ecdsa_from_raw(algorithm, &bytes), (expected, key_format_type) => { let alg = public_key @@ -613,10 +388,10 @@ impl PublicKey { .unwrap_or("unknown length".to_string()); let actual = format!("{alg} ({len}) as {key_format_type}"); - return Err(PublicKeyError::AlgorithmMismatch { + Err(PublicKeyError::AlgorithmMismatch { expected, actual, - }); + }) } } } @@ -627,16 +402,267 @@ impl PublicKey { modulus, public_exponent, }, - ) => rsa_encode(&public_exponent, &modulus), + ) => Ok(Self { + algorithm, + public_key: rsa_encode(&public_exponent, &modulus), + }), - mat => { - return Err(kmip::client::Error::DeserializeError(format!( - "Fetched KMIP object has unsupported key material type: {mat}" - )))?; - } + mat => Err(kmip::client::Error::DeserializeError(format!( + "Fetched KMIP object has unsupported key material type: {mat}" + )) + .into()), + } + } + + /// Create a public key from a key stored on a KMIP server. + /// + /// This is a thin wrapper around + /// [`Self::for_key_id_and_dnssec_algorithm`]. + pub fn for_key_url( + public_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + Self::for_key_id_and_dnssec_algorithm( + public_key_url.key_id(), + public_key_url.algorithm(), + conn_pool, + ) + } + + /// The DNSSEC algorithm of the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// Generate a DNSKEY RR or this public key. + pub fn dnskey(&self, flags: u16) -> Dnskey> { + // SAFETY: The key came from a KMIP server and was validated to have + // the expected length when the KMIP server response was parsed by + // fetch_public_key(). + Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()).unwrap() + } +} + +impl PublicKey { + /// Parse an RSA key encoded in the PKCS#1 format. + /// + /// # Panics + /// + /// Panics if the specified DNS security algorithm for the key does not use + /// RSA. + fn parse_rsa_from_pkcs1( + algorithm: SecurityAlgorithm, + bytes: &[u8], + ) -> Result { + // Ensure the specified algorithm uses RSA. + assert!(matches!( + algorithm, + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 + )); + + // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public key data like so: + // RSAPublicKey::=SEQUENCE{ + // modulus INTEGER, -- n + // publicExponent INTEGER -- e } + + // TODO: Decode this manually, to avoid the 'bcder' dependency? + let (modulus, public_exponent) = bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + let modulus = bcder::Unsigned::take_from(cons)?; + let public_exponent = bcder::Unsigned::take_from(cons)?; + Ok((modulus, public_exponent)) + }) + }) + .map_err(|err| { + kmip::client::Error::DeserializeError(format!( + "Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}" + )) + })?; + + let public_key = domain::crypto::common::rsa_encode( + public_exponent.as_slice(), + modulus.as_slice(), + ); + + Ok(Self { + algorithm, + public_key, + }) + } + + /// Parse an RSA key encoded in the KMIP "raw" format convention. + /// + /// # Panics + /// + /// Panics if the specified DNS security algorithm for the key does not use + /// RSA. + fn parse_rsa_from_raw( + algorithm: SecurityAlgorithm, + bytes: &[u8], + ) -> Result { + // Ensure the specified algorithm uses RSA. + assert!(matches!( + algorithm, + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 + )); + + // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE (2 elem) + // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) + // parameter ANY NULL + // subjectPublicKey BIT STRING (2160 bit) 001100001000001000000001000010100000001010000010000000010000000100000… + // SEQUENCE (2 elem) + // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… + // INTEGER 65537 + + // TODO: Decode this manually, to avoid the 'bcder' dependency? + let (modulus, public_exponent) = + bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != RSA_ENCRYPTION_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); + } + // Ignore the parameters. + Ok(()) + })?; + cons.take_sequence(|cons| { + let modulus = bcder::Unsigned::take_from(cons)?; + let public_exponent = bcder::Unsigned::take_from(cons)?; + Ok((modulus, public_exponent)) + }) + }) + }) + .map_err(|err| { + kmip::client::Error::DeserializeError(format!( + "Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}" + )) + })?; + + let public_key = domain::crypto::common::rsa_encode( + public_exponent.as_slice(), + modulus.as_slice(), + ); + + Ok(Self { + algorithm, + public_key, + }) + } + + /// Parse an ECDSA key encoded in the KMIP "raw" format convention. + /// + /// # Panics + /// + /// Panics if the specified DNS security algorithm for the key does not use + /// RSA. + fn parse_ecdsa_from_raw( + algorithm: SecurityAlgorithm, + bytes: &[u8], + ) -> Result { + // Ensure the specified algorithm uses ECDSA. + // TODO: Support ECDSAP384SHA384. + assert!(matches!(algorithm, SecurityAlgorithm::ECDSAP256SHA256)); + + // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + // + // From: https://www.rfc-editor.org/rfc/rfc5480.html#section-2.1.1 + // The parameter for id-ecPublicKey is as follows and MUST always be + // present: + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. + // -- Details for SpecifiedECDomain can be found in [X9.62]. + // -- Any future additions to this CHOICE should be coordinated + // -- with ANSI X9. + let bits = bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != EC_PUBLIC_KEY_OID { + Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) + } else { + let named_curve = Oid::take_from(cons)?; + if named_curve != SECP256R1_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); + } + Ok(()) + } + })?; + let bits = BitString::take_from(cons)?; + Ok(bits) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; + + // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 + // "The subjectPublicKey from SubjectPublicKeyInfo + // is the ECC public key. ECC public keys have the + // following syntax: + // + // ECPoint ::= OCTET STRING + // ... + // The first octet of the OCTET STRING indicates + // whether the key is compressed or uncompressed. + // The uncompressed form is indicated by 0x04 and + // the compressed form is indicated by either 0x02 + // or 0x03 (see 2.3.3 in [SEC1]). The public key + // MUST be rejected if any other value is included + // in the first octet." + let Some(octets) = bits.octet_slice() else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; }; - Ok(octets) + // Expect octet string to be [, + // <32-byte X value>, <32-byte Y value>]. + if octets.len() != 65 { + return Err(kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", + base16::encode_display(octets), + octets.len() + )))?; + } + + // Note: OpenDNSSEC doesn't support the compressed + // form either. + let compression_flag = octets[0]; + if compression_flag != 0x04 { + return Err(kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}" + )))?; + } + + // Expect octet string to be X | Y (| denotes + // concatenation) where X and Y are each 32 bytes + // (because P-256 uses 256 bit values and 256 bits are + // 32 bytes). Skip the compression flag. + let public_key = octets[1..].to_vec(); + + Ok(Self { + algorithm, + public_key, + }) } } @@ -1547,12 +1573,13 @@ mod tests { use std::time::SystemTime; use std::vec::Vec; + use domain::base::iana::SecurityAlgorithm; use kmip::client::ConnectionSettings; use kmip::client::pool::ConnectionManager; use domain::crypto::sign::SignRaw; - use super::sign::generate; + use super::{PublicKey, sign::generate}; fn init_logging() { use tracing_subscriber::EnvFilter; @@ -1566,6 +1593,63 @@ mod tests { .init(); } + /// Test [`PublicKey::parse_rsa_from_pkcs1()`]. + #[test] + fn parse_rsa_key_from_pkcs1() { + // TODO: Find real-world samples. + let bytes = [48, 6, 2, 1, 127, 2, 1, 42]; + let key = PublicKey::parse_rsa_from_pkcs1( + SecurityAlgorithm::RSASHA256, + &bytes, + ) + .unwrap(); + assert_eq!(key.algorithm, SecurityAlgorithm::RSASHA256); + assert_eq!(key.public_key, [1, 42, 127]); + } + + /// Test [`PublicKey::parse_rsa_from_raw()`]. + #[test] + fn parse_rsa_key_from_raw() { + // TODO: Find real-world samples. + let bytes = [ + 48, 21, 48, 11, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 48, 6, 2, + 1, 127, 2, 1, 42, + ]; + let key = + PublicKey::parse_rsa_from_raw(SecurityAlgorithm::RSASHA256, &bytes) + .unwrap(); + assert_eq!(key.algorithm, SecurityAlgorithm::RSASHA256); + assert_eq!(key.public_key, [1, 42, 127]); + } + + /// Test [`PublicKey::parse_ecdsa_from_raw()`]. + #[test] + fn parse_ecdsa_key_from_raw() { + // TODO: Find real-world samples. + let bytes = [ + 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, + 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let key = PublicKey::parse_ecdsa_from_raw( + SecurityAlgorithm::ECDSAP256SHA256, + &bytes, + ) + .unwrap(); + assert_eq!(key.algorithm, SecurityAlgorithm::ECDSAP256SHA256); + assert_eq!( + key.public_key, + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0 + ] + ); + } + #[test] #[ignore = "Requires running PyKMIP"] fn pykmip_connect() { From 6fee5ea3a26630284696cb0d1c2c614e8ded4d75 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 21:39:11 +0100 Subject: [PATCH 08/12] Refactor and test ECDSA signature parsing Also catches a logic bug in the parser. --- src/lib.rs | 137 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 83 insertions(+), 54 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6563376..7ddd302 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -998,60 +998,9 @@ pub mod sign { )), (SecurityAlgorithm::ECDSAP256SHA256, _) => { - // ECDSA signature received from Fortanix DSM, decoded - // using this command: - // - // $ echo '' | xxd -r -p | dumpasn1 - - // 0 69: SEQUENCE { - // 2 33: INTEGER - // : 00 C6 A7 D1 2E A1 0C B4 96 BD D9 A5 48 2C 9B F4 - // : 0C EC 9F FC EF 1A 0D 59 BB B9 24 F3 FE DA DC F8 - // : 9E - // 37 32: INTEGER - // : 4B A7 22 69 F2 F8 65 88 63 D0 25 D3 A9 D5 92 4F - // : A2 21 BD 59 CD 27 60 6D 16 C3 79 EF B4 0A CA 33 - // : } - // - // Where the two integer values are known as 'r' and 's'. - let (r, s) = bcder::Mode::Der - .decode(&*signed.signature_data, |cons| { - cons.take_sequence(|cons| { - let r = bcder::Unsigned::take_from(cons)?; - let s = bcder::Unsigned::take_from(cons)?; - Ok((r, s)) - }) - }) - .map_err(|err| { - error!("Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}"); - SignError - })?; - let (mut r, mut s) = (r.as_slice(), s.as_slice()); - - // In DER, there can be at most one leading zero byte, - // because the high bit might be set and that would - // otherwise indicate a negative integer. Strip it. - for x in [&mut r, &mut s] { - *x = match *x { - [0, 0x80..=0xFF, ..] => &x[1..], - // Badly formatted signature. - [0, _, ..] => { - error!( - "Leading zeros in ECDSA signature integer" - ); - return Err(SignError); - } - x => x, - }; - - if x.len() > 32 { - error!("Overly long ECDSA signature integer"); - return Err(SignError); - } - } - - let mut signature = Box::new([0u8; 64]); - signature[..32 - r.len()].copy_from_slice(r); - signature[..64 - r.len()].copy_from_slice(s); + let signature = Self::parse_ecdsa_sig_from_x962( + &signed.signature_data, + )?; Ok(Signature::EcdsaP256Sha256(signature)) } @@ -1068,6 +1017,67 @@ pub mod sign { } } } + + /// Parse an ECDSA signature from the X9.62 ASN.1 DER format. + pub(crate) fn parse_ecdsa_sig_from_x962( + bytes: &[u8], + ) -> Result, SignError> { + // ECDSA signature received from Fortanix DSM, decoded + // using this command: + // + // $ echo '' | xxd -r -p | dumpasn1 - + // 0 69: SEQUENCE { + // 2 33: INTEGER + // : 00 C6 A7 D1 2E A1 0C B4 96 BD D9 A5 48 2C 9B F4 + // : 0C EC 9F FC EF 1A 0D 59 BB B9 24 F3 FE DA DC F8 + // : 9E + // 37 32: INTEGER + // : 4B A7 22 69 F2 F8 65 88 63 D0 25 D3 A9 D5 92 4F + // : A2 21 BD 59 CD 27 60 6D 16 C3 79 EF B4 0A CA 33 + // : } + // + // Where the two integer values are known as 'r' and 's'. + let (r, s) = bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + let r = bcder::Unsigned::take_from(cons)?; + let s = bcder::Unsigned::take_from(cons)?; + Ok((r, s)) + }) + }) + .map_err(|err| { + error!( + "Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}" + ); + SignError + })?; + let (mut r, mut s) = (r.as_slice(), s.as_slice()); + + // In DER, there can be at most one leading zero byte, + // because the high bit might be set and that would + // otherwise indicate a negative integer. Strip it. + for x in [&mut r, &mut s] { + *x = match *x { + [0, 0x80..=0xFF, ..] => &x[1..], + // Badly formatted signature. + [0, _, ..] => { + error!("Leading zeros in ECDSA signature integer"); + return Err(SignError); + } + x => x, + }; + + if x.len() > 32 { + error!("Overly long ECDSA signature integer"); + return Err(SignError); + } + } + + let mut signature = Box::new([0u8; 64]); + signature[32 - r.len()..32].copy_from_slice(r); + signature[64 - r.len()..64].copy_from_slice(s); + Ok(signature) + } } //----------- SignQueue -------------------------------------------------- @@ -1579,6 +1589,8 @@ mod tests { use domain::crypto::sign::SignRaw; + use crate::sign::KeyPair; + use super::{PublicKey, sign::generate}; fn init_logging() { @@ -1650,6 +1662,23 @@ mod tests { ); } + /// Test [`KeyPair::parse_ecdsa_sig_from_x962()`]. + #[test] + fn parse_ecdsa_sig_from_x962() { + // TODO: Find real-world samples. + let bytes = [48, 6, 2, 1, 21, 2, 1, 47]; + let signature = KeyPair::parse_ecdsa_sig_from_x962(&bytes).unwrap(); + assert_eq!( + *signature, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 47 + ] + ); + } + #[test] #[ignore = "Requires running PyKMIP"] fn pykmip_connect() { From 8b7063da5fd8e7b06cfdde48fe3dac079b06be74 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 25 Nov 2025 21:42:06 +0100 Subject: [PATCH 09/12] Bump minimum 'lazy_static' version Should resolve the minimum versions CI check. --- Cargo.lock | 1 + Cargo.toml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f43ba6e..8368cce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,7 @@ dependencies = [ "bcder", "domain", "kmip-protocol", + "lazy_static", "tracing", "tracing-subscriber", "url", diff --git a/Cargo.toml b/Cargo.toml index ad1921d..5abf6b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,3 +79,8 @@ features = ["v4"] [dev-dependencies.tracing-subscriber] version = "0.3.19" features = ["env-filter"] + +# NOTE: 'tracing-subscriber' transitively introduces 'lazy_static' with a low +# version specifier, resulting in compilation failures. +[dev-dependencies.lazy_static] +version = "1.5.0" From 9efbd97c230d6d0261c56e84d3e2e2509183e08f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 26 Nov 2025 09:30:54 +0100 Subject: [PATCH 10/12] Re-export 'kmip_protocol' dependency --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 7ddd302..d8693f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,9 @@ use domain::{ pub use kmip::client::{ClientCertificate, ConnectionSettings}; +// Re-export dependencies pub use domain; +pub use kmip; //------------ Constants ----------------------------------------------------- From b009e1a34515c956eac538a00645cabfebbd2b8f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 26 Nov 2025 09:36:24 +0100 Subject: [PATCH 11/12] Move re-exports under 'dep' submodule --- src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8693f3..ebe80c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,9 +25,11 @@ use domain::{ pub use kmip::client::{ClientCertificate, ConnectionSettings}; -// Re-export dependencies -pub use domain; -pub use kmip; +/// Dependency re-exports +pub mod dep { + pub use domain; + pub use kmip; +} //------------ Constants ----------------------------------------------------- From cd231ed9f4264180dc0016ee7ba741784f5314e1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 26 Nov 2025 11:31:39 +0100 Subject: [PATCH 12/12] Match 'domain's MSRV at 1.85 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5abf6b0..f082dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.0.1" edition = "2024" # The MSRV is at least 4 versions behind stable (about half a year). -rust-version = "1.87.0" +rust-version = "1.85.0" license = "BSD-3-Clause" readme = "README.md"