openpgp_piv_sequoia/
lib.rs

1//! Library for PIV HSM usage with Sequoia PGP.
2//!
3//! Example use, performing an OpenPGP decryption operation on a PIV device:
4//!
5//! ```no_run
6//! use openpgp_piv_sequoia::Opiv;
7//! use sequoia_openpgp::{parse::Parse, Cert};
8//! use yubikey::piv::SlotId;
9//!
10//! let mut opiv = Opiv::open(16019180)?;
11//! opiv.verify_pin("123456".as_bytes())?;
12//!
13//! let cert = Cert::from_file("example-cert.asc")?;
14//!
15//! opiv.decrypt(
16//!     SlotId::KeyManagement,
17//!     &mut std::io::stdin(),
18//!     &mut std::io::stdout(),
19//!     cert,
20//! )?;
21//!
22//! # Ok::<(), anyhow::Error>(())
23//! ```
24
25use std::sync::{Arc, Mutex};
26
27use anyhow::Context;
28use openpgp_x509_sequoia::types::{AlgorithmId, PublicKeyInfo};
29use sequoia_openpgp::crypto::mpi;
30use sequoia_openpgp::packet::key::{PublicParts, SecretKeyMaterial, SecretParts, UnspecifiedRole};
31use sequoia_openpgp::packet::Key;
32use sequoia_openpgp::parse::stream::DecryptorBuilder;
33use sequoia_openpgp::parse::Parse;
34use sequoia_openpgp::policy::NullPolicy;
35use sequoia_openpgp::types::Curve;
36use sequoia_openpgp::Cert;
37use x509_certificate::X509Certificate;
38use yubikey::certificate::CertInfo;
39use yubikey::piv::SlotId;
40use yubikey::{MgmKey, PinPolicy, TouchPolicy, YubiKey};
41
42use crate::decryptor::Helper;
43
44mod decryptor;
45mod signer;
46mod util;
47
48/// OpenPGP PIV device
49pub struct Opiv {
50    yk: YubiKey,
51}
52
53impl From<YubiKey> for Opiv {
54    fn from(yk: YubiKey) -> Self {
55        Self { yk }
56    }
57}
58
59impl Opiv {
60    /// Open PIV device by serial
61    pub fn open(serial: u32) -> anyhow::Result<Self> {
62        let yk =
63            YubiKey::open_by_serial(yubikey::Serial::from(serial)).context("card not found")?;
64
65        Ok(Opiv { yk })
66    }
67
68    /// Verify device PIN
69    pub fn verify_pin(&mut self, pin: &[u8]) -> anyhow::Result<()> {
70        Ok(self.yk.verify_pin(pin)?)
71    }
72
73    /// Find OpenPGP [`Key`] that corresponds to the key data in `slot`.
74    ///
75    /// A matching key is determined by comparing MPIs with the component keys
76    /// in `cert`.
77    pub fn key(
78        &mut self,
79        slot: SlotId,
80        cert: Cert,
81    ) -> anyhow::Result<Key<PublicParts, UnspecifiedRole>> {
82        if let Ok(ykcert) = yubikey::Certificate::read(&mut self.yk, slot) {
83            let x509 = ykcert.into_buffer();
84            let x509cert: X509Certificate = X509Certificate::from_der(x509.as_slice())?;
85
86            openpgp_x509_sequoia::find_key_by_x509cert(&x509cert, &cert)
87        } else {
88            Err(anyhow::anyhow!(
89                "Couldn't read certificate for slot {:?}",
90                slot
91            ))
92        }
93    }
94
95    /// Get a [`PivKeyPair`] that can perform OpenPGP signing or decryption
96    /// operations using the key material in `slot`.
97    ///
98    /// `cert` is required as a source of OpenPGP metadata (it must contain
99    /// a component key that matches the key material in `slot`).
100    pub fn keypair(mut self, slot: SlotId, cert: Cert) -> anyhow::Result<PivKeyPair> {
101        let key = self.key(slot, cert)?;
102
103        let yk = Arc::new(Mutex::new(self.yk));
104        Ok(PivKeyPair::new(key, slot, yk))
105    }
106
107    /// Perform a signing operation on the card, using the key material in
108    /// `slot`.
109    ///
110    /// `cert` is required as a source of OpenPGP metadata (it must contain
111    /// a component key that matches the key material in `slot`).
112    pub fn sign(
113        self,
114        slot: SlotId,
115        input: &mut (dyn std::io::Read + Send + Sync),
116        output: &mut (dyn std::io::Write + Send + Sync),
117        cert: Cert,
118    ) -> anyhow::Result<()> {
119        let keypair = self.keypair(slot, cert)?;
120
121        signer::sign_on_card(keypair, input, output)
122    }
123
124    /// Perform a decryption operation on the card, using the key material in
125    /// `slot`.
126    ///
127    /// `cert` is required as a source of OpenPGP metadata (it must contain
128    /// a component key that matches the key material in `slot`).
129    pub fn decrypt(
130        mut self,
131        slot: SlotId,
132        input: &mut (dyn std::io::Read + Send + Sync),
133        output: &mut (dyn std::io::Write + Send + Sync),
134        cert: Cert,
135    ) -> anyhow::Result<()> {
136        // Get matching OpenPGP public key for id from cert
137        let public = self.key(slot, cert)?;
138
139        let helper = Helper {
140            yk: Arc::new(Mutex::new(self.yk)),
141            slot,
142            public: &public,
143        };
144
145        // Now, create a decryptor with a helper using the given Certs.
146        let policy = &NullPolicy::new();
147        let mut decryptor =
148            DecryptorBuilder::from_reader(input)?.with_policy(policy, None, helper)?;
149
150        // Decrypt the data.
151        std::io::copy(&mut decryptor, output)?;
152
153        Ok(())
154    }
155
156    /// Upload key material from OpenPGP `key` to the specified `slot`.
157    ///
158    /// Also sets the corresponding public key data and a self-signed
159    /// certificate for the slot.
160    pub fn upload_key(
161        &mut self,
162        key: &Key<SecretParts, UnspecifiedRole>,
163        slot: SlotId,
164        common_name: &str,
165        pin: &[u8],
166        mgm_key: MgmKey,
167    ) -> anyhow::Result<()> {
168        // -- Transform the subkey 'key' into piv data structures --
169
170        // Handle private cryptographic material
171        let unenc = if let SecretKeyMaterial::Unencrypted(ref u) = key.secret() {
172            u
173        } else {
174            return Err(anyhow::anyhow!("Can't access private key material"));
175        };
176
177        // Container to temporarily keep private key material
178        enum PrivateKey {
179            R(yubikey::piv::RsaKeyData),
180            E(mpi::ProtectedMPI),
181        }
182
183        let secret_key_material = unenc.map(|mpis| mpis.clone());
184        let privkey = match secret_key_material {
185            mpi::SecretKeyMaterial::RSA { p, q, .. } => {
186                PrivateKey::R(yubikey::piv::RsaKeyData::new(p.value(), q.value()))
187            }
188            mpi::SecretKeyMaterial::ECDSA { scalar } | mpi::SecretKeyMaterial::ECDH { scalar } => {
189                PrivateKey::E(scalar)
190            }
191            s => {
192                return Err(anyhow::anyhow!(
193                    "Unsupported type of SecretKeyMaterial: {:?}",
194                    s.algo()
195                ))
196            }
197        };
198
199        // - make yubikey::certificate::PublicKeyInfo
200        let pub_key_info = match key.parts_as_public().mpis() {
201            mpi::PublicKey::RSA { e, n } => {
202                match n.value().len() {
203                    // Rsa2048 (allow for some leading 0 bits)
204                    248..=256 => {
205                        let rsa_pub = rsa::RsaPublicKey::new(
206                            rsa::BigUint::from_bytes_be(n.value()),
207                            rsa::BigUint::from_bytes_be(e.value()),
208                        )?;
209
210                        PublicKeyInfo::Rsa {
211                            algorithm: AlgorithmId::Rsa2048,
212                            pubkey: rsa_pub,
213                        }
214                    }
215                    len => {
216                        return Err(anyhow::anyhow!("Unexpected RSA modulus length {}", len * 8))
217                    }
218                }
219            }
220            mpi::PublicKey::ECDH { curve, q, .. } | mpi::PublicKey::ECDSA { curve, q, .. } => {
221                match curve {
222                    Curve::NistP256 => {
223                        let p256 = p256::EncodedPoint::from_bytes(q.value()).map_err(|e| {
224                            anyhow::anyhow!("Error while creating EncodedPoint: {e:?}")
225                        })?;
226
227                        PublicKeyInfo::EcP256(p256)
228                    }
229                    Curve::NistP384 => {
230                        let p384 = p384::EncodedPoint::from_bytes(q.value()).map_err(|e| {
231                            anyhow::anyhow!("Error while creating EncodedPoint: {e:?}")
232                        })?;
233
234                        PublicKeyInfo::EcP384(p384)
235                    }
236                    _ => return Err(anyhow::anyhow!("Unsupported curve {curve:?}")),
237                }
238            }
239
240            pk => return Err(anyhow::anyhow!("Unexpected public key {:?}", pk)),
241        };
242
243        let reader_name = self.yk.name();
244        log::trace!("Using '{reader_name}' [{}]", self.yk.version());
245
246        self.yk.authenticate(mgm_key)?;
247        log::trace!("MgmKey authenticated");
248
249        // -- upload priv key to card in Slot 'slot' --
250        log::trace!("Upload private key");
251        match privkey {
252            PrivateKey::R(rsa) => yubikey::piv::import_rsa_key(
253                &mut self.yk,
254                slot,
255                // Only Rsa2048 can arrive here
256                yubikey::piv::AlgorithmId::Rsa2048,
257                rsa,
258                TouchPolicy::Default,
259                PinPolicy::Default,
260            )?,
261            PrivateKey::E(scalar) => {
262                let algo = match pub_key_info {
263                    PublicKeyInfo::EcP256(_) => yubikey::piv::AlgorithmId::EccP256,
264                    PublicKeyInfo::EcP384(_) => yubikey::piv::AlgorithmId::EccP384,
265                    _ => {
266                        return Err(anyhow::anyhow!(
267                            "Unexpected ECC algorithm {:?}",
268                            pub_key_info.algorithm()
269                        ))
270                    }
271                };
272
273                yubikey::piv::import_ecc_key(
274                    &mut self.yk,
275                    slot,
276                    algo,
277                    scalar.value(),
278                    TouchPolicy::Default,
279                    PinPolicy::Default,
280                )?
281            }
282        }
283
284        // -- generate and upload certificate --
285        let tbs_cert = openpgp_x509_sequoia::generate_x509(&pub_key_info, key, common_name, &[]);
286
287        self.yk.verify_pin(pin)?;
288        log::trace!("PIN verified\n");
289
290        // function to self-sign
291        let mut signer = |data: &[u8], algo: AlgorithmId| {
292            let data = match algo {
293                AlgorithmId::Rsa2048 => {
294                    let em_len = 2048 / 8;
295                    util::pad_pkcs1_5(data, em_len)?
296                }
297                _ => data.to_vec(),
298            };
299
300            let algo: yubikey::piv::AlgorithmId = match algo {
301                AlgorithmId::Rsa2048 => Ok(yubikey::piv::AlgorithmId::Rsa2048),
302                AlgorithmId::EccP256 => Ok(yubikey::piv::AlgorithmId::EccP256),
303                AlgorithmId::EccP384 => Ok(yubikey::piv::AlgorithmId::EccP384),
304                _ => Err(anyhow::anyhow!("Unsupported algo {algo:?}")),
305            }?;
306
307            yubikey::piv::sign_data(&mut self.yk, &data, algo, slot)
308                .map(|b| b.to_vec())
309                .map_err(|e| e.into())
310        };
311
312        // - assemble cert -
313        let cert =
314            openpgp_x509_sequoia::self_sign_x509(tbs_cert, pub_key_info.algorithm(), &mut signer)?;
315
316        // Write cert to card
317        let cert = yubikey::Buffer::from(cert);
318        let cert = yubikey::certificate::Certificate::from_bytes(cert)?;
319
320        cert.write(&mut self.yk, slot, CertInfo::Uncompressed)?;
321        log::debug!("created certificate object");
322
323        Ok(())
324    }
325
326    /// Reset PIV device
327    pub fn reset(&mut self) -> anyhow::Result<()> {
328        const BAD_PIV_PIN: &[u8] = &[1, 1, 1]; // FIXME
329
330        // Block pin and puk
331        for _ in 1..5 {
332            let _ = self.yk.verify_pin(BAD_PIV_PIN); // "verify" with wrong PIN to block
333        }
334        self.yk.block_puk()?;
335
336        self.yk.reset_device().context("Reset failed")
337    }
338
339    /// XXX: escape hatch for direct device access (will be removed)
340    pub fn as_yk(&self) -> &YubiKey {
341        &self.yk
342    }
343
344    /// XXX: escape hatch for direct device access (will be removed)
345    pub fn as_yk_mut(&mut self) -> &mut YubiKey {
346        &mut self.yk
347    }
348
349    /// XXX: escape hatch for direct device access (will be removed)
350    pub fn into_yk(self) -> YubiKey {
351        self.yk
352    }
353}
354
355/// PIV implementation of [`sequoia_openpgp::crypto::Signer`]
356/// and [`sequoia_openpgp::crypto::Decryptor`].
357pub struct PivKeyPair {
358    public: Key<PublicParts, UnspecifiedRole>,
359    slot: SlotId,
360    yk: Arc<Mutex<YubiKey>>,
361}
362
363impl PivKeyPair {
364    pub fn new(
365        public: Key<PublicParts, UnspecifiedRole>,
366        slot: SlotId,
367        yk: Arc<Mutex<YubiKey>>,
368    ) -> Self {
369        Self { public, slot, yk }
370    }
371}