use std::fmt::Display;
use std::vec;
use base64::Engine;
use ed25519_dalek::ed25519;
use crate::errors::Result;
use crate::util::validate_comment;
use crate::{ErrorKind, SError, ALG_SIZE, KID_SIZE, SIGALG_PREHASHED, SIG_SIZE};
#[derive(Debug, Clone)]
pub struct SignatureBox<'s> {
pub(crate) untrusted_comment: Option<&'s str>,
pub(crate) trusted_comment: Option<&'s str>,
pub(crate) signature: Signature,
}
fn parse_signature(s: &'_ str) -> Result<SignatureBox<'_>> {
let mut lines = s.lines();
let untrusted_comment = lines
.next()
.ok_or_else(|| {
SError::new(
crate::ErrorKind::SignatureError,
"missing untrusted comment",
)
})?
.strip_prefix("untrusted comment: ")
.ok_or_else(|| {
SError::new(
crate::ErrorKind::SignatureError,
"missing untrusted comment",
)
})?;
validate_comment(Some(untrusted_comment), ErrorKind::SignatureError)?;
let sig = lines
.next()
.ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing signature"))?;
let decoder = base64::engine::general_purpose::STANDARD;
let sig_format = decoder
.decode(sig.as_bytes())
.map_err(|e| SError::new(crate::ErrorKind::SignatureError, e))?;
if sig_format.len() != ALG_SIZE + KID_SIZE + SIG_SIZE {
return Err(SError::new(
crate::ErrorKind::SignatureError,
"invalid signature length",
));
}
let sig_alg = &sig_format[..ALG_SIZE];
let key_id = &sig_format[ALG_SIZE..ALG_SIZE + KID_SIZE];
let sig = &sig_format[ALG_SIZE + KID_SIZE..];
let trusted_comment = lines
.next()
.ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing trusted comment"))?
.strip_prefix("trusted comment: ")
.ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing trusted comment"))?;
validate_comment(Some(trusted_comment), ErrorKind::SignatureError)?;
let global_sig = lines
.next()
.ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing global signature"))?;
if lines.next().is_some() {
return Err(SError::new(
crate::ErrorKind::SignatureError,
"unexpected extra data",
));
}
let global_sig_format = decoder
.decode(global_sig.as_bytes())
.map_err(|e| SError::new(crate::ErrorKind::SignatureError, e))?;
if global_sig_format.len() != 64 {
return Err(SError::new(
crate::ErrorKind::SignatureError,
"invalid global signature length",
));
}
SignatureBox::new(
Some(untrusted_comment),
Some(trusted_comment),
Signature::new(
sig_alg.try_into().unwrap(),
key_id.try_into().unwrap(),
sig.try_into().unwrap(),
ed25519::Signature::from_bytes(&global_sig_format.try_into().unwrap()),
),
)
}
#[cfg(test)]
#[test]
fn test_parse_signature() {
use crate::{sign, KeyPairBox};
let password = b"password";
let k = KeyPairBox::generate(Some(password), None, None).unwrap();
let file = sign(
Some(&k.public_key_box),
&k.secret_key_box,
Some(password),
"test".as_bytes(),
Some("trusted comment"),
Some("untrusted comment"),
)
.unwrap()
.to_string();
let sig = parse_signature(&file).unwrap();
assert_eq!(file, sig.to_string());
}
#[cfg(test)]
#[test]
fn test_parse_signature_rejects_extra_lines() {
use crate::{sign, KeyPairBox};
let password = b"password";
let keypair = KeyPairBox::generate(Some(password), None, None).unwrap();
let file = format!(
"{}extra\n",
sign(
Some(&keypair.public_key_box),
&keypair.secret_key_box,
Some(password),
"test".as_bytes(),
Some("trusted comment"),
Some("untrusted comment"),
)
.unwrap()
);
assert!(parse_signature(&file).is_err());
}
#[cfg(test)]
#[test]
fn test_parse_signature_rejects_control_char_comments() {
use crate::{sign, KeyPairBox};
let password = b"password";
let keypair = KeyPairBox::generate(Some(password), None, None).unwrap();
let file = sign(
Some(&keypair.public_key_box),
&keypair.secret_key_box,
Some(password),
"test".as_bytes(),
Some("trusted comment"),
Some("untrusted comment"),
)
.unwrap()
.to_string()
.replace(
"trusted comment: trusted comment",
"trusted comment: trusted\0comment",
);
assert!(parse_signature(&file).is_err());
}
impl Display for SignatureBox<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = String::new();
s.push_str("untrusted comment: ");
if let Some(c) = self.untrusted_comment {
s.push_str(c);
}
s.push('\n');
let encoder = base64::engine::general_purpose::STANDARD;
let mut sig_format = vec![];
sig_format.extend_from_slice(&self.signature.sig_alg);
sig_format.extend_from_slice(&self.signature.key_id);
sig_format.extend_from_slice(&self.signature.sig.to_bytes());
let sig = encoder.encode(&sig_format);
s.push_str(&sig);
s.push('\n');
s.push_str("trusted comment: ");
if let Some(c) = self.trusted_comment {
s.push_str(c);
}
s.push('\n');
let global_sig = encoder.encode(self.signature.global_sig.to_bytes());
s.push_str(&global_sig);
s.push('\n');
write!(f, "{}", s)
}
}
impl<'s> SignatureBox<'s> {
pub(crate) fn new(
untrusted_comment: Option<&'s str>,
trusted_comment: Option<&'s str>,
signature: Signature,
) -> Result<Self> {
validate_comment(untrusted_comment, ErrorKind::SignatureError)?;
validate_comment(trusted_comment, ErrorKind::SignatureError)?;
Ok(Self {
untrusted_comment,
trusted_comment,
signature,
})
}
pub fn is_prehashed(&self) -> bool {
self.signature.sig_alg == SIGALG_PREHASHED
}
pub fn untrusted_comment(&self) -> Option<&'s str> {
self.untrusted_comment
}
pub fn trusted_comment(&self) -> Option<&'s str> {
self.trusted_comment
}
pub fn key_id(&self) -> &[u8; KID_SIZE] {
&self.signature.key_id
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<SignatureBox<'_>> {
parse_signature(s)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Signature {
pub sig_alg: [u8; ALG_SIZE],
pub key_id: [u8; KID_SIZE],
pub sig: ed25519::Signature,
pub global_sig: ed25519::Signature,
}
impl Signature {
pub fn new(
sig_alg: [u8; ALG_SIZE],
key_id: [u8; KID_SIZE],
sig: ed25519::Signature,
global_sig: ed25519::Signature,
) -> Self {
Self {
sig_alg,
key_id,
sig,
global_sig,
}
}
}