Skip to content

Commit

Permalink
bitcoin: add silent payments crate to support sending to SP addresses
Browse files Browse the repository at this point in the history
BIP-352: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki

A silent payment address contains two pubkeys, B_scan and
B_spend. From that, Taproot outputs can be created by using a ECDH
shared secret - see the BIP Overview section.

The created output is returned to the host along with a DLEQ proof the
host can use to independently verify the output was created
correctly. This mitigates bugs, potential memory
corruption (bit-flips), etc. in the firmware leading to catastrophic
failure.

The added vendored deps serde/serde_json etc. are dev-dependencies to
run the tests in the new crate, not used for firmware builds.

**A note about multiple silent payment outputs per transaction:**

In general, a transaction can send to an arbitrary amount of silent
payment addresses. If there are multiple, then for each unique
B_scan (multiple outputs to the same recipient/B_scan), a counter `k`
is incremented. See
https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#creating-outputs.

To be able to verify the silent payment address, we need to derive the
output and check that it matches the provided output. For each SP
output, we'd then need to know the SP address and the counter `k`.

The problem with this is that we also need to check, per B_scan, that
each `k=0, 1, ...` is used starting at zero with no holes. This
requires non-constant memory. We could still do it and support a
limited (but likely high) number of SP outputs per transaction, but
that complicates the code. For this reason, we restrict to only one SP
output per transaction.
  • Loading branch information
benma committed Sep 7, 2024
1 parent 03485d3 commit 9095b8e
Show file tree
Hide file tree
Showing 188 changed files with 57,905 additions and 4 deletions.
2 changes: 1 addition & 1 deletion external/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ string(REPLACE "-mfpu=fpv4-sp-d16" "" MODIFIED_C_FLAGS ${MODIFIED_C_FLAGS_TMP})
# wally-core

# configure flags for secp256k1 bundled in libwally core, to reduce memory consumption
set(LIBWALLY_SECP256k1_FLAGS --with-ecmult-window=2 --with-ecmult-gen-precision=2 --enable-ecmult-static-precomputation --enable-module-schnorrsig)
set(LIBWALLY_SECP256k1_FLAGS --with-ecmult-window=2 --with-ecmult-gen-precision=2 --enable-ecmult-static-precomputation --enable-module-schnorrsig --enable-module-ecdsa-adaptor)
set(LIBWALLY_CONFIGURE_FLAGS --enable-static --disable-shared --disable-tests ${LIBWALLY_SECP256k1_FLAGS})
if(SANITIZE_ADDRESS)
set(LIBWALLY_CFLAGS "-fsanitize=address")
Expand Down
57 changes: 57 additions & 0 deletions src/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ members = [
"bitbox02",
"bitbox02-sys",
"erc20_params",
"streaming-silent-payments",
]

resolver = "2"

