|
2 | 2 | //!
|
3 | 3 | //! [ZIP 304]: https://zips.z.cash/zip-0304
|
4 | 4 |
|
| 5 | +use core::{fmt, str::FromStr}; |
| 6 | + |
| 7 | +use base64::{prelude::BASE64_STANDARD, Engine}; |
5 | 8 | use bellman::{
|
6 | 9 | gadgets::multipack,
|
7 | 10 | groth16::{verify_proof, Proof},
|
@@ -89,6 +92,71 @@ impl Signature {
|
89 | 92 | }
|
90 | 93 | }
|
91 | 94 |
|
| 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 | + |
92 | 160 | /// Signs an arbitrary message for the given [`PaymentAddress`] and [`SLIP 44`] coin type.
|
93 | 161 | ///
|
94 | 162 | /// 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 {}
|
298 | 366 |
|
299 | 367 | #[cfg(test)]
|
300 | 368 | mod tests {
|
| 369 | + use std::string::ToString; |
| 370 | + |
301 | 371 | use crate::{
|
302 | 372 | circuit::SpendParameters,
|
303 | 373 | keys::ExpandedSpendingKey,
|
304 |
| - zip304::{sign_message, verify_message}, |
| 374 | + zip304::{sign_message, verify_message, Signature}, |
305 | 375 | Diversifier,
|
306 | 376 | };
|
307 | 377 |
|
@@ -358,4 +428,35 @@ mod tests {
|
358 | 428 | assert_ne!(&sig1.to_bytes()[..], &sig1_b.to_bytes()[..]);
|
359 | 429 | assert_ne!(sig1.nullifier, sig1_b.nullifier);
|
360 | 430 | }
|
| 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, ¶ms); |
| 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 | + } |
361 | 462 | }
|
0 commit comments