use sequoia_openpgp::parse::stream::{
MessageLayer, MessageStructure, VerificationHelper, VerifierBuilder,
};
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::serialize::stream::{Message, Signer};
use sequoia_openpgp::{Cert, KeyHandle};
use std::io::{Read, Write};
#[derive(Debug)]
pub enum SignError {
BadKey(String),
NoSigningKey,
Sign(String),
}
impl std::fmt::Display for SignError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignError::BadKey(e) => write!(f, "bad signing key: {e}"),
SignError::NoSigningKey => write!(f, "no signing-capable secret key"),
SignError::Sign(e) => write!(f, "signing failed: {e}"),
}
}
}
impl std::error::Error for SignError {}
pub fn clearsign(plaintext: &[u8], cert_bytes: &[u8]) -> Result<Vec<u8>, SignError> {
let policy = StandardPolicy::new();
let cert = Cert::from_bytes(cert_bytes).map_err(|e| SignError::BadKey(e.to_string()))?;
let keypair = cert
.keys()
.with_policy(&policy, None)
.secret()
.for_signing()
.next()
.ok_or(SignError::NoSigningKey)?
.key()
.clone()
.into_keypair()
.map_err(|e| SignError::Sign(e.to_string()))?;
let mut sink: Vec<u8> = Vec::new();
{
let message = Message::new(&mut sink);
let mut signer = Signer::new(message, keypair)
.map_err(|e| SignError::Sign(e.to_string()))?
.cleartext()
.build()
.map_err(|e| SignError::Sign(e.to_string()))?;
signer
.write_all(plaintext)
.map_err(|e| SignError::Sign(e.to_string()))?;
signer
.finalize()
.map_err(|e| SignError::Sign(e.to_string()))?;
}
Ok(sink)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerificationResult {
Valid = 0,
KeyMissing = 1,
NotValid = 2,
NotSigned = 3,
Expired = 4,
}
#[derive(Debug, Clone)]
pub struct Verification {
pub result: VerificationResult,
pub plaintext: Option<Vec<u8>>,
}
struct Helper<'a> {
certs: &'a [Cert],
result: VerificationResult,
saw_signature: bool,
}
impl VerificationHelper for &mut Helper<'_> {
fn get_certs(&mut self, ids: &[KeyHandle]) -> sequoia_openpgp::Result<Vec<Cert>> {
let _ = ids;
Ok(self.certs.to_vec())
}
fn check(&mut self, structure: MessageStructure) -> sequoia_openpgp::Result<()> {
for layer in structure {
if let MessageLayer::SignatureGroup { results } = layer {
self.saw_signature = true;
self.result = summarize(&results);
}
}
Ok(())
}
}
fn summarize(
results: &[Result<
sequoia_openpgp::parse::stream::GoodChecksum<'_>,
sequoia_openpgp::parse::stream::VerificationError<'_>,
>],
) -> VerificationResult {
use sequoia_openpgp::parse::stream::VerificationError;
let mut best = VerificationResult::NotValid;
for r in results {
match r {
Ok(_) => return VerificationResult::Valid,
Err(VerificationError::MissingKey { .. }) => {
best = VerificationResult::KeyMissing;
}
Err(VerificationError::UnboundKey { .. }) | Err(VerificationError::BadKey { .. }) => {
if best == VerificationResult::NotValid {
best = VerificationResult::Expired;
}
}
Err(_) => {}
}
}
best
}
pub fn parse_keyring(keyring: &[Vec<u8>]) -> Result<Vec<Cert>, String> {
keyring
.iter()
.map(|b| Cert::from_bytes(b).map_err(|e| e.to_string()))
.collect()
}
pub fn verify_clearsigned(signed: &[u8], certs: &[Cert]) -> Verification {
let policy = StandardPolicy::new();
let mut helper = Helper {
certs,
result: VerificationResult::NotValid,
saw_signature: false,
};
let builder = match VerifierBuilder::from_bytes(signed) {
Ok(b) => b,
Err(_) => {
return Verification {
result: VerificationResult::NotSigned,
plaintext: None,
}
}
};
let mut verifier = match builder.with_policy(&policy, None, &mut helper) {
Ok(v) => v,
Err(_) => {
return Verification {
result: VerificationResult::NotSigned,
plaintext: None,
}
}
};
let mut plaintext = Vec::new();
let read = verifier.read_to_end(&mut plaintext);
let result = if !helper.saw_signature {
VerificationResult::NotSigned
} else {
match read {
Ok(_) => helper.result,
Err(_) if certs.is_empty() => VerificationResult::KeyMissing,
Err(_) => VerificationResult::NotValid,
}
};
Verification {
result,
plaintext: Some(plaintext),
}
}
#[cfg(test)]
mod tests {
use super::*;
use sequoia_openpgp::cert::CertBuilder;
use sequoia_openpgp::serialize::Serialize;
#[test]
fn clearsign_produces_a_signed_message() {
let (cert, _) = CertBuilder::new().add_signing_subkey().generate().unwrap();
let mut tsk = Vec::new();
cert.as_tsk().serialize(&mut tsk).unwrap();
let signed = clearsign(b"bazaar testament short form 3 strict\n", &tsk).unwrap();
let text = String::from_utf8(signed).unwrap();
assert!(text.starts_with("-----BEGIN PGP SIGNED MESSAGE-----"));
assert!(text.contains("bazaar testament short form 3 strict"));
assert!(text.contains("-----BEGIN PGP SIGNATURE-----"));
}
#[test]
fn bad_key_is_rejected() {
assert!(matches!(
clearsign(b"x", b"not a key"),
Err(SignError::BadKey(_))
));
}
#[test]
fn verify_round_trips_a_signed_message() {
let (cert, _) = CertBuilder::new().add_signing_subkey().generate().unwrap();
let mut tsk = Vec::new();
cert.as_tsk().serialize(&mut tsk).unwrap();
let plaintext = b"bazaar-ng testament short form 1\nrevision-id: r1\n";
let signed = clearsign(plaintext, &tsk).unwrap();
let v = verify_clearsigned(&signed, std::slice::from_ref(&cert));
assert_eq!(v.result, VerificationResult::Valid);
assert_eq!(v.plaintext.as_deref(), Some(&plaintext[..]));
}
#[test]
fn verify_reports_missing_key() {
let (signer, _) = CertBuilder::new().add_signing_subkey().generate().unwrap();
let mut tsk = Vec::new();
signer.as_tsk().serialize(&mut tsk).unwrap();
let signed = clearsign(b"hello\n", &tsk).unwrap();
let v = verify_clearsigned(&signed, &[]);
assert_eq!(v.result, VerificationResult::KeyMissing);
}
#[test]
fn verify_unsigned_content() {
let v = verify_clearsigned(b"just some text, not a signature\n", &[]);
assert_eq!(v.result, VerificationResult::NotSigned);
assert_eq!(v.plaintext, None);
}
}