diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2024-05-30 11:31:55 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2024-05-30 11:31:55 +0200 |
commit | 376c1ed15c8cd6e6786fc5fd38bbb2d64875f8cc (patch) | |
tree | 2a9cd483685a063321fc4efb2f2f82a9ce48d61a | |
parent | b03c218566195745f7958b0e8f57e1964a9da978 (diff) |
net: Remove the `pks` module.
- The `pks` module provided a method to communicate with remote
servers providing decryption and signing services. However, there
are several problems with this:
- The protocol design does not fit into our vision of how a secret
key store should work, and with the advent of the Sequoia
Keystore (which does fit that vision) has become obsolete.
- There is no downstream consumer of this API in the Sequoia
project.
- There are no integration tests, so the code is difficult to
maintain.
- The code requires porting to newer crates, and without
integration tests and users this is difficult to do reliably.
- The name unfortunately collides with the OpenPGP Key Server and
associated protocol pks, an ancestor of hkp.
- Arguably, this module should have gone into the sequoia-ipc
crate and not into the sequoia-net crate in the first place.
- Remove it.
-rw-r--r-- | net/src/lib.rs | 1 | ||||
-rw-r--r-- | net/src/pks.rs | 392 |
2 files changed, 0 insertions, 393 deletions
diff --git a/net/src/lib.rs b/net/src/lib.rs index 063e55f9..2f8216ca 100644 --- a/net/src/lib.rs +++ b/net/src/lib.rs @@ -60,7 +60,6 @@ use sequoia_openpgp::{ #[macro_use] mod macros; pub mod dane; mod email; -pub mod pks; pub mod updates; pub mod wkd; diff --git a/net/src/pks.rs b/net/src/pks.rs deleted file mode 100644 index 479a3c5a..00000000 --- a/net/src/pks.rs +++ /dev/null @@ -1,392 +0,0 @@ -//! Private Key Store communication. -//! -//! Functions in this module can be used to sign and decrypt using -//! remote keys using the [Private Key Store][PKS] protocol. -//! -//! [PKS]: https://gitlab.com/wiktor/pks -//! # Examples -//! ``` -//! use sequoia_net::pks; -//! # let p: sequoia_openpgp::crypto::Password = vec![1, 2, 3].into(); -//! # let key = sequoia_openpgp::cert::CertBuilder::general_purpose(None, Some("alice@example.org")) -//! # .generate().unwrap().0.keys().next().unwrap().key().clone(); -//! -//! match pks::unlock_signer("http://localhost:3000/", key, &p) { -//! Ok(signer) => { /* use signer for signing */ }, -//! Err(e) => { eprintln!("Could not unlock signer: {:?}", e); } -//! } -//! ``` - -use std::convert::{TryFrom, TryInto}; - -use sequoia_openpgp as openpgp; - -use openpgp::packet::Key; -use openpgp::packet::key::{PublicParts, UnspecifiedRole}; -use openpgp::crypto::{Password, Decryptor, Signer, mpi, SessionKey, ecdh}; -use openpgp::types::HashAlgorithm; -use openpgp::Fingerprint; - -use hyper::{Body, Client, Uri, client::HttpConnector, Request, HeaderMap, header::HeaderValue}; -use hyper_tls::HttpsConnector; - -use base64::Engine; -use base64::engine::general_purpose::STANDARD as base64std; - -use super::Result; -use url::Url; - -#[derive(thiserror::Error, Debug)] -/// Errors returned from Private Key Store functions. -pub enum Error { - /// Unlocking the key did not return a Location header. - #[error("Key unlock did not return a Location header")] - NoKeyLocation, - - /// Unlocking the key failed with given error code. - /// - /// The error code is the [HTTP response code] returned by - /// the Private Key Store implementation. - /// - /// [HTTP response code]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status - #[error("Key unlock failed with error: {0}")] - KeyUnlockFailed(u16), - - /// Private Key Store operation failed with given error code. - /// - /// The error code is the [HTTP response code] returned by - /// the Private Key Store implementation. - /// - /// [HTTP response code]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status - #[error("Private Key Store operation failed: {0}")] - OperationFailed(u16), -} - -/// Contains a description of the unlocked key. -struct KeyDescriptor { - /// URL of the endpoint that can be used to interact with the key. - url: Uri, - /// List of content types that this key accepts. This value is `None` if the key - /// did not indicate accepted content types. - accepted_types: Option<Vec<String>>, -} - -impl TryFrom<&HeaderMap<HeaderValue>> for KeyDescriptor { - type Error = anyhow::Error; - - fn try_from(headers: &HeaderMap<HeaderValue>) -> Result<Self> { - if let Some(location) = headers.get("Location") { - let accepted_types = if let Some(accepted_types) = headers.get("Accept-Post") { - Some( - accepted_types - .to_str()? - .split(',') - .map(|typ| typ.trim().to_string()) - .collect::<Vec<_>>(), - ) - } else { - None - }; - Ok(Self { - url: location.to_str()?.parse()?, - accepted_types, - }) - } else { - Err(Error::NoKeyLocation.into()) - } - } -} - -/// Returns request parameters for given arguments. -/// -/// Computes target URL and optional authentication data for given input -/// arguments. -fn create_request_params(store_uri: &str, fingerprint: &Fingerprint, capability: &str) - -> Result<(Url, Option<String>)> { - let url = Url::parse(store_uri)?; - let auth = if !url.username().is_empty() { - let password = url.password().unwrap_or_default(); - let credentials = format!("{}:{}", url.username(), password); - Some(format!("Basic {}", base64std.encode(credentials))) - } else { - None - }; - let mut url = url.join(&fingerprint.to_hex())?; - - url.query_pairs_mut().append_pair("capability", capability); - Ok((url, auth)) -} - -/// Returns an unlocked key descriptor. -/// -/// Unlocks a key using the given password and on success returns a key descriptor -/// that can be used for signing or decryption. -fn create_descriptor(store_uri: &str, key: &Key<PublicParts, UnspecifiedRole>, - p: &Password, capability: &str) -> Result<KeyDescriptor> { - let fpr = &key.fingerprint(); - let (url, auth) = create_request_params(store_uri, fpr, capability)?; - let uri: hyper::Uri = url.as_str().parse()?; - let mut request = Request::builder() - .method("POST") - .uri(uri); - - if let Some(auth) = auth { - request = request.header(hyper::header::AUTHORIZATION, auth); - } - - let rt = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build()?; - - let request = request.body(Body::from(p.map(|p|p.as_ref().to_vec())))?; - - let client = Client::builder().build(HttpsConnector::new()); - let response = rt.block_on(client.request(request))?; - - if !response.status().is_success() { - return Err(Error::KeyUnlockFailed(response.status().into()).into()); - } - - response.headers().try_into() -} - -/// Unlock a remote key for signing. -/// -/// Look up a private key corresponding to the public key passed as a -/// parameter and return a [`Signer`] trait object that will utilize -/// that private key for signing. -/// -/// # Errors -/// -/// This function fails if the key cannot be found on the remote store -/// or if the password is not correct. -/// -/// # Examples -/// ``` -/// use sequoia_net::pks; -/// # let p: sequoia_openpgp::crypto::Password = vec![1, 2, 3].into(); -/// # let key = sequoia_openpgp::cert::CertBuilder::general_purpose(None, Some("alice@example.org")) -/// # .generate().unwrap().0.keys().next().unwrap().key().clone(); -/// -/// match pks::unlock_signer("http://localhost:3000/", key, &p) { -/// Ok(signer) => { /* use signer for signing */ }, -/// Err(e) => { eprintln!("Could not unlock signer: {:?}", e); } -/// } -/// ``` -pub fn unlock_signer(store_uri: impl AsRef<str>, key: Key<PublicParts, UnspecifiedRole>, - p: &Password) -> Result<Box<dyn Signer + Send + Sync>> { - let description = create_descriptor(store_uri.as_ref(), &key, p, "sign")?; - Ok(Box::new(PksClient::new(key, description)?)) -} - -/// Unlock a remote key for decryption. -/// -/// Look up a private key corresponding to the public key passed as a -/// parameter and return a [`Decryptor`] trait object that will utilize -/// that private key for decryption. -/// -/// # Errors -/// -/// This function fails if the key cannot be found on the remote store -/// or if the password is not correct. -/// -/// # Examples -/// ``` -/// use sequoia_net::pks; -/// # let p: sequoia_openpgp::crypto::Password = vec![1, 2, 3].into(); -/// # let key = sequoia_openpgp::cert::CertBuilder::general_purpose(None, Some("alice@example.org")) -/// # .generate().unwrap().0.keys().next().unwrap().key().clone(); -/// -/// match pks::unlock_decryptor("http://localhost:3000/", key, &p) { -/// Ok(decryptor) => { /* use decryptor for decryption */ }, -/// Err(e) => { eprintln!("Could not unlock decryptor: {:?}", e); } -/// } -/// ``` -pub fn unlock_decryptor(store_uri: impl AsRef<str>, key: Key<PublicParts, UnspecifiedRole>, - p: &Password) -> Result<Box<dyn Decryptor + Send + Sync>> { - let description = create_descriptor(store_uri.as_ref(), &key, p, "decrypt")?; - Ok(Box::new(PksClient::new(key, description)?)) -} - -struct PksClient { - location: Uri, - public: Key<PublicParts, UnspecifiedRole>, - client: hyper::client::Client<HttpsConnector<HttpConnector>>, - acceptable_hashes: Vec<HashAlgorithm>, - rt: tokio::runtime::Runtime, -} - -impl PksClient { - fn new( - public: Key<PublicParts, UnspecifiedRole>, - description: KeyDescriptor, - ) -> Result<Self> { - let client = Client::builder().build(HttpsConnector::new()); - - let rt = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build()?; - - let acceptable_types = description.accepted_types.unwrap_or_default(); - let mut acceptable_hashes = acceptable_types.iter().flat_map(|typ| match typ.as_ref() { - "application/vnd.pks.digest.sha1" => Some(HashAlgorithm::SHA1), - "application/vnd.pks.digest.sha256" => Some(HashAlgorithm::SHA256), - "application/vnd.pks.digest.sha384" => Some(HashAlgorithm::SHA384), - "application/vnd.pks.digest.sha512" => Some(HashAlgorithm::SHA512), - _ => None, - }).collect::<Vec<_>>(); - acceptable_hashes.sort(); - - Ok(Self { location: description.url, public, client, rt, acceptable_hashes }) - } - - fn make_request(&mut self, body: Vec<u8>, content_type: &str) -> Result<Vec<u8>> { - let request = Request::builder() - .method("POST") - .uri(&self.location) - .header("Content-Type", content_type) - .body(Body::from(body))?; - let response = self.rt.block_on(self.client.request(request))?; - - if !response.status().is_success() { - return Err(Error::OperationFailed(response.status().into()).into()); - } - - Ok(self.rt.block_on(hyper::body::to_bytes(response))?.to_vec()) - } -} - -impl Decryptor for PksClient { - fn public(&self) -> &Key<PublicParts, UnspecifiedRole> { - &self.public - } - - fn decrypt( - &mut self, - ciphertext: &mpi::Ciphertext, - plaintext_len: Option<usize>, - ) -> openpgp::Result<SessionKey> { - match (ciphertext, self.public.mpis()) { - (mpi::Ciphertext::RSA { c }, mpi::PublicKey::RSA { .. }) => - Ok(self.make_request(c.value().to_vec(), "application/vnd.pks.rsa.ciphertext")?.into()) - , - (mpi::Ciphertext::ECDH { e, .. }, mpi::PublicKey::ECDH { .. }) => { - #[allow(non_snake_case)] - let S = self.make_request(e.value().to_vec(), "application/vnd.pks.ecdh.point")?.into(); - Ok(ecdh::decrypt_unwrap2(&self.public, &S, ciphertext, - plaintext_len)?) - }, - (ciphertext, public) => Err(openpgp::Error::InvalidOperation(format!( - "unsupported combination of key pair {:?} \ - and ciphertext {:?}", - public, ciphertext)).into() - ), - } - } -} - -impl Signer for PksClient { - fn public(&self) -> &Key<PublicParts, UnspecifiedRole> { - &self.public - } - - fn acceptable_hashes(&self) -> &[HashAlgorithm] { - &self.acceptable_hashes - } - - fn sign( - &mut self, - hash_algo: openpgp::types::HashAlgorithm, - digest: &[u8], - ) -> openpgp::Result<openpgp::crypto::mpi::Signature> { - use openpgp::types::PublicKeyAlgorithm; - - let content_type = match hash_algo { - HashAlgorithm::SHA1 => "application/vnd.pks.digest.sha1", - HashAlgorithm::SHA256 => "application/vnd.pks.digest.sha256", - HashAlgorithm::SHA384 => "application/vnd.pks.digest.sha384", - HashAlgorithm::SHA512 => "application/vnd.pks.digest.sha512", - _ => "application/octet-stream", - }; - - let sig = self.make_request(digest.into(), content_type)?; - - match (self.public.pk_algo(), self.public.mpis()) { - #[allow(deprecated)] - (PublicKeyAlgorithm::RSASign, mpi::PublicKey::RSA { .. }) - | ( - PublicKeyAlgorithm::RSAEncryptSign, - mpi::PublicKey::RSA { .. }, - ) => - Ok(mpi::Signature::RSA { s: mpi::MPI::new(&sig) }), - (PublicKeyAlgorithm::EdDSA, mpi::PublicKey::EdDSA { .. }) => { - let r = mpi::MPI::new(&sig[..32]); - let s = mpi::MPI::new(&sig[32..]); - - Ok(mpi::Signature::EdDSA { r, s }) - } - ( - PublicKeyAlgorithm::ECDSA, - mpi::PublicKey::ECDSA { .. }, - ) => { - let len_2 = sig.len() / 2; - let r = mpi::MPI::new(&sig[..len_2]); - let s = mpi::MPI::new(&sig[len_2..]); - - Ok(mpi::Signature::ECDSA { r, s }) - } - - (pk_algo, _) => Err(openpgp::Error::InvalidOperation(format!( - "unsupported combination of algorithm {:?} and key {:?}", - pk_algo, self.public)).into()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - #[test] - fn test_decrypt_url() { - let fingerprint = &Fingerprint::from_str("43B24E4557BBCD10225EDDB97123242412A19C9B").unwrap(); - let (url, auth) = create_request_params("http://localhost:3000", fingerprint, "decrypt").unwrap(); - assert_eq!(url.as_str(), "http://localhost:3000/43B24E4557BBCD10225EDDB97123242412A19C9B?capability=decrypt"); - assert!(auth.is_none()); - } - - #[test] - fn test_sign_url() { - let fingerprint = &Fingerprint::from_str("43B24E4557BBCD10225EDDB97123242412A19C9B").unwrap(); - let (url, auth) = create_request_params("http://localhost:3000", fingerprint, "sign").unwrap(); - assert_eq!(url.as_str(), "http://localhost:3000/43B24E4557BBCD10225EDDB97123242412A19C9B?capability=sign"); - assert!(auth.is_none()); - } - - #[test] - fn test_sign_url_with_slash() { - let fingerprint = &Fingerprint::from_str("43B24E4557BBCD10225EDDB97123242412A19C9B").unwrap(); - let (url, auth) = create_request_params("http://localhost:3000/", fingerprint, "sign").unwrap(); - assert_eq!(url.as_str(), "http://localhost:3000/43B24E4557BBCD10225EDDB97123242412A19C9B?capability=sign"); - assert!(auth.is_none()); - } - - #[test] - fn test_sign_url_with_subdirectory() { - let fingerprint = &Fingerprint::from_str("43B24E4557BBCD10225EDDB97123242412A19C9B").unwrap(); - let (url, auth) = create_request_params("http://localhost:3000/keys/", fingerprint, "sign").unwrap(); - assert_eq!(url.as_str(), "http://localhost:3000/keys/43B24E4557BBCD10225EDDB97123242412A19C9B?capability=sign"); - assert!(auth.is_none()); - } - - #[test] - fn test_sign_url_with_credentials() { - let fingerprint = &Fingerprint::from_str("43B24E4557BBCD10225EDDB97123242412A19C9B").unwrap(); - let (url, auth) = create_request_params("http://a:b@localhost:3000", fingerprint, "sign").unwrap(); - assert_eq!(url.as_str(), "http://a:b@localhost:3000/43B24E4557BBCD10225EDDB97123242412A19C9B?capability=sign"); - assert_eq!("Basic YTpi", auth.unwrap()); - } -} |