Skip to content

Commit

Permalink
feat: gpg-agent support
Browse files Browse the repository at this point in the history
  • Loading branch information
dumbasPL committed Sep 19, 2024
1 parent 7cd71be commit 990c8cd
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 18 deletions.
141 changes: 139 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ lazy_static = "1.4.0"

# crypto
sequoia-openpgp = { version = "1.21.1" }
sequoia-gpg-agent = { version = "0.4.2" }
90 changes: 78 additions & 12 deletions server/src/repository/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
use crate::config::CONFIG;
use crate::repository::PRIV_KEY_FILE;
use crate::repository::{GPG_AGENT_SOCKET, KEY_FILE};
use anyhow::Context;
use sequoia_openpgp::crypto::{KeyPair, Password};
use sequoia_gpg_agent::keyinfo::KeyProtection;
use sequoia_gpg_agent::sequoia_ipc::Keygrip;
use sequoia_gpg_agent::{self as gpg_agent, Agent, PinentryMode};
use sequoia_openpgp::crypto::{self, Password, Signer};
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::serialize::stream::{Message, Signer};
use sequoia_openpgp::serialize::stream::{self, Message};
use sequoia_openpgp::serialize::Serialize;
use sequoia_openpgp::Cert;
use std::path::Path;
use std::{fs, io};

pub fn should_sign_packages() -> bool {
Path::new(PRIV_KEY_FILE).exists()
Path::new(KEY_FILE).exists()
}

