Skip to content

Commit 8e0071e

Browse files
committed
Implement Base64 encoding for ZIP 304 signatures
1 parent 6ee4513 commit 8e0071e

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ aes = "0.8"
7474
fpe = { version = "0.6", default-features = false, features = ["alloc"] }
7575
zip32 = { version = "0.2", default-features = false }
7676

77+
# ZIP 304
78+
base64 = "0.22"
7779

7880
[dev-dependencies]
7981
chacha20poly1305 = "0.10"

src/zip304.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
//!
33
//! [ZIP 304]: https://zips.z.cash/zip-0304
44
5+
use core::{fmt, str::FromStr};
6+
7+
use base64::{prelude::BASE64_STANDARD, Engine};
58
use bellman::{
69
gadgets::multipack,
710
groth16::{verify_proof, Proof},
@@ -89,6 +92,71 @@ impl Signature {
8992
}
9093
}
9194

95+
/// Errors that can occur when parsing a ZIP 304 signature from a string.
96+
#[derive(Clone, Debug, PartialEq, Eq)]
97+
pub enum ParseError {
98+
Base64(base64::DecodeSliceError),
99+
InvalidLength,
100+
InvalidPrefix,
101+
}
102+
103+
impl From<base64::DecodeSliceError> for ParseError {
104+
fn from(e: base64::DecodeSliceError) -> Self {
105+
ParseError::Base64(e)
106+
}
107+
}
108+
109+
impl fmt::Display for ParseError {
110+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111+
match self {
112+
ParseError::Base64(e) => write!(f, "Invalid Base64: {}", e),
113+
ParseError::InvalidLength => {
114+
write!(
115+
f,
116+
"Signature length is invalid (should be 435 characters including prefix)"
117+
)
118+
}
119+
ParseError::InvalidPrefix => write!(f, "Invalid prefix (should be 'zip304:')"),
120+
}
121+
}
122+
}
123+
124+
#[cfg(feature = "std")]
125+
impl std::error::Error for ParseError {
126+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127+
match self {
128+
ParseError::Base64(e) => Some(e),
129+
_ => None,
130+
}
131+
}
132+
}
133+
134+
impl FromStr for Signature {
135+
type Err = ParseError;
136+
137+
fn from_str(s: &str) -> Result<Self, Self::Err> {
138+
match s.split_at(7) {
139+
("zip304:", encoded) => {
140+
if encoded.len() == 428 {
141+
// We need an extra byte to decode into.
142+
let mut bytes = [0; 321];
143+
assert_eq!(BASE64_STANDARD.decode_slice(encoded, &mut bytes)?, 320);
144+
Ok(Signature::from_bytes(&bytes[..320].try_into().unwrap()))
145+
} else {
146+
Err(ParseError::InvalidLength)
147+
}
148+
}
149+
_ => Err(ParseError::InvalidPrefix),
150+
}
151+
}
152+
}
153+
154+
impl fmt::Display for Signature {
155+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156+
write!(f, "zip304:{}", BASE64_STANDARD.encode(self.to_bytes()))
157+
}
158+
}
159+
92160
/// Signs an arbitrary message for the given [`PaymentAddress`] and [`SLIP 44`] coin type.
93161
///
94162
/// The coin type is used here in its index form, not its hardened form (i.e. 133 for
@@ -298,10 +366,12 @@ impl std::error::Error for InvalidSignature {}
298366

299367
#[cfg(test)]
300368
mod tests {
369+
use std::string::ToString;
370+
301371
use crate::{
302372
circuit::SpendParameters,
303373
keys::ExpandedSpendingKey,
304-
zip304::{sign_message, verify_message},
374+
zip304::{sign_message, verify_message, Signature},
305375
Diversifier,
306376
};
307377

@@ -358,4 +428,35 @@ mod tests {
358428
assert_ne!(&sig1.to_bytes()[..], &sig1_b.to_bytes()[..]);
359429
assert_ne!(sig1.nullifier, sig1_b.nullifier);
360430
}
431+
432+
#[test]
433+
fn encoding_round_trip() {
434+
let (spend_buf, _) = wagyu_zcash_parameters::load_sapling_parameters();
435+
let params = SpendParameters::read(&spend_buf[..], false)
436+
.expect("Sapling parameters should be valid");
437+
438+
let expsk = ExpandedSpendingKey::from_spending_key(&[42; 32][..]);
439+
let addr = {
440+
let diversifier = Diversifier([0; 11]);
441+
expsk
442+
.proof_generation_key()
443+
.to_viewing_key()
444+
.to_payment_address(diversifier)
445+
.unwrap()
446+
};
447+
448+
let msg = b"Foo bar";
449+
let sig = sign_message(&expsk, addr.clone(), 1, msg, &params);
450+
451+
let sigs_equal = |a: Signature, b: &Signature| {
452+
a.nullifier == b.nullifier
453+
&& a.rk == b.rk
454+
&& a.zkproof == b.zkproof
455+
&& a.spend_auth_sig == b.spend_auth_sig
456+
};
457+
458+
assert!(sigs_equal(Signature::from_bytes(&sig.to_bytes()), &sig));
459+
460+
assert!(sigs_equal(sig.to_string().parse().unwrap(), &sig));
461+
}
361462
}

0 commit comments

Comments
 (0)