use std::marker::PhantomData;
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpub};
use bitcoin::psbt::Psbt;
use prost::Message;
use crate::Interpreter;
use super::api;
use super::error::{BitBoxDeviceError, BitBoxError};
use super::noise::{HandshakeState, NoiseState};
use super::proto as pb;
use super::sign::{OurKey, Transaction, TxOutput, apply_signatures, is_schnorr};
use super::{
OP_HER_COMEZ_TEH_HANDSHAEK, OP_I_CAN_HAS_HANDSHAEK, OP_I_CAN_HAS_PAIRIN_VERIFICASHUN,
OP_NOISE_MSG, OP_UNLOCK, RESPONSE_SUCCESS, SetupMode,
};
use super::{antiklepto, policy};
#[derive(Clone, Debug)]
pub enum BitBoxCommand {
UnlockAndPair,
GetVersion,
GetMasterFingerprint,
GetXpub {
keypath: DerivationPath,
display: bool,
},
ShowSimpleAddress {
keypath: DerivationPath,
simple_type: pb::btc_script_config::SimpleType,
display: bool,
},
ShowPolicyAddress {
keypath: DerivationPath,
policy: policy::Policy,
display: bool,
},
IsScriptConfigRegistered {
policy: policy::Policy,
},
RegisterScriptConfig {
policy: policy::Policy,
name: String,
},
SignPsbt {
psbt: Box<Psbt>,
force_script_config: Option<pb::BtcScriptConfigWithKeypath>,
policy: Option<policy::Policy>,
},
SignMessage {
keypath: DerivationPath,
simple_type: pb::btc_script_config::SimpleType,
message: Vec<u8>,
},
ShowDescriptorAddress {
policy: policy::Policy,
change: bool,
index: u32,
display: bool,
},
Setup {
label: String,
mode: SetupMode,
timestamp: u32,
timezone_offset: i32,
},
Wipe,
Restore {
label: String,
timestamp: u32,
timezone_offset: i32,
},
TogglePassphrase,
RestoreFromMnemonic {
timestamp: u32,
timezone_offset: i32,
},
Backup,
}
#[derive(Debug)]
pub enum BitBoxResponse {
TaskDone,
DeviceAction(
bool,
),
Info(
BitBoxDeviceInfo,
),
MasterFingerprint(
Fingerprint,
),
Xpub(
Xpub,
),
Address(
String,
),
IsRegistered(
bool,
),
Registered,
SignedPsbt(
Box<Psbt>,
),
Signature(
u8,
bitcoin::secp256k1::ecdsa::Signature,
),
Backup,
}
#[derive(Clone, Debug)]
pub struct BitBoxDeviceInfo {
pub version: String,
pub name: String,
pub initialized: bool,
}
pub struct BitBoxTransmit {
pub payload: Vec<u8>,
pub encrypted: bool,
}
enum State {
New,
WaitUnlockAck,
WaitHandshakeInit,
WaitHandshake1(HandshakeState),
WaitHandshake2(HandshakeState),
WaitPairingConfirm(HandshakeState, Option<String>),
WaitEncryptedResponse(EncryptedContext),
SignPsbtWaitFingerprint(SignInit),
SignPsbtWaitNext(Box<SignCtx>),
SignMessageWaitCommitment {
host_nonce: [u8; 32],
},
SignMessageWaitSig {
host_nonce: [u8; 32],
signer_commitment: Vec<u8>,
},
ShowAddrWaitFingerprint(ShowAddrInit),
NativePolicyWaitFingerprint(NativePolicyInit),
NativePolicyWaitXpub(NativePolicyResolution),
SetupWaitInfo(SetupInit),
SetupWaitName(SetupInit),
SetupWaitPassword {
timestamp: u32,
timezone_offset: i32,
},
SetupWaitBackup,
SetupWaitRestore,
WipeWaitInfo,
WipeWaitReset,
RestoreWaitInfo(RestoreInit),
RestoreWaitName(RestoreInit),
RestoreWaitMnemonic,
TogglePassphraseWaitInfo,
TogglePassphraseWaitSet,
Finished(BitBoxResponse),
}
struct SetupInit {
label: String,
mode: SetupMode,
timestamp: u32,
timezone_offset: i32,
}
struct RestoreInit {
label: String,
timestamp: u32,
timezone_offset: i32,
}
struct SignInit {
psbt: Box<Psbt>,
force_script_config: Option<pb::BtcScriptConfigWithKeypath>,
policy: Option<policy::Policy>,
}
struct ShowAddrInit {
policy: policy::Policy,
change: bool,
index: u32,
display: bool,
}
struct NativePolicyInit {
policy: policy::Policy,
action: NativePolicyAction,
}
enum NativePolicyAction {
Register {
name: String,
},
IsRegistered,
Address {
keypath: DerivationPath,
display: bool,
},
DescriptorAddress {
change: bool,
index: u32,
display: bool,
},
Sign {
psbt: Box<Psbt>,
},
}
struct NativePolicyResolution {
init: NativePolicyInit,
resolver: policy::AccountResolver,
}
struct SignCtx {
psbt: Box<Psbt>,
transaction: Transaction,
our_keys: Vec<OurKey>,
sigs: Vec<Vec<u8>>,
is_inputs_pass2: bool,
phase: SignPhase,
}
#[allow(clippy::enum_variant_names)]
enum SignPhase {
ExpectNext,
ExpectNextWithSig,
ExpectHostNonce { host_nonce: [u8; 32] },
ExpectAntikleptoSig {
host_nonce: [u8; 32],
signer_commitment: Vec<u8>,
},
}
enum SignStep {
Continue { ctx: Box<SignCtx>, bytes: Vec<u8> },
Done { psbt: Box<Psbt> },
}
enum EncryptedContext {
Version,
MasterFingerprint,
Xpub,
Address,
IsRegistered,
RegisterScriptConfig,
RestoreFromMnemonic,
Backup,
}
pub struct BitBoxInterpreter<'a, C, T, R, E> {
state: State,
noise: &'a mut NoiseState,
network: bitcoin::Network,
_marker: PhantomData<(C, T, R, E)>,
}
impl<'a, C, T, R, E> BitBoxInterpreter<'a, C, T, R, E> {
pub fn new(noise: &'a mut NoiseState) -> Self {
Self {
state: State::New,
noise,
network: bitcoin::Network::Bitcoin,
_marker: PhantomData,
}
}
pub fn with_network(mut self, network: bitcoin::Network) -> Self {
self.network = network;
self
}
}
fn framed(op: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + payload.len());
out.push(op);
out.extend_from_slice(payload);
out
}
fn expect_success(response: &[u8]) -> Result<&[u8], BitBoxError> {
if response.is_empty() || response[0] != RESPONSE_SUCCESS {
return Err(BitBoxError::UnexpectedResponse);
}
Ok(&response[1..])
}
fn encrypted_transmit(bytes: Vec<u8>) -> BitBoxTransmit {
BitBoxTransmit {
payload: bytes,
encrypted: true,
}
}
fn plain_transmit(bytes: Vec<u8>) -> BitBoxTransmit {
BitBoxTransmit {
payload: bytes,
encrypted: false,
}
}
fn build_sign_init_request(coin: pb::BtcCoin, tx: &Transaction) -> pb::request::Request {
pb::request::Request::BtcSignInit(pb::BtcSignInitRequest {
coin: coin as _,
script_configs: tx.script_configs.clone(),
output_script_configs: vec![],
version: tx.version,
num_inputs: tx.inputs.len() as _,
num_outputs: tx.outputs.len() as _,
locktime: tx.locktime,
format_unit: pb::btc_sign_init_request::FormatUnit::Default as _,
contains_silent_payment_outputs: false,
})
}
fn descriptor_keypath(
policy: &policy::Policy,
our_fingerprint: Fingerprint,
change: bool,
index: u32,
) -> Result<DerivationPath, BitBoxError> {
let our_key = policy
.pubkeys
.iter()
.find(|k| k.master_fingerprint == Some(our_fingerprint))
.ok_or(BitBoxError::InvalidInput(
"device key not found in descriptor policy",
))?;
let account = our_key.path.as_ref().ok_or(BitBoxError::InvalidInput(
"device key has no origin path in descriptor policy",
))?;
let full = account.extend([
ChildNumber::from_normal_idx(u32::from(change))
.map_err(|_| BitBoxError::InvalidInput("invalid change value"))?,
ChildNumber::from_normal_idx(index)
.map_err(|_| BitBoxError::InvalidInput("invalid address index"))?,
]);
Ok(full)
}
fn policy_script_config(
policy: &policy::Policy,
fingerprint: &[u8],
) -> Result<pb::BtcScriptConfigWithKeypath, BitBoxError> {
let our_key = policy
.pubkeys
.iter()
.find(|k| {
k.master_fingerprint
.map(|fp| fp.as_bytes().to_vec())
.as_deref()
== Some(fingerprint)
})
.ok_or(BitBoxError::InvalidInput(
"device key not found in descriptor policy",
))?;
let account = our_key.path.clone().ok_or(BitBoxError::InvalidInput(
"device key has no origin path in descriptor policy",
))?;
Ok(pb::BtcScriptConfigWithKeypath {
script_config: Some(policy.clone().into_script_config(None)?),
keypath: account.to_u32_vec(),
})
}
fn decode_sign_next(
response: pb::response::Response,
) -> Result<pb::BtcSignNextResponse, BitBoxError> {
match response {
pb::response::Response::BtcSignNext(next) => Ok(next),
pb::response::Response::Btc(pb::BtcResponse {
response: Some(pb::btc_response::Response::SignNext(next)),
}) => Ok(next),
_ => Err(BitBoxError::UnexpectedResponse),
}
}
impl<C, T, R, E> BitBoxInterpreter<'_, C, T, R, E> {
fn start_native_policy(&mut self, init: NativePolicyInit) -> Result<Vec<u8>, BitBoxError> {
let bytes = self.build_encrypted(api::root_fingerprint_request())?;
self.state = State::NativePolicyWaitFingerprint(init);
Ok(bytes)
}
fn native_xpub_request(
&mut self,
pending: &NativePolicyResolution,
) -> Result<Vec<u8>, BitBoxError> {
let key = pending.resolver.current_key(&pending.init.policy);
let path = key
.path
.as_ref()
.ok_or(BitBoxError::InvalidInput("missing multisig origin"))?;
self.build_encrypted(api::xpub_request(
api::coin_from_network(self.network),
path,
api::xpub_type_from_network(self.network),
false,
))
}
fn resolve_native_policy(
&mut self,
init: NativePolicyInit,
fingerprint: Fingerprint,
) -> Result<Vec<u8>, BitBoxError> {
let resolver = policy::AccountResolver::new(&init.policy, fingerprint, self.network)?;
let pending = NativePolicyResolution { init, resolver };
let bytes = self.native_xpub_request(&pending)?;
self.state = State::NativePolicyWaitXpub(pending);
Ok(bytes)
}
fn start_psbt_sign(
&mut self,
psbt: Box<Psbt>,
fingerprint: &[u8],
config: Option<pb::BtcScriptConfigWithKeypath>,
account: Option<&policy::OwnedAccount>,
) -> Result<Vec<u8>, BitBoxError> {
let (transaction, our_keys) =
Transaction::from_psbt_with_account(fingerprint, &psbt, config, account)?;
let bytes = self.build_encrypted(build_sign_init_request(
api::coin_from_network(self.network),
&transaction,
))?;
self.state = State::SignPsbtWaitNext(Box::new(SignCtx {
psbt,
transaction,
our_keys,
sigs: Vec::new(),
is_inputs_pass2: false,
phase: SignPhase::ExpectNext,
}));
Ok(bytes)
}
fn complete_native_policy(
&mut self,
action: NativePolicyAction,
config: pb::BtcScriptConfig,
account: policy::OwnedAccount,
) -> Result<Vec<u8>, BitBoxError> {
let coin = api::coin_from_network(self.network);
match action {
NativePolicyAction::Register { name } => self.start_encrypted_query(
api::register_script_config_request(
coin,
config,
Some(&account.path),
pb::btc_register_script_config_request::XPubType::AutoXpubTpub,
Some(&name),
),
EncryptedContext::RegisterScriptConfig,
),
NativePolicyAction::IsRegistered => self.start_encrypted_query(
api::is_script_config_registered_request(coin, config, Some(&account.path)),
EncryptedContext::IsRegistered,
),
NativePolicyAction::DescriptorAddress {
change,
index,
display,
} => {
let keypath = account.address_path(change, index)?;
self.complete_native_policy(
NativePolicyAction::Address { keypath, display },
config,
account,
)
}
NativePolicyAction::Address { keypath, display } => {
account.validate_address_path(&keypath)?;
self.start_encrypted_query(
api::address_request(coin, &keypath, config, display),
EncryptedContext::Address,
)
}
NativePolicyAction::Sign { psbt } => {
let forced = pb::BtcScriptConfigWithKeypath {
script_config: Some(config),
keypath: account.path.to_u32_vec(),
};
self.start_psbt_sign(
psbt,
account.fingerprint.as_bytes(),
Some(forced),
Some(&account),
)
}
}
}
fn drive_sign(
&mut self,
mut ctx: Box<SignCtx>,
data: Vec<u8>,
) -> Result<SignStep, BitBoxError> {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
let response = api::decode_response(&decrypted)?;
let next_response = match std::mem::replace(&mut ctx.phase, SignPhase::ExpectNext) {
SignPhase::ExpectNext => decode_sign_next(response)?,
SignPhase::ExpectNextWithSig => {
let next = decode_sign_next(response)?;
if !next.has_signature {
return Err(BitBoxError::BtcSign("missing schnorr signature".into()));
}
ctx.sigs.push(next.signature.clone());
next
}
SignPhase::ExpectHostNonce { host_nonce } => {
let next = decode_sign_next(response)?;
let ty = pb::btc_sign_next_response::Type::try_from(next.r#type)
.map_err(|_| BitBoxError::UnexpectedResponse)?;
if ty != pb::btc_sign_next_response::Type::HostNonce {
return Err(BitBoxError::UnexpectedResponse);
}
let signer_commitment = next
.anti_klepto_signer_commitment
.as_ref()
.ok_or(BitBoxError::UnexpectedResponse)?
.commitment
.clone();
let request = pb::request::Request::Btc(pb::BtcRequest {
request: Some(pb::btc_request::Request::AntikleptoSignature(
pb::AntiKleptoSignatureRequest {
host_nonce: host_nonce.to_vec(),
},
)),
});
let bytes = self.build_encrypted(request)?;
ctx.phase = SignPhase::ExpectAntikleptoSig {
host_nonce,
signer_commitment,
};
return Ok(SignStep::Continue { ctx, bytes });
}
SignPhase::ExpectAntikleptoSig {
host_nonce,
signer_commitment,
} => {
let next = decode_sign_next(response)?;
if !next.has_signature {
return Err(BitBoxError::BtcSign("missing anti-klepto signature".into()));
}
antiklepto::verify_ecdsa(&host_nonce, &signer_commitment, &next.signature)?;
ctx.sigs.push(next.signature.clone());
next
}
};
let ty = pb::btc_sign_next_response::Type::try_from(next_response.r#type)
.map_err(|_| BitBoxError::UnexpectedResponse)?;
match ty {
pb::btc_sign_next_response::Type::Input => {
let input_index = next_response.index as usize;
if input_index >= ctx.transaction.inputs.len() {
return Err(BitBoxError::UnexpectedResponse);
}
let input = &ctx.transaction.inputs[input_index];
let script_config_index = input.script_config_index as usize;
let input_is_schnorr =
is_schnorr(&ctx.transaction.script_configs[script_config_index]);
let perform_antiklepto = ctx.is_inputs_pass2 && !input_is_schnorr;
let host_nonce = if perform_antiklepto {
Some(antiklepto::gen_host_nonce()?)
} else {
None
};
let request = pb::request::Request::BtcSignInput(pb::BtcSignInputRequest {
prev_out_hash: input.prev_out_hash.clone(),
prev_out_index: input.prev_out_index,
prev_out_value: input.prev_out_value,
sequence: input.sequence,
keypath: input.keypath.to_u32_vec(),
script_config_index: input.script_config_index,
host_nonce_commitment: host_nonce.as_ref().map(|n| {
pb::AntiKleptoHostNonceCommitment {
commitment: antiklepto::host_commit(n).to_vec(),
}
}),
});
let bytes = self.build_encrypted(request)?;
if let Some(nonce) = host_nonce {
ctx.phase = SignPhase::ExpectHostNonce { host_nonce: nonce };
} else if ctx.is_inputs_pass2 {
ctx.phase = SignPhase::ExpectNextWithSig;
} else {
ctx.phase = SignPhase::ExpectNext;
}
if !ctx.is_inputs_pass2 && input_index == ctx.transaction.inputs.len() - 1 {
ctx.is_inputs_pass2 = true;
}
Ok(SignStep::Continue { ctx, bytes })
}
pb::btc_sign_next_response::Type::PrevtxInit => {
let prevtx = ctx.transaction.inputs[next_response.index as usize].get_prev_tx()?;
let request = pb::request::Request::Btc(pb::BtcRequest {
request: Some(pb::btc_request::Request::PrevtxInit(
pb::BtcPrevTxInitRequest {
version: prevtx.version,
num_inputs: prevtx.inputs.len() as _,
num_outputs: prevtx.outputs.len() as _,
locktime: prevtx.locktime,
},
)),
});
let bytes = self.build_encrypted(request)?;
Ok(SignStep::Continue { ctx, bytes })
}
pb::btc_sign_next_response::Type::PrevtxInput => {
let prevtx = ctx.transaction.inputs[next_response.index as usize].get_prev_tx()?;
let prev_input = &prevtx.inputs[next_response.prev_index as usize];
let request = pb::request::Request::Btc(pb::BtcRequest {
request: Some(pb::btc_request::Request::PrevtxInput(
pb::BtcPrevTxInputRequest {
prev_out_hash: prev_input.prev_out_hash.clone(),
prev_out_index: prev_input.prev_out_index,
signature_script: prev_input.signature_script.clone(),
sequence: prev_input.sequence,
},
)),
});
let bytes = self.build_encrypted(request)?;
Ok(SignStep::Continue { ctx, bytes })
}
pb::btc_sign_next_response::Type::PrevtxOutput => {
let prevtx = ctx.transaction.inputs[next_response.index as usize].get_prev_tx()?;
let prev_output = &prevtx.outputs[next_response.prev_index as usize];
let request = pb::request::Request::Btc(pb::BtcRequest {
request: Some(pb::btc_request::Request::PrevtxOutput(
pb::BtcPrevTxOutputRequest {
value: prev_output.value,
pubkey_script: prev_output.pubkey_script.clone(),
},
)),
});
let bytes = self.build_encrypted(request)?;
Ok(SignStep::Continue { ctx, bytes })
}
pb::btc_sign_next_response::Type::Output => {
let output = &ctx.transaction.outputs[next_response.index as usize];
let request = pb::request::Request::BtcSignOutput(match output {
TxOutput::Internal(o) => pb::BtcSignOutputRequest {
ours: true,
value: o.value,
keypath: o.keypath.to_u32_vec(),
script_config_index: o.script_config_index,
..Default::default()
},
TxOutput::External(o) => pb::BtcSignOutputRequest {
ours: false,
value: o.value,
r#type: o.payload.output_type as _,
payload: o.payload.data.clone(),
..Default::default()
},
});
let bytes = self.build_encrypted(request)?;
Ok(SignStep::Continue { ctx, bytes })
}
pb::btc_sign_next_response::Type::Done => {
let SignCtx {
mut psbt,
sigs,
our_keys,
..
} = *ctx;
apply_signatures(&mut psbt, &sigs, &our_keys)?;
Ok(SignStep::Done { psbt })
}
pb::btc_sign_next_response::Type::HostNonce
| pb::btc_sign_next_response::Type::PaymentRequest => {
Err(BitBoxError::UnexpectedResponse)
}
}
}
fn build_encrypted(&mut self, request: pb::request::Request) -> Result<Vec<u8>, BitBoxError> {
let proto_msg = pb::Request {
request: Some(request),
};
let encoded = proto_msg.encode_to_vec();
let encrypted = self.noise.encrypt(&encoded)?;
Ok(framed(OP_NOISE_MSG, &encrypted))
}
fn start_encrypted_query(
&mut self,
request: pb::request::Request,
ctx: EncryptedContext,
) -> Result<Vec<u8>, BitBoxError> {
let bytes = self.build_encrypted(request)?;
self.state = State::WaitEncryptedResponse(ctx);
Ok(bytes)
}
fn decode_encrypted_response(
&mut self,
data: Vec<u8>,
) -> Result<pb::response::Response, BitBoxError> {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
api::decode_response(&decrypted)
}
fn decode_device_action_response(&mut self, data: Vec<u8>) -> Result<bool, BitBoxError> {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
let response = pb::Response::decode(decrypted.as_slice())?;
match response.response {
Some(pb::response::Response::Success(_)) => Ok(true),
Some(pb::response::Response::Error(pb::Error { code, message })) => {
if BitBoxDeviceError::from_code(code) == BitBoxDeviceError::Generic {
Ok(false)
} else {
Err(BitBoxError::from_reply(code, message))
}
}
_ => Err(BitBoxError::UnexpectedResponse),
}
}
fn start_setup_initialization(&mut self, setup: SetupInit) -> Result<Vec<u8>, BitBoxError> {
let SetupInit {
mode,
timestamp,
timezone_offset,
..
} = setup;
let request = match mode {
SetupMode::NewWallet { entropy } => {
self.state = State::SetupWaitPassword {
timestamp,
timezone_offset,
};
api::set_password_request(entropy.as_bytes())
}
SetupMode::RestoreFromMnemonic => {
self.state = State::SetupWaitRestore;
api::restore_from_mnemonic_request(timestamp, timezone_offset)
}
};
self.build_encrypted(request)
}
fn handle_encrypted_response(
&mut self,
ctx: EncryptedContext,
data: Vec<u8>,
) -> Result<BitBoxResponse, BitBoxError> {
let response = self.decode_encrypted_response(data)?;
use pb::response::Response as R;
match (ctx, response) {
(EncryptedContext::Version, R::DeviceInfo(info)) => {
Ok(BitBoxResponse::Info(BitBoxDeviceInfo {
version: info.version,
name: info.name,
initialized: info.initialized,
}))
}
(EncryptedContext::MasterFingerprint, R::Fingerprint(f)) => {
if f.fingerprint.len() != 4 {
return Err(BitBoxError::InvalidSignature);
}
let mut fp = [0u8; 4];
fp.copy_from_slice(&f.fingerprint);
Ok(BitBoxResponse::MasterFingerprint(Fingerprint::from(&fp)))
}
(EncryptedContext::Xpub, R::Pub(p)) => {
use std::str::FromStr;
let xpub = Xpub::from_str(&p.r#pub)
.map_err(|e| BitBoxError::BtcSign(format!("bad xpub: {e}")))?;
Ok(BitBoxResponse::Xpub(xpub))
}
(EncryptedContext::Address, R::Pub(p)) => Ok(BitBoxResponse::Address(p.r#pub)),
(
EncryptedContext::IsRegistered,
R::Btc(pb::BtcResponse {
response: Some(pb::btc_response::Response::IsScriptConfigRegistered(x)),
}),
) => Ok(BitBoxResponse::IsRegistered(x.is_registered)),
(
EncryptedContext::RegisterScriptConfig,
R::Btc(pb::BtcResponse {
response: Some(pb::btc_response::Response::Success(_)),
}),
) => Ok(BitBoxResponse::Registered),
(EncryptedContext::RestoreFromMnemonic, R::Success(_)) => Ok(BitBoxResponse::TaskDone),
(EncryptedContext::Backup, R::Success(_)) => Ok(BitBoxResponse::Backup),
_ => Err(BitBoxError::UnexpectedResponse),
}
}
}
impl<C, T, R, E> Interpreter for BitBoxInterpreter<'_, C, T, R, E>
where
C: TryInto<BitBoxCommand, Error = BitBoxError>,
T: From<BitBoxTransmit>,
R: From<BitBoxResponse>,
E: From<BitBoxError>,
{
type Command = C;
type Transmit = T;
type Response = R;
type Error = E;
fn start(&mut self, command: Self::Command) -> Result<Self::Transmit, Self::Error> {
let command: BitBoxCommand = command.try_into()?;
match command {
BitBoxCommand::UnlockAndPair => {
self.state = State::WaitUnlockAck;
Ok(plain_transmit(vec![OP_UNLOCK]).into())
}
BitBoxCommand::GetVersion => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self
.start_encrypted_query(api::device_info_request(), EncryptedContext::Version)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::GetMasterFingerprint => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.start_encrypted_query(
api::root_fingerprint_request(),
EncryptedContext::MasterFingerprint,
)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::GetXpub { keypath, display } => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let coin = api::coin_from_network(self.network);
let xpub_type = api::xpub_type_from_network(self.network);
let bytes = self.start_encrypted_query(
api::xpub_request(coin, &keypath, xpub_type, display),
EncryptedContext::Xpub,
)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::ShowSimpleAddress {
keypath,
simple_type,
display,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let coin = api::coin_from_network(self.network);
let script_config = api::make_script_config_simple(simple_type);
let bytes = self.start_encrypted_query(
api::address_request(coin, &keypath, script_config, display),
EncryptedContext::Address,
)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::ShowPolicyAddress {
keypath,
policy,
display,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
if policy.is_native_multisig() {
let bytes = self.start_native_policy(NativePolicyInit {
policy,
action: NativePolicyAction::Address { keypath, display },
})?;
return Ok(encrypted_transmit(bytes).into());
}
let coin = api::coin_from_network(self.network);
let script_config = policy.into_script_config(None)?;
let bytes = self.start_encrypted_query(
api::address_request(coin, &keypath, script_config, display),
EncryptedContext::Address,
)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::IsScriptConfigRegistered { policy } => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
if policy.is_native_multisig() {
let bytes = self.start_native_policy(NativePolicyInit {
policy,
action: NativePolicyAction::IsRegistered,
})?;
return Ok(encrypted_transmit(bytes).into());
}
let coin = api::coin_from_network(self.network);
let script_config = policy.into_script_config(None)?;
let bytes = self.start_encrypted_query(
api::is_script_config_registered_request(coin, script_config, None),
EncryptedContext::IsRegistered,
)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::RegisterScriptConfig { policy, name } => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
if policy.is_native_multisig() {
let bytes = self.start_native_policy(NativePolicyInit {
policy,
action: NativePolicyAction::Register { name },
})?;
return Ok(encrypted_transmit(bytes).into());
}
let coin = api::coin_from_network(self.network);
let script_config = policy.into_script_config(None)?;
let bytes = self.start_encrypted_query(
api::register_script_config_request(
coin,
script_config,
None,
pb::btc_register_script_config_request::XPubType::AutoXpubTpub,
Some(&name),
),
EncryptedContext::RegisterScriptConfig,
)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::SignPsbt {
psbt,
force_script_config,
policy,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let policy = match policy {
Some(policy) if policy.is_native_multisig() => {
if force_script_config.is_some() {
return Err(BitBoxError::InvalidInput(
"native multisig policy cannot override its script config",
)
.into());
}
let bytes = self.start_native_policy(NativePolicyInit {
policy,
action: NativePolicyAction::Sign { psbt },
})?;
return Ok(encrypted_transmit(bytes).into());
}
policy => policy,
};
let bytes = self.build_encrypted(api::root_fingerprint_request())?;
self.state = State::SignPsbtWaitFingerprint(SignInit {
psbt,
force_script_config,
policy,
});
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::SignMessage {
keypath,
simple_type,
message,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let host_nonce = antiklepto::gen_host_nonce()?;
let coin = api::coin_from_network(self.network);
let request = pb::request::Request::Btc(pb::BtcRequest {
request: Some(pb::btc_request::Request::SignMessage(
pb::BtcSignMessageRequest {
coin: coin as _,
script_config: Some(pb::BtcScriptConfigWithKeypath {
script_config: Some(api::make_script_config_simple(simple_type)),
keypath: keypath.to_u32_vec(),
}),
msg: message,
host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
commitment: antiklepto::host_commit(&host_nonce).to_vec(),
}),
},
)),
});
let bytes = self.build_encrypted(request)?;
self.state = State::SignMessageWaitCommitment { host_nonce };
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::ShowDescriptorAddress {
policy,
change,
index,
display,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.build_encrypted(api::root_fingerprint_request())?;
self.state = State::ShowAddrWaitFingerprint(ShowAddrInit {
policy,
change,
index,
display,
});
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::Setup {
label,
mode,
timestamp,
timezone_offset,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.build_encrypted(api::device_info_request())?;
self.state = State::SetupWaitInfo(SetupInit {
label,
mode,
timestamp,
timezone_offset,
});
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::Wipe => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.build_encrypted(api::device_info_request())?;
self.state = State::WipeWaitInfo;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::Restore {
label,
timestamp,
timezone_offset,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.build_encrypted(api::device_info_request())?;
self.state = State::RestoreWaitInfo(RestoreInit {
label,
timestamp,
timezone_offset,
});
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::TogglePassphrase => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.build_encrypted(api::device_info_request())?;
self.state = State::TogglePassphraseWaitInfo;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::RestoreFromMnemonic {
timestamp,
timezone_offset,
} => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let request =
pb::request::Request::RestoreFromMnemonic(pb::RestoreFromMnemonicRequest {
timestamp,
timezone_offset,
});
let bytes =
self.start_encrypted_query(request, EncryptedContext::RestoreFromMnemonic)?;
Ok(encrypted_transmit(bytes).into())
}
BitBoxCommand::Backup => {
if !self.noise.is_paired() {
return Err(BitBoxError::Noise("not paired").into());
}
let bytes = self.start_encrypted_query(
api::show_mnemonic_request(),
EncryptedContext::Backup,
)?;
Ok(encrypted_transmit(bytes).into())
}
}
}
fn exchange(&mut self, data: Vec<u8>) -> Result<Option<Self::Transmit>, Self::Error> {
let state = std::mem::replace(&mut self.state, State::New);
match state {
State::New | State::Finished(_) => Ok(None),
State::WaitUnlockAck => {
let _ = data;
self.state = State::WaitHandshakeInit;
Ok(Some(plain_transmit(vec![OP_I_CAN_HAS_HANDSHAEK]).into()))
}
State::WaitHandshakeInit => {
if data.as_slice() != [RESPONSE_SUCCESS] {
return Err(BitBoxError::Noise("handshake init rejected").into());
}
let (host, msg1) = self.noise.start_handshake()?;
self.state = State::WaitHandshake1(host);
Ok(Some(
plain_transmit(framed(OP_HER_COMEZ_TEH_HANDSHAEK, &msg1)).into(),
))
}
State::WaitHandshake1(mut host) => {
let bb02_msg_1 = expect_success(&data)?.to_vec();
let host_msg_2 = NoiseState::handshake_read_write(&mut host, &bb02_msg_1)?;
self.state = State::WaitHandshake2(host);
Ok(Some(
plain_transmit(framed(OP_HER_COMEZ_TEH_HANDSHAEK, &host_msg_2)).into(),
))
}
State::WaitHandshake2(mut host) => {
let bb02_msg_2 = expect_success(&data)?.to_vec();
let (device_wants_verify, remote_static) =
NoiseState::handshake_finalize(&mut host, &bb02_msg_2)?;
let already_paired = self
.noise
.data()
.contains_device_static_pubkey(&remote_static);
if !already_paired || device_wants_verify {
let hash: [u8; 32] = host
.get_hash()
.try_into()
.map_err(|_| BitBoxError::Noise("bad handshake hash"))?;
let pairing_code = NoiseState::pairing_code_from_hash(&hash);
self.noise.on_pairing_code(&pairing_code);
self.state = State::WaitPairingConfirm(host, Some(pairing_code));
Ok(Some(
plain_transmit(vec![OP_I_CAN_HAS_PAIRIN_VERIFICASHUN]).into(),
))
} else {
self.noise.finalize(host, None)?;
self.state = State::Finished(BitBoxResponse::TaskDone);
Ok(None)
}
}
State::WaitPairingConfirm(host, pairing_code) => {
if data.as_slice() != [RESPONSE_SUCCESS] {
return Err(BitBoxError::NoisePairingRejected.into());
}
let remote_static = host
.get_rs()
.ok_or(BitBoxError::Noise("no remote static key"))?
.to_vec();
self.noise.confirm_pairing(&remote_static)?;
self.noise.finalize(host, pairing_code)?;
self.state = State::Finished(BitBoxResponse::TaskDone);
Ok(None)
}
State::WaitEncryptedResponse(ctx) => {
let result = self.handle_encrypted_response(ctx, data)?;
self.state = State::Finished(result);
Ok(None)
}
State::SignPsbtWaitFingerprint(init) => {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
let response = api::decode_response(&decrypted)?;
let fingerprint = match response {
pb::response::Response::Fingerprint(f) => f.fingerprint,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
let SignInit {
psbt,
force_script_config,
policy,
} = init;
let force_script_config = match (force_script_config, policy) {
(Some(config), _) => Some(config),
(None, Some(policy)) => Some(policy_script_config(&policy, &fingerprint)?),
(None, None) => None,
};
let bytes = self.start_psbt_sign(psbt, &fingerprint, force_script_config, None)?;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::SignPsbtWaitNext(ctx) => match self.drive_sign(ctx, data)? {
SignStep::Continue { ctx, bytes } => {
self.state = State::SignPsbtWaitNext(ctx);
Ok(Some(encrypted_transmit(bytes).into()))
}
SignStep::Done { psbt } => {
self.state = State::Finished(BitBoxResponse::SignedPsbt(psbt));
Ok(None)
}
},
State::SignMessageWaitCommitment { host_nonce } => {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
let response = api::decode_response(&decrypted)?;
let signer_commitment = match response {
pb::response::Response::Btc(pb::BtcResponse {
response: Some(pb::btc_response::Response::AntikleptoSignerCommitment(c)),
}) => c.commitment,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
let request = pb::request::Request::Btc(pb::BtcRequest {
request: Some(pb::btc_request::Request::AntikleptoSignature(
pb::AntiKleptoSignatureRequest {
host_nonce: host_nonce.to_vec(),
},
)),
});
let bytes = self.build_encrypted(request)?;
self.state = State::SignMessageWaitSig {
host_nonce,
signer_commitment,
};
Ok(Some(encrypted_transmit(bytes).into()))
}
State::SignMessageWaitSig {
host_nonce,
signer_commitment,
} => {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
let response = api::decode_response(&decrypted)?;
let signature = match response {
pb::response::Response::Btc(pb::BtcResponse {
response: Some(pb::btc_response::Response::SignMessage(m)),
}) => m.signature,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
if signature.len() != 65 {
return Err(BitBoxError::InvalidSignature.into());
}
antiklepto::verify_ecdsa(&host_nonce, &signer_commitment, &signature)?;
let sig = bitcoin::secp256k1::ecdsa::Signature::from_compact(&signature[..64])
.map_err(|_| BitBoxError::InvalidSignature)?;
let header = 27 + 4 + signature[64];
self.state = State::Finished(BitBoxResponse::Signature(header, sig));
Ok(None)
}
State::NativePolicyWaitFingerprint(init) => {
let fingerprint = match self.decode_encrypted_response(data)? {
pb::response::Response::Fingerprint(f) => {
let bytes: [u8; 4] = f
.fingerprint
.as_slice()
.try_into()
.map_err(|_| BitBoxError::UnexpectedResponse)?;
Fingerprint::from(bytes)
}
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
let bytes = self.resolve_native_policy(init, fingerprint)?;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::NativePolicyWaitXpub(mut pending) => {
let xpub: Xpub = match self.decode_encrypted_response(data)? {
pb::response::Response::Pub(p) => p
.r#pub
.parse()
.map_err(|_| BitBoxError::UnexpectedResponse)?,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
let bytes = if pending.resolver.accept_xpub(&pending.init.policy, xpub)? {
let (config, account) = pending.resolver.finish(pending.init.policy)?;
self.complete_native_policy(pending.init.action, config, account)?
} else {
let bytes = self.native_xpub_request(&pending)?;
self.state = State::NativePolicyWaitXpub(pending);
bytes
};
Ok(Some(encrypted_transmit(bytes).into()))
}
State::ShowAddrWaitFingerprint(init) => {
let body = expect_success(&data)?;
let decrypted = self.noise.decrypt(body)?;
let response = api::decode_response(&decrypted)?;
let fingerprint = match response {
pb::response::Response::Fingerprint(f) => f.fingerprint,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
if fingerprint.len() != 4 {
return Err(BitBoxError::InvalidSignature.into());
}
let mut fp = [0u8; 4];
fp.copy_from_slice(&fingerprint);
let ShowAddrInit {
policy,
change,
index,
display,
} = init;
if policy.is_native_multisig() {
let bytes = self.resolve_native_policy(
NativePolicyInit {
policy,
action: NativePolicyAction::DescriptorAddress {
change,
index,
display,
},
},
Fingerprint::from(fp),
)?;
return Ok(Some(encrypted_transmit(bytes).into()));
}
let keypath = descriptor_keypath(&policy, Fingerprint::from(&fp), change, index)?;
let coin = api::coin_from_network(self.network);
let script_config = policy.into_script_config(None)?;
let bytes = self.build_encrypted(api::address_request(
coin,
&keypath,
script_config,
display,
))?;
self.state = State::WaitEncryptedResponse(EncryptedContext::Address);
Ok(Some(encrypted_transmit(bytes).into()))
}
State::SetupWaitInfo(setup) => {
let response = self.decode_encrypted_response(data)?;
let info = match response {
pb::response::Response::DeviceInfo(info) => info,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
if info.initialized {
return Err(BitBoxError::AlreadyInitialized.into());
}
if setup.label.is_empty() {
let bytes = self.start_setup_initialization(setup)?;
Ok(Some(encrypted_transmit(bytes).into()))
} else {
let bytes = self.build_encrypted(api::set_device_name_request(&setup.label))?;
self.state = State::SetupWaitName(setup);
Ok(Some(encrypted_transmit(bytes).into()))
}
}
State::SetupWaitName(setup) => {
match self.decode_encrypted_response(data)? {
pb::response::Response::Success(_) => {}
_ => return Err(BitBoxError::UnexpectedResponse.into()),
}
let bytes = self.start_setup_initialization(setup)?;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::SetupWaitPassword {
timestamp,
timezone_offset,
} => {
if !self.decode_device_action_response(data)? {
self.state = State::Finished(BitBoxResponse::DeviceAction(false));
return Ok(None);
}
let bytes =
self.build_encrypted(api::create_backup_request(timestamp, timezone_offset))?;
self.state = State::SetupWaitBackup;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::SetupWaitBackup => {
let success = self.decode_device_action_response(data)?;
self.state = State::Finished(BitBoxResponse::DeviceAction(success));
Ok(None)
}
State::SetupWaitRestore => {
match self.decode_encrypted_response(data)? {
pb::response::Response::Success(_) => {}
_ => return Err(BitBoxError::UnexpectedResponse.into()),
}
self.state = State::Finished(BitBoxResponse::DeviceAction(true));
Ok(None)
}
State::WipeWaitInfo => {
let response = self.decode_encrypted_response(data)?;
let info = match response {
pb::response::Response::DeviceInfo(info) => info,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
if !info.initialized {
return Err(BitBoxError::NotInitialized.into());
}
let bytes = self.build_encrypted(api::reset_request())?;
self.state = State::WipeWaitReset;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::WipeWaitReset => {
let success = self.decode_device_action_response(data)?;
self.state = State::Finished(BitBoxResponse::DeviceAction(success));
Ok(None)
}
State::RestoreWaitInfo(restore) => {
let response = self.decode_encrypted_response(data)?;
let info = match response {
pb::response::Response::DeviceInfo(info) => info,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
if info.initialized {
return Err(BitBoxError::AlreadyInitialized.into());
}
if restore.label.is_empty() {
let bytes = self.build_encrypted(api::restore_from_mnemonic_request(
restore.timestamp,
restore.timezone_offset,
))?;
self.state = State::RestoreWaitMnemonic;
Ok(Some(encrypted_transmit(bytes).into()))
} else {
let bytes =
self.build_encrypted(api::set_device_name_request(&restore.label))?;
self.state = State::RestoreWaitName(restore);
Ok(Some(encrypted_transmit(bytes).into()))
}
}
State::RestoreWaitName(restore) => {
match self.decode_encrypted_response(data)? {
pb::response::Response::Success(_) => {}
_ => return Err(BitBoxError::UnexpectedResponse.into()),
}
let bytes = self.build_encrypted(api::restore_from_mnemonic_request(
restore.timestamp,
restore.timezone_offset,
))?;
self.state = State::RestoreWaitMnemonic;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::RestoreWaitMnemonic => {
match self.decode_encrypted_response(data)? {
pb::response::Response::Success(_) => {}
_ => return Err(BitBoxError::UnexpectedResponse.into()),
}
self.state = State::Finished(BitBoxResponse::DeviceAction(true));
Ok(None)
}
State::TogglePassphraseWaitInfo => {
let response = self.decode_encrypted_response(data)?;
let info = match response {
pb::response::Response::DeviceInfo(info) => info,
_ => return Err(BitBoxError::UnexpectedResponse.into()),
};
if !info.initialized {
return Err(BitBoxError::NotInitialized.into());
}
let bytes = self.build_encrypted(api::set_mnemonic_passphrase_enabled_request(
!info.mnemonic_passphrase_enabled,
))?;
self.state = State::TogglePassphraseWaitSet;
Ok(Some(encrypted_transmit(bytes).into()))
}
State::TogglePassphraseWaitSet => {
match self.decode_encrypted_response(data)? {
pb::response::Response::Success(_) => {}
_ => return Err(BitBoxError::UnexpectedResponse.into()),
}
self.state = State::Finished(BitBoxResponse::DeviceAction(true));
Ok(None)
}
}
}
fn end(self) -> Result<Self::Response, Self::Error> {
match self.state {
State::Finished(response) => Ok(response.into()),
_ => Err(BitBoxError::UnexpectedResponse.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::bip32::DerivationPath;
use miniscript::descriptor::WalletPolicy;
use std::str::FromStr;
fn policy_from(descriptor: &str) -> policy::Policy {
policy::Policy::from_wallet_policy(&WalletPolicy::from_str(descriptor).unwrap()).unwrap()
}
#[test]
fn descriptor_keypath_appends_change_and_index() {
let policy = policy_from(
"wsh(or_d(pk([f5acc2fd/48'/1'/0'/2']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/<0;1>/*),and_v(v:pkh([00000000/48'/1'/0'/2']tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/<0;1>/*),older(100))))",
);
let our_fp = Fingerprint::from_str("f5acc2fd").unwrap();
let keypath = descriptor_keypath(&policy, our_fp, true, 7).unwrap();
assert_eq!(
keypath,
DerivationPath::from_str("m/48'/1'/0'/2'/1/7").unwrap()
);
}
#[test]
fn descriptor_keypath_requires_our_key() {
let policy = policy_from(
"wsh(pk([f5acc2fd/48'/1'/0'/2']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/<0;1>/*))",
);
let unknown = Fingerprint::from_str("deadbeef").unwrap();
assert!(descriptor_keypath(&policy, unknown, false, 0).is_err());
}
const DECAYING_POLICY: &str = "wsh(or_d(pk([f5acc2fd/48'/1'/0'/2']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/<0;1>/*),and_v(v:pkh([00000000/48'/1'/0'/2']tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/<0;1>/*),older(100))))";
#[test]
fn policy_script_config_uses_account_keypath() {
let policy = policy_from(DECAYING_POLICY);
let fingerprint = Fingerprint::from_str("f5acc2fd").unwrap();
let config = policy_script_config(&policy, &fingerprint.as_bytes()[..]).unwrap();
assert_eq!(
config.keypath,
DerivationPath::from_str("m/48'/1'/0'/2'")
.unwrap()
.to_u32_vec()
);
assert!(matches!(
config.script_config,
Some(pb::BtcScriptConfig {
config: Some(pb::btc_script_config::Config::Policy(_))
})
));
}
#[test]
fn policy_script_config_requires_our_key() {
let policy = policy_from(DECAYING_POLICY);
let unknown = Fingerprint::from_str("deadbeef").unwrap();
assert!(policy_script_config(&policy, &unknown.as_bytes()[..]).is_err());
}
}