Skip to content

Commit 39d761c

Browse files
committed
bitcoin: add silent payments crate to support sending to SP addresses
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.
1 parent f819645 commit 39d761c

File tree

188 files changed

+57918
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

188 files changed

+57918
-4
lines changed

external/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ string(REPLACE "-mfpu=fpv4-sp-d16" "" MODIFIED_C_FLAGS ${MODIFIED_C_FLAGS_TMP})
1717
# wally-core
1818

1919
# configure flags for secp256k1 bundled in libwally core, to reduce memory consumption
20-
set(LIBWALLY_SECP256k1_FLAGS --with-ecmult-window=2 --with-ecmult-gen-precision=2 --enable-ecmult-static-precomputation --enable-module-schnorrsig)
20+
set(LIBWALLY_SECP256k1_FLAGS --with-ecmult-window=2 --with-ecmult-gen-precision=2 --enable-ecmult-static-precomputation --enable-module-schnorrsig --enable-module-ecdsa-adaptor)
2121
set(LIBWALLY_CONFIGURE_FLAGS --enable-static --disable-shared --disable-tests ${LIBWALLY_SECP256k1_FLAGS})
2222
if(SANITIZE_ADDRESS)
2323
set(LIBWALLY_CFLAGS "-fsanitize=address")

src/rust/Cargo.lock

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ members = [
2222
"bitbox02",
2323
"bitbox02-sys",
2424
"erc20_params",
25+
"streaming-silent-payments",
2526
]
2627

2728
resolver = "2"
2829

2930
[workspace.dependencies]
31+
bech32 = { version = "0.11.0", default-features = false }
3032
bitcoin = { version = "0.32.2", default-features = false }
3133
hex = { version = "0.4", default-features = false, features = ["alloc"] }
3234
num-bigint = { version = "0.4.3", default-features = false }

src/rust/bitbox02-rust/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ num-bigint = { workspace = true, optional = true }
4141
num-traits = { version = "0.2", default-features = false }
4242
# If you change this, also change src/rust/.cargo/config.toml.
4343
bip32-ed25519 = { git = "https://github.com/BitBoxSwiss/rust-bip32-ed25519", tag = "v0.2.0", optional = true }
44-
bech32 = { version = "0.11.0", default-features = false, features = ["alloc"], optional = true }
45-
blake2 = { version = "0.10.6", default-features = false, features = ["size_opt"], optional = true }
44+
bech32 = { workspace = true, optional = true }
45+
blake2 = { version = "0.10.6", default-features = false, optional = true }
4646
minicbor = { version = "0.24.0", default-features = false, features = ["alloc"], optional = true }
4747
crc = { version = "3.0.1", optional = true }
4848
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["hazmat", "digest"], optional = true }

src/rust/bitbox02-sys/build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ pub fn main() -> Result<(), &'static str> {
224224
.args(["--allowlist-function", "wally_hash160"])
225225
.args(["--allowlist-function", "wally_sha512"])
226226
.args(["--allowlist-function", "printf"])
227+
.args(["--allowlist-function", "bitbox_secp256k1_dleq_prove"])
228+
.args(["--allowlist-function", "bitbox_secp256k1_dleq_verify"])
227229
.arg("wrapper.h")
228230
.arg("--")
229231
.arg("-DPB_NO_PACKED_STRUCTS=1")

src/rust/bitbox02-sys/wrapper.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <reset.h>
2222
#include <screen.h>
2323
#include <sd.h>
24+
#include <secp256k1_ecdsa_adaptor.h>
2425
#include <secp256k1_ecdsa_s2c.h>
2526
#include <securechip/securechip.h>
2627
#include <system.h>

src/rust/bitbox02/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ license = "Apache-2.0"
2525
bitbox02-sys = {path="../bitbox02-sys"}
2626
util = {path = "../util"}
2727
zeroize = { workspace = true }
28+
bitcoin = { workspace = true }
29+
hex = { workspace = true }
2830

2931
[features]
3032
# Only to be enabled in unit tests.

src/rust/bitbox02/src/secp256k1.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 Shift Crypto AG
1+
// Copyright 2022-2024 Shift Crypto AG
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use bitcoin::secp256k1::ffi::CPtr;
16+
1517
use alloc::vec::Vec;
1618

1719
pub fn ecdsa_anti_exfil_host_commit(rand32: &[u8]) -> Result<Vec<u8>, ()> {
@@ -27,3 +29,90 @@ pub fn ecdsa_anti_exfil_host_commit(rand32: &[u8]) -> Result<Vec<u8>, ()> {
2729
_ => Err(()),
2830
}
2931
}
32+
33+
pub fn dleq_prove(
34+
sk: &[u8; 32],
35+
gen2: &bitcoin::secp256k1::PublicKey,
36+
p1: &bitcoin::secp256k1::PublicKey,
37+
p2: &bitcoin::secp256k1::PublicKey,
38+
) -> Result<Vec<u8>, ()> {
39+
let mut s = [0u8; 32];
40+
let mut e = [0u8; 32];
41+
let result = unsafe {
42+
bitbox02_sys::bitbox_secp256k1_dleq_prove(
43+
bitbox02_sys::wally_get_secp_context(),
44+
s.as_mut_ptr(),
45+
e.as_mut_ptr(),
46+
sk.as_ptr(),
47+
gen2.as_c_ptr() as _,
48+
p1.as_c_ptr() as _,
49+
p2.as_c_ptr() as _,
50+
)
51+
};
52+
if result == 1 {
53+
let mut result = s.to_vec();
54+
result.extend(&e);
55+
Ok(result)
56+
} else {
57+
Err(())
58+
}
59+
}
60+
61+
pub fn dleq_verify(
62+
proof: [u8; 64],
63+
gen2: &bitcoin::secp256k1::PublicKey,
64+
p1: &bitcoin::secp256k1::PublicKey,
65+
p2: &bitcoin::secp256k1::PublicKey,
66+
) -> Result<(), ()> {
67+
let result = unsafe {
68+
bitbox02_sys::bitbox_secp256k1_dleq_verify(
69+
bitbox02_sys::wally_get_secp_context(),
70+
proof[..32].as_ptr(),
71+
proof[32..].as_ptr(),
72+
p1.as_c_ptr() as _,
73+
gen2.as_c_ptr() as _,
74+
p2.as_c_ptr() as _,
75+
)
76+
};
77+
if result == 1 {
78+
Ok(())
79+
} else {
80+
Err(())
81+
}
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
88+
89+
#[test]
90+
fn test_dleq() {
91+
let secp = Secp256k1::new();
92+
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";
93+
let seckey = SecretKey::from_slice(seckey_bytes).unwrap();
94+
95+
let pubkey = seckey.public_key(&secp);
96+
97+
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";
98+
let other_base = PublicKey::from_slice(other_base_bytes).unwrap();
99+
100+
let other_pubkey = other_base;
101+
let other_pubkey = other_pubkey.mul_tweak(&secp, &seckey.into()).unwrap();
102+
let proof = dleq_prove(seckey_bytes, &other_base, &pubkey, &other_pubkey).unwrap();
103+
// Check against fixture so potential upstream changes in the DLEQ implementation get
104+
// caught. Incompatible changes can break BitBox client libraries that rely on this
105+
// specific DLEQ implementation.
106+
assert_eq!(
107+
hex::encode(&proof),
108+
"6c885f825f6ce7565bc6d0bfda90506b11e2682dfe943f5a85badf1c8a96edc5f5e03f5ee2c58bf979646fbada920f9f1c5bd92805fb5b01534b42d26a550f79",
109+
);
110+
dleq_verify(
111+
proof.try_into().unwrap(),
112+
&other_base,
113+
&pubkey,
114+
&other_pubkey,
115+
)
116+
.unwrap();
117+
}
118+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2024 Shift Crypto AG
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
[package]
16+
name = "streaming-silent-payments"
17+
version = "0.1.0"
18+
authors = ["Shift Crypto AG <[email protected]>"]
19+
edition = "2021"
20+
license = "Apache-2.0"
21+
22+
[dependencies]
23+
bitcoin = { workspace = true }
24+
bech32 = { workspace = true }
25+
bitbox02 = { path = "../bitbox02" }
26+
27+
[dev-dependencies]
28+
serde = { version = "1.0", features = ["derive"] }
29+
hex = { workspace = true }
30+
serde_json = "1.0"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// File copied and adapted from:
2+
// https://github.com/cygnet3/rust-silentpayments/blob/395b153b6d98ea33a59306c1a8a189d4ca152571/src/utils/hash.rs
3+
4+
#![allow(non_snake_case)]
5+
6+
use bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine};
7+
use bitcoin::secp256k1::{PublicKey, Scalar};
8+
9+
sha256t_hash_newtype! {
10+
struct InputsTag = hash_str("BIP0352/Inputs");
11+
12+
/// BIP0352-tagged hash with tag \"Inputs\".
13+
///
14+
/// This is used for computing the inputs hash.
15+
#[hash_newtype(forward)]
16+
struct InputsHash(_);
17+
18+
pub(crate) struct SharedSecretTag = hash_str("BIP0352/SharedSecret");
19+
20+
/// BIP0352-tagged hash with tag \"SharedSecret\".
21+
///
22+
/// This hash type is for computing the shared secret.
23+
#[hash_newtype(forward)]
24+
pub(crate) struct SharedSecretHash(_);
25+
}
26+
27+
impl InputsHash {
28+
pub(crate) fn from_outpoint_and_A_sum(
29+
smallest_outpoint: &bitcoin::OutPoint,
30+
A_sum: PublicKey,
31+
) -> InputsHash {
32+
let mut eng = InputsHash::engine();
33+
eng.input(&bitcoin::consensus::serialize(smallest_outpoint));
34+
eng.input(&A_sum.serialize());
35+
InputsHash::from_engine(eng)
36+
}
37+
pub(crate) fn to_scalar(self) -> Scalar {
38+
// This is statistically extremely unlikely to panic.
39+
Scalar::from_be_bytes(self.to_byte_array()).unwrap()
40+
}
41+
}
42+
43+
impl SharedSecretHash {
44+
pub(crate) fn from_ecdh_and_k(ecdh: &PublicKey, k: u32) -> SharedSecretHash {
45+
let mut eng = SharedSecretHash::engine();
46+
eng.input(&ecdh.serialize());
47+
eng.input(&k.to_be_bytes());
48+
SharedSecretHash::from_engine(eng)
49+
}
50+
}
51+
52+
pub(crate) fn calculate_input_hash(outpoint: &bitcoin::OutPoint, A_sum: PublicKey) -> Scalar {
53+
InputsHash::from_outpoint_and_A_sum(outpoint, A_sum).to_scalar()
54+
}

0 commit comments

Comments
 (0)