use {
chrono::SubsecRound,
digest::Digest,
pgp::{
crypto::hash::{HashAlgorithm, Hasher},
packet::{Packet, SignatureConfig, SignatureType, Subpacket, SubpacketData},
types::{PublicKeyTrait, SecretKeyTrait},
Signature,
},
std::{
cmp::Ordering,
collections::HashMap,
io::{self, BufRead, Cursor, Read},
},
};
const HEADER: &str = "-----BEGIN PGP SIGNED MESSAGE-----";
const HEADER_LF: &str = "-----BEGIN PGP SIGNED MESSAGE-----\n";
const HEADER_CRLF: &str = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
const SIGNATURE_ARMOR_LF: &str = "-----BEGIN PGP SIGNATURE-----\n";
const SIGNATURE_ARMOR_CRLF: &str = "-----BEGIN PGP SIGNATURE-----\r\n";
#[derive(Clone)]
pub enum CleartextHasher {
Md5(md5::Md5),
Sha1(sha1::Sha1),
Sha256(sha2::Sha256),
Sha384(sha2::Sha384),
Sha512(sha2::Sha512),
}
impl CleartextHasher {
pub fn md5() -> Self {
Self::Md5(md5::Md5::new())
}
pub fn sha1() -> Self {
Self::Sha1(sha1::Sha1::new())
}
pub fn sha256() -> Self {
Self::Sha256(sha2::Sha256::new())
}
pub fn sha384() -> Self {
Self::Sha384(sha2::Sha384::new())
}
pub fn sha512() -> Self {
Self::Sha512(sha2::Sha512::new())
}
pub fn algorithm(&self) -> HashAlgorithm {
match self {
Self::Md5(_) => HashAlgorithm::MD5,
Self::Sha1(_) => HashAlgorithm::SHA1,
Self::Sha256(_) => HashAlgorithm::SHA2_256,
Self::Sha384(_) => HashAlgorithm::SHA2_384,
Self::Sha512(_) => HashAlgorithm::SHA2_512,
}
}
}
impl std::io::Write for CleartextHasher {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.update(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Hasher for CleartextHasher {
fn update(&mut self, data: &[u8]) {
match self {
Self::Md5(digest) => digest.update(data),
Self::Sha1(digest) => digest.update(data),
Self::Sha256(digest) => digest.update(data),
Self::Sha384(digest) => digest.update(data),
Self::Sha512(digest) => digest.update(data),
}
}
fn finish(self: Box<Self>) -> Vec<u8> {
match *self {
Self::Md5(digest) => digest.finalize().to_vec(),
CleartextHasher::Sha1(digest) => digest.finalize().to_vec(),
CleartextHasher::Sha256(digest) => digest.finalize().to_vec(),
CleartextHasher::Sha384(digest) => digest.finalize().to_vec(),
CleartextHasher::Sha512(digest) => digest.finalize().to_vec(),
}
}
fn finish_reset_into(&mut self, out: &mut [u8]) {
let res = match self {
Self::Md5(ref mut digest) => digest.finalize_reset().to_vec(),
CleartextHasher::Sha1(ref mut digest) => digest.finalize_reset().to_vec(),
CleartextHasher::Sha256(ref mut digest) => digest.finalize_reset().to_vec(),
CleartextHasher::Sha384(ref mut digest) => digest.finalize_reset().to_vec(),
CleartextHasher::Sha512(ref mut digest) => digest.finalize_reset().to_vec(),
};
out.copy_from_slice(&res.as_slice()[..out.len()]);
}
}
enum ReaderState {
Initial,
Hashes,
CleartextEmpty(bool),
CleartextBuffered(String),
Signatures,
Eof,
}
pub struct CleartextSignatureReader<R: BufRead> {
reader: R,
state: ReaderState,
hashers: HashMap<u8, CleartextHasher>,
signatures: Vec<Signature>,
}
impl<R: BufRead> CleartextSignatureReader<R> {
pub fn new(reader: R) -> Self {
Self {
state: ReaderState::Initial,
reader,
hashers: HashMap::new(),
signatures: vec![],
}
}
pub fn finalize(self) -> CleartextSignatures {
CleartextSignatures {
hashers: self.hashers,
signatures: self.signatures,
}
}
}
impl<R: BufRead> Read for CleartextSignatureReader<R> {
fn read(&mut self, dest: &mut [u8]) -> std::io::Result<usize> {
loop {
match &mut self.state {
ReaderState::Initial => {
let mut line = String::with_capacity(HEADER_CRLF.len());
self.reader.read_line(&mut line)?;
if !matches!(line.as_str(), HEADER_LF | HEADER_CRLF) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"bad PGP cleartext header; expected `{}`; got `{}`",
HEADER, line
),
));
}
self.state = ReaderState::Hashes;
}
ReaderState::Hashes => {
let mut line = String::with_capacity(16);
self.reader.read_line(&mut line)?;
if let Some(hash) = line.strip_prefix("Hash: ") {
for hash in hash.split(',') {
let hash = hash.trim();
if !hash.is_empty() {
let hasher = match hash {
"MD5" => CleartextHasher::md5(),
"SHA1" => CleartextHasher::sha1(),
"SHA256" => CleartextHasher::sha256(),
"SHA384" => CleartextHasher::sha384(),
"SHA512" => CleartextHasher::sha512(),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported PGP hash type: {}", hash),
));
}
};
self.hashers
.entry(u8::from(hasher.algorithm()))
.or_insert(hasher);
}
}
} else if line.trim().is_empty() {
if self.hashers.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"bad PGP cleartext signature; no Hash headers",
));
}
self.state = ReaderState::CleartextEmpty(false);
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"bad PGP cleartext signature; expected Hash: header; got {}",
line.trim_end()
),
));
}
}
ReaderState::CleartextEmpty(previous_read) => {
let mut line = String::with_capacity(128);
self.reader.read_line(&mut line)?;
let emit = if let Some(stripped) = line.strip_prefix("- ") {
stripped
} else if matches!(line.as_str(), SIGNATURE_ARMOR_LF | SIGNATURE_ARMOR_CRLF) {
self.state = ReaderState::Signatures;
continue;
} else {
line.as_str()
};
let no_eol = emit.trim_end_matches(|c| c == '\r' || c == '\n');
for hasher in self.hashers.values_mut() {
if *previous_read {
hasher.update(b"\r\n");
}
hasher.update(no_eol.as_bytes());
}
return match dest.len().cmp(&emit.as_bytes().len()) {
Ordering::Equal | Ordering::Greater => {
let count = emit.as_bytes().len();
let dest = &mut dest[0..count];
dest.copy_from_slice(emit.as_bytes());
self.state = ReaderState::CleartextEmpty(true);
Ok(count)
}
Ordering::Less => {
let (to_copy, remaining) = emit.split_at(dest.len());
dest.copy_from_slice(to_copy.as_bytes());
self.state = ReaderState::CleartextBuffered(remaining.to_string());
Ok(to_copy.as_bytes().len())
}
};
}
ReaderState::CleartextBuffered(ref mut remaining) => {
return match dest.len().cmp(&remaining.as_bytes().len()) {
Ordering::Equal | Ordering::Greater => {
let count = remaining.as_bytes().len();
let dest = &mut dest[0..count];
dest.copy_from_slice(remaining.as_bytes());
self.state = ReaderState::CleartextEmpty(true);
Ok(count)
}
Ordering::Less => {
let count = dest.len();
let (to_copy, remaining) = remaining.split_at(count);
dest.copy_from_slice(to_copy.as_bytes());
self.state = ReaderState::CleartextBuffered(remaining.to_string());
Ok(count)
}
};
}
ReaderState::Signatures => {
let mut buffer = SIGNATURE_ARMOR_LF.as_bytes().to_vec();
self.reader.read_to_end(&mut buffer)?;
let mut dearmor = pgp::armor::Dearmor::new(io::Cursor::new(buffer));
dearmor
.read_header()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
if !matches!(dearmor.typ, Some(pgp::armor::BlockType::Signature)) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"failed to parse PGP signature armor",
));
}
for packet in pgp::packet::PacketParser::new(dearmor) {
match packet {
Ok(Packet::Signature(signature)) => {
self.signatures.push(signature);
}
Ok(packet) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"unexpected PGP packet seen; expected Signature; got {:?}",
packet.tag()
),
));
}
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("PGP packet parsing error: {:?}", e),
));
}
}
}
self.state = ReaderState::Eof;
return Ok(0);
}
ReaderState::Eof => {
return Ok(0);
}
}
}
}
}
pub struct CleartextSignatures {
hashers: HashMap<u8, CleartextHasher>,
signatures: Vec<Signature>,
}
impl CleartextSignatures {
pub fn iter_signatures(&self) -> impl Iterator<Item = &Signature> {
self.signatures.iter()
}
pub fn iter_signatures_from_key<'slf, 'key: 'slf>(
&'slf self,
key: &'key impl PublicKeyTrait,
) -> impl Iterator<Item = &'slf Signature> {
self.signatures
.iter()
.filter(|sig| sig.issuer().iter().any(|issuer| &key.key_id() == *issuer))
}
pub fn verify(&self, key: &impl PublicKeyTrait) -> pgp::errors::Result<usize> {
if self.signatures.is_empty() {
return Err(pgp::errors::Error::Message(
"no PGP signatures present".to_string(),
));
}
let mut valid_signatures = 0;
for sig in self.iter_signatures_from_key(key) {
let mut hasher = Box::new(
self.hashers
.get(&(u8::from(sig.config.hash_alg)))
.ok_or_else(|| {
pgp::errors::Error::Message(format!(
"could not find hasher matching signature hash algorithm ({:?})",
sig.config.hash_alg
))
})?
.clone(),
);
let len = sig.config.hash_signature_data(&mut *hasher)?;
hasher.update(&sig.config.trailer(len)?);
let digest = hasher.finish();
if digest[0..2] != sig.signed_hash_value {
return Err(pgp::errors::Error::Message(
"invalid signed hash value".into(),
));
}
key.verify_signature(sig.config.hash_alg, &digest, &sig.signature)?;
valid_signatures += 1;
}
match valid_signatures {
0 => Err(pgp::errors::Error::Message(
"no signatures signed by provided key".into(),
)),
_ => Ok(valid_signatures),
}
}
}
pub fn cleartext_sign<PW, R>(
key: &impl SecretKeyTrait,
key_pw: PW,
hash_algorithm: HashAlgorithm,
data: R,
) -> pgp::errors::Result<String>
where
PW: FnOnce() -> String,
R: BufRead,
{
if !matches!(
hash_algorithm,
HashAlgorithm::MD5
| HashAlgorithm::SHA1
| HashAlgorithm::RIPEMD160
| HashAlgorithm::SHA2_256
| HashAlgorithm::SHA2_384
| HashAlgorithm::SHA2_512
| HashAlgorithm::SHA2_224,
) {
return Err(pgp::errors::Error::Unsupported(
"hash algorithm unsupported for cleartext signatures".to_string(),
));
}
let mut dashed_lines = vec![];
let mut source_lines = vec![];
for line in data.lines() {
let line = line?;
dashed_lines.push(if line.starts_with('-') || line.starts_with("From ") {
format!("- {}", line.trim_end())
} else {
line.trim_end().to_string()
});
source_lines.push(line.trim_end().to_string());
}
let cleartext = source_lines.join("\r\n").into_bytes();
let hashed_subpackets = vec![
Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint())),
Subpacket::regular(SubpacketData::SignatureCreationTime(
chrono::Utc::now().trunc_subsecs(0),
)),
];
let unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(key.key_id()))];
let mut config = SignatureConfig::v4(SignatureType::Text, key.algorithm(), hash_algorithm);
config.hashed_subpackets = hashed_subpackets;
config.unhashed_subpackets = unhashed_subpackets;
let signature = config.sign(key, key_pw, Cursor::new(cleartext))?;
let packet = Packet::Signature(signature);
let mut writer = Cursor::new(Vec::<u8>::new());
pgp::armor::write(
&packet,
pgp::armor::BlockType::Signature,
&mut writer,
None,
true,
)?;
let signature_string = String::from_utf8(writer.into_inner())
.map_err(|e| pgp::errors::Error::Utf8Error(e.utf8_error()))?;
let lines = vec![
HEADER.to_string(),
format!(
"Hash: {}",
match hash_algorithm {
HashAlgorithm::MD5 => "MD5",
HashAlgorithm::SHA1 => "SHA1",
HashAlgorithm::RIPEMD160 => "RIPEMD160",
HashAlgorithm::SHA2_256 => "SHA256",
HashAlgorithm::SHA2_384 => "SHA384",
HashAlgorithm::SHA2_512 => "SHA512",
HashAlgorithm::SHA2_224 => "SHA224",
_ => panic!("hash algorithm should have been validated above"),
}
),
"".to_string(),
]
.into_iter()
.chain(dashed_lines)
.chain(std::iter::once(signature_string))
.collect::<Vec<_>>();
Ok(lines.join("\n"))
}