fn get_keypair() -> anyhow::Result<KeyPair> {
let cert = Cert::from_file(PRIV_KEY_FILE).context("failed to read private key file")?;
fn get_local_keypair() -> anyhow::Result<crypto::KeyPair> {
let cert = Cert::from_file(KEY_FILE).context("failed to read private key file")?;
let policy = StandardPolicy::new();

let key = cert
Expand Down Expand Up @@ -46,13 +49,76 @@ fn get_keypair() -> anyhow::Result<KeyPair> {
key.into_keypair().context("failed to create keypair")
}

pub fn sign(output: &Path, file: &Path) -> anyhow::Result<()> {
let keypair = get_keypair()?;
async fn get_agent_keypair() -> anyhow::Result<gpg_agent::KeyPair> {
let cert = Cert::from_file(KEY_FILE).context("Failed to read public key file")?;
let policy = StandardPolicy::new();

let mut agent = Agent::connect_to_agent(GPG_AGENT_SOCKET)
.await
.context("Failed to connect to gpg-agent")?
.suppress_pinentry();

agent.card_info().await.context("Failed to update card info")?;
let keys = agent.list_keys().await.context("Failed to list keys")?;

let (key, agent_key) = cert
.keys()
.with_policy(&policy, None)
.supported()
.alive()
.revoked(false)
.for_signing()
.map(|k| k.key())
.find_map(|k| {
if let Ok(keygrip) = Keygrip::of(k.mpis()) {
return keys
.iter()
// we can't use keys that require user confirmation
.filter(|key| !key.confirmation_required() && !key.key_disabled())
.find(|key| key.keygrip() == &keygrip)
.map(|key| (k, key));
}
None
})
.context("No suitable key found in gpg-agent")?;

let keypair = agent.keypair(&key).context("Failed to create keypair")?;

match &CONFIG.sign_key_password {
Some(password) => {
let password = Password::from(password.clone());
let keypair = keypair.set_pinentry_mode(PinentryMode::Loopback).with_password(password);

Ok(keypair)
}
None => match agent_key.protection() {
KeyProtection::NotProtected => Ok(keypair),
KeyProtection::Protected => {
Err(anyhow::anyhow!("private key is protected but no password was provided"))
}
KeyProtection::UnknownProtection => Ok(keypair), // will likely fail but try anyway
_ => Err(anyhow::anyhow!("unsupported key protection")),
},
}
}

async fn get_keypair() -> anyhow::Result<Box<dyn Signer + Send + Sync>> {
if Path::new(GPG_AGENT_SOCKET).exists() {
Ok(Box::new(get_agent_keypair().await?))
} else {
Ok(Box::new(get_local_keypair()?))
}
}

pub async fn sign(output: &Path, file: &Path) -> anyhow::Result<()> {
let keypair = get_keypair().await.context("failed to get keypair")?;
let mut sink = fs::File::create(output).context("failed to create signature sink")?;
let message = Message::new(&mut sink);

let mut message =
Signer::new(message, keypair).detached().build().context("failed to create signer")?;
let mut message = stream::Signer::new(message, keypair)
.detached()
.build()
.context("failed to create signer")?;
let mut source = fs::File::open(file).context("failed to open source file")?;
io::copy(&mut source, &mut message).context("failed to sign file")?;

Expand All @@ -61,8 +127,8 @@ pub fn sign(output: &Path, file: &Path) -> anyhow::Result<()> {
}

pub fn get_public_key_bytes<W: io::Write + Send + Sync>(output: &mut W) -> anyhow::Result<()> {
let cert = Cert::from_file(PRIV_KEY_FILE).context("failed to read private key file")?;
// this behaviour is not very well documented from the sequoia side. The code to
let cert = Cert::from_file(KEY_FILE).context("failed to read private key file")?;
// this behavior is not very well documented from the sequoia side. The code to
// serialize public keys can be found in the code for the `sq` cli here: https://gitlab.com/sequoia-pgp/sequoia-sq/-/blame/main/src/commands/key/export.rs?ref_type=heads#L103
let mut writer =
sequoia_openpgp::armor::Writer::new(output, sequoia_openpgp::armor::Kind::PublicKey)
Expand Down
9 changes: 6 additions & 3 deletions server/src/repository/manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@ fn sig_path(path: &Path) -> PathBuf {
))
}

fn sign_repository(name: &str, dir: &Path) -> anyhow::Result<()> {
async fn sign_repository(name: &str, dir: &Path) -> anyhow::Result<()> {
let db_path = &dir.join(format!("{name}.db"));
let db_archive_path = &dir.join(db_file(name));
let files_path = &dir.join(format!("{name}.files"));
let files_archive_path = &dir.join(format!("{name}.files.tar.gz"));

crypto::sign(&sig_path(db_archive_path), db_archive_path)
.await
.context("failed to sign repository database")?;
if !sig_path(db_path).exists() {
unix::fs::symlink(sig_path(db_archive_path), sig_path(db_path))
.context("failed to link repository database signature")?;
}
crypto::sign(&sig_path(files_archive_path), files_archive_path)
.await
.context("failed to sign repository files")?;
if !sig_path(files_path).exists() {
unix::fs::symlink(sig_path(files_archive_path), sig_path(files_path))
Expand All @@ -47,7 +49,7 @@ pub async fn add(name: &str, packages: &Vec<String>, dir: &Path) -> anyhow::Resu

if output.status.success() {
if crypto::should_sign_packages() {
sign_repository(name, dir)?;
sign_repository(name, dir).await?;
}
Ok(())
} else {
Expand All @@ -65,7 +67,7 @@ pub async fn remove(name: &str, packages: &Vec<String>, dir: &Path) -> anyhow::R

if output.status.success() {
if crypto::should_sign_packages() {
sign_repository(name, dir)?;
sign_repository(name, dir).await?;
}
Ok(())
} else {
Expand All @@ -77,6 +79,7 @@ pub async fn sign(files: &Vec<String>, base_path: &Path) -> anyhow::Result<()> {
for file in files {
let path = &base_path.join(file);
crypto::sign(&sig_path(path), path)
.await
.context(format!("failed to create signature for file: {file}"))?;
}
Ok(())
Expand Down
Loading

0 comments on commit 990c8cd

Please sign in to comment.