[workspace.dependencies]
bech32 = { version = "0.11.0", default-features = false }
bitcoin = { version = "0.32.2", default-features = false }
hex = { version = "0.4", default-features = false, features = ["alloc"] }
num-bigint = { version = "0.4.3", default-features = false }
Expand Down
4 changes: 2 additions & 2 deletions src/rust/bitbox02-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ num-bigint = { workspace = true, optional = true }
num-traits = { version = "0.2", default-features = false }
# If you change this, also change src/rust/.cargo/config.toml.
bip32-ed25519 = { git = "https://github.com/BitBoxSwiss/rust-bip32-ed25519", tag = "v0.2.0", optional = true }
bech32 = { version = "0.11.0", default-features = false, features = ["alloc"], optional = true }
blake2 = { version = "0.10.6", default-features = false, features = ["size_opt"], optional = true }
bech32 = { workspace = true, optional = true }
blake2 = { version = "0.10.6", default-features = false, optional = true }
minicbor = { version = "0.24.0", default-features = false, features = ["alloc"], optional = true }
crc = { version = "3.0.1", optional = true }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["hazmat", "digest"], optional = true }
Expand Down
2 changes: 2 additions & 0 deletions src/rust/bitbox02-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ pub fn main() -> Result<(), &'static str> {
.args(["--allowlist-function", "wally_hash160"])
.args(["--allowlist-function", "wally_sha512"])
.args(["--allowlist-function", "printf"])
.args(["--allowlist-function", "bitbox_secp256k1_dleq_prove"])
.args(["--allowlist-function", "bitbox_secp256k1_dleq_verify"])
.arg("wrapper.h")
.arg("--")
.arg("-DPB_NO_PACKED_STRUCTS=1")
Expand Down
1 change: 1 addition & 0 deletions src/rust/bitbox02-sys/wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <screen.h>
#include <sd.h>
#include <secp256k1_ecdsa_s2c.h>
#include <secp256k1_ecdsa_adaptor.h>
#include <securechip/securechip.h>
#include <system.h>
#include <time.h>
Expand Down
2 changes: 2 additions & 0 deletions src/rust/bitbox02/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ license = "Apache-2.0"
bitbox02-sys = {path="../bitbox02-sys"}
util = {path = "../util"}
zeroize = { workspace = true }
bitcoin = { workspace = true }
hex = { workspace = true }

[features]
# Only to be enabled in unit tests.
Expand Down
91 changes: 90 additions & 1 deletion src/rust/bitbox02/src/secp256k1.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 Shift Crypto AG
// Copyright 2022-2024 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.
Expand All @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use bitcoin::secp256k1::ffi::CPtr;

use alloc::vec::Vec;

pub fn ecdsa_anti_exfil_host_commit(rand32: &[u8]) -> Result<Vec<u8>, ()> {
Expand All @@ -27,3 +29,90 @@ pub fn ecdsa_anti_exfil_host_commit(rand32: &[u8]) -> Result<Vec<u8>, ()> {
_ => Err(()),
}
}

pub fn dleq_prove(
sk: &[u8; 32],
gen2: &bitcoin::secp256k1::PublicKey,
p1: &bitcoin::secp256k1::PublicKey,
p2: &bitcoin::secp256k1::PublicKey,
) -> Result<Vec<u8>, ()> {
let mut s = [0u8; 32];
let mut e = [0u8; 32];
let result = unsafe {
bitbox02_sys::bitbox_secp256k1_dleq_prove(
bitbox02_sys::wally_get_secp_context(),
s.as_mut_ptr(),
e.as_mut_ptr(),
sk.as_ptr(),
gen2.as_c_ptr() as _,
p1.as_c_ptr() as _,
p2.as_c_ptr() as _,
)
};
if result == 1 {
let mut result = s.to_vec();
result.extend(&e);
Ok(result)
} else {
Err(())
}
}

