summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWiktor Kwapisiewicz <wiktor@metacode.biz>2021-09-28 11:10:33 +0200
committerWiktor Kwapisiewicz <wiktor@metacode.biz>2021-11-03 10:09:21 +0100
commit4b887a8cea6d7893b98874d4e4ff29e2ebe76543 (patch)
tree270c1698d3b1ba62981a380552fe8d69d3dacfa8
parent8785a2b7b154663989ee328096af037d4fb17b3f (diff)
net: Implement Signer and Decryptor for remote keys.
- Add sequoia_net::pks::unlock_signer. - Add sequoia_net::pks::unlock_decryptor.
-rw-r--r--Cargo.lock1
-rw-r--r--net/Cargo.toml3
-rw-r--r--net/src/lib.rs5
-rw-r--r--net/src/pks.rs257
4 files changed, 265 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9c04d915..d08a8d3f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2805,6 +2805,7 @@ name = "sequoia-net"
version = "0.23.0"
dependencies = [
"anyhow",
+ "base64",
"futures-util",
"http",
"hyper",
diff --git a/net/Cargo.toml b/net/Cargo.toml
index cd3ce1ed..6475598e 100644
--- a/net/Cargo.toml
+++ b/net/Cargo.toml
@@ -35,10 +35,11 @@ tempfile = "3.1"
thiserror = "1.0.2"
url = "2.1"
zbase32 = "0.1.2"
+tokio = { version = "1", features = [ "macros" ] }
+base64 = ">=0.12"
[dev-dependencies]
rand = { version = "0.7", default-features = false, features = [ "getrandom" ] }
-tokio = { version = "1", features = [ "macros" ] }
[lib]
bench = false
diff --git a/net/src/lib.rs b/net/src/lib.rs
index cf94557a..4f66b415 100644
--- a/net/src/lib.rs
+++ b/net/src/lib.rs
@@ -3,8 +3,12 @@
//! This crate provides access to keyservers using the [HKP] protocol,
//! and searching and publishing [Web Key Directories].
//!
+//! Additionally the `pks` module exposes private key operations using
+//! the [PKS][PKS] protocol.
+//!
//! [HKP]: https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
//! [Web Key Directories]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service
+//! [PKS]: https://gitlab.com/wiktor/pks
//!
//! # Examples
//!
@@ -56,6 +60,7 @@ use openpgp::{
serialize::Serialize,
};
+pub mod pks;
pub mod updates;
pub mod wkd;
diff --git a/net/src/pks.rs b/net/src/pks.rs
new file mode 100644
index 00000000..4c27c63a
--- /dev/null
+++ b/net/src/pks.rs
@@ -0,0 +1,257 @@
+//! 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 sequoia_openpgp as openpgp;
+
+use openpgp::packet::Key;
+use openpgp::packet::key::{PublicParts, UnspecifiedRole};
+use openpgp::crypto::{Password, Decryptor, Signer, mpi, SessionKey, ecdh};
+
+use hyper::{Body, Client, Uri, client::HttpConnector, Request};
+use hyper_tls::HttpsConnector;
+
+use super::Result;
+use url::Url;
+
+/// Returns a capability URL for given key's capability.
+///
+/// Unlocks a key using given password and on success returns a capability
+/// URL that can be used for signing or decryption.
+fn create_uri(store_uri: &str, key: &Key<PublicParts, UnspecifiedRole>,
+ p: &Password, capability: &str) -> Result<Uri> {
+ let mut url = Url::parse(&store_uri)?;
+ let auth = if !url.username().is_empty() {
+ let credentials = format!("{}:{}", url.username(), url.password().unwrap_or_default());
+ Some(format!("Basic {}", base64::encode(credentials)))
+ } else {
+ None
+ };
+
+ let client = Client::builder().build(HttpsConnector::new());
+
+ url.query_pairs_mut().append_pair("capability", capability);
+
+ let uri: hyper::Uri = url.join(&key.fingerprint().to_hex())?.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 response = rt.block_on(client.request(request))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow::anyhow!("PKS Key unlock failed."));
+ }
+
+ if let Some(location) = response.headers().get("Location") {
+ Ok(location.to_str()?.parse()?)
+ } else {
+ Err(anyhow::anyhow!("Key unlock did not return a Location header."))
+ }
+}
+
+/// 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 capability = create_uri(store_uri.as_ref(), &key, p, "sign")?;
+ Ok(Box::new(PksClient::new(key, capability)?))
+}
+
+/// 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 capability = create_uri(store_uri.as_ref(), &key, p, "decrypt")?;
+ Ok(Box::new(PksClient::new(key, capability)?))
+}
+
+struct PksClient {
+ location: Uri,
+ public: Key<PublicParts, UnspecifiedRole>,
+ client: hyper::client::Client<HttpsConnector<HttpConnector>>,
+ rt: tokio::runtime::Runtime,
+}
+
+impl PksClient {
+ fn new(
+ public: Key<PublicParts, UnspecifiedRole>,
+ location: Uri,
+ ) -> Result<Self> {
+ let client = Client::builder().build(HttpsConnector::new());
+
+ let rt = tokio::runtime::Builder::new_current_thread()
+ .enable_io()
+ .enable_time()
+ .build()?;
+
+ Ok(Self { location, public, client, rt })
+ }
+
+ fn make_request<T>(&mut self, body: Vec<u8>, hash: T) -> Result<Vec<u8>>
+ where T: Into<Option<String>> {
+ let hash = hash.into();
+ let location = if let Some(hash) = hash {
+ format!("{}?hash={}", self.location, hash).parse::<Uri>()?
+ } else {
+ self.location.clone()
+ };
+
+ let request = Request::builder()
+ .method("POST")
+ .uri(location)
+ .body(Body::from(body))?;
+ let response = self.rt.block_on(self.client.request(request))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow::anyhow!("PKS Decryption failed."));
+ }
+
+ let response = self.rt.block_on(hyper::body::to_bytes(response))?.to_vec();
+ Ok(response)
+ }
+}
+
+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(), None)?.into())
+ ,
+ (mpi::Ciphertext::ECDH { e, .. }, mpi::PublicKey::ECDH { .. }) => {
+ #[allow(non_snake_case)]
+ let S = self.make_request(e.value().to_vec(), None)?.into();
+ Ok(ecdh::decrypt_unwrap(&self.public, &S, ciphertext)?)
+ },
+ (ciphertext, public) => Err(anyhow::anyhow!(
+ "Unsupported combination of ciphertext {:?} \
+ and public key {:?} ",
+ ciphertext,
+ public
+ )),
+ }
+ }
+}
+
+impl Signer for PksClient {
+ fn public(&self) -> &Key<PublicParts, UnspecifiedRole> {
+ &self.public
+ }
+
+ fn sign(
+ &mut self,
+ hash_algo: openpgp::types::HashAlgorithm,
+ digest: &[u8],
+ ) -> openpgp::Result<openpgp::crypto::mpi::Signature> {
+ use openpgp::types::PublicKeyAlgorithm;
+
+ let sig = self.make_request(digest.into(), hash_algo.to_string())?;
+
+ 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(anyhow::anyhow!(
+ "Unsupported combination of algorithm {:?} and pubkey {:?}",
+ pk_algo,
+ self.public
+ )),
+ }
+ }
+}