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 super::Result;
use url::Url;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Key unlock did not return a Location header")]
NoKeyLocation,
#[error("Key unlock failed with error: {0}")]
KeyUnlockFailed(u16),
#[error("Private Key Store operation failed: {0}")]
OperationFailed(u16),
}
struct KeyDescriptor {
url: Uri,
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())
}
}
}
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 {}", base64::encode(credentials)))
} else {
None
};
let mut url = url.join(&fingerprint.to_hex())?;
url.query_pairs_mut().append_pair("capability", capability);
Ok((url, auth))
}
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()
}
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)?))
}
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_unwrap(&self.public, &S, ciphertext)?)
},
(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());
}
}