use crate::{DigestAlgorithm, SigningKey, SmimeError};
use std::time::SystemTime;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use cms::{
cert::CertificateChoices,
content_info::{CmsVersion, ContentInfo},
signed_data::{
CertificateSet, DigestAlgorithmIdentifiers, EncapsulatedContentInfo, SignatureValue,
SignedAttributes, SignedData, SignerIdentifier, SignerInfo, SignerInfos,
},
};
use der::{
asn1::{ObjectIdentifier, OctetStringRef, SetOfVec},
Any, AnyRef, Decode, Encode, Tag,
};
use sha2::{Digest, Sha256, Sha384, Sha512};
use spki::AlgorithmIdentifierOwned;
use x509_cert::{
attr::{Attribute, AttributeValue},
certificate::Certificate,
};
use const_oid::db::rfc5911::{ID_CONTENT_TYPE, ID_DATA, ID_MESSAGE_DIGEST, ID_SIGNING_TIME};
use const_oid::db::rfc5912::{
ECDSA_WITH_SHA_256, ECDSA_WITH_SHA_384, ID_EC_PUBLIC_KEY, ID_SHA_256, ID_SHA_384, ID_SHA_512,
RSA_ENCRYPTION, SHA_256_WITH_RSA_ENCRYPTION, SHA_384_WITH_RSA_ENCRYPTION,
SHA_512_WITH_RSA_ENCRYPTION,
};
pub fn sign(
content_mime: &[u8],
key: &dyn SigningKey,
now: SystemTime,
) -> Result<Vec<u8>, SmimeError> {
let cert = key.certificate();
let digest_alg = key
.preferred_digest_algorithm()
.unwrap_or_else(|| select_digest_for_cert(cert));
let eci = EncapsulatedContentInfo {
econtent_type: ID_DATA,
econtent: None,
};
let msg_digest = hash_content(content_mime, &digest_alg);
let ct_attr = make_content_type_attribute(ID_DATA)?;
let md_attr = make_message_digest_attribute(&msg_digest)?;
let st_attr = make_signing_time_attribute(now)?;
let mut attrs_vec: SetOfVec<Attribute> = SetOfVec::new();
attrs_vec.insert(ct_attr)?;
attrs_vec.insert(md_attr)?;
attrs_vec.insert(st_attr)?;
let signed_attrs: SignedAttributes = attrs_vec;
let attrs_der = signed_attrs.to_der()?;
let raw_sig = key.sign(&attrs_der, &digest_alg)?;
let sig_alg_oid = signature_algorithm_oid(cert, &digest_alg)?;
let signature_algorithm = AlgorithmIdentifierOwned {
oid: sig_alg_oid,
parameters: None,
};
let sid = SignerIdentifier::from(cert);
let digest_algorithm = AlgorithmIdentifierOwned {
oid: digest_alg_oid(&digest_alg),
parameters: None,
};
let signer_info_version = match &sid {
SignerIdentifier::IssuerAndSerialNumber(_) => CmsVersion::V1,
SignerIdentifier::SubjectKeyIdentifier(_) => CmsVersion::V3,
};
let signed_data_version = signer_info_version;
let signature_value = SignatureValue::new(raw_sig)?;
let signer_info = SignerInfo {
version: signer_info_version,
sid,
digest_alg: digest_algorithm.clone(),
signed_attrs: Some(signed_attrs),
signature_algorithm,
signature: signature_value,
unsigned_attrs: None,
};
let mut digest_alg_set: DigestAlgorithmIdentifiers = SetOfVec::new();
digest_alg_set.insert(digest_algorithm)?;
let mut cert_choices: CertificateSet = CertificateSet(SetOfVec::new());
cert_choices
.0
.insert(CertificateChoices::Certificate(cert.clone()))?;
let mut signer_infos_set: SignerInfos = SignerInfos(SetOfVec::new());
signer_infos_set.0.insert(signer_info)?;
let signed_data = SignedData {
version: signed_data_version,
digest_algorithms: digest_alg_set,
encap_content_info: eci,
certificates: Some(cert_choices),
crls: None,
signer_infos: signer_infos_set,
};
let sd_der = signed_data.to_der()?;
let content_ref = AnyRef::try_from(sd_der.as_slice())?;
let content_info = ContentInfo {
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
content: Any::from(content_ref),
};
let p7s_der = content_info.to_der()?;
let micalg = micalg_param(&digest_alg);
let boundary = random_boundary(content_mime)?;
let p7s_b64 = BASE64.encode(&p7s_der);
let p7s_b64_wrapped = wrap_base64(&p7s_b64, 76);
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(b"MIME-Version: 1.0\r\n");
out.extend_from_slice(
b"Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\";\r\n",
);
out.extend_from_slice(b"\tmicalg=");
out.extend_from_slice(micalg.as_bytes());
out.extend_from_slice(b"; boundary=\"");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\"\r\n");
out.extend_from_slice(b"\r\n");
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\r\n");
out.extend_from_slice(content_mime);
out.extend_from_slice(b"\r\n");
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\r\n");
out.extend_from_slice(b"Content-Type: application/pkcs7-signature; name=smime.p7s\r\n");
out.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
out.extend_from_slice(b"Content-Disposition: attachment; filename=smime.p7s\r\n");
out.extend_from_slice(b"\r\n");
out.extend_from_slice(p7s_b64_wrapped.as_bytes());
out.extend_from_slice(b"\r\n");
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"--\r\n");
Ok(out)
}
fn make_content_type_attribute(content_type: ObjectIdentifier) -> Result<Attribute, SmimeError> {
let value = AttributeValue::new(Tag::ObjectIdentifier, content_type.as_bytes())?;
let mut values: SetOfVec<AttributeValue> = SetOfVec::new();
values.insert(value)?;
Ok(Attribute {
oid: ID_CONTENT_TYPE,
values,
})
}
fn make_message_digest_attribute(digest: &[u8]) -> Result<Attribute, SmimeError> {
let os_ref = OctetStringRef::new(digest)?;
let value = AttributeValue::new(Tag::OctetString, os_ref.as_bytes())?;
let mut values: SetOfVec<AttributeValue> = SetOfVec::new();
values.insert(value)?;
Ok(Attribute {
oid: ID_MESSAGE_DIGEST,
values,
})
}
fn make_signing_time_attribute(now: SystemTime) -> Result<Attribute, SmimeError> {
let time = x509_cert::time::Time::try_from(now)
.map_err(|e| SmimeError::MalformedInput(format!("signing time out of range: {e}")))?;
let time_der = time.to_der()?;
let value = AttributeValue::from_der(&time_der)?;
let mut values: SetOfVec<AttributeValue> = SetOfVec::new();
values.insert(value)?;
Ok(Attribute {
oid: ID_SIGNING_TIME,
values,
})
}
fn hash_content(content: &[u8], alg: &DigestAlgorithm) -> Vec<u8> {
match alg {
DigestAlgorithm::Sha256 => Sha256::digest(content).to_vec(),
DigestAlgorithm::Sha384 => Sha384::digest(content).to_vec(),
DigestAlgorithm::Sha512 => Sha512::digest(content).to_vec(),
}
}
fn digest_alg_oid(alg: &DigestAlgorithm) -> ObjectIdentifier {
match alg {
DigestAlgorithm::Sha256 => ID_SHA_256,
DigestAlgorithm::Sha384 => ID_SHA_384,
DigestAlgorithm::Sha512 => ID_SHA_512,
}
}
fn micalg_param(alg: &DigestAlgorithm) -> &'static str {
match alg {
DigestAlgorithm::Sha256 => "sha-256",
DigestAlgorithm::Sha384 => "sha-384",
DigestAlgorithm::Sha512 => "sha-512",
}
}
fn signature_algorithm_oid(
cert: &Certificate,
digest_alg: &DigestAlgorithm,
) -> Result<ObjectIdentifier, SmimeError> {
let spki_oid = cert
.tbs_certificate()
.subject_public_key_info()
.algorithm
.oid;
if spki_oid == RSA_ENCRYPTION {
return Ok(match digest_alg {
DigestAlgorithm::Sha256 => SHA_256_WITH_RSA_ENCRYPTION,
DigestAlgorithm::Sha384 => SHA_384_WITH_RSA_ENCRYPTION,
DigestAlgorithm::Sha512 => SHA_512_WITH_RSA_ENCRYPTION,
});
}
if spki_oid == ID_EC_PUBLIC_KEY {
return match digest_alg {
DigestAlgorithm::Sha256 => Ok(ECDSA_WITH_SHA_256),
DigestAlgorithm::Sha384 => Ok(ECDSA_WITH_SHA_384),
DigestAlgorithm::Sha512 => Err(SmimeError::UnsupportedAlgorithm(
"P-521 keys are not supported; use P-256 or P-384".into(),
)),
};
}
Err(SmimeError::UnsupportedAlgorithm(format!(
"SPKI OID {spki_oid} not supported for signing"
)))
}
fn select_digest_for_cert(cert: &Certificate) -> DigestAlgorithm {
use const_oid::db::rfc5912::{SECP_384_R_1, SECP_521_R_1};
let spki = cert.tbs_certificate().subject_public_key_info();
if spki.algorithm.oid == ID_EC_PUBLIC_KEY {
let curve = spki
.algorithm
.parameters
.as_ref()
.and_then(|p| p.decode_as::<der::asn1::ObjectIdentifier>().ok());
if let Some(c) = curve {
if c == SECP_384_R_1 {
return DigestAlgorithm::Sha384;
}
if c == SECP_521_R_1 {
return DigestAlgorithm::Sha512;
}
}
}
DigestAlgorithm::Sha256
}
fn random_boundary(content: &[u8]) -> Result<String, SmimeError> {
for _ in 0..8 {
let mut rand_bytes = [0u8; 16];
getrandom::fill(&mut rand_bytes).map_err(|e| SmimeError::RngFailure(format!("{e}")))?;
let mut hex = String::with_capacity(32);
for b in rand_bytes {
use std::fmt::Write as _;
write!(hex, "{b:02x}").expect("writing to String cannot fail");
}
let boundary = format!("----=_Part_{hex}");
if !content
.windows(boundary.len())
.any(|w| w == boundary.as_bytes())
{
return Ok(boundary);
}
}
Err(SmimeError::RngFailure(
"MIME boundary collision: failed to generate a unique boundary after 8 attempts".into(),
))
}
fn wrap_base64(b64: &str, width: usize) -> String {
let mut out = String::with_capacity(b64.len() + (b64.len() / width + 1) * 2);
for chunk in b64.as_bytes().chunks(width) {
out.push_str(
core::str::from_utf8(chunk)
.expect("base64 output is a str — from_utf8 always succeeds"),
);
out.push_str("\r\n");
}
out
}