From 59b047a96f92203a89e22b9cd1adc1fe648cfd43 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 20 Jun 2023 03:14:35 +0200 Subject: [PATCH] miniscript: add a scaffold of parsing wsh() policies We add a policies.rs file with functions to parse and validate a policy containing miniscript. Policy specification: https://github.com/bitcoin/bips/pull/1389 We only support `wsh()` policies for now. Taproot or other policy fragments could be added in the future. More validation checks are coming in later commits, such as: - At least one key must be ours - No duplicate keys possible in the policy - No duplicate keys in the keys list - All keys in the keys list are used, and all key references (@0, ...) are valid. - ...? Also coming in later commits: - Derive a pkScript at a keypath, generate receive address from that - Policy registration (very similar to how multisig registration works today) - Signing transactions --- src/rust/bitbox02-rust/src/hww/api/bitcoin.rs | 29 ++- .../src/hww/api/bitcoin/policies.rs | 218 ++++++++++++++++++ 2 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs index e378d6c51a..8f867c686b 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs @@ -21,6 +21,7 @@ pub mod common; pub mod keypath; mod multisig; pub mod params; +mod policies; mod registration; mod script; pub mod signmsg; @@ -38,8 +39,8 @@ use crate::keystore; use pb::btc_pub_request::{Output, XPubType}; use pb::btc_request::Request; use pb::btc_script_config::multisig::ScriptType as MultisigScriptType; -use pb::btc_script_config::Multisig; use pb::btc_script_config::{Config, SimpleType}; +use pb::btc_script_config::{Multisig, Policy}; use pb::response::Response; use pb::BtcCoin; use pb::BtcScriptConfig; @@ -144,7 +145,7 @@ pub fn derive_address_simple( .address(coin_params)?) } -/// Processes a SimpleType (single-sig) adress api call. +/// Processes a SimpleType (single-sig) address api call. async fn address_simple( coin: BtcCoin, simple_type: SimpleType, @@ -164,7 +165,7 @@ async fn address_simple( Ok(Response::Pub(pb::PubResponse { r#pub: address })) } -/// Processes a multisig adress api call. +/// Processes a multisig address api call. pub async fn address_multisig( coin: BtcCoin, multisig: &Multisig, @@ -205,6 +206,25 @@ pub async fn address_multisig( Ok(Response::Pub(pb::PubResponse { r#pub: address })) } +/// Processes a policy address api call. +async fn address_policy( + coin: BtcCoin, + policy: &Policy, + _keypath: &[u32], + _display: bool, +) -> Result { + let parsed = policies::parse(policy)?; + parsed.validate(coin)?; + + // TODO: check that the policy was registered before. + + // TODO: confirm policy registration + + // TODO: create address at keypath and do user verification + + todo!(); +} + /// Handle a Bitcoin xpub/address protobuf api call. pub async fn process_pub(request: &pb::BtcPubRequest) -> Result { let coin = match BtcCoin::from_i32(request.coin) { @@ -233,6 +253,9 @@ pub async fn process_pub(request: &pb::BtcPubRequest) -> Result Some(Output::ScriptConfig(BtcScriptConfig { config: Some(Config::Multisig(ref multisig)), })) => address_multisig(coin, multisig, &request.keypath, request.display).await, + Some(Output::ScriptConfig(BtcScriptConfig { + config: Some(Config::Policy(ref policy)), + })) => address_policy(coin, policy, &request.keypath, request.display).await, _ => Err(Error::InvalidInput), } } diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs new file mode 100644 index 0000000000..81a2bba5f7 --- /dev/null +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs @@ -0,0 +1,218 @@ +// Copyright 2023 Shift Crypto AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::pb; +use super::Error; +use pb::BtcCoin; + +use pb::btc_script_config::Policy; + +use alloc::string::String; + +use core::str::FromStr; + +// Arbitrary limit of keys that can be present in a policy. +const MAX_KEYS: usize = 20; + +// We only support Bitcoin testnet for now. +fn check_enabled(coin: BtcCoin) -> Result<(), Error> { + if !matches!(coin, BtcCoin::Tbtc) { + return Err(Error::InvalidInput); + } + Ok(()) +} + +/// See `ParsedPolicy`. +#[derive(Debug)] +pub struct Wsh<'a> { + policy: &'a Policy, + miniscript_expr: miniscript::Miniscript, +} + +/// Result of `parse()`. +#[derive(Debug)] +pub enum ParsedPolicy<'a> { + // `wsh(...)` policies + Wsh(Wsh<'a>), + // `tr(...)` Taproot etc. in the future. +} + +impl<'a> ParsedPolicy<'a> { + fn get_policy(&self) -> &Policy { + match self { + Self::Wsh(Wsh { ref policy, .. }) => policy, + } + } + + /// Validate a policy. + /// - Coin is supported (only Bitcoin testnet for now) + /// - Number of keys + /// - TODO: many more checks. + pub fn validate(&self, coin: BtcCoin) -> Result<(), Error> { + check_enabled(coin)?; + + let policy = self.get_policy(); + + if policy.keys.len() > MAX_KEYS { + return Err(Error::InvalidInput); + } + + // TODO: more checks + + Ok(()) + } +} + +/// Parses a policy as specified by 'Wallet policies': https://github.com/bitcoin/bips/pull/1389. +/// Only `wsh()` is supported for now. +/// Example: `wsh(pk(@0/**))`. +pub fn parse(policy: &Policy) -> Result { + let desc = policy.policy.as_str(); + match desc.as_bytes() { + // Match wsh(...). + [b'w', b's', b'h', b'(', .., b')'] => { + let miniscript_expr: miniscript::Miniscript = + miniscript::Miniscript::from_str(&desc[4..desc.len() - 1]) + .or(Err(Error::InvalidInput))?; + + Ok(ParsedPolicy::Wsh(Wsh { + policy, + miniscript_expr, + })) + } + _ => Err(Error::InvalidInput), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::vec::Vec; + + use crate::bip32::parse_xpub; + use bitbox02::testing::mock_unlocked; + use util::bip32::HARDENED; + + const SOME_XPUB_1: &str = "xpub6FMWuwbCA9KhoRzAMm63ZhLspk5S2DM5sePo8J8mQhcS1xyMbAqnc7Q7UescVEVFCS6qBMQLkEJWQ9Z3aDPgBov5nFUYxsJhwumsxM4npSo"; + + const KEYPATH_ACCOUNT: &[u32] = &[48 + HARDENED, 1 + HARDENED, 0 + HARDENED, 3 + HARDENED]; + + // Creates a policy key without fingerprint/keypath from an xpub string. + fn make_key(xpub: &str) -> pb::KeyOriginInfo { + pb::KeyOriginInfo { + root_fingerprint: vec![], + keypath: vec![], + xpub: Some(parse_xpub(xpub).unwrap()), + } + } + + // Creates a policy for one of our own keys at keypath. + fn make_our_key(keypath: &[u32]) -> pb::KeyOriginInfo { + let our_xpub = crate::keystore::get_xpub(keypath).unwrap(); + pb::KeyOriginInfo { + root_fingerprint: crate::keystore::root_fingerprint().unwrap(), + keypath: keypath.to_vec(), + xpub: Some(our_xpub.into()), + } + } + + fn make_policy(policy: &str, keys: &[pb::KeyOriginInfo]) -> Policy { + Policy { + policy: policy.into(), + keys: keys.to_vec(), + } + } + + #[test] + fn test_parse_wsh_miniscript() { + // Parse a valid example and check that the keys are collected as is as strings. + let policy = make_policy("wsh(pk(@0/**))", &[]); + match parse(&policy).unwrap() { + ParsedPolicy::Wsh(Wsh { + ref miniscript_expr, + .. + }) => { + assert_eq!( + miniscript_expr.iter_pk().collect::>(), + vec!["@0/**"] + ); + } + } + + // Parse another valid example and check that the keys are collected as is as strings. + let policy = make_policy("wsh(or_b(pk(@0/**),s:pk(@1/**)))", &[]); + match parse(&policy).unwrap() { + ParsedPolicy::Wsh(Wsh { + ref miniscript_expr, + .. + }) => { + assert_eq!( + miniscript_expr.iter_pk().collect::>(), + vec!["@0/**", "@1/**"] + ); + } + } + + // Unknown top-level fragment. + assert_eq!( + parse(&make_policy("unknown(pk(@0/**))", &[])).unwrap_err(), + Error::InvalidInput, + ); + + // Unknown script fragment. + assert_eq!( + parse(&make_policy("wsh(unknown(@0/**))", &[])).unwrap_err(), + Error::InvalidInput, + ); + + // Miniscript type-check fails (should be `or_b(pk(@0/**),s:pk(@1/**))`). + assert_eq!( + parse(&make_policy("wsh(or_b(pk(@0/**),pk(@1/**)))", &[])).unwrap_err(), + Error::InvalidInput, + ); + } + + #[test] + fn test_parse_validate() { + let our_key = make_our_key(KEYPATH_ACCOUNT); + + // All good. + assert!(parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()])) + .unwrap() + .validate(BtcCoin::Tbtc) + .is_ok()); + + // Unsupported coins + for coin in [BtcCoin::Btc, BtcCoin::Ltc, BtcCoin::Tltc] { + assert_eq!( + parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()])) + .unwrap() + .validate(coin), + Err(Error::InvalidInput) + ); + } + + // Too many keys. + let many_keys: Vec = (0..=20) + .map(|i| make_our_key(&[48 + HARDENED, 1 + HARDENED, i + HARDENED, 3 + HARDENED])) + .collect(); + assert_eq!( + parse(&make_policy("wsh(pk(@0/**))", &many_keys)) + .unwrap() + .validate(BtcCoin::Tbtc), + Err(Error::InvalidInput) + ); + } +}