1use std::io;
4use std::time::Duration;
5
6use bitcoin::bip32::{Xpriv, Xpub};
7use bitcoin::hashes::{sha256, Hash, HashEngine};
8use bitcoin::secp256k1::{schnorr, Message, PublicKey};
9
10use crate::SECP;
11use crate::encode::{ProtocolDecodingError, ProtocolEncoding, ReadExt, WriteExt};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct XpubRecoveryAuthorization {
20 expiry: i64,
21 sig: schnorr::Signature,
22}
23
24impl XpubRecoveryAuthorization {
25 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark xpub recovery authorization:";
26
27 pub const MAX_LIFETIME: Duration = Duration::from_secs(10 * 60);
29
30 fn signable_message(xpub: &Xpub, server_pubkey: PublicKey, expiry: i64) -> Message {
31 let mut eng = sha256::Hash::engine();
32 eng.input(Self::CHALLENGE_MESSAGE_PREFIX);
33 eng.input(&server_pubkey.serialize());
34 eng.input(&xpub.encode());
35 eng.input(&expiry.to_le_bytes());
36 Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
37 }
38
39 pub fn new(
40 xpriv: &Xpriv,
41 server_pubkey: PublicKey,
42 expiry: chrono::DateTime<chrono::Local>,
43 ) -> XpubRecoveryAuthorization {
44 let expiry = expiry.timestamp();
45 let xpub = Xpub::from_priv(&SECP, xpriv);
46 let msg = Self::signable_message(&xpub, server_pubkey, expiry);
47 let keypair = xpriv.to_keypair(&SECP);
48 XpubRecoveryAuthorization {
49 expiry: expiry,
50 sig: SECP.sign_schnorr_with_aux_rand(&msg, &keypair, &rand::random()),
51 }
52 }
53
54 pub fn expiry(&self) -> chrono::DateTime<chrono::Local> {
56 chrono::DateTime::from_timestamp_secs(self.expiry)
57 .expect("we guarantee valid timestamp")
58 .with_timezone(&chrono::Local)
59 }
60
61 pub fn verify(&self, xpub: &Xpub, server_pubkey: PublicKey) -> bool {
63 let msg = Self::signable_message(xpub, server_pubkey, self.expiry);
64 SECP.verify_schnorr(&self.sig, &msg, &xpub.public_key.into()).is_ok()
65 }
66
67 pub fn is_valid_now(&self) -> bool {
72 const LEEWAY: Duration = Duration::from_secs(5);
73 let now = chrono::Local::now();
74 let expiry = self.expiry();
75 expiry >= now - LEEWAY && expiry <= now + Self::MAX_LIFETIME + LEEWAY
76 }
77}
78
79impl ProtocolEncoding for XpubRecoveryAuthorization {
80 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
81 w.emit_slice(&self.expiry.to_le_bytes())?;
82 self.sig.encode(w)?;
83 Ok(())
84 }
85
86 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
87 Ok(Self {
88 expiry: {
89 let timestamp = i64::from_le_bytes(r.read_byte_array()?);
90 let _ = chrono::DateTime::from_timestamp_secs(timestamp)
92 .ok_or_else(|| ProtocolDecodingError::invalid("invalid timestamp"))?;
93 timestamp
94 },
95 sig: ProtocolEncoding::decode(r)?,
96 })
97 }
98}
99
100#[cfg(test)]
101mod test {
102 use bitcoin::NetworkKind;
103 use bitcoin::secp256k1::Keypair;
104
105 use super::*;
106
107 fn new_xpriv(seed: u8) -> Xpriv {
108 Xpriv::new_master(NetworkKind::Test, &[seed; 32]).unwrap()
109 }
110
111 fn server_pubkey(seed: u8) -> PublicKey {
112 Keypair::from_seckey_slice(&SECP, &[seed; 32]).unwrap().public_key()
113 }
114
115 #[test]
116 fn authorization_verifies_for_its_xpub_and_server_only() {
117 let xpriv = new_xpriv(1);
118 let xpub = Xpub::from_priv(&SECP, &xpriv);
119 let expiry = chrono::Local::now() + Duration::from_secs(60);
120 let auth = XpubRecoveryAuthorization::new(&xpriv, server_pubkey(2), expiry);
121
122 assert!(auth.verify(&xpub, server_pubkey(2)));
123 assert!(auth.is_valid_now());
124 assert!(!auth.verify(&xpub, server_pubkey(3)), "bound to the server");
125 let other = Xpub::from_priv(&SECP, &new_xpriv(4));
126 assert!(!auth.verify(&other, server_pubkey(2)), "bound to the xpub");
127
128 let decoded = XpubRecoveryAuthorization::deserialize(&auth.serialize()).unwrap();
129 assert_eq!(decoded, auth);
130 }
131
132 #[test]
133 fn authorization_expiry_window() {
134 let xpriv = new_xpriv(1);
135 let now = chrono::Local::now();
136
137 let expired = XpubRecoveryAuthorization::new(&xpriv, server_pubkey(2),
138 now - Duration::from_secs(60));
139 assert!(!expired.is_valid_now());
140
141 let too_long = XpubRecoveryAuthorization::new(&xpriv, server_pubkey(2),
142 now + XpubRecoveryAuthorization::MAX_LIFETIME + Duration::from_secs(60));
143 assert!(!too_long.is_valid_now());
144 }
145}