Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b243514
feat(signer): add MASTER_SEED_LABEL, nostr_identity_keys, and try_clone
theivess Apr 4, 2026
4aae531
feat(signer): add MASTER_SEED_LABEL constants, vault_only filter, and…
theivess Apr 4, 2026
93ed9fd
feat(settings): rename liquid_wallet_signer_fingerprint to master_sig…
theivess Apr 4, 2026
c539de5
feat(launcher): store master seed with 'master_' label instead of 'li…
theivess Apr 4, 2026
0272f9b
fix(launcher): clean up stale 'liquid' references and use MASTER_SEED…
theivess Apr 4, 2026
53ebc8b
fix(launcher): skip Cube-count limit on non-mainnet networks
theivess Apr 4, 2026
507fbf2
refactor(launcher): simplify network guard to positive-match on Bitco…
theivess Apr 4, 2026
c989cfd
feat(vault): derive hot-signer from master seed in developer mode
theivess Apr 4, 2026
20251ae
fix(vault): add dev-mode warning log, hot-signer badge, and message h…
theivess Apr 4, 2026
d9386dc
fix(test): enable developer_mode in hot-signer descriptor test
theivess Apr 4, 2026
fc248c5
Added in Passkey and cleanup
satoshisound Apr 10, 2026
14a9af4
more work
satoshisound Apr 11, 2026
5e9e44c
cleanup docs
satoshisound Apr 11, 2026
4829964
cleanup
satoshisound Apr 11, 2026
23b3840
fix bugs
satoshisound Apr 12, 2026
ef484ad
cleanup
satoshisound Apr 12, 2026
efb4149
fix linting error
satoshisound Apr 12, 2026
88e9249
remove dead code
satoshisound Apr 12, 2026
ea823e5
cleanup
satoshisound Apr 12, 2026
8c31e64
fix bug
satoshisound Apr 12, 2026
b378181
fix: Mnemonic not cleared on successful verification before async com…
satoshisound Apr 12, 2026
d9973b9
Merge pull request #160 from coincubetech/feature/passkey
satoshisound Apr 13, 2026
d4d7566
cleanup
satoshisound Apr 13, 2026
71adfd0
Merge branch 'master' into feat/unified-master-seed
satoshisound Apr 13, 2026
508f5cf
fix merge issues
satoshisound Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,24 @@ FORCE_ISOCODE=

# Breez SDK API Key (for Lightning Network features)
# Get your API key at: https://sdk-doc-liquid.breez.technology/guide/getting_started.html#api-key
BREEZ_API_KEY=
BREEZ_API_KEY=

# --- Feature Flags ---
# Enable the passkey flow for Cube creation (WebAuthn + PRF extension).
# Requires macOS code signing with associated-domains entitlement + an AASA
# file hosted at coincube.io/.well-known/apple-app-site-association.
# Default: off. Set to 1 / true / yes to enable the "Use Passkey" toggle
# in the Create Cube form.
COINCUBE_ENABLE_PASSKEY=0

# --- Passkey Ceremony Configuration ---
# URL of the hosted passkey ceremony page (WebAuthn + PRF). The non-macOS
# webview fallback navigates here; macOS uses AuthenticationServices natively
# but still needs RP_ID below to match the ceremony domain.
# Default (unset): http://localhost:8080/passkey
COINCUBE_PASSKEY_CEREMONY_URL=

# Relying Party ID for WebAuthn — must match the ceremony page's domain
# (no scheme, no path). For production set to the real domain, e.g.
# coincube.io. Default (unset): localhost
COINCUBE_PASSKEY_RP_ID=
28 changes: 28 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion coincube-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ log = "0.4"
# Used for generating mnemonics
getrandom = "0.3.1"

# Used for the hot signer
# Used for the master signer
bip39 = "2.0"
argon2 = "0.5.3"
aes-gcm = "0.10.3"
Expand Down
88 changes: 83 additions & 5 deletions coincube-core/src/border_wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use miniscript::bitcoin::{
};
use zeroize::Zeroizing;

use crate::signer::HotSigner;
use crate::signer::MasterSigner;

/// A secret-bearing grid recovery phrase.
///
Expand Down Expand Up @@ -77,6 +77,38 @@ impl GridRecoveryPhrase {
})
}

/// Derive a deterministic GridRecoveryPhrase from a master signer using BIP-85.
///
/// Uses derivation path `m/83696968'/39'/0'/12'/0'` (BIP-85: purpose / BIP-39 app
/// / English / 12 words / index 0). The child private key is processed through
/// HMAC-SHA-512 with the BIP-85 domain tag to produce 16 bytes of entropy for a
/// 12-word mnemonic.
pub fn from_master_signer(
signer: &MasterSigner,
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
) -> Result<Self, BorderWalletError> {
use miniscript::bitcoin::hashes::{sha512, Hash, HashEngine, Hmac, HmacEngine};

let path: bip32::DerivationPath = "m/83696968'/39'/0'/12'/0'"
.parse()
.expect("hardcoded path is valid");
let child_xpriv = signer.xpriv_at(&path, secp);

// BIP-85: HMAC-SHA-512 with domain tag "bip-entropy-from-k"
let mut engine = HmacEngine::<sha512::Hash>::new(b"bip-entropy-from-k");
engine.input(&child_xpriv.to_priv().to_bytes());
let hmac = Hmac::<sha512::Hash>::from_engine(engine);

// Take first 16 bytes for 128 bits of entropy → 12-word mnemonic
let entropy = &hmac.to_byte_array()[..16];
let mnemonic = bip39::Mnemonic::from_entropy(entropy)
.map_err(|_| BorderWalletError::InvalidRecoveryPhrase)?;

Ok(Self {
phrase: Zeroizing::new(mnemonic.to_string()),
})
}

