Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ureq http client with tls support
Browse files Browse the repository at this point in the history
- supports both native-tls and rustls
mzachar committed Oct 15, 2024
1 parent b5ae19f commit 5397285
Showing 14 changed files with 592 additions and 562 deletions.
494 changes: 460 additions & 34 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -50,6 +50,11 @@ pbkdf2 = { version = "0.12", optional = true, default-features = false, features
serde_json = { version = "1", optional = true }
rusb = { version = "0.9.4", optional = true }
tiny_http = { version = "0.12", optional = true }
ureq = { version = "2.10.1", optional = true, default-features = false, features = ["gzip"] }
native-tls = { version = "0", optional = true }
rustls = { version = "0", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2.1.3", optional = true }
rustls-platform-verifier = { version = "0.3.4", optional = true }

[dev-dependencies]
ed25519-dalek = "2"
@@ -61,14 +66,19 @@ x509-cert = { version = "0.2.5", features = ["builder"] }
[features]
default = ["http", "passwords", "setup"]
http-server = ["tiny_http"]
http = []
http = ["ureq"]
native-tls = ["ureq/native-tls", "dep:native-tls", "_tls"]
native-tls-vendored = ["native-tls", "native-tls/vendored"]
rustls = ["ureq/tls", "dep:rustls", "dep:rustls-pemfile", "dep:rustls-platform-verifier", "_tls"]
mockhsm = ["ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"]
passwords = ["hmac", "pbkdf2"]
secp256k1 = ["k256"]
setup = ["passwords", "serde_json", "uuid/serde"]
untested = []
usb = ["rusb"]

_tls = []

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
19 changes: 7 additions & 12 deletions src/connector/error.rs
Original file line number Diff line number Diff line change
@@ -4,9 +4,6 @@ use crate::error::{BoxError, Context};
use std::{fmt, io, num::ParseIntError, str::Utf8Error};
use thiserror::Error;

#[cfg(feature = "http")]
use super::http;

/// `yubihsm-connector` related errors
pub type Error = crate::Error<ErrorKind>;

@@ -67,15 +64,13 @@ impl From<io::Error> for Error {
}

#[cfg(feature = "http")]
impl From<http::client::Error> for Error {
fn from(err: http::client::Error) -> Error {
impl From<ureq::Error> for Error {
fn from(err: ureq::Error) -> Error {
let kind = match err.kind() {
http::client::ErrorKind::AddrInvalid => ErrorKind::AddrInvalid,
http::client::ErrorKind::IoError => ErrorKind::IoError,
http::client::ErrorKind::ParseError | http::client::ErrorKind::ResponseError => {
ErrorKind::ResponseError
}
http::client::ErrorKind::RequestError => ErrorKind::RequestError,
ureq::ErrorKind::Dns => ErrorKind::AddrInvalid,
ureq::ErrorKind::Io => ErrorKind::IoError,
ureq::ErrorKind::HTTP => ErrorKind::ResponseError,
_ => ErrorKind::RequestError,
};

kind.context(err).into()
@@ -91,7 +86,7 @@ impl From<rusb::Error> for Error {
rusb::Error::Pipe => format_err!(ErrorKind::UsbError, "lost connection to USB device"),
_ => format_err!(ErrorKind::UsbError, "{}", err),
}
.into()
.into()
}
}

3 changes: 2 additions & 1 deletion src/connector/http.rs
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@
//!
//! <https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-connector/>
pub(super) mod client;
mod config;
#[cfg(feature = "http")]
mod connection;
#[cfg(feature = "http-server")]
mod server;
@@ -12,6 +12,7 @@ pub use self::config::HttpConfig;
#[cfg(feature = "http-server")]
pub use self::server::Server;

#[cfg(feature = "http")]
use self::connection::HttpConnection;
use crate::connector::{self, Connectable, Connection};

18 changes: 0 additions & 18 deletions src/connector/http/client.rs

This file was deleted.

90 changes: 0 additions & 90 deletions src/connector/http/client/connection.rs

This file was deleted.

149 changes: 0 additions & 149 deletions src/connector/http/client/error.rs

This file was deleted.

39 changes: 0 additions & 39 deletions src/connector/http/client/path.rs

This file was deleted.

18 changes: 0 additions & 18 deletions src/connector/http/client/request.rs

This file was deleted.

6 changes: 0 additions & 6 deletions src/connector/http/client/response.rs

This file was deleted.

18 changes: 0 additions & 18 deletions src/connector/http/client/response/body.rs

This file was deleted.

165 changes: 0 additions & 165 deletions src/connector/http/client/response/reader.rs

This file was deleted.

26 changes: 25 additions & 1 deletion src/connector/http/config.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
#[cfg(feature = "_tls")]
use std::path::PathBuf;


/// Default timeouts for reading and writing (5 seconds)
pub const DEFAULT_TIMEOUT_MILLIS: u64 = 5000;
@@ -15,6 +18,14 @@ pub struct HttpConfig {
/// Port `yubihsm-connector` process is listening on
pub port: u16,

/// Use https if true
#[cfg(feature = "_tls")]
pub tls: bool,

/// CA certificate to validate the server certificate
#[cfg(feature = "_tls")]
pub cacert: Option<PathBuf>,

/// Timeout for connecting, reading, and writing in milliseconds
pub timeout_ms: u64,
}
@@ -28,6 +39,12 @@ impl Default for HttpConfig {
// Default `yubihsm-connector` port
port: 12345,

#[cfg(feature = "_tls")]
tls: false,

#[cfg(feature = "_tls")]
cacert: None,

// 5 seconds
timeout_ms: DEFAULT_TIMEOUT_MILLIS,
}
@@ -36,7 +53,14 @@ impl Default for HttpConfig {

impl Display for HttpConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO: HTTPS support
#[cfg(feature = "_tls")]
if self.tls {
write!(f, "https://{}:{}", self.addr, self.port)
} else {
write!(f, "http://{}:{}", self.addr, self.port)
}

#[cfg(not(feature = "_tls"))]
write!(f, "http://{}:{}", self.addr, self.port)
}
}
97 changes: 87 additions & 10 deletions src/connector/http/connection.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
//! Persistent HTTP connection to `yubihsm-connector`
use super::{client, config::HttpConfig};
use std::io::Read;
use std::time::Duration;
#[cfg(feature = "_tls")]
use std::sync::Arc;
#[cfg(feature = "native-tls")]
use native_tls::{Certificate, TlsConnector};
#[cfg(feature = "rustls")]
use rustls::ClientConfig;
use ureq::{Agent, AgentBuilder};
use super::{config::HttpConfig};
use crate::connector::{self, Connection};
use uuid::Uuid;

const MAX_BODY_SIZE: u64 = 1024 ^ 3;/*1MB*/
const USER_AGENT: &str = concat!("yubihsm.rs ", env!("CARGO_PKG_VERSION"));

/// Connection to YubiHSM via HTTP requests to `yubihsm-connector`.
///
/// The `yubihsm-connector` service is a small HTTP(S) service which exposes a
@@ -15,29 +27,52 @@ use uuid::Uuid;
/// <https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-connector/>
pub struct HttpConnection {
/// HTTP connection
connection: client::Connection,
agent: Agent,

base_url: String,
}

impl HttpConnection {
/// Open a connection to a `yubihsm-connector` service
pub(crate) fn open(config: &HttpConfig) -> Result<Self, connector::Error> {
let connection = client::Connection::open(&config.addr, config.port, &Default::default())?;
let mut agent = AgentBuilder::new()
.timeout(Duration::from_millis(config.timeout_ms))
.user_agent(USER_AGENT);

#[cfg(feature = "native-tls")]
if config.tls {
agent = agent.tls_connector(Arc::new(build_tls_connector(config)?));
}

Ok(HttpConnection { connection })
#[cfg(feature = "rustls")]
if config.tls {
agent = agent.tls_config(Arc::new(build_tls_config(config)?));
}

Ok(HttpConnection {
agent: agent.build(),
base_url: format!("{config}"),
})
}

/// Make an HTTP POST request to a `yubihsm-connector` service
pub(super) fn post(
&self,
path: &str,
_uuid: Uuid,
uuid: Uuid,
body: &[u8],
) -> Result<Vec<u8>, connector::Error> {
// TODO: send UUID as `X-Request-ID` header, zero copy body creation
Ok(self
.connection
.post(path, &client::request::Body::new(body))?
.into_vec())
let response = self.agent.post(&format!("{}{}", self.base_url, path))
.set("X-Request-ID", &uuid.to_string())
.send_bytes(body)?;

let mut data = response.header("Content-Length")
.and_then(|len| len.parse::<usize>().ok())
.map(|len| Vec::with_capacity(len))
.unwrap_or(Vec::new());

response.into_reader().take(MAX_BODY_SIZE).read_to_end(&mut data)?;
Ok(data)
}
}

@@ -52,3 +87,45 @@ impl Connection for HttpConnection {
.map(Into::into)
}
}

#[cfg(feature = "native-tls")]
fn build_tls_connector(config: &HttpConfig) -> Result<TlsConnector, connector::Error> {
use std::fs;
use crate::connector::ErrorKind;

let mut builder = TlsConnector::builder();

if let Some(path) = config.cacert.as_ref() {
let data = fs::read(path)?;
let cert = Certificate::from_pem(&data)
.map_err(|e| ErrorKind::IoError.context(e))?;

builder.add_root_certificate(cert);
}

builder.build()
.map_err(|e| ErrorKind::IoError.context(e).into())
}

#[cfg(feature = "rustls")]
pub fn build_tls_config(config: &HttpConfig) -> Result<ClientConfig, connector::Error> {
use std::fs::File;
use std::io::BufReader;
use rustls::RootCertStore;
use crate::connector::ErrorKind;

match config.cacert.as_ref() {
None => Ok(rustls_platform_verifier::tls_config()),
Some(cert_path) => {
let mut root_store = RootCertStore::empty();
let mut reader = BufReader::new(File::open(cert_path)?);
for cert in rustls_pemfile::certs(&mut reader) {
root_store.add(cert?)
.map_err(|e| ErrorKind::IoError.context(e))?;
}
Ok(ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth())
}
}
}

0 comments on commit 5397285

Please sign in to comment.