Skip to main content

zcash_keys/keys/
transparent.rs

1//! Types and encodings for standalone transparent spending keys.
2//!
3//! These types are provided for compatibility with encodings used in zcashd RPC APIs and
4//! serialization formats.
5
6#[cfg(feature = "transparent-inputs")]
7pub mod gap_limits;
8
9#[cfg(feature = "transparent-key-encoding")]
10use {
11    bip32::{PrivateKey, PrivateKeyBytes},
12    core::array::TryFromSliceError,
13    secp256k1::{PublicKey, SecretKey},
14    secrecy::{ExposeSecret, SecretString, SecretVec, Zeroize, zeroize::ZeroizeOnDrop},
15    zcash_protocol::consensus::NetworkType,
16};
17
18/// An error indicating that a DER-encoded secret key could not be decoded.
19#[cfg(feature = "transparent-key-encoding")]
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct DerDecodeError;
22
23#[cfg(feature = "transparent-key-encoding")]
24impl core::fmt::Display for DerDecodeError {
25    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26        write!(f, "Invalid DER encoding for transparent secret key")
27    }
28}
29
30/// Errors that can occur in the parsing of Bitcoin-style base58-encoded secret key material
31#[cfg(feature = "transparent-key-encoding")]
32#[derive(Debug)]
33pub enum ParseError {
34    /// The data being decoded had an incorrect length, an incorrect prefix, or has the correct
35    /// length for a compressed encoding but has its final byte not equal to `1`.
36    InvalidEncoding,
37    /// Errors that occur in base58 decoding.
38    Base58(bs58::decode::Error),
39    /// Errors that occur when a decoded binary value does not correspond to a valid secp256k1 secret key.
40    Bip32(bip32::Error),
41}
42
43#[cfg(feature = "transparent-key-encoding")]
44impl core::fmt::Debug for Key {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        f.debug_struct("Key")
47            .field("secret", &"...")
48            .field("compressed", &self.compressed)
49            .finish()
50    }
51}
52
53#[cfg(feature = "transparent-key-encoding")]
54impl From<bs58::decode::Error> for ParseError {
55    fn from(value: bs58::decode::Error) -> Self {
56        ParseError::Base58(value)
57    }
58}
59
60#[cfg(feature = "transparent-key-encoding")]
61impl From<TryFromSliceError> for ParseError {
62    fn from(_: TryFromSliceError) -> Self {
63        ParseError::InvalidEncoding
64    }
65}
66
67#[cfg(feature = "transparent-key-encoding")]
68impl From<bip32::Error> for ParseError {
69    fn from(value: bip32::Error) -> Self {
70        ParseError::Bip32(value)
71    }
72}
73
74/// A secp256k1 secret key, along with a flag indicating whether a compressed encoding should be
75/// used when performing DER serialization.
76#[cfg(feature = "transparent-key-encoding")]
77pub struct Key {
78    secret: SecretKey,
79    compressed: bool,
80}
81
82#[cfg(feature = "transparent-key-encoding")]
83impl Key {
84    /// Constructs a new key value from a secret key and a flag indicating whether
85    /// the compressed encoding should be used when performing DER serialization.
86    pub fn new(secret: SecretKey, compressed: bool) -> Self {
87        Self { secret, compressed }
88    }
89
90    /// Decodes a base58-encoded secret key.
91    ///
92    /// This corresponds to <https://github.com/zcash/zcash/blob/1f1f7a385adc048154e7f25a3a0de76f3658ca09/src/key_io.cpp#L282>
93    pub fn decode_base58(network: NetworkType, encoded: &SecretString) -> Result<Self, ParseError> {
94        let decoded = SecretVec::new(
95            bs58::decode(encoded.expose_secret())
96                .with_check(None)
97                .into_vec()?,
98        );
99        let prefix = crate::constants::b58_secret_key_prefix(network);
100        let decoded_len = decoded.expose_secret().len();
101        let compressed =
102            decoded_len == (33 + prefix.len()) && decoded.expose_secret().last() == Some(&1);
103        if (decoded_len == 32 + prefix.len() || compressed)
104            && decoded.expose_secret()[0..prefix.len()] == prefix
105        {
106            let key_end = decoded_len - if compressed { 1 } else { 0 };
107            let bytes = PrivateKeyBytes::try_from(&decoded.expose_secret()[prefix.len()..key_end])?;
108            Ok(Self {
109                secret: SecretKey::from_bytes(&bytes)?,
110                compressed,
111            })
112        } else {
113            Err(ParseError::InvalidEncoding)
114        }
115    }
116
117    /// Encodes a base58-encoded secret key.
118    ///
119    /// This corresponds to <https://github.com/zcash/zcash/blob/1f1f7a385adc048154e7f25a3a0de76f3658ca09/src/key_io.cpp#L298>
120    pub fn encode_base58(&self, network: NetworkType) -> SecretString {
121        let input = SecretVec::new(
122            crate::constants::b58_secret_key_prefix(network)
123                .iter()
124                .chain(self.secret.to_secret_bytes().iter())
125                .chain(self.compressed.then_some(&1))
126                .copied()
127                .collect(),
128        );
129
130        SecretString::new(
131            bs58::encode(input.expose_secret())
132                .with_check()
133                .into_string(),
134        )
135    }
136
137    /// Returns the wrapped secp256k1 secret key.
138    pub fn secret(&self) -> &SecretKey {
139        &self.secret
140    }
141
142    /// Returns the value of the compressed flag.
143    pub fn compressed(&self) -> bool {
144        self.compressed
145    }
146
147    /// Derives the secp256k1 public key corresponding to the secret key.
148    pub fn pubkey(&self) -> PublicKey {
149        self.secret.public_key()
150    }
151
152    /// Generates the "openssh-inspired" DER encoding of the secret key used by zcashd.
153    pub fn der_encode(&self) -> SecretVec<u8> {
154        // Ported from https://github.com/zcash/zcash/blob/1f1f7a385adc048154e7f25a3a0de76f3658ca09/src/key.cpp#L93
155        // The original c++ code is retained as comments.
156
157        //    secp256k1_pubkey pubkey;
158        //    size_t pubkeylen = 0;
159        //    if (!secp256k1_ec_pubkey_create(ctx, &pubkey, key32)) {
160        //        *seckeylen = 0;
161        //        return 0;
162        //    }
163        let keypair = self.secret().keypair();
164
165        if self.compressed {
166            let begin = [0x30, 0x81, 0xD3, 0x02, 0x01, 0x01, 0x04, 0x20];
167            let middle = [
168                0xA0, 0x81, 0x85, 0x30, 0x81, 0x82, 0x02, 0x01, 0x01, 0x30, 0x2C, 0x06, 0x07, 0x2A,
169                0x86, 0x48, 0xCE, 0x3D, 0x01, 0x01, 0x02, 0x21, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
170                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
171                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFC, 0x2F, 0x30,
172                0x06, 0x04, 0x01, 0x00, 0x04, 0x01, 0x07, 0x04, 0x21, 0x02, 0x79, 0xBE, 0x66, 0x7E,
173                0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, 0x02, 0x9B,
174                0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98,
175                0x02, 0x21, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
176                0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF,
177                0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41, 0x02, 0x01, 0x01, 0xA1, 0x24, 0x03, 0x22,
178                0x00,
179            ];
180            //        unsigned char *ptr = seckey;
181            //        memcpy(ptr, begin, sizeof(begin)); ptr += sizeof(begin);
182            //        memcpy(ptr, key32, 32); ptr += 32;
183            //        memcpy(ptr, middle, sizeof(middle)); ptr += sizeof(middle);
184            //        pubkeylen = CPubKey::COMPRESSED_PUBLIC_KEY_SIZE;
185            //        secp256k1_ec_pubkey_serialize(ctx, ptr, &pubkeylen, &pubkey, SECP256K1_EC_COMPRESSED);
186            //        ptr += pubkeylen;
187            //        *seckeylen = ptr - seckey;
188            //        assert(*seckeylen == CKey::COMPRESSED_PRIVATE_KEY_SIZE);
189            SecretVec::new(
190                begin
191                    .iter()
192                    .chain(keypair.to_secret_bytes().iter())
193                    .chain(middle.iter())
194                    .chain(keypair.public_key().serialize().iter())
195                    .copied()
196                    .collect(),
197            )
198        } else {
199            let begin = [0x30, 0x82, 0x01, 0x13, 0x02, 0x01, 0x01, 0x04, 0x20];
200            let middle = [
201                0xA0, 0x81, 0xA5, 0x30, 0x81, 0xA2, 0x02, 0x01, 0x01, 0x30, 0x2C, 0x06, 0x07, 0x2A,
202                0x86, 0x48, 0xCE, 0x3D, 0x01, 0x01, 0x02, 0x21, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
203                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
204                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFC, 0x2F, 0x30,
205                0x06, 0x04, 0x01, 0x00, 0x04, 0x01, 0x07, 0x04, 0x41, 0x04, 0x79, 0xBE, 0x66, 0x7E,
206                0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, 0x02, 0x9B,
207                0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98,
208                0x48, 0x3A, 0xDA, 0x77, 0x26, 0xA3, 0xC4, 0x65, 0x5D, 0xA4, 0xFB, 0xFC, 0x0E, 0x11,
209                0x08, 0xA8, 0xFD, 0x17, 0xB4, 0x48, 0xA6, 0x85, 0x54, 0x19, 0x9C, 0x47, 0xD0, 0x8F,
210                0xFB, 0x10, 0xD4, 0xB8, 0x02, 0x21, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
211                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF,
212                0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41, 0x02, 0x01, 0x01,
213                0xA1, 0x44, 0x03, 0x42, 0x00,
214            ];
215            //        unsigned char *ptr = seckey;
216            //        memcpy(ptr, begin, sizeof(begin)); ptr += sizeof(begin);
217            //        memcpy(ptr, key32, 32); ptr += 32;
218            //        memcpy(ptr, middle, sizeof(middle)); ptr += sizeof(middle);
219            //        pubkeylen = CPubKey::PUBLIC_KEY_SIZE;
220            //        secp256k1_ec_pubkey_serialize(ctx, ptr, &pubkeylen, &pubkey, SECP256K1_EC_UNCOMPRESSED);
221            //        ptr += pubkeylen;
222            //        *seckeylen = ptr - seckey;
223            //        assert(*seckeylen == CKey::PRIVATE_KEY_SIZE);
224            SecretVec::new(
225                begin
226                    .iter()
227                    .chain(keypair.to_secret_bytes().iter())
228                    .chain(middle.iter())
229                    .chain(keypair.public_key().serialize_uncompressed().iter())
230                    .copied()
231                    .collect(),
232            )
233        }
234    }
235
236    // Decodes a secret key from the "openssl-inspired" DER encoding used by zcashd.
237    pub fn der_decode(encoded: &SecretVec<u8>, compressed: bool) -> Result<Self, DerDecodeError> {
238        // Ported from https://github.com/zcash/zcash/blob/1f1f7a385adc048154e7f25a3a0de76f3658ca09/src/key.cpp#L36
239        // The original c++ code is retained as comments.
240
241        let seckey = encoded.expose_secret();
242
243        //    /* sequence header */
244        //    if (end - seckey < 1 || *seckey != 0x30u) {
245        //        return 0;
246        //    }
247        //    seckey++;
248        let seckey = match seckey.split_first() {
249            Some((&0x30, rest)) => Ok(rest),
250            _ => Err(DerDecodeError),
251        }?;
252
253        //    /* sequence length constructor */
254        //    if (end - seckey < 1 || !(*seckey & 0x80u)) {
255        //        return 0;
256        //    }
257        //    size_t lenb = *seckey & ~0x80u; seckey++;
258        //    if (lenb < 1 || lenb > 2) {
259        //        return 0;
260        //    }
261        //    if (end - seckey < lenb) {
262        //        return 0;
263        //    }
264        let (lenb, seckey) = match seckey.split_first() {
265            Some((lenb, seckey)) if lenb & 0x80 != 0 => Ok((usize::from(lenb & !0x80), seckey)),
266            _ => Err(DerDecodeError),
267        }?;
268        if !(1..=2).contains(&lenb) {
269            return Err(DerDecodeError);
270        }
271        if seckey.len() < lenb {
272            return Err(DerDecodeError);
273        }
274
275        //    /* sequence length */
276        //    size_t len = seckey[lenb-1] | (lenb > 1 ? seckey[lenb-2] << 8 : 0u);
277        //    seckey += lenb;
278        //    if (end - seckey < len) {
279        //        return 0;
280        //    }
281        let len_low_bits = usize::from(seckey[lenb - 1]);
282        let len_high_bits = if lenb > 1 {
283            usize::from(seckey[lenb - 2]) << 8
284        } else {
285            0
286        };
287        let len = len_low_bits | len_high_bits;
288        let seckey = &seckey[lenb..];
289        if seckey.len() < len {
290            return Err(DerDecodeError);
291        }
292
293        //    /* sequence element 0: version number (=1) */
294        //    if (end - seckey < 3 || seckey[0] != 0x02u || seckey[1] != 0x01u || seckey[2] != 0x01u) {
295        //        return 0;
296        //    }
297        //    seckey += 3;
298        let seckey = match seckey.split_at(3) {
299            (&[0x02, 0x01, 0x01], rest) => Ok(rest),
300            _ => Err(DerDecodeError),
301        }?;
302
303        //    /* sequence element 1: octet string, up to 32 bytes */
304        //    if (end - seckey < 2 || seckey[0] != 0x04u) {
305        //        return 0;
306        //    }
307        //    size_t oslen = seckey[1];
308        //    seckey += 2;
309        //    if (oslen > 32 || end - seckey < oslen) {
310        //        return 0;
311        //    }
312        //    memcpy(out32 + (32 - oslen), seckey, oslen);
313        //    if (!secp256k1_ec_seckey_verify(ctx, out32)) {
314        //        memset(out32, 0, 32);
315        //        return 0;
316        //    }
317        //    return 1;
318        //}
319        if seckey.len() < 2 || seckey[0] != 0x04 {
320            return Err(DerDecodeError);
321        }
322        let oslen = usize::from(seckey[1]);
323        let seckey = &seckey[2..];
324        if oslen > 32 || seckey.len() < oslen {
325            return Err(DerDecodeError);
326        }
327        let mut secret_buf = [0u8; 32];
328        secret_buf[(32 - oslen)..].copy_from_slice(&seckey[..oslen]);
329        let secret_key = SecretKey::from_bytes(&secret_buf);
330        secret_buf.zeroize();
331
332        secret_key
333            .map(|secret| Self { secret, compressed })
334            .map_err(|_| DerDecodeError)
335    }
336}
337
338#[cfg(feature = "transparent-key-encoding")]
339impl Zeroize for Key {
340    fn zeroize(&mut self) {
341        self.secret.non_secure_erase();
342    }
343}
344
345#[cfg(feature = "transparent-key-encoding")]
346impl ZeroizeOnDrop for Key {}
347
348#[cfg(all(
349    feature = "transparent-key-encoding",
350    any(test, feature = "test-dependencies")
351))]
352pub mod test_vectors;
353
354#[cfg(all(test, feature = "transparent-key-encoding"))]
355mod tests {
356    use rand::{Rng, RngExt, SeedableRng as _};
357    use rand_chacha::ChaChaRng;
358    use secp256k1::SecretKey;
359    use secrecy::SecretString;
360    use zcash_protocol::consensus::NetworkType;
361    use zcash_script::script::Evaluable;
362
363    use super::{
364        Key,
365        test_vectors::{INVALID, VALID, VectorKind},
366    };
367    use crate::address::Address;
368
369    /// Generates a uniformly random secp256k1 secret key.
370    fn random_secret_key(rng: &mut impl Rng) -> SecretKey {
371        loop {
372            let mut bytes = [0u8; 32];
373            rng.fill_bytes(&mut bytes);
374            if let Ok(secret) = SecretKey::from_secret_bytes(bytes) {
375                return secret;
376            }
377        }
378    }
379
380    #[test]
381    #[cfg(feature = "transparent-key-encoding")]
382    fn der_encoding_roundtrip() {
383        let mut rng = ChaChaRng::from_seed([0u8; 32]);
384        for _ in 0..100 {
385            let secret = random_secret_key(&mut rng);
386            let compressed = rng.random_bool(0.5);
387            let key = Key { secret, compressed };
388
389            let encoded = key.der_encode();
390            let decoded = Key::der_decode(&encoded, compressed).unwrap();
391
392            assert_eq!(key.secret(), decoded.secret());
393        }
394    }
395
396    /// Checks that parsed keys match test payload.
397    #[test]
398    fn base58_keys_valid_parse() {
399        for v in VALID {
400            match v.kind {
401                VectorKind::Privkey { is_compressed } => {
402                    // Must be valid private key
403                    let secret = &SecretString::new(v.base58_encoding.into());
404                    let privkey = Key::decode_base58(v.network, secret).unwrap();
405                    assert_eq!(privkey.compressed, is_compressed);
406                    assert_eq!(
407                        hex::encode(privkey.secret.to_secret_bytes()),
408                        v.raw_bytes_hex
409                    );
410
411                    // Private key must be invalid public key
412                    assert_eq!(
413                        zcash_address::ZcashAddress::try_from_encoded(v.base58_encoding),
414                        Err(zcash_address::ParseError::NotZcash),
415                    );
416                }
417                VectorKind::Pubkey => {
418                    // Must be valid public key
419                    let destination =
420                        match zcash_address::ZcashAddress::try_from_encoded(v.base58_encoding)
421                            .unwrap()
422                            .convert_if_network::<Address>(v.network)
423                            .unwrap()
424                        {
425                            Address::Transparent(addr) => addr,
426                            _ => panic!("{} is not a transparent address", v.base58_encoding),
427                        };
428                    let script = destination.script();
429                    assert_eq!(hex::encode(script.to_bytes()), v.raw_bytes_hex);
430
431                    // Public key must be invalid private key
432                    assert!(
433                        Key::decode_base58(v.network, &SecretString::new(v.base58_encoding.into()))
434                            .is_err()
435                    );
436                }
437            }
438        }
439    }
440
441    /// Checks that Base58 key parsing code is robust against a variety of corrupted data.
442    #[test]
443    fn base58_keys_invalid() {
444        for &encoded in INVALID {
445            assert!(
446                Key::decode_base58(NetworkType::Main, &SecretString::new(encoded.into())).is_err()
447            );
448            assert_eq!(
449                zcash_address::ZcashAddress::try_from_encoded(encoded),
450                Err(zcash_address::ParseError::NotZcash),
451            );
452        }
453    }
454
455    #[test]
456    #[cfg(feature = "transparent-key-encoding")]
457    fn key_debug_redaction() {
458        let mut rng = ChaChaRng::from_seed([0u8; 32]);
459        let secret = random_secret_key(&mut rng);
460        let key = Key {
461            secret,
462            compressed: true,
463        };
464        let debug_str = format!("{key:?}");
465        assert!(debug_str.contains("secret: \"...\""));
466        assert!(debug_str.contains("compressed: true"));
467    }
468}