Skip to content

Commit c191682

Browse files
authored
Remove PGP and SSH signature support (#220)
1 parent da2828b commit c191682

17 files changed

+95
-972
lines changed

DESIGN.md

Lines changed: 6 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,6 @@ Open Questions
88
since deriving a key with an explicit context seems like good practice, but
99
ed25519-dalek doesn't support it.
1010

11-
- *Should filepack attempt to support PGP v5 (LibraPGP) or PGP v6 signatures?*
12-
In the case of v5, no additional data is needed, we would only be generating
13-
different message bytes. In the case of v6, we need an additional 32-byte
14-
salt. Because the bech32m PGP signature string is already `signature1ap4…`,
15-
it would be easy to add v5 and v6 variants.
16-
17-
- *Should filepack attempt to support other signature schemes, like signify and
18-
minisign?* Any existing signature scheme which uses Ed25519 and SHA-512 and
19-
signs a superset of the message signed by the core filepack scheme could be
20-
supported and treated interchangeably. I think it's really only a question of
21-
demand.
22-
2311
- *Should filepack bech32m signature strings include the public key?* This
2412
would allow them to be self-describing and potentially easier to handle. If
2513
we did this, the map of public keys to signatures in the notes struct would
@@ -35,55 +23,15 @@ Open Questions
3523
will be detectable by failing to verify, but I want to make sure my
3624
understanding is correct.
3725

38-
- *Will signatures made with existing PGP and SSH keys pass strict
39-
verification?* We use ed25519-dalek's strict verification of signatures. This
40-
prevents certain kind of signature malleability issues, however it was not
41-
part of the original RFC, and I wonder if there are any users with existing
42-
PGP and SSH keys which fail strict verification.
43-
4426
Closed Questions
45-
46-
- *Should filepack signatures include an additional byte specifying the hashing
47-
algorithm?* Currently, three filepack signature types are supported,
48-
filepack, PGP, and SSH. All use Ed25519 as the signature scheme. The filepack
49-
signature scheme signs an unhashed message, so no other hashing algorithms
50-
are in use. SSH on the other hand signs a message including a hash of the
51-
message, and PGP signs a hash of an outer message that includes the inner
52-
message. Currently, we only allow SHA-512 as the hash algorithm in SSH and
53-
PGP signatures. This is both because it is the default algorithm that
54-
`ssh-keygen` and `gpg` use when creating Ed25519 signatures, and because
55-
Ed25519 uses SHA-512 internally, so using any other hashing algorithm
56-
introduces a new dependency. So, and here we finally get to the question,
57-
should we add a byte to the `signature1a…` bech32 representation of
58-
signatures which represents the hashing algorithm? This would allow us to
59-
extend the existing signature encoding scheme to support multiple hashing
60-
algorithms, although we would only do that if absolutely necessary, since
61-
SHA-512 is a perfectly good default, and the only reason to add additional
62-
algorithms would be something like supporting existing signing devices which
63-
do not support SHA-512. **Conclusion: This seems a very common degree of
64-
freedom in signature schemes, so I added it. Filepack, PGP, and SSH
65-
signatures now start with `af0q`, `ap4p`, and, `as0p`. The last character is
66-
the hashing algorithm, filepack does not hash the message, so it's `q`, which
67-
is the 0th bech32m element, PGP and SSH are restricted to SHA-512, which is
68-
`p`, or the 1st element. No support exists for alternative combinations of
69-
signature scheme and hash function, this is only in case we have to add
70-
support in the future.**
71-
7227
----------------
7328

74-
- *Should filepack re-use an existing signature system, like SSH or PGP?* I
75-
looked into both, but the formats and algorithms are incredibly complex, and
76-
allow a huge number of unnecessary degrees of freedom. **Conclusion: I added
77-
support for multiple signature schemes, filepack, GPG v4, and SSH. Filepack
78-
is the simple scheme which filepack uses when signing messages using keys it
79-
manages. The GPG and SSH schemes are compatible with GPG and SSH, but
80-
restricted to Ed25519 as the signature scheme, and SHA-512 as the hash
81-
algorithm. This allows signatures to be created using GPG or SSH, using
82-
existing keys, key management, and signers, and imported into filepack. All
83-
signature schemes sign the same message, wrapped in the case of GPG and SSH,
84-
and the resulting message and public key are always Ed25519 keys, and aside
85-
from the difference in message bytes, all signature schemes are verified in
86-
the same way.**
29+
- *Should filepack support SSH or PGP Ed25519 signatures?* Supporting SSH and
30+
PGP signatures would let developers reuse existing Ed25519 keys, and make use
31+
of existing key managment systems and hardware signers **Conclusion: I added
32+
support for SSH and PGP signatures, but ultimately decided to remove it. This
33+
is something that adds complexity, so I want to do it based on actual demand,
34+
concrete benefits, and actual use-cases. **
8735

8836
- *Should Bech32m be used for fingerprints, public keys, private keys, and
8937
signatures?* This would make them distinct and easy to identify, and give

bin/generate-gpg-test-files

Lines changed: 0 additions & 58 deletions
This file was deleted.

bin/generate-ssh-test-files

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/bech32_decoder.rs

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@ pub(crate) struct Bech32Decoder<'a> {
77
}
88

99
impl<'a> Bech32Decoder<'a> {
10-
pub(crate) fn byte_array<const LEN: usize>(&mut self) -> Result<[u8; LEN], Bech32Error> {
10+
pub(crate) fn byte_array<const LEN: usize>(mut self) -> Result<[u8; LEN], Bech32Error> {
1111
let mut array = [0; LEN];
1212

1313
for (slot, byte) in array.iter_mut().zip(self.bytes(LEN)?) {
1414
*slot = byte;
1515
}
1616

17+
let excess = self.data.len() - self.i;
18+
19+
ensure! {
20+
excess == 0,
21+
bech32_error::Overlong { excess, ty: self.ty },
22+
}
23+
1724
Ok(array)
1825
}
1926

@@ -43,39 +50,6 @@ impl<'a> Bech32Decoder<'a> {
4350
Ok(fes.fes_to_bytes())
4451
}
4552

46-
pub(crate) fn done(self) -> Result<(), Bech32Error> {
47-
let excess = self.data.len() - self.i;
48-
49-
ensure! {
50-
excess == 0,
51-
bech32_error::Overlong { excess, ty: self.ty },
52-
}
53-
54-
Ok(())
55-
}
56-
57-
pub(crate) fn fe(&mut self) -> Result<Fe32, Bech32Error> {
58-
let next = self
59-
.data
60-
.get(self.i)
61-
.map(|c| Fe32::from_char_unchecked(*c))
62-
.context(bech32_error::Truncated { ty: self.ty })?;
63-
64-
self.i += 1;
65-
66-
Ok(next)
67-
}
68-
69-
pub(crate) fn fe_array<const LEN: usize>(&mut self) -> Result<[Fe32; LEN], Bech32Error> {
70-
let mut array = [Fe32::Q; LEN];
71-
72-
for slot in &mut array {
73-
*slot = self.fe()?;
74-
}
75-
76-
Ok(array)
77-
}
78-
7953
fn fes(
8054
&mut self,
8155
len: usize,
@@ -93,11 +67,6 @@ impl<'a> Bech32Decoder<'a> {
9367
Ok(fes.iter().map(|c| Fe32::from_char_unchecked(*c)))
9468
}
9569

96-
pub(crate) fn into_bytes(mut self) -> Result<Vec<u8>, Bech32Error> {
97-
let fes = self.data.len() - self.i;
98-
Ok(self.bytes(fes * 5 / 8)?.collect())
99-
}
100-
10170
pub(crate) fn new(ty: Bech32Type, s: &'a str) -> Result<Self, Bech32Error> {
10271
let hrp_string =
10372
CheckedHrpstring::new::<bech32::Bech32m>(s).context(bech32_error::Decode { ty })?;
@@ -125,3 +94,49 @@ impl<'a> Bech32Decoder<'a> {
12594
})
12695
}
12796
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
102+
#[test]
103+
fn length() {
104+
#[track_caller]
105+
fn case(s: &str) {
106+
use bech32::Checksum;
107+
assert!(s.len() <= bech32::Bech32m::CODE_LENGTH);
108+
}
109+
110+
case(test::FINGERPRINT);
111+
case(test::PUBLIC_KEY);
112+
case(test::PRIVATE_KEY);
113+
case(test::SIGNATURE);
114+
}
115+
116+
#[test]
117+
fn private_key_round_trip() {
118+
assert_eq!(
119+
test::PRIVATE_KEY
120+
.parse::<PrivateKey>()
121+
.unwrap()
122+
.display_secret()
123+
.to_string(),
124+
test::PRIVATE_KEY,
125+
);
126+
}
127+
128+
#[test]
129+
fn round_trip() {
130+
#[track_caller]
131+
fn case<T: FromStr + ToString>(s: &str)
132+
where
133+
T::Err: fmt::Debug,
134+
{
135+
assert_eq!(s.parse::<T>().unwrap().to_string(), s);
136+
}
137+
138+
case::<Fingerprint>(test::FINGERPRINT);
139+
case::<PublicKey>(test::PUBLIC_KEY);
140+
case::<Signature>(test::SIGNATURE);
141+
}
142+
}

src/bech32_encoder.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ impl Bech32Encoder {
1010
self.fes.extend(bytes.iter().copied().bytes_to_fes());
1111
}
1212

13-
pub(crate) fn fes(&mut self, fes: &[Fe32]) {
14-
self.fes.extend_from_slice(fes);
15-
}
16-
1713
pub(crate) fn new(ty: Bech32Type) -> Self {
1814
Self {
1915
fes: vec![BECH32_VERSION],

src/fingerprint.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ impl FromStr for Fingerprint {
2323
type Err = Bech32Error;
2424

2525
fn from_str(s: &str) -> Result<Self, Self::Err> {
26-
let mut decoder = Bech32Decoder::new(Bech32Type::Fingerprint, s)?;
26+
let decoder = Bech32Decoder::new(Bech32Type::Fingerprint, s)?;
2727
let inner = decoder.byte_array()?;
28-
decoder.done()?;
2928
Ok(Self(inner.into()))
3029
}
3130
}

src/lib.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ use {
5454
path_error::PathError,
5555
public_key_error::PublicKeyError,
5656
sign_options::SignOptions,
57-
signature_error::SignatureError,
58-
signature_scheme::SignatureSchemeType,
5957
style::Style,
6058
subcommand::Subcommand,
6159
tag::Tag,
@@ -94,9 +92,7 @@ use {
9492
sync::LazyLock,
9593
time::{SystemTime, SystemTimeError, UNIX_EPOCH},
9694
},
97-
strum::{
98-
EnumDiscriminants, EnumIter, EnumString, IntoDiscriminant, IntoEnumIterator, IntoStaticStr,
99-
},
95+
strum::{EnumDiscriminants, EnumIter, EnumString, IntoEnumIterator, IntoStaticStr},
10096
url::Url,
10197
usized::IntoU64,
10298
walkdir::WalkDir,
@@ -106,11 +102,11 @@ pub use self::{
106102
directory::Directory, entry::Entry, error::Error, file::File, fingerprint::Fingerprint,
107103
hash::Hash, manifest::Manifest, message::Message, note::Note, private_key::PrivateKey,
108104
public_key::PublicKey, relative_path::RelativePath, serialized_message::SerializedMessage,
109-
signature::Signature, signature_scheme::SignatureScheme,
105+
signature::Signature,
110106
};
111107

112108
#[cfg(test)]
113-
use std::collections::HashSet;
109+
use {std::collections::HashSet, strum::IntoDiscriminant};
114110

115111
#[cfg(test)]
116112
fn tempdir() -> tempfile::TempDir {
@@ -134,6 +130,19 @@ macro_rules! assert_matches {
134130
}
135131
}
136132

133+
#[cfg(test)]
134+
macro_rules! assert_matches_regex {
135+
($haystack:expr, $re:expr $(,)?) => {{
136+
let haystack = $haystack;
137+
let re = Regex::new(&$re).unwrap();
138+
assert!(
139+
re.is_match(&haystack),
140+
"assertion failed: `{haystack:?}` does not match `{}`",
141+
re.as_str(),
142+
);
143+
}};
144+
}
145+
137146
mod arguments;
138147
mod bech32_decoder;
139148
mod bech32_encoder;
@@ -187,8 +196,6 @@ mod relative_path;
187196
mod serialized_message;
188197
mod sign_options;
189198
mod signature;
190-
mod signature_error;
191-
mod signature_scheme;
192199
mod style;
193200
mod subcommand;
194201
mod tag;

src/note.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ mod tests {
6565
test::PUBLIC_KEY,
6666
test::SIGNATURE,
6767
);
68-
assert_eq!(
68+
69+
assert_matches_regex! {
6970
serde_json::from_str::<Note>(&json).unwrap_err().to_string(),
70-
"invalid entry: found duplicate key at line 1 column 405",
71-
);
71+
r"invalid entry: found duplicate key at line 1 column \d+",
72+
}
7273
}
7374

7475
#[test]

0 commit comments

Comments
 (0)