pub fn dleq_verify(
proof: [u8; 64],
gen2: &bitcoin::secp256k1::PublicKey,
p1: &bitcoin::secp256k1::PublicKey,
p2: &bitcoin::secp256k1::PublicKey,
) -> Result<(), ()> {
let result = unsafe {
bitbox02_sys::bitbox_secp256k1_dleq_verify(
bitbox02_sys::wally_get_secp_context(),
proof[..32].as_ptr(),
proof[32..].as_ptr(),
p1.as_c_ptr() as _,
gen2.as_c_ptr() as _,
p2.as_c_ptr() as _,
)
};
if result == 1 {
Ok(())
} else {
Err(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};

#[test]
fn test_dleq() {
let secp = Secp256k1::new();
let seckey_bytes = b"\x07\x7e\xb7\x5a\x52\xec\xa2\x4c\xde\xdf\x05\x8c\x92\xf1\xca\x8b\x9d\x48\x41\x77\x1f\xd6\xba\xa3\xd2\x78\x85\xfb\x5b\x49\xfb\xa2";
let seckey = SecretKey::from_slice(seckey_bytes).unwrap();

let pubkey = seckey.public_key(&secp);

let other_base_bytes = b"\x03\x89\x14\x0f\x7b\xb8\x52\xf0\x20\xf1\x54\xe5\x59\x08\xfe\x36\x99\xdc\x9f\x65\x15\x3e\x68\x15\x27\xf0\xd5\x5a\xab\xed\x93\x7f\x4b";
let other_base = PublicKey::from_slice(other_base_bytes).unwrap();

let other_pubkey = other_base;
let other_pubkey = other_pubkey.mul_tweak(&secp, &seckey.into()).unwrap();
let proof = dleq_prove(seckey_bytes, &other_base, &pubkey, &other_pubkey).unwrap();
// Check against fixture so potential upstream changes in the DLEQ implementation get
// caught. Incompatible changes can break BitBox client libraries that rely on this
// specific DLEQ implementation.
assert_eq!(
hex::encode(&proof),
"6c885f825f6ce7565bc6d0bfda90506b11e2682dfe943f5a85badf1c8a96edc5f5e03f5ee2c58bf979646fbada920f9f1c5bd92805fb5b01534b42d26a550f79",
);
dleq_verify(
proof.try_into().unwrap(),
&other_base,
&pubkey,
&other_pubkey,
)
.unwrap();
}
}
30 changes: 30 additions & 0 deletions src/rust/streaming-silent-payments/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2024 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.

[package]
name = "streaming-silent-payments"
version = "0.1.0"
authors = ["Shift Crypto AG <[email protected]>"]
edition = "2021"
license = "Apache-2.0"

[dependencies]
bitcoin = { workspace = true }
bech32 = { workspace = true }
bitbox02 = { path = "../bitbox02" }

[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }
hex = { workspace = true }
serde_json = "1.0"
54 changes: 54 additions & 0 deletions src/rust/streaming-silent-payments/src/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// File copied and adapted from:
// https://github.com/cygnet3/rust-silentpayments/blob/395b153b6d98ea33a59306c1a8a189d4ca152571/src/utils/hash.rs

#![allow(non_snake_case)]

use bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine};
use bitcoin::secp256k1::{PublicKey, Scalar};

sha256t_hash_newtype! {
struct InputsTag = hash_str("BIP0352/Inputs");

/// BIP0352-tagged hash with tag \"Inputs\".
///
/// This is used for computing the inputs hash.
#[hash_newtype(forward)]
struct InputsHash(_);

pub(crate) struct SharedSecretTag = hash_str("BIP0352/SharedSecret");

/// BIP0352-tagged hash with tag \"SharedSecret\".
///
/// This hash type is for computing the shared secret.
#[hash_newtype(forward)]
pub(crate) struct SharedSecretHash(_);
}

impl InputsHash {
pub(crate) fn from_outpoint_and_A_sum(
smallest_outpoint: &bitcoin::OutPoint,
A_sum: PublicKey,
) -> InputsHash {
let mut eng = InputsHash::engine();
eng.input(&bitcoin::consensus::serialize(smallest_outpoint));
eng.input(&A_sum.serialize());
InputsHash::from_engine(eng)
}
pub(crate) fn to_scalar(self) -> Scalar {
// This is statistically extremely unlikely to panic.
Scalar::from_be_bytes(self.to_byte_array()).unwrap()
}
}

impl SharedSecretHash {
pub(crate) fn from_ecdh_and_k(ecdh: &PublicKey, k: u32) -> SharedSecretHash {
let mut eng = SharedSecretHash::engine();
eng.input(&ecdh.serialize());
eng.input(&k.to_be_bytes());
SharedSecretHash::from_engine(eng)
}
}

pub(crate) fn calculate_input_hash(outpoint: &bitcoin::OutPoint, A_sum: PublicKey) -> Scalar {
InputsHash::from_outpoint_and_A_sum(outpoint, A_sum).to_scalar()
}
Loading

0 comments on commit 9095b8e

Please sign in to comment.