/// Access the phrase as a string slice. Use only for grid generation and display.
pub fn as_str(&self) -> &str {
&self.phrase
Expand Down Expand Up @@ -153,7 +185,7 @@ pub fn derive_enrollment(
/// Sign a PSBT using a transiently reconstructed Border Wallet key.
///
/// This function:
/// 1. Creates a transient `HotSigner` from the reconstructed mnemonic
/// 1. Creates a transient `MasterSigner` from the reconstructed mnemonic
/// 2. Verifies the fingerprint matches the expected enrollment fingerprint
/// 3. Signs the PSBT
/// 4. All secret material is dropped when this function returns
Expand All @@ -167,7 +199,7 @@ pub fn sign_psbt_with_border_wallet(
) -> Result<(Fingerprint, Psbt), BorderWalletError> {
let secp = secp256k1::Secp256k1::new();

let signer = HotSigner::from_mnemonic(network, mnemonic)
let signer = MasterSigner::from_mnemonic(network, mnemonic)
.map_err(|e| BorderWalletError::KeyDerivation(e.to_string()))?;

let actual_fingerprint = signer.fingerprint(&secp);
Expand All @@ -185,11 +217,23 @@ pub fn sign_psbt_with_border_wallet(
Ok((actual_fingerprint, signed_psbt))
}

/// The standard derivation path for Border Wallet signers.
/// The default derivation path for Border Wallet signers.
///
/// Uses BIP-48 multisig path with script type 2 (Taproot):
/// Uses the BIP-48 native segwit multisig path:
/// - Mainnet: m/48'/0'/0'/2'
/// - Testnet/Signet: m/48'/1'/0'/2'
///
/// Per BIP-48, script type `2'` is **P2WSH** (native segwit multisig).
/// COINCUBE currently re-uses this same path for both P2WSH and Taproot
/// multisig vaults, because BIP-48 does not define a Taproot multisig
/// script type and there is no consensus standard for one. A proposed
/// extension using `3'` for Taproot multisig
/// ([bitcoin/bips#1473](https://github.com/bitcoin/bips/pull/1473))
/// was closed without merging in May 2024.
///
/// If/when a Taproot multisig path standard emerges, revisit this and
/// the related helpers in `coincube-gui/src/utils/mod.rs` and
/// `coincube-gui/src/installer/step/descriptor/editor/key.rs`.
pub fn default_derivation_path(network: Network) -> bip32::DerivationPath {
let coin_type = match network {
Network::Bitcoin => 0,
Expand Down Expand Up @@ -454,4 +498,38 @@ mod tests {
let debug_str = format!("{:?}", enrollment);
assert!(debug_str.contains("BorderWalletEnrollment"));
}

#[test]
fn test_grid_recovery_phrase_from_master_signer_deterministic() {
let secp = secp256k1::Secp256k1::new();
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let mnemonic = bip39::Mnemonic::parse_in(bip39::Language::English, phrase).unwrap();
let signer = MasterSigner::from_mnemonic(Network::Testnet, mnemonic).unwrap();

let grp1 = GridRecoveryPhrase::from_master_signer(&signer, &secp).unwrap();
let grp2 = GridRecoveryPhrase::from_master_signer(&signer, &secp).unwrap();

// Same master signer always produces the same grid recovery phrase.
assert_eq!(grp1.as_str(), grp2.as_str());

// Must be a valid 12-word BIP39 mnemonic.
let words: Vec<&str> = grp1.as_str().split_whitespace().collect();
assert_eq!(words.len(), 12);
assert!(bip39::Mnemonic::parse_in(bip39::Language::English, grp1.as_str()).is_ok());

// Different master signer produces a different grid phrase.
let other = MasterSigner::generate(Network::Testnet).unwrap();
let grp3 = GridRecoveryPhrase::from_master_signer(&other, &secp).unwrap();
assert_ne!(grp1.as_str(), grp3.as_str());
}

#[test]
fn test_grid_recovery_phrase_from_master_signer_differs_from_random() {
let secp = secp256k1::Secp256k1::new();
let signer = MasterSigner::generate(Network::Testnet).unwrap();
let derived = GridRecoveryPhrase::from_master_signer(&signer, &secp).unwrap();
// The derived phrase should produce a valid grid.
let grid = derived.generate_grid();
assert_eq!(grid.cells().len(), WordGrid::TOTAL_CELLS);
}
}
4 changes: 2 additions & 2 deletions coincube-core/src/descriptors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -825,14 +825,14 @@ impl DerivedSinglePathCoincubeDesc {
mod tests {
use super::*;

use crate::signer::HotSigner;
use crate::signer::MasterSigner;
use bitcoin::{hashes::Hash, Sequence};
use miniscript::bitcoin::bip32::Fingerprint;

fn random_desc_key(
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
) -> descriptor::DescriptorPublicKey {
let signer = HotSigner::generate(bitcoin::Network::Bitcoin).unwrap();
let signer = MasterSigner::generate(bitcoin::Network::Bitcoin).unwrap();
let xpub_str = format!(
"[{}]{}/<0;1>/*",
signer.fingerprint(secp),
Expand Down
Loading
Loading