use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::Context;
use cryptoki::context::Pkcs11;
use cryptoki::session::UserType;
use cryptoki::slot::Slot;
use cryptoki::types::AuthPin;
use openssl::pkey::PKey;
use sequoia_openpgp::crypto::Password;
use serde::{Deserialize, Serialize};
use sqlx::SqliteConnection;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use uuid::Uuid;
use crate::error::ServerError;
use crate::ipc_common::IpcClient;
use crate::protocol;
use crate::server::config::Pkcs11Binding;
use crate::{
protocol::{DigestAlgorithm, Signature},
server::{Config, crypto, db},
};
type KeyMap = HashMap<String, UnlockedKey>;
enum UnlockedKey {
Private {
key: PKey<openssl::pkey::Private>,
},
Pkcs11 {
module: PathBuf,
pkcs11: Pkcs11,
slot: Slot,
pin: AuthPin,
},
}
#[derive(Serialize, Deserialize)]
#[allow(clippy::exhaustive_enums)]
#[doc(hidden)]
struct BindingWithPin {
public_key: PathBuf,
private_key: String,
pin: String,
}
impl From<BindingWithPin> for Pkcs11Binding {
fn from(value: BindingWithPin) -> Self {
Pkcs11Binding {
certificate: value.public_key,
private_key: Some(value.private_key),
pin: Some(Password::from(value.pin)),
}
}
}
#[derive(Serialize, Deserialize)]
#[allow(clippy::exhaustive_enums)]
#[doc(hidden)]
enum Request {
Config {
user: String,
database_path: String,
session_id: Uuid,
pkcs11_bindings: Vec<BindingWithPin>,
},
Unlock {
key: String,
password: String,
},
Sign {
key: String,
digests: Vec<(DigestAlgorithm, String)>,
},
}
#[derive(Serialize, Deserialize)]
#[allow(clippy::exhaustive_enums)]
#[doc(hidden)]
enum Response {
Signatures { signatures: Vec<Signature> },
PgpSign { payload_size: usize },
Success {},
Failure { reason: String },
}
pub(crate) struct Client {
inner: IpcClient,
}
impl Client {
pub(crate) async fn new(
user: String,
config: Config,
session_id: Uuid,
) -> anyhow::Result<Self> {
let inner = IpcClient::new(&config.signer_socket_path).await?;
let mut client = Self { inner };
tracing::trace!("requesting signing helper config");
let mut bindings = vec![];
for binding in config.pkcs11_bindings.iter() {
if let (Some(private_key), Some(pin)) = (&binding.private_key, &binding.pin) {
let pin = pin.map(|p| String::from_utf8(p.to_vec()))?;
bindings.push(BindingWithPin {
public_key: binding.certificate.clone(),
private_key: private_key.clone(),
pin,
});
}
}
let database_path = config
.database()
.as_os_str()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Database path isn't valid UTF8"))?
.to_string();
client
.inner
.request(
&Request::Config {
user,
database_path,
session_id,
pkcs11_bindings: bindings,
},
None,
)
.await?;
tracing::trace!("requested signing helper config");
Ok(client)
}
#[instrument(skip_all, err, fields(key))]
pub(crate) async fn unlock_request(
&mut self,
key: String,
password: String,
) -> Result<protocol::Response, ServerError> {
let response = self
.inner
.request(&Request::Unlock { key, password }, None)
.await?;
let response = serde_json::from_value(response).map_err(|error| {
tracing::error!(?error, "helper returned invalid response");
ServerError::Internal
})?;
match response {
Response::Failure { reason } => {
tracing::error!(reason, "Failed to unlock key");
Err(ServerError::Internal)
}
Response::Success {} => Ok(protocol::Response::Unlock {}),
_ => {
tracing::error!("helper returned invalid response");
Err(ServerError::Internal)
}
}
}
#[instrument(skip_all, err, fields(key))]
pub(crate) async fn sign_request(
&mut self,
key: String,
digests: Vec<(DigestAlgorithm, String)>,
) -> Result<Vec<Signature>, ServerError> {
let response = self
.inner
.request(&Request::Sign { key, digests }, None)
.await?;
let response = serde_json::from_value(response).map_err(|error| {
tracing::error!(?error, "helper returned invalid response");
ServerError::Internal
})?;
match response {
Response::Signatures { signatures } => Ok(signatures),
Response::Failure { reason } => {
tracing::error!(reason, "Failed to unlock key");
Err(ServerError::Internal)
}
_ => {
tracing::error!("helper returned invalid response");
Err(ServerError::Internal)
}
}
}
pub(crate) async fn shutdown(self) -> anyhow::Result<()> {
self.inner.shutdown().await?;
Ok(())
}
}
#[instrument(name = "siguldry-signer", skip_all, fields(session_id = tracing::field::Empty))]
pub async fn serve<
R: AsyncRead + Unpin + std::fmt::Debug,
W: AsyncWrite + Unpin + std::fmt::Debug,
>(
halt_token: CancellationToken,
requests: R,
mut responses: W,
) -> anyhow::Result<()> {
tracing::info!("Handling requests");
let mut requests = BufReader::new(requests).lines();
let mut key_passwords: KeyMap = HashMap::new();
let (user, database_path, pkcs11_bindings) = tokio::select! {
_ = halt_token.cancelled() => {
tracing::info!("siguldry-helper received shut down signal");
return Ok(())
}
request = requests.next_line() => {
match request? {
Some(request) => {
let request: Request = serde_json::from_str(&request)?;
match request {
Request::Config { user, database_path, session_id, pkcs11_bindings } => {
tracing::Span::current().record("session_id", session_id.to_string());
let mut response = serde_json::to_string(&Response::Success { })?;
response.push('\n');
responses.write_all(response.as_bytes()).await?;
let bindings = pkcs11_bindings.into_iter().map(|b| b.into()).collect::<Vec<Pkcs11Binding>>();
(user, database_path, bindings)},
_ => return Err(anyhow::anyhow!("The first message must configure this helper"))
}
},
None => return Ok(())
}
}
};
let db_pool = db::pool(&database_path, true).await?;
let mut db_conn = db_pool.acquire().await?;
let user = db::User::get(&mut db_conn, &user).await?;
drop(db_conn);
tracing::debug!(user.name, "siguldry-signer is configured and ready to use");
loop {
let request = tokio::select! {
_ = halt_token.cancelled() => {
tracing::info!("siguldry-signer received shut down signal");
break;
}
request = requests.next_line() => request,
}?;
tracing::debug!("siguldry-signer got request");
let request = if let Some(request) = request {
serde_json::from_str(&request)?
} else {
tracing::info!("siguldry-signer received EOF and is shutting down");
break;
};
let response = match request {
Request::Config {
user: _,
database_path: _,
session_id: _,
pkcs11_bindings: _,
} => Response::Failure {
reason: "helper cannot be configured twice".to_string(),
},
Request::Unlock { key, password } => {
let mut conn = db_pool.begin().await?;
match unlock(
&mut conn,
&mut key_passwords,
&pkcs11_bindings,
&user,
key,
Password::from(password),
)
.await
{
Ok(_) => Response::Success {},
Err(error) => Response::Failure {
reason: error.to_string(),
},
}
}
Request::Sign { key, digests } => {
let mut conn = db_pool.begin().await?;
match sign(&mut conn, &mut key_passwords, &key, digests).await {
Ok(signatures) => Response::Signatures { signatures },
Err(error) => Response::Failure {
reason: error.to_string(),
},
}
}
};
tracing::trace!("About to write response");
let mut response = serde_json::to_string(&response)?;
response.push('\n');
responses.write_all(response.as_bytes()).await?;
tracing::trace!("Successfully wrote response");
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[instrument(skip_all, err, fields(key = key_name))]
async fn unlock(
conn: &mut SqliteConnection,
key_passwords: &mut KeyMap,
pkcs11_bindings: &[Pkcs11Binding],
user: &db::User,
key_name: String,
user_password: Password,
) -> anyhow::Result<()> {
let key = db::Key::get(conn, &key_name).await?;
let key_access = db::KeyAccess::get(conn, &key, user).await?;
let password = crypto::binding::decrypt_key_password(
pkcs11_bindings,
user_password.clone(),
&key_access.encrypted_passphrase,
)
.await?;
if let Some(token_id) = key.pkcs11_token_id {
let db_token = db::Pkcs11Token::get(conn, token_id).await?;
let pin = password
.map(|p| String::from_utf8(p.to_vec()))
.map(AuthPin::from)?;
let unlocked_key = if let Some(UnlockedKey::Pkcs11 {
module: _,
pkcs11,
slot: _,
pin: _,
}) = key_passwords.values().find(|k| match k {
UnlockedKey::Pkcs11 {
module,
pkcs11: _,
slot: _,
pin: _,
} => &db_token.module_path == module,
UnlockedKey::Private { .. } => false,
}) {
let slot = db_token.slot(pkcs11)?;
let session = pkcs11.open_ro_session(slot)?;
session
.login(cryptoki::session::UserType::User, Some(&pin))
.context("Failed to login to the PKCS#11 token")?;
UnlockedKey::Pkcs11 {
module: db_token.module_path,
pkcs11: pkcs11.clone(),
slot,
pin,
}
} else {
let pkcs11 = db_token.intialize()?;
let slot = db_token.slot(&pkcs11)?;
let session = pkcs11.open_ro_session(slot)?;
session
.login(cryptoki::session::UserType::User, Some(&pin))
.context("Failed to login to the PKCS#11 token")?;
UnlockedKey::Pkcs11 {
module: db_token.module_path,
pkcs11,
slot,
pin,
}
};
key_passwords.insert(key.name, unlocked_key);
} else {
let private_key = crypto::binding::decrypt_private_key(
&key,
&key_access.encrypted_passphrase,
pkcs11_bindings,
user_password,
)
.await?;
key_passwords.insert(key.name, UnlockedKey::Private { key: private_key });
}
return Ok(());
}
#[instrument(skip_all, err, fields(key = key_name))]
async fn sign(
conn: &mut SqliteConnection,
key_passwords: &mut KeyMap,
key_name: &str,
digests: Vec<(DigestAlgorithm, String)>,
) -> anyhow::Result<Vec<Signature>> {
let key = db::Key::get(conn, key_name).await?;
let unlocked_key = key_passwords
.get(key_name)
.ok_or_else(|| anyhow::anyhow!("You need to unlock the key"))?;
let signatures = match unlocked_key {
UnlockedKey::Private { key: private_key } => {
crypto::signing::sign_with_softkey(&key, private_key, digests)
}
UnlockedKey::Pkcs11 {
module: _,
pkcs11,
slot,
pin,
} => {
let session = pkcs11.open_ro_session(*slot)?;
session.login(UserType::User, Some(pin))?;
crypto::signing::sign_with_pkcs11(&key, &session, digests)
}
}?;
Ok(signatures)
}