Skip to content

Commit 7301d8e

Browse files
authored
Add PGP signature scheme (#196)
1 parent c9635cf commit 7301d8e

20 files changed

+1428
-145
lines changed

Cargo.lock

Lines changed: 741 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ pretty_assertions = "1.4.1"
4545
ssh-key = "0.6.0"
4646
tempfile = "3.24.0"
4747

48+
[dev-dependencies.sequoia-openpgp]
49+
version = "2.1.0"
50+
default-features = false
51+
features = ["allow-experimental-crypto", "allow-variable-time-crypto", "crypto-rust"]
52+
4853
[lints.clippy]
4954
all = { level = "deny", priority = -1 }
5055
arbitrary-source-item-ordering = "deny"

src/bech32m.rs

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ pub(crate) trait Bech32m<const PREFIX: usize, const DATA: usize> {
66
const HRP: Hrp;
77
const TYPE: &'static str;
88

9-
fn decode_bech32m(s: &str) -> Result<([Fe32; PREFIX], [u8; DATA]), Bech32mError> {
9+
type Suffix: Bech32mSuffix;
10+
11+
fn decode_bech32m(s: &str) -> Result<Bech32mPayload<PREFIX, DATA, Self::Suffix>, Bech32mError> {
1012
let hrp_string = CheckedHrpstring::new::<bech32::Bech32m>(s)
1113
.context(bech32m_error::Decode { ty: Self::TYPE })?;
1214

@@ -28,8 +30,6 @@ pub(crate) trait Bech32m<const PREFIX: usize, const DATA: usize> {
2830
bech32m_error::UnsupportedVersion { ty: Self::TYPE, version },
2931
}
3032

31-
Self::validate_padding(&hrp_string).context(bech32m_error::Padding { ty: Self::TYPE })?;
32-
3333
let mut prefix = [Fe32::Q; PREFIX];
3434

3535
for (actual, fe32) in prefix.iter_mut().enumerate() {
@@ -42,38 +42,46 @@ pub(crate) trait Bech32m<const PREFIX: usize, const DATA: usize> {
4242

4343
let mut data = [0; DATA];
4444

45-
{
46-
let mut bytes = fe32s.fes_to_bytes();
47-
48-
let mut actual = 0;
49-
for byte in &mut data {
50-
*byte = bytes.next().context(bech32m_error::DataLength {
51-
actual,
52-
expected: DATA,
53-
ty: Self::TYPE,
54-
})?;
55-
actual += 1;
56-
}
57-
58-
actual += bytes.count();
59-
60-
ensure! {
61-
actual == DATA,
62-
bech32m_error::DataLength {
63-
actual,
64-
expected: DATA,
65-
ty: Self::TYPE,
66-
},
67-
}
45+
let mut bytes = fe32s.fes_to_bytes();
46+
47+
for (actual, byte) in data.iter_mut().enumerate() {
48+
*byte = bytes.next().context(bech32m_error::DataLength {
49+
actual,
50+
expected: DATA,
51+
ty: Self::TYPE,
52+
})?;
6853
}
6954

70-
Ok((prefix, data))
55+
let suffix = Self::Suffix::from_bytes(Self::TYPE, bytes)?;
56+
57+
Self::validate_padding(&hrp_string).context(bech32m_error::Padding { ty: Self::TYPE })?;
58+
59+
Ok(Bech32mPayload {
60+
data,
61+
prefix,
62+
suffix,
63+
})
7164
}
7265

73-
fn encode_bech32m(f: &mut Formatter, prefix: [Fe32; PREFIX], data: [u8; DATA]) -> fmt::Result {
66+
fn encode_bech32m(
67+
f: &mut Formatter,
68+
payload: Bech32mPayload<PREFIX, DATA, Self::Suffix>,
69+
) -> fmt::Result {
70+
let Bech32mPayload {
71+
data,
72+
prefix,
73+
suffix,
74+
} = payload;
75+
7476
let chars = prefix
7577
.into_iter()
76-
.chain(data.iter().copied().bytes_to_fes())
78+
.chain(
79+
data
80+
.iter()
81+
.copied()
82+
.chain(suffix.into_bytes())
83+
.bytes_to_fes(),
84+
)
7785
.with_checksum::<bech32::Bech32m>(&Self::HRP)
7886
.with_witness_version(VERSION)
7987
.chars();
@@ -90,6 +98,10 @@ pub(crate) trait Bech32m<const PREFIX: usize, const DATA: usize> {
9098

9199
fe32s.next().unwrap();
92100

101+
for _ in 0..PREFIX {
102+
fe32s.next().unwrap();
103+
}
104+
93105
let Some((i, last)) = fe32s.enumerate().last() else {
94106
return Ok(());
95107
};
@@ -117,11 +129,12 @@ mod tests {
117129
impl Bech32m<0, 0> for EmptyPublicKey {
118130
const HRP: Hrp = Hrp::parse_unchecked("public");
119131
const TYPE: &'static str = "public key";
132+
type Suffix = ();
120133
}
121134

122135
impl Display for EmptyPublicKey {
123136
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
124-
Self::encode_bech32m(f, [], [])
137+
Self::encode_bech32m(f, Bech32mPayload::from_data([]))
125138
}
126139
}
127140

@@ -130,11 +143,12 @@ mod tests {
130143
impl Bech32m<0, 33> for LongPublicKey {
131144
const HRP: Hrp = Hrp::parse_unchecked("public");
132145
const TYPE: &'static str = "public key";
146+
type Suffix = ();
133147
}
134148

135149
impl Display for LongPublicKey {
136150
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
137-
Self::encode_bech32m(f, [], [0; 33])
151+
Self::encode_bech32m(f, Bech32mPayload::from_data([0; 33]))
138152
}
139153
}
140154

@@ -189,7 +203,7 @@ mod tests {
189203

190204
case(
191205
&LongPublicKey.to_string(),
192-
"expected bech32m public key to have 32 data bytes but found 33",
206+
"expected bech32m public key to have 0 suffix bytes but found 1",
193207
);
194208

195209
let public_key = test::PUBLIC_KEY.parse::<PublicKey>().unwrap();
@@ -241,6 +255,7 @@ mod tests {
241255
impl Bech32m<2, 0> for PrefixedType {
242256
const HRP: Hrp = Hrp::parse_unchecked("test");
243257
const TYPE: &'static str = "test";
258+
type Suffix = ();
244259
}
245260

246261
let bech32m = []

src/bech32m_error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ pub enum Bech32mError {
3838
actual: usize,
3939
ty: &'static str,
4040
},
41+
#[snafu(display(
42+
"expected bech32m {ty} to have {} but found {actual}",
43+
Count(*expected, "suffix byte"),
44+
))]
45+
SuffixLength {
46+
expected: usize,
47+
actual: usize,
48+
ty: &'static str,
49+
},
4150
#[snafu(display("bech32m {ty} version `{version}` is not supported"))]
4251
UnsupportedVersion {
4352
ty: &'static str,

src/bech32m_payload.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use super::*;
2+
3+
#[derive(Debug)]
4+
pub(crate) struct Bech32mPayload<const PREFIX: usize, const DATA: usize, T> {
5+
pub(crate) data: [u8; DATA],
6+
pub(crate) prefix: [Fe32; PREFIX],
7+
pub(crate) suffix: T,
8+
}
9+
10+
impl<const DATA: usize> Bech32mPayload<0, DATA, ()> {
11+
pub(crate) fn from_data(data: [u8; DATA]) -> Self {
12+
Self {
13+
data,
14+
prefix: [],
15+
suffix: (),
16+
}
17+
}
18+
19+
pub(crate) fn into_data(self) -> [u8; DATA] {
20+
self.data
21+
}
22+
}

src/bech32m_suffix.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use super::*;
2+
3+
type Bytes<'a> = FesToBytes<AsciiToFe32Iter<'a>>;
4+
5+
pub(crate) trait Bech32mSuffix: Sized {
6+
fn from_bytes(ty: &'static str, bytes: Bytes) -> Result<Self, Bech32mError>;
7+
8+
fn into_bytes(self) -> Vec<u8>;
9+
}
10+
11+
impl Bech32mSuffix for () {
12+
fn from_bytes(ty: &'static str, bytes: Bytes) -> Result<Self, Bech32mError> {
13+
let actual = bytes.count();
14+
15+
ensure! {
16+
actual == 0,
17+
bech32m_error::SuffixLength {
18+
actual,
19+
expected: 0usize,
20+
ty,
21+
},
22+
}
23+
24+
Ok(())
25+
}
26+
27+
fn into_bytes(self) -> Vec<u8> {
28+
Vec::new()
29+
}
30+
}
31+
32+
impl Bech32mSuffix for Vec<u8> {
33+
fn from_bytes(_ty: &'static str, bytes: Bytes) -> Result<Self, Bech32mError> {
34+
Ok(bytes.collect())
35+
}
36+
37+
fn into_bytes(self) -> Vec<u8> {
38+
self
39+
}
40+
}

src/display_secret.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ pub(crate) struct DisplaySecret(pub(crate) PrivateKey);
44

55
impl Display for DisplaySecret {
66
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
7-
PrivateKey::encode_bech32m(f, [], self.0.as_secret_bytes())
7+
PrivateKey::encode_bech32m(f, Bech32mPayload::from_data(self.0.as_secret_bytes()))
88
}
99
}

src/fingerprint.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ impl Fingerprint {
1515
impl Bech32m<0, { Fingerprint::LEN }> for Fingerprint {
1616
const HRP: Hrp = Hrp::parse_unchecked("package");
1717
const TYPE: &'static str = "package fingerprint";
18+
type Suffix = ();
1819
}
1920

2021
impl Display for Fingerprint {
2122
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
22-
Self::encode_bech32m(f, [], *self.as_bytes())
23+
Self::encode_bech32m(f, Bech32mPayload::from_data(*self.as_bytes()))
2324
}
2425
}
2526

2627
impl FromStr for Fingerprint {
2728
type Err = Bech32mError;
2829

2930
fn from_str(s: &str) -> Result<Self, Self::Err> {
30-
let ([], data) = Self::decode_bech32m(s)?;
31-
Ok(Self(data.into()))
31+
Ok(Self(Self::decode_bech32m(s)?.into_data().into()))
3232
}
3333
}

src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use {
2121
arguments::Arguments,
2222
bech32m::Bech32m,
2323
bech32m_error::Bech32mError,
24+
bech32m_payload::Bech32mPayload,
25+
bech32m_suffix::Bech32mSuffix,
2426
component::Component,
2527
component_error::ComponentError,
2628
count::Count,
@@ -63,7 +65,10 @@ use {
6365
},
6466
bech32::{
6567
ByteIterExt, Fe32, Fe32IterExt, Hrp,
66-
primitives::decode::{CheckedHrpstring, CheckedHrpstringError, PaddingError},
68+
primitives::{
69+
decode::{AsciiToFe32Iter, CheckedHrpstring, CheckedHrpstringError, PaddingError},
70+
iter::FesToBytes,
71+
},
6772
},
6873
blake3::Hasher,
6974
camino::{Utf8Component, Utf8Path, Utf8PathBuf},
@@ -133,6 +138,8 @@ macro_rules! assert_matches {
133138
mod arguments;
134139
mod bech32m;
135140
mod bech32m_error;
141+
mod bech32m_payload;
142+
mod bech32m_suffix;
136143
mod component;
137144
mod component_error;
138145
mod count;
@@ -189,6 +196,8 @@ mod tag;
189196
mod ticked;
190197
mod utf8_path_ext;
191198

199+
#[cfg(test)]
200+
mod pgp;
192201
#[cfg(test)]
193202
mod ssh;
194203
#[cfg(test)]

0 commit comments

Comments
 (0)