Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ default-members = ["api", "cli"]

[workspace.dependencies]
base64 = "0.22.0"
pem = "3"
clap = { version = "4.4.0", features = ["derive"] }
ed25519-dalek = { version = "2.0.0", features = ["digest"] }
getrandom = { version = ">= 0.3.0, < 0.5.0", features = ["std"] }
Expand Down
1 change: 1 addition & 0 deletions api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"]
[dependencies]
base64 = { workspace = true, optional = true }
ed25519-dalek.workspace = true
pem.workspace = true
thiserror.workspace = true
zip = { workspace = true, optional = true }

Expand Down
171 changes: 171 additions & 0 deletions api/src/keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//! Key encoding and decoding utilities

use crate::{KEYPAIR_LENGTH, PUBLIC_KEY_LENGTH, SigningKey, VerifyingKey};

/// PEM tag used for public (verifying) keys
pub const PUBLIC_KEY_PEM_TAG: &str = "ZIPSIGN PUBLIC KEY";

/// PEM tag used for private (signing) keys
pub const PRIVATE_KEY_PEM_TAG: &str = "ZIPSIGN PRIVATE KEY";

crate::Error! {
/// An error returned by [`parse_signing_key`] and [`parse_verifying_key`]
pub struct ParseKeyError(ParseKey) {
#[error("expected key length {0}, got {1}")]
Length(usize, usize),
#[error("the PEM data could not be parsed")]
Pem(#[source] pem::PemError),
#[error("expected PEM tag {0:?}, got {1:?}")]
Tag(&'static str, String),
#[error("the key data was invalid")]
Key(#[source] ed25519_dalek::SignatureError),
}
}

/// Encode a signing (private) key as a PEM string
pub fn encode_signing_key(key: &SigningKey) -> String {
pem::encode(&pem::Pem::new(PRIVATE_KEY_PEM_TAG, key.to_keypair_bytes()))
}

/// Encode a verifying (public) key as a PEM string
pub fn encode_verifying_key(key: &VerifyingKey) -> String {
pem::encode(&pem::Pem::new(PUBLIC_KEY_PEM_TAG, key.as_bytes()))
}

/// Parse a signing key from either raw bytes (64-byte keypair) or PEM format
pub fn parse_signing_key(input: &[u8]) -> Result<SigningKey, ParseKeyError> {
let bytes = decode_key(input, PRIVATE_KEY_PEM_TAG, KEYPAIR_LENGTH)?;
let mut arr = [0u8; KEYPAIR_LENGTH];
arr.copy_from_slice(&bytes);
SigningKey::from_keypair_bytes(&arr).map_err(|e| ParseKey::Key(e).into())
}

/// Parse a verifying key from either raw bytes (32-byte key) or PEM format
pub fn parse_verifying_key(input: &[u8]) -> Result<VerifyingKey, ParseKeyError> {
let bytes = decode_key(input, PUBLIC_KEY_PEM_TAG, PUBLIC_KEY_LENGTH)?;
let mut arr = [0u8; PUBLIC_KEY_LENGTH];
arr.copy_from_slice(&bytes);
VerifyingKey::from_bytes(&arr).map_err(|e| ParseKey::Key(e).into())
}

/// Extract raw key bytes from either PEM or raw-byte input, validating length
fn decode_key(
input: &[u8],
expected_tag: &'static str,
expected_len: usize,
) -> Result<Vec<u8>, ParseKeyError> {
let begin = format!("-----BEGIN {}-----", expected_tag);
if input.starts_with(begin.as_bytes()) {
let p = pem::parse(input).map_err(ParseKey::Pem)?;
if p.tag() != expected_tag {
return Err(ParseKey::Tag(expected_tag, p.tag().to_owned()).into());
}
let contents = p.contents();
if contents.len() != expected_len {
return Err(ParseKey::Length(expected_len, contents.len()).into());
}
Ok(contents.to_owned())
} else {
if input.len() != expected_len {
return Err(ParseKey::Length(expected_len, input.len()).into());
}
Ok(input.to_owned())
}
}

#[cfg(test)]
mod tests {
use ed25519_dalek::SecretKey;

use super::*;

fn test_signing_key() -> SigningKey {
SigningKey::from_bytes(&SecretKey::default())
}

#[test]
fn encode_signing_key_has_correct_pem_header() {
let pem = encode_signing_key(&test_signing_key());
assert!(
pem.starts_with(&format!("-----BEGIN {}-----", PRIVATE_KEY_PEM_TAG)),
"unexpected header: {pem}",
);
}

#[test]
fn encode_verifying_key_has_correct_pem_header() {
let pem = encode_verifying_key(&test_signing_key().verifying_key());
assert!(
pem.starts_with(&format!("-----BEGIN {}-----", PUBLIC_KEY_PEM_TAG)),
"unexpected header: {pem}",
);
}

#[test]
fn pem_signing_key_round_trip() {
let key = test_signing_key();
let pem = encode_signing_key(&key);
let parsed = parse_signing_key(pem.as_bytes()).expect("parse failed");
assert_eq!(key.to_keypair_bytes(), parsed.to_keypair_bytes());
}

#[test]
fn pem_verifying_key_round_trip() {
let key = test_signing_key().verifying_key();
let pem = encode_verifying_key(&key);
let parsed = parse_verifying_key(pem.as_bytes()).expect("parse failed");
assert_eq!(key.as_bytes(), parsed.as_bytes());
}

#[test]
fn parse_signing_key_from_raw_bytes() {
let key = test_signing_key();
let parsed = parse_signing_key(&key.to_keypair_bytes()).expect("parse failed");
assert_eq!(key.to_keypair_bytes(), parsed.to_keypair_bytes());
}

#[test]
fn parse_verifying_key_from_raw_bytes() {
let key = test_signing_key().verifying_key();
let parsed = parse_verifying_key(key.as_bytes()).expect("parse failed");
assert_eq!(key.as_bytes(), parsed.as_bytes());
}

#[test]
fn parse_signing_key_wrong_tag() {
let pem = encode_verifying_key(&test_signing_key().verifying_key());
let err = parse_signing_key(pem.as_bytes()).expect_err("should fail");
assert!(
format!("{err}").contains("expected key length 64"),
"unexpected error: {err}"
);
}

#[test]
fn parse_verifying_key_wrong_tag() {
let pem = encode_signing_key(&test_signing_key());
let err = parse_verifying_key(pem.as_bytes()).expect_err("should fail");
assert!(
format!("{err}").contains("expected key length 32"),
"unexpected error: {err}"
);
}

#[test]
fn parse_signing_key_wrong_length_raw() {
let err = parse_signing_key(&[0u8; 16]).expect_err("should fail");
assert!(
format!("{err}").contains("expected key length"),
"unexpected error: {err}"
);
}

#[test]
fn parse_verifying_key_wrong_length_raw() {
let err = parse_verifying_key(&[0u8; 16]).expect_err("should fail");
assert!(
format!("{err}").contains("expected key length"),
"unexpected error: {err}"
);
}
}
3 changes: 3 additions & 0 deletions api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#![doc = include_str!("../README.md")]

mod constants;
pub mod keys;
pub mod sign;
#[cfg(any(feature = "sign-zip", feature = "unsign-zip"))]
mod sign_unsign_zip;
Expand Down Expand Up @@ -102,6 +103,8 @@ pub enum ZipsignError {
NoMatch(#[from] self::verify::NoMatch),
/// An error returned by [`collect_keys()`][self::verify::collect_keys]
CollectKeys(#[from] self::verify::CollectKeysError),
/// An error returned by [`read_verifying_keys()`][self::verify::read_verifying_keys]
ReadVerifyingKeys(#[from] self::verify::ReadVerifyingKeysError),
/// An error returned by [`read_signatures()`][self::verify::read_signatures]
ReadSignatures(#[from] self::verify::ReadSignaturesError),
/// An error returned by [`verify_tar()`][self::verify::verify_tar]
Expand Down
18 changes: 9 additions & 9 deletions api/src/sign/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,36 @@ pub use self::tar::{SignTarError, copy_and_sign_tar};
#[cfg(feature = "sign-zip")]
pub use self::zip::{SignZipError, copy_and_sign_zip};
use crate::constants::{BUF_LIMIT, HEADER_SIZE, MAGIC_HEADER, SignatureCountLeInt};
use crate::{KEYPAIR_LENGTH, Prehash, SIGNATURE_LENGTH, SignatureError, SigningKey};
use crate::{Prehash, SIGNATURE_LENGTH, SignatureError, SigningKey};

crate::Error! {
/// An error returned by [`read_signing_keys()`]
pub struct ReadSigningKeysError(KeysError) {
#[error("input #{1} did not contain a valid key")]
Construct(#[source] ed25519_dalek::ed25519::Error, usize),
#[error("no signing keys provided")]
Empty,
#[error("input #{1} did not contain a valid key")]
Parse(#[source] crate::keys::ParseKeyError, usize),
#[error("could not read key in file #{1}")]
Read(#[source] std::io::Error, usize),
}
}

/// Read signing keys from an [`Iterator`] of [readable][Read] inputs
/// Read signing keys from an [`Iterator`] of [readable][Read] inputs.
/// Each input may contain either a raw 64-byte keypair or a PEM-encoded key.
pub fn read_signing_keys<I, R>(inputs: I) -> Result<Vec<SigningKey>, ReadSigningKeysError>
where
I: IntoIterator<Item = std::io::Result<R>>,
R: Read,
{
// read signing keys
let mut keys = inputs
.into_iter()
.enumerate()
.map(|(key_index, input)| {
let mut key = [0; KEYPAIR_LENGTH];
input
.and_then(|mut input| input.read_exact(&mut key))
let mut buf = Vec::new();
let _: usize = input
.and_then(|mut input| input.read_to_end(&mut buf))
.map_err(|err| KeysError::Read(err, key_index))?;
SigningKey::from_keypair_bytes(&key).map_err(|err| KeysError::Construct(err, key_index))
crate::keys::parse_signing_key(&buf).map_err(|err| KeysError::Parse(err, key_index))
})
.collect::<Result<Vec<_>, _>>()?;
if keys.is_empty() {
Expand Down
37 changes: 37 additions & 0 deletions api/src/verify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,43 @@ use crate::{
PUBLIC_KEY_LENGTH, Prehash, SIGNATURE_LENGTH, Signature, SignatureError, VerifyingKey,
};

crate::Error! {
/// An error returned by [`read_verifying_keys()`]
pub struct ReadVerifyingKeysError(VerifyKeysError) {
#[error("no verifying keys provided")]
Empty,
#[error("input #{1} did not contain a valid key")]
Parse(#[source] crate::keys::ParseKeyError, usize),
#[error("could not read key in file #{1}")]
Read(#[source] std::io::Error, usize),
}
}

/// Read verifying keys from an [`Iterator`] of [readable][Read] inputs.
/// Each input may contain either a raw 32-byte key or a PEM-encoded key.
pub fn read_verifying_keys<I, R>(inputs: I) -> Result<Vec<VerifyingKey>, ReadVerifyingKeysError>
where
I: IntoIterator<Item = std::io::Result<R>>,
R: Read,
{
let keys = inputs
.into_iter()
.enumerate()
.map(|(key_index, input)| {
let mut buf = Vec::new();
let _: usize = input
.and_then(|mut input| input.read_to_end(&mut buf))
.map_err(|err| VerifyKeysError::Read(err, key_index))?;
crate::keys::parse_verifying_key(&buf)
.map_err(|err| VerifyKeysError::Parse(err, key_index))
})
.collect::<Result<Vec<_>, _>>()?;
if keys.is_empty() {
return Err(VerifyKeysError::Empty.into());
}
Ok(keys)
}

crate::Error! {
/// An error returned by [`collect_keys()`]
pub struct CollectKeysError(KeysError) {
Expand Down
38 changes: 26 additions & 12 deletions cli/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::os::unix::prelude::OpenOptionsExt;
use std::path::PathBuf;

use clap::Parser;
use ed25519_dalek::{KEYPAIR_LENGTH, SecretKey, SigningKey};
use ed25519_dalek::{SecretKey, SigningKey};
use zipsign_api::keys::{
ParseKeyError, encode_signing_key, encode_verifying_key, parse_signing_key,
};

/// Generate a signing key
#[derive(Debug, Parser, Clone)]
Expand All @@ -20,6 +23,9 @@ pub(crate) struct Cli {
/// Overwrite output files if they exists
#[arg(long, short = 'f')]
force: bool,
/// Write keys in PEM format instead of raw binary
#[arg(long, short = 'p')]
pem: bool,
}

#[derive(Debug, thiserror::Error)]
Expand All @@ -32,8 +38,8 @@ pub(crate) enum Error {
Write(#[source] std::io::Error, PathBuf),
#[error("could not read from {1:?}")]
Read(#[source] std::io::Error, PathBuf),
#[error("no valid key found in from {1:?}")]
IllegalKey(#[source] ed25519_dalek::SignatureError, PathBuf),
#[error("no valid key found in {1:?}")]
ParseKey(#[source] ParseKeyError, PathBuf),
#[error("could not get random data")]
Random(#[source] getrandom::Error),
}
Expand All @@ -45,13 +51,13 @@ pub(crate) fn main(args: Cli) -> Result<(), Error> {
Ok(f) => f,
Err(err) => return Err(Error::OpenRead(err, args.private_key)),
};
let mut key = [0; KEYPAIR_LENGTH];
if let Err(err) = f.read_exact(&mut key) {
return Err(Error::Read(err, args.private_key));
let mut buf = Vec::new();
if let Err(err) = f.read_to_end(&mut buf) {
return Err(Error::Read(err, args.private_key.clone()));
}
match SigningKey::from_keypair_bytes(&key) {
match parse_signing_key(&buf) {
Ok(key) => key,
Err(err) => return Err(Error::IllegalKey(err, args.private_key)),
Err(err) => return Err(Error::ParseKey(err, args.private_key)),
}
} else {
let mut secret = SecretKey::default();
Expand All @@ -69,8 +75,12 @@ pub(crate) fn main(args: Cli) -> Result<(), Error> {
Ok(f) => f,
Err(err) => return Err(Error::OpenWrite(err, args.private_key)),
};
f.write_all(&key.to_keypair_bytes())
.map_err(|err| Error::Write(err, args.private_key))?;
if args.pem {
f.write_all(encode_signing_key(&key).as_bytes())
} else {
f.write_all(&key.to_keypair_bytes())
}
.map_err(|err| Error::Write(err, args.private_key))?;
key
};

Expand All @@ -84,8 +94,12 @@ pub(crate) fn main(args: Cli) -> Result<(), Error> {
Ok(f) => f,
Err(err) => return Err(Error::OpenWrite(err, args.verifying_key)),
};
f.write_all(key.verifying_key().as_bytes())
.map_err(|err| Error::Write(err, args.verifying_key))
if args.pem {
f.write_all(encode_verifying_key(&key.verifying_key()).as_bytes())
} else {
f.write_all(key.verifying_key().as_bytes())
}
.map_err(|err| Error::Write(err, args.verifying_key))
}

#[allow(dead_code)]
Expand Down
Loading