use std::io::Cursor;
use std::path::Path;
use std::{collections::HashMap, io::Read};
use anyhow::Context;
use bytes::{Buf, Bytes};
use openssl::ssl::{SslConnector, SslFiletype, SslMethod, SslVerifyMode, SslVersion};
use openssl::x509::X509;
use serde::Serialize;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite};
use tracing::{Instrument, instrument};
use crate::v1::connection::Connection;
use crate::v1::error::ClientError as Error;
pub struct Password(String);
impl Password {
pub(crate) fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Password").field(&"*****").finish()
}
}
impl std::fmt::Display for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Password").field(&"*****").finish()
}
}
impl From<String> for Password {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for Password {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl TryFrom<&Path> for Password {
type Error = Error;
fn try_from(value: &Path) -> Result<Self, Self::Error> {
let passphrase = std::fs::read_to_string(value)?
.lines()
.next()
.and_then(|pass| {
let pass = pass.trim();
if !pass.is_empty() { Some(pass) } else { None }
})
.ok_or_else(|| {
anyhow::anyhow!(
"Passphrase file {} does not contain a password on the first line",
value.display()
)
})?
.to_string();
Ok(Self(passphrase))
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum KeyType {
GnuPG {
real_name: Option<String>,
comment: Option<String>,
email: Option<String>,
expire_date: Option<String>,
},
Ecc,
Rsa,
}
impl std::fmt::Display for KeyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyType::GnuPG { .. } => write!(f, "gnupg"),
KeyType::Ecc => write!(f, "ECC"),
KeyType::Rsa => write!(f, "RSA"),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub enum CertificateType {
Ca,
CodeSigning,
SslServer,
}
impl std::fmt::Display for CertificateType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CertificateType::Ca => write!(f, "ca"),
CertificateType::CodeSigning => write!(f, "codesigning"),
CertificateType::SslServer => write!(f, "sslserver"),
}
}
}
#[derive(Serialize, Debug, Clone)]
pub(crate) enum Command {
ListUsers {
user: String,
},
UserInfo {
user: String,
name: String,
},
NewUser {
user: String,
name: String,
admin: bool,
},
DeleteUser {
user: String,
name: String,
},
ModifyUser {
user: String,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
admin: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
new_name: Option<String>,
},
KeyUserInfo {
user: String,
name: String,
key: String,
},
ModifyKeyUser {
user: String,
name: String,
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
key_admin: Option<bool>,
},
ListKeys {
user: String,
},
NewKey {
user: String,
key: String,
keytype: String,
#[serde(skip_serializing_if = "Option::is_none")]
initial_key_admin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name_real: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name_comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
expire_date: Option<String>,
},
ImportKey {
user: String,
key: String,
keytype: String,
#[serde(skip_serializing_if = "Option::is_none")]
initial_key_admin: Option<String>,
},
DeleteKey {
user: String,
key: String,
},
ModifyKey {
user: String,
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
new_name: Option<String>,
},
ListKeyUsers {
user: String,
key: String,
},
GrantKeyAccess {
user: String,
key: String,
name: String,
},
ChangeKeyExpiration {
user: String,
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
expire_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
subkey: Option<String>,
},
RevokeKeyAccess {
user: String,
key: String,
name: String,
},
GetPublicKey {
user: String,
key: String,
},
ChangePassphrase {
user: String,
key: String,
},
SignCertificate {
user: String,
issuer_key: String,
subject_key: String,
subject: String,
validity: String,
subject_certificate_name: String,
certificate_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
issuer_certificate_name: Option<String>,
},
SignPe {
user: String,
key: String,
cert_name: String,
},
ListBindingMethods {
user: String,
},
}
pub mod responses {
#[derive(Debug, Clone)]
pub struct User {
pub(crate) name: String,
pub(crate) admin: bool,
}
impl User {
pub fn name(&self) -> &str {
&self.name
}
pub fn admin(&self) -> bool {
self.admin
}
}
#[derive(Debug, Clone)]
pub struct KeyUserInfo {
pub(crate) user: String,
pub(crate) key: String,
pub(crate) admin: bool,
}
impl KeyUserInfo {
pub fn user(&self) -> &str {
&self.user
}
pub fn key(&self) -> &str {
&self.key
}
pub fn admin(&self) -> bool {
self.admin
}
}
#[derive(Debug, Clone)]
pub struct PublicKey {
pub(crate) key_name: String,
pub(crate) data: Vec<u8>,
}
impl PublicKey {
pub fn key_name(&self) -> &str {
&self.key_name
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.data.clone())
}
}
}
#[derive(Debug, Clone)]
pub struct Client {
tls_config: TlsConfig,
bridge_hostname: String,
bridge_port: u16,
server_hostname: String,
user_name: String,
}
#[derive(Debug, Clone)]
pub struct TlsConfig {
connector: SslConnector,
}
impl TlsConfig {
pub fn new<P: AsRef<std::path::Path>>(
certificate: P,
private_key: P,
private_key_passphrase: Option<P>,
certificate_authority: P,
) -> Result<Self, Error> {
let mut connector = SslConnector::builder(SslMethod::tls())?;
connector.set_verify(SslVerifyMode::PEER);
connector.set_min_proto_version(Some(SslVersion::TLS1_2))?;
connector.set_max_proto_version(Some(SslVersion::TLS1_2))?;
connector.set_ca_file(&certificate_authority)?;
let mut private_key_buf = vec![];
std::fs::File::open(private_key)?.read_to_end(&mut private_key_buf)?;
let private_key = match &private_key_passphrase {
Some(passphrase_path) => {
let mut passphrase = vec![];
std::fs::File::open(passphrase_path)?.read_to_end(&mut passphrase)?;
openssl::pkey::PKey::private_key_from_pem_passphrase(&private_key_buf, &passphrase)?
}
None => openssl::pkey::PKey::private_key_from_pem(&private_key_buf)?,
};
connector.set_private_key(&private_key)?;
connector.set_certificate_file(&certificate, SslFiletype::PEM)?;
connector.check_private_key()?;
Ok(Self {
connector: connector.build(),
})
}
pub fn ssl(&self, hostname: &str) -> Result<openssl::ssl::Ssl, Error> {
let ssl = self.connector.configure()?.into_ssl(hostname)?;
tracing::debug!(verify_mode=?ssl.ssl_context().verify_mode(), hostname=hostname, "Created SSL connection config");
Ok(ssl)
}
}
fn get_payload_pipe() -> (
tokio::task::JoinHandle<Result<Vec<u8>, std::io::Error>>,
tokio::io::WriteHalf<tokio::io::SimplexStream>,
) {
let (mut payload_reader, payload_writer) = tokio::io::simplex(4096);
let payload = tokio::spawn(
async move {
let mut payload = vec![];
payload_reader.read_to_end(&mut payload).await?;
tracing::debug!(payload_length = payload.len(), "Response payload received",);
Ok::<_, std::io::Error>(payload)
}
.in_current_span(),
);
(payload, payload_writer)
}
impl Client {
pub fn new(
tls_config: TlsConfig,
bridge_hostname: String,
bridge_port: u16,
server_hostname: String,
user_name: String,
) -> Self {
Self {
tls_config,
bridge_hostname,
bridge_port,
server_hostname,
user_name,
}
}
async fn connect(&self) -> Result<Connection, Error> {
let ssl = self.tls_config.ssl(&self.bridge_hostname)?;
Ok(Connection::connect((self.bridge_hostname.as_str(), self.bridge_port), ssl).await?)
}
#[instrument(skip_all)]
pub async fn users(&self, admin_passphrase: Password) -> Result<Vec<String>, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ListUsers {
user: self.user_name.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
let mut num_users = response
.fields
.get("num-users")
.map(|b| Bytes::from(b.clone()))
.ok_or(anyhow::anyhow!("missing expected field 'num-users'"))?;
if num_users.len() != 4 {
return Err(anyhow::anyhow!(
"the 'num-users' field was {} bytes; expected 4",
num_users.len()
)
.into());
}
let num_users: usize = num_users
.get_u32()
.try_into()
.context("the number of users couldn't be converted to usize")?;
let users = payload
.split(|byte| *byte == 0)
.filter_map(|name| {
if !name.is_empty() {
String::from_utf8(name.into()).ok()
} else {
None
}
})
.collect::<Vec<_>>();
if users.len() != num_users {
return Err(anyhow::anyhow!(
"Server response indicated {} users, but {} names were sent!",
num_users,
users.len()
)
.into());
}
Ok(users)
}
#[instrument(skip_all)]
pub async fn get_user(
&self,
admin_passphrase: Password,
name: String,
) -> Result<responses::User, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::UserInfo {
user: self.user_name.clone(),
name: name.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
let admin = response
.fields
.get("admin")
.and_then(|b| b.first())
.map(|b| *b == 1)
.ok_or(anyhow::anyhow!("missing expected field 'admin'"))?;
Ok(responses::User { name, admin })
}
#[instrument(skip_all)]
pub async fn create_user(
&self,
admin_passphrase: Password,
name: String,
admin: bool,
user_passphrase: Option<Password>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
user_passphrase
.as_ref()
.map(|p| inner_request.insert("new-password", p.as_bytes()));
let response = connection
.outer_request::<tokio::io::Empty>(
Command::NewUser {
user: self.user_name.clone(),
name,
admin,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn modify_user(
&self,
admin_passphrase: Password,
name: String,
new_name: Option<String>,
admin: Option<bool>,
user_passphrase: Option<Password>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
user_passphrase
.as_ref()
.map(|p| inner_request.insert("new-password", p.as_bytes()));
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ModifyUser {
user: self.user_name.clone(),
name,
new_name,
admin,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn delete_user(&self, admin_passphrase: Password, name: String) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::DeleteUser {
user: self.user_name.clone(),
name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn key_user_info(
&self,
admin_passphrase: Password,
name: String,
key: String,
) -> Result<responses::KeyUserInfo, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::KeyUserInfo {
user: self.user_name.clone(),
name: name.clone(),
key: key.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
let admin = response
.fields
.get("key-admin")
.and_then(|b| b.first())
.map(|b| *b == 1)
.ok_or(anyhow::anyhow!("missing expected field 'admin'"))?;
Ok(responses::KeyUserInfo {
user: name,
key,
admin,
})
}
#[instrument(skip_all)]
pub async fn modify_key_user(
&self,
admin_passphrase: Password,
name: String,
key: String,
key_admin: Option<bool>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ModifyKeyUser {
user: self.user_name.clone(),
name: name.clone(),
key: key.clone(),
key_admin,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn keys(&self, admin_passphrase: Password) -> Result<Vec<String>, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ListKeys {
user: self.user_name.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
let mut num_keys = response
.fields
.get("num-keys")
.map(|b| Bytes::from(b.clone()))
.ok_or(anyhow::anyhow!("missing expected field 'num-keys'"))?;
if num_keys.len() != 4 {
return Err(anyhow::anyhow!(
"the 'num-keys' field was {} bytes; expected 4",
num_keys.len()
)
.into());
}
let num_keys: usize = num_keys
.get_u32()
.try_into()
.context("the number of keys couldn't be converted to usize")?;
let keys = payload
.split(|byte| *byte == 0)
.filter_map(|name| {
if !name.is_empty() {
String::from_utf8(name.into()).ok()
} else {
None
}
})
.collect::<Vec<_>>();
if keys.len() != num_keys {
return Err(anyhow::anyhow!(
"Server response indicated {} users, but {} names were sent!",
num_keys,
keys.len()
)
.into());
}
Ok(keys)
}
#[instrument(skip_all)]
pub async fn new_key(
&self,
admin_passphrase: Password,
key_passphrase: Password,
key_name: String,
key_type: KeyType,
initial_key_admin: Option<String>,
) -> Result<responses::PublicKey, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
inner_request.insert("passphrase", key_passphrase.as_bytes());
let keytype = key_type.to_string();
let (name_real, name_comment, name_email, expire_date) = match key_type {
KeyType::GnuPG {
real_name,
comment,
email,
expire_date,
} => (real_name, comment, email, expire_date),
KeyType::Ecc | KeyType::Rsa => (None, None, None, None),
};
let response = connection
.outer_request::<tokio::io::Empty>(
Command::NewKey {
user: self.user_name.clone(),
key: key_name.clone(),
keytype,
initial_key_admin,
name_real,
name_comment,
name_email,
expire_date,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
Ok(responses::PublicKey {
key_name,
data: payload,
})
}
#[instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn import_key(
&self,
admin_passphrase: Password,
key_passphrase: Password,
new_key_passphrase: Password,
key_name: String,
key_pem: &[u8],
key_type: KeyType,
initial_key_admin: Option<String>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let request_payload = Cursor::new(key_pem);
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
inner_request.insert("passphrase", key_passphrase.as_bytes());
inner_request.insert("new-passphrase", new_key_passphrase.as_bytes());
let response = connection
.outer_request(
Command::ImportKey {
user: self.user_name.clone(),
key: key_name,
keytype: key_type.to_string(),
initial_key_admin,
},
Some(request_payload),
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn delete_key(
&self,
admin_passphrase: Password,
key_name: String,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::DeleteKey {
user: self.user_name.clone(),
key: key_name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn modify_key(
&self,
admin_passphrase: Password,
key_name: String,
new_key_name: Option<String>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ModifyKey {
user: self.user_name.clone(),
key: key_name,
new_name: new_key_name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn key_users(
&self,
admin_passphrase: Password,
key_name: String,
) -> Result<Vec<String>, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ListKeyUsers {
user: self.user_name.clone(),
key: key_name.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
let mut num_users = response
.fields
.get("num-users")
.map(|b| Bytes::from(b.clone()))
.ok_or(anyhow::anyhow!("missing expected field 'num-users'"))?;
if num_users.len() != 4 {
return Err(anyhow::anyhow!(
"the 'num-users' field was {} bytes; expected 4",
num_users.len()
)
.into());
}
let num_users: usize = num_users
.get_u32()
.try_into()
.context("the number of users couldn't be converted to usize")?;
let users = payload
.split(|byte| *byte == 0)
.filter_map(|name| {
if !name.is_empty() {
String::from_utf8(name.into()).ok()
} else {
None
}
})
.collect::<Vec<_>>();
if users.len() != num_users {
return Err(anyhow::anyhow!(
"Server response indicated {} users, but {} names were sent!",
num_users,
users.len()
)
.into());
}
Ok(users)
}
#[instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn grant_key_access(
&self,
admin_passphrase: Password,
key_name: String,
key_passphrase: Password,
user_name: String,
user_passphrase: Password,
client_binding: Option<String>,
server_binding: Option<String>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
inner_request.insert("passphrase", key_passphrase.as_bytes());
inner_request.insert("new-passphrase", user_passphrase.as_bytes());
let client_binding_bytes = client_binding.unwrap_or_default();
if !client_binding_bytes.is_empty() {
inner_request.insert("client-binding", client_binding_bytes.as_bytes());
}
let server_binding_bytes = server_binding.unwrap_or_default();
if !server_binding_bytes.is_empty() {
inner_request.insert("server-binding", server_binding_bytes.as_bytes());
}
let response = connection
.outer_request::<tokio::io::Empty>(
Command::GrantKeyAccess {
user: self.user_name.clone(),
key: key_name,
name: user_name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn change_key_expiration(
&self,
admin_passphrase: Password,
key_name: String,
key_passphrase: Password,
subkey_id: Option<String>,
expire_date: Option<String>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
inner_request.insert("passphrase", key_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ChangeKeyExpiration {
user: self.user_name.clone(),
key: key_name,
expire_date,
subkey: subkey_id,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn revoke_key_access(
&self,
admin_passphrase: Password,
key_name: String,
user_name: String,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::RevokeKeyAccess {
user: self.user_name.clone(),
key: key_name,
name: user_name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn get_public_key(
&self,
admin_passphrase: Password,
key_name: String,
) -> Result<responses::PublicKey, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::GetPublicKey {
user: self.user_name.clone(),
key: key_name.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
Ok(responses::PublicKey {
key_name,
data: payload,
})
}
#[instrument(skip_all)]
pub async fn change_passphrase(
&self,
key_name: String,
current_key_passphrase: Password,
new_key_passphrase: Password,
client_binding: Option<String>,
server_binding: Option<String>,
) -> Result<(), Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("passphrase", current_key_passphrase.as_bytes());
inner_request.insert("new-passphrase", new_key_passphrase.as_bytes());
let client_binding_bytes = client_binding.unwrap_or_default();
if !client_binding_bytes.is_empty() {
inner_request.insert("client-binding", client_binding_bytes.as_bytes());
}
let server_binding_bytes = server_binding.unwrap_or_default();
if !server_binding_bytes.is_empty() {
inner_request.insert("server-binding", server_binding_bytes.as_bytes());
}
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ChangePassphrase {
user: self.user_name.clone(),
key: key_name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
assert!(payload.is_empty());
Ok(())
}
#[instrument(skip_all)]
pub async fn sign_pe<I, O>(
&self,
input: I,
output: O,
key_passphrase: Password,
key_name: String,
cert_name: String,
) -> Result<(), Error>
where
I: AsyncRead + AsyncSeek + Unpin,
O: AsyncWrite + Unpin,
{
let connection = self.connect().await?;
let op = Command::SignPe {
user: self.user_name.clone(),
key: key_name,
cert_name,
};
let mut inner_request = HashMap::new();
inner_request.insert("passphrase", key_passphrase.as_bytes());
let response = connection
.outer_request(op, Some(input))
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(output)
.await?;
tracing::info!(?response.fields, response.status_code, "Got response fields");
Ok(())
}
#[instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn sign_certificate(
&self,
issuer_key_name: String,
issuer_key_passphrase: Password,
issuer_certificate_name: Option<String>,
subject_key_name: String,
subject_certificate_name: String,
subject_certificate_type: CertificateType,
subject_common_name: String,
validity: u32,
) -> Result<X509, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("passphrase", issuer_key_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::SignCertificate {
user: self.user_name.clone(),
issuer_key: issuer_key_name,
subject_key: subject_key_name,
subject: format!("CN={subject_common_name}"),
validity: format!("{validity}y"),
subject_certificate_name,
certificate_type: subject_certificate_type.to_string(),
issuer_certificate_name,
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
let certificate = X509::from_pem(&payload)?;
Ok(certificate)
}
#[instrument(skip_all)]
pub async fn server_binding_methods(
&self,
admin_passphrase: Password,
) -> Result<Vec<String>, Error> {
let connection = self.connect().await?;
let (payload_reader, payload_writer) = get_payload_pipe();
let mut inner_request = HashMap::new();
inner_request.insert("password", admin_passphrase.as_bytes());
let response = connection
.outer_request::<tokio::io::Empty>(
Command::ListBindingMethods {
user: self.user_name.clone(),
},
None,
)
.await?
.inner_request(self.tls_config.ssl(&self.server_hostname)?, inner_request)
.await?
.response(payload_writer)
.await?;
tracing::info!(response.status_code, "Sigul response received");
let payload = payload_reader
.await
.context("response payload could not be read")??;
let mut num_methods = response
.fields
.get("num-methods")
.map(|b| Bytes::from(b.clone()))
.ok_or(anyhow::anyhow!("missing expected field 'num-methods'"))?;
if num_methods.len() != 4 {
return Err(anyhow::anyhow!(
"the 'num-methods' field was {} bytes; expected 4",
num_methods.len()
)
.into());
}
let num_methods: usize = num_methods
.get_u32()
.try_into()
.context("the number of keys couldn't be converted to usize")?;
let binding_methods = payload
.split(|byte| *byte == 0)
.filter_map(|method| {
if !method.is_empty() {
String::from_utf8(method.into()).ok()
} else {
None
}
})
.collect::<Vec<_>>();
if binding_methods.len() != num_methods {
return Err(anyhow::anyhow!(
"Server response indicated {} binding methods, but {} methods were sent!",
num_methods,
binding_methods.len()
)
.into());
}
Ok(binding_methods)
}
}