|
| 1 | +// Copyright lowRISC contributors (OpenTitan project). |
| 2 | +// Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 3 | +// SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +use anyhow::{Result, anyhow}; |
| 6 | +use const_oid::ObjectIdentifier; |
| 7 | +use cryptoki::mechanism::vendor_defined::VendorDefinedMechanism; |
| 8 | +use cryptoki::mechanism::Mechanism; |
| 9 | +use cryptoki::object::Attribute; |
| 10 | +use cryptoki::session::Session; |
| 11 | +use der::{Encode, EncodePem}; |
| 12 | +use serde::{Deserialize, Serialize}; |
| 13 | +use std::any::Any; |
| 14 | +use std::fs; |
| 15 | +use std::path::PathBuf; |
| 16 | +use std::str::FromStr; |
| 17 | +use x509_cert::name::Name; |
| 18 | +use x509_cert::request::{CertReq, CertReqInfo}; |
| 19 | +use x509_cert::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; |
| 20 | + |
| 21 | +use crate::commands::{BasicResult, Dispatch}; |
| 22 | +use crate::error::HsmError; |
| 23 | +use crate::module::Module; |
| 24 | +use crate::util::attribute::{AttributeMap, AttributeType, KeyType, MechanismType, ObjectClass}; |
| 25 | +use crate::util::helper; |
| 26 | + |
| 27 | +// ML-DSA-87 OID: 2.16.840.1.101.3.4.3.19 |
| 28 | +const OID_MLDSA_87: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.19"); |
| 29 | + |
| 30 | +#[derive(clap::Args, Debug, Serialize, Deserialize)] |
| 31 | +pub struct ExportCsr { |
| 32 | + #[arg(long)] |
| 33 | + id: Option<String>, |
| 34 | + #[arg(short, long)] |
| 35 | + label: Option<String>, |
| 36 | + #[arg(long)] |
| 37 | + subject: String, |
| 38 | + #[arg(short, long)] |
| 39 | + output: PathBuf, |
| 40 | +} |
| 41 | + |
| 42 | +impl ExportCsr { |
| 43 | + fn run_command(&self, session: &Session) -> Result<()> { |
| 44 | + // Find the private key |
| 45 | + let mut attrs = helper::search_spec(self.id.as_deref(), self.label.as_deref())?; |
| 46 | + attrs.push(Attribute::Class(ObjectClass::PrivateKey.try_into()?)); |
| 47 | + attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?)); |
| 48 | + let private_key = helper::find_one_object(session, &attrs)?; |
| 49 | + |
| 50 | + // Determine public key label |
| 51 | + let pub_label_string = if let Some(l) = self.label.as_deref() { |
| 52 | + if l.ends_with(".priv") { |
| 53 | + Some(l.replace(".priv", ".pub")) |
| 54 | + } else { |
| 55 | + Some(l.to_string()) |
| 56 | + } |
| 57 | + } else { |
| 58 | + None |
| 59 | + }; |
| 60 | + let pub_label = pub_label_string.as_deref(); |
| 61 | + |
| 62 | + // Find the public key (needed for CSR) |
| 63 | + let mut pub_attrs = helper::search_spec(self.id.as_deref(), pub_label)?; |
| 64 | + pub_attrs.push(Attribute::Class(ObjectClass::PublicKey.try_into()?)); |
| 65 | + pub_attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?)); |
| 66 | + let public_key = helper::find_one_object(session, &pub_attrs)?; |
| 67 | + |
| 68 | + // Get public key value |
| 69 | + let map = AttributeMap::from_object(session, public_key)?; |
| 70 | + let val = map.get(&AttributeType::Value).ok_or(anyhow!("Public key does not contain a value"))?; |
| 71 | + let pub_key_bytes: Vec<u8> = val.try_into()?; |
| 72 | + |
| 73 | + // Construct Subject Name |
| 74 | + let subject = Name::from_str(&self.subject).map_err(|e| anyhow!("Invalid subject: {}", e))?; |
| 75 | + |
| 76 | + // Create CertReqInfo |
| 77 | + let algorithm = AlgorithmIdentifierOwned { |
| 78 | + oid: OID_MLDSA_87, |
| 79 | + parameters: None, |
| 80 | + }; |
| 81 | + let subject_public_key_info = SubjectPublicKeyInfoOwned { |
| 82 | + algorithm: algorithm.clone(), |
| 83 | + subject_public_key: x509_cert::der::asn1::BitString::from_bytes(&pub_key_bytes) |
| 84 | + .map_err(|e| anyhow!("Invalid public key bytes: {}", e))?, |
| 85 | + }; |
| 86 | + |
| 87 | + let info = CertReqInfo { |
| 88 | + version: x509_cert::request::Version::V1, |
| 89 | + subject, |
| 90 | + public_key: subject_public_key_info, |
| 91 | + attributes: Default::default(), |
| 92 | + }; |
| 93 | + |
| 94 | + // Serialize Info to sign |
| 95 | + let tbs_bytes = info.to_der().map_err(|e| anyhow!("Failed to encode CertReqInfo: {}", e))?; |
| 96 | + |
| 97 | + // Sign the request using HSM |
| 98 | + // Using VendorDefinedMechanism for MLDSA signature generation |
| 99 | + // to avoid type mismatch with native Mechanism::MlDsa if params are tricky. |
| 100 | + let mechanism = Mechanism::VendorDefined(VendorDefinedMechanism::new::<()>( |
| 101 | + MechanismType::MlDsa.try_into()?, |
| 102 | + None, |
| 103 | + )); |
| 104 | + |
| 105 | + let signature_bytes = session.sign(&mechanism, private_key, &tbs_bytes) |
| 106 | + .map_err(|e| anyhow!("HSM signing failed: {}", e))?; |
| 107 | + |
| 108 | + let signature = x509_cert::der::asn1::BitString::from_bytes(&signature_bytes) |
| 109 | + .map_err(|e| anyhow!("Invalid signature bytes: {}", e))?; |
| 110 | + |
| 111 | + let cert_req = CertReq { |
| 112 | + info, |
| 113 | + algorithm, |
| 114 | + signature, |
| 115 | + }; |
| 116 | + |
| 117 | + // Encode to PEM |
| 118 | + let pem = cert_req.to_pem(Default::default()) |
| 119 | + .map_err(|e| anyhow!("Failed to encode CSR to PEM: {}", e))?; |
| 120 | + |
| 121 | + fs::write(&self.output, pem.as_bytes())?; |
| 122 | + |
| 123 | + Ok(()) |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +#[typetag::serde(name = "mldsa-export-csr")] |
| 128 | +impl Dispatch for ExportCsr { |
| 129 | + fn run( |
| 130 | + &self, |
| 131 | + _context: &dyn Any, |
| 132 | + _hsm: &Module, |
| 133 | + session: Option<&Session>, |
| 134 | + ) -> Result<Box<dyn erased_serde::Serialize>> { |
| 135 | + let session = session.ok_or(HsmError::SessionRequired)?; |
| 136 | + self.run_command(session)?; |
| 137 | + Ok(Box::<BasicResult>::default()) |
| 138 | + } |
| 139 | +} |
0 commit comments