use std::pin::Pin;
use anyhow::{Context, Result};
use super::OpenPgpError;
pub use super::armor::ArmoredSignature;
use super::armor::{BlockType, armor};
use super::key::{Fingerprint, UncompressedPoint, primary_key_packet};
use super::packet::{new_format_packet, subpacket};
use super::signature::{
P256Scalar, SignedObject, creation_time_subpacket, digest, hashed_portion,
issuer_fingerprint_subpacket, issuer_key_id_subpacket, key_hash_prefix, signature_packet,
};
const SIGNING_KEY_FLAGS: u8 = 0x03;
const PREFERRED_SYMMETRIC_AES128: u8 = 0x07;
const PREFERRED_HASH_SHA256: u8 = 0x08;
const PREFERRED_COMPRESSION_NONE: u8 = 0x00;
const SUBPACKET_KEY_FLAGS: u8 = 27;
const SUBPACKET_PREFERRED_SYMMETRIC: u8 = 11;
const SUBPACKET_PREFERRED_HASH: u8 = 21;
const SUBPACKET_PREFERRED_COMPRESSION: u8 = 22;
const TAG_PUBLIC_KEY: u8 = 6;
const TAG_USER_ID: u8 = 13;
pub struct EcdsaSignature {
pub r: [u8; 32],
pub s: [u8; 32],
}
pub type SignDigestFuture<'a> = Pin<Box<dyn Future<Output = Result<EcdsaSignature>> + Send + 'a>>;
pub trait SignDigest {
fn sign_digest<'a>(
&'a self,
signer: UncompressedPoint,
digest: [u8; 32],
) -> SignDigestFuture<'a>;
}
#[derive(Clone, Debug)]
pub struct UserId(String);
impl UserId {
pub fn parse(value: String) -> Result<Self, OpenPgpError> {
if value.is_empty() {
return Err(OpenPgpError::EmptyUserId);
}
if value.contains(['\0', '\r', '\n']) {
return Err(OpenPgpError::UserIdControlByte);
}
Ok(Self(value))
}
fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
#[derive(Clone, Copy)]
pub struct SigningKey {
pub point: UncompressedPoint,
pub created: u32,
}
impl SigningKey {
pub fn fingerprint(&self) -> Fingerprint {
primary_key_packet(self.point, self.created).fingerprint
}
}
pub struct OpenPgpKey {
pub user_id: UserId,
pub signing: SigningKey,
}
async fn build_signature<S: SignDigest + ?Sized>(
object: SignedObject,
hashed_subpackets: Vec<u8>,
key: SigningKey,
data_to_hash: &[u8],
signer: &S,
) -> Result<Vec<u8>> {
let issuer = key.fingerprint();
let hashed = hashed_portion(object, &hashed_subpackets);
let digest_bytes = digest(data_to_hash, &hashed);
let EcdsaSignature { r, s } = signer
.sign_digest(key.point, digest_bytes)
.await
.with_context(|| format!("failed to sign the {object} digest"))?;
let (r, s) = P256Scalar::parse(r)
.zip(P256Scalar::parse(s))
.ok_or(OpenPgpError::SignatureScalarOutOfRange)
.context("encode the OpenPGP signature packet")?;
Ok(signature_packet(
hashed,
&issuer_key_id_subpacket(issuer),
&digest_bytes,
r.as_bytes(),
s.as_bytes(),
))
}
pub async fn export_public_key<S: SignDigest + ?Sized>(
key: &OpenPgpKey,
signer: &S,
) -> Result<String> {
let primary = primary_key_packet(key.signing.point, key.signing.created);
let user_id_bytes = key.user_id.as_bytes();
let self_hashed = {
let mut hashed = creation_time_subpacket(key.signing.created);
for (kind, value) in [
(SUBPACKET_KEY_FLAGS, SIGNING_KEY_FLAGS),
(SUBPACKET_PREFERRED_SYMMETRIC, PREFERRED_SYMMETRIC_AES128),
(SUBPACKET_PREFERRED_HASH, PREFERRED_HASH_SHA256),
(SUBPACKET_PREFERRED_COMPRESSION, PREFERRED_COMPRESSION_NONE),
] {
hashed.extend_from_slice(&subpacket(kind, false, &[value]));
}
hashed.extend_from_slice(&issuer_fingerprint_subpacket(primary.fingerprint));
hashed
};
let self_signed_data = {
let mut data = key_hash_prefix(&primary.body);
data.push(0xb4);
data.extend_from_slice(&(user_id_bytes.len() as u32).to_be_bytes());
data.extend_from_slice(user_id_bytes);
data
};
let self_signature = build_signature(
SignedObject::UserId,
self_hashed,
key.signing,
&self_signed_data,
signer,
)
.await?;
let mut data = new_format_packet(TAG_PUBLIC_KEY, &primary.body);
data.extend_from_slice(&new_format_packet(TAG_USER_ID, user_id_bytes));
data.extend_from_slice(&self_signature);
Ok(armor(BlockType::PublicKeyBlock, &data))
}
pub async fn armored_detached_signature<S: SignDigest + ?Sized>(
key: SigningKey,
data: &[u8],
signer: &S,
now: u32,
) -> Result<ArmoredSignature> {
let mut hashed = creation_time_subpacket(now);
hashed.extend_from_slice(&issuer_fingerprint_subpacket(key.fingerprint()));
let packet = build_signature(SignedObject::Document, hashed, key, data, signer).await?;
Ok(ArmoredSignature::from_parts(
&packet,
now,
key.fingerprint(),
))
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
struct CellSigner {
next_scalar: Cell<u8>,
}
impl SignDigest for CellSigner {
fn sign_digest<'a>(
&'a self,
_signer: UncompressedPoint,
_digest: [u8; 32],
) -> SignDigestFuture<'a> {
let scalar = self.next_scalar.get();
self.next_scalar.set(scalar.wrapping_add(1));
let signature = EcdsaSignature {
r: [scalar; 32],
s: [scalar; 32],
};
Box::pin(async move { Ok(signature) })
}
}
#[tokio::test]
async fn armored_detached_signature_retains_metadata_and_accepts_a_non_sync_signer() {
let cell_signer = CellSigner {
next_scalar: Cell::new(1),
};
let signer: &dyn SignDigest = &cell_signer;
let key = SigningKey {
point: [4; 65]
.try_into()
.expect("an uncompressed point should parse"),
created: 1_700_000_000,
};
let signature = armored_detached_signature(key, b"document", signer, 1_700_000_001)
.await
.expect("the owned signature future should complete");
assert_eq!(signature.created(), 1_700_000_001);
assert_eq!(signature.fingerprint(), &key.fingerprint());
assert_eq!(cell_signer.next_scalar.get(), 2);
}
#[tokio::test]
async fn export_public_key_rejects_a_self_signature_scalar_at_or_above_the_curve_order() {
let cell_signer = CellSigner {
next_scalar: Cell::new(0xff),
};
let key = OpenPgpKey {
user_id: UserId::parse("Ada <ada@example.com>".to_owned()).unwrap(),
signing: SigningKey {
point: [4; 65]
.try_into()
.expect("an uncompressed point should parse"),
created: 1_700_000_000,
},
};
let error = export_public_key(&key, &cell_signer)
.await
.expect_err("an out of range scalar should be rejected");
assert!(matches!(
error.downcast_ref::<OpenPgpError>(),
Some(OpenPgpError::SignatureScalarOutOfRange)
));
}
#[test]
fn user_id_parse_rejects_a_nul_or_a_newline() {
for value in ["Ada\u{0}", "Ada\r\nFrom: forged", "Ada\n<ada@example.com>"] {
let error =
UserId::parse(value.to_string()).expect_err("a control byte should be rejected");
assert!(
matches!(error, OpenPgpError::UserIdControlByte),
"{value:?} should be rejected as a control byte"
);
}
}
}