openpgp_x509_sequoia/
lib.rs

1//! Helper crate for using X.509 certificates in an OpenPGP context.
2//!
3//! (Work in progress, not yet intended for general use!)
4
5use std::ops::DerefMut;
6
7use anyhow::Result;
8use asn1_rs::nom::AsBytes;
9use chrono::{DateTime, Utc};
10use sequoia_openpgp::packet::key::{PublicParts, SecretParts, UnspecifiedRole};
11use sequoia_openpgp::packet::Key;
12use sequoia_openpgp::Cert;
13use sha2::{Digest, Sha256, Sha384, Sha512};
14use x509::der::write::{der_octet_string, der_sequence};
15use x509::write::algorithm_identifier;
16use x509_certificate::X509Certificate;
17use zeroize::Zeroizing;
18
19use crate::types::{AlgorithmId, DigestId, PublicKeyInfo, SigId};
20
21pub mod experimental;
22pub mod types;
23
24// FIXME: this is a Yubikey PIV constant, just used as a placeholder here
25const CB_OBJ_MAX: usize = 3072 - 9;
26
27fn aid_to_sid(algo_id: AlgorithmId) -> SigId {
28    match algo_id {
29        AlgorithmId::Rsa2048 => SigId::Sha256WithRsaEncryption,
30        AlgorithmId::Rsa3072 => SigId::Sha384WithRsaEncryption,
31        AlgorithmId::Rsa4096 => SigId::Sha512WithRsaEncryption,
32
33        AlgorithmId::EccP256 => SigId::EcdsaWithSha256,
34        AlgorithmId::EccP384 => SigId::EcdsaWithSha384,
35        AlgorithmId::EccP521 => SigId::EcdsaWithSha512,
36    }
37}
38
39/// Generate an X.509 certificate based on an OpenPGP key
40///
41/// Minimal metadata for the OpenPGP key is stored in the X.509 certificate:
42///
43/// - The X.509 serial is set to the OpenPGP V4 fingerprint
44/// - The X.509 "not before" field is set to the OpenPGP key creation time
45///
46/// (Note that these two metadata items correspond to what is stored
47/// on OpenPGP card devices)
48pub fn generate_x509(
49    subject_pki: &PublicKeyInfo,
50    key: &Key<SecretParts, UnspecifiedRole>,
51    common_name: &str,
52    extensions: &[x509::Extension<&'static [u64]>],
53) -> Zeroizing<Vec<u8>> {
54    let creation: DateTime<Utc> = key.creation_time().into();
55
56    // Set serial to key's OpenPGP V4 Fingerprint
57    let fp: [u8; 20] = key
58        .fingerprint()
59        .as_bytes()
60        .try_into()
61        .expect("fingerprint len != 20");
62    let serial: Vec<u8> = fp.into();
63
64    let subject = x509::RelativeDistinguishedName::common_name(common_name);
65
66    let signature_algorithm = aid_to_sid(subject_pki.algorithm());
67
68    // Serialize X.509 certificate into a Vec<u8>
69    let mut tbs_cert = Zeroizing::new(Vec::with_capacity(CB_OBJ_MAX));
70
71    cookie_factory::gen(
72        x509::write::tbs_certificate::<&mut Vec<u8>, SigId, PublicKeyInfo, &'static [u64]>(
73            &serial,
74            &signature_algorithm,
75            // Issuer and subject are the same in self-signed certificates.
76            &[subject.clone()],
77            creation,
78            None, // no expiration for now
79            &[subject],
80            subject_pki,
81            extensions,
82        ),
83        tbs_cert.deref_mut(),
84    )
85    .expect("can serialize to Vec");
86
87    tbs_cert
88}
89
90/// Self-sign an X.509 certificate
91#[allow(clippy::type_complexity)]
92pub fn self_sign_x509(
93    tbs_cert: Zeroizing<Vec<u8>>,
94    algo_id: AlgorithmId,
95    signer: &mut dyn FnMut(&[u8], AlgorithmId) -> Result<Vec<u8>>,
96) -> Result<Vec<u8>> {
97    fn sig<Alg: x509::AlgorithmIdentifier>(
98        h: &'_ [u8],
99        algorithm_ident: &'_ Alg,
100        algo_id: AlgorithmId,
101        signer: &mut dyn FnMut(&[u8], AlgorithmId) -> Result<Vec<u8>>,
102    ) -> Result<Vec<u8>> {
103        let t = cookie_factory::gen_simple(
104            der_sequence((algorithm_identifier(algorithm_ident), der_octet_string(h))),
105            vec![],
106        )
107        .expect("can serialize into Vec");
108
109        // make signature on card
110        signer(&t, algo_id)
111    }
112
113    let signature_algorithm = aid_to_sid(algo_id);
114
115    let signature: Vec<_> = match signature_algorithm {
116        SigId::Sha256WithRsaEncryption => {
117            let h = Sha256::digest(&tbs_cert);
118            sig(&h, &DigestId::Sha256, algo_id, signer)?
119        }
120        SigId::Sha384WithRsaEncryption => {
121            let h = Sha384::digest(&tbs_cert);
122            sig(&h, &DigestId::Sha384, algo_id, signer)?
123        }
124        SigId::Sha512WithRsaEncryption => {
125            let h = Sha512::digest(&tbs_cert);
126            sig(&h, &DigestId::Sha512, algo_id, signer)?
127        }
128        SigId::EcdsaWithSha256 => signer(&Sha256::digest(&tbs_cert), algo_id)?,
129        SigId::EcdsaWithSha384 => signer(&Sha384::digest(&tbs_cert), algo_id)?,
130        SigId::EcdsaWithSha512 => signer(&Sha512::digest(&tbs_cert), algo_id)?,
131    };
132
133    let mut data = Zeroizing::new(Vec::with_capacity(CB_OBJ_MAX));
134
135    cookie_factory::gen(
136        x509::write::certificate(&tbs_cert, &signature_algorithm, &signature),
137        data.deref_mut(),
138    )
139    .expect("can serialize to Vec");
140
141    Ok(data.to_vec())
142}
143
144/// Find the `sequoia_openpgp::packet::Key` from `cert` that
145/// matches the public key material in `X509Certificate`, if any
146pub fn find_key_by_x509cert(
147    x509cert: &X509Certificate,
148    cert: &Cert,
149) -> Result<Key<PublicParts, UnspecifiedRole>> {
150    use sequoia_openpgp::crypto::mpi::PublicKey;
151
152    let x509_cert = x509_certificate::rfc5280::Certificate::from(x509cert.clone());
153
154    if let Ok(rsa_pub) = x509cert.rsa_public_key_data() {
155        for k in cert.keys() {
156            if let PublicKey::RSA { n, .. } = k.key().mpis() {
157                let modulus = rsa_pub.modulus.as_slice();
158                if modulus.len() < n.value().len() {
159                    // x509 "modulus" is shorter than OpenPGP key "n".
160                    // We don't expect a need to add padding to the X.509
161                    // data, so we don't attempt to compare these two keys.
162                    // We assume that they can't be "the same".
163                    continue;
164                }
165
166                // Check if X.509 key and OpenPGP key have the same public
167                // key material.
168                //
169                // (unwrap is ok: we just checked that "modulus" is not
170                // shorter than n.)
171                if modulus == n.value_padded(modulus.len()).unwrap().as_ref() {
172                    return Ok(k.key().clone());
173                }
174            }
175        }
176    } else {
177        let ai = x509_cert.tbs_certificate.subject_public_key_info.algorithm;
178
179        if ai.algorithm.0.as_bytes() != [42, 134, 72, 206, 61, 2, 1] {
180            return Err(anyhow::anyhow!("Unexpected KeyAlgorithm {:?}", ai));
181        }
182
183        let ec = x509_cert
184            .tbs_certificate
185            .subject_public_key_info
186            .subject_public_key;
187
188        for k in cert.keys() {
189            match k.key().mpis() {
190                PublicKey::ECDSA { q, .. } | PublicKey::ECDH { q, .. } => {
191                    if ec.octet_bytes().as_ref() == q.value() {
192                        return Ok(k.key().clone());
193                    }
194                }
195                _ => {}
196            }
197        }
198    }
199
200    Err(anyhow::anyhow!("Didn't find matching key in Cert"))
201}