Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

6 changes: 2 additions & 4 deletions examples/tokio-tss/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ impl EncryptedRouter {
if let Some(target) = message.to {
let (send_channel, _) = self.peer_channels.get_mut(&target).ok_or_else(|| {
TssError::Protocol(format!(
"party {} has no encrypted channel for peer {}",
self_id, target
"party {self_id} has no encrypted channel for peer {target}"
))
})?;

Expand Down Expand Up @@ -211,8 +210,7 @@ pub fn create_encrypted_routers(

if left_shared != right_shared {
return Err(TssError::Encryption(format!(
"mismatched shared secret for parties {} and {}",
left, right
"mismatched shared secret for parties {left} and {right}"
)));
}

Expand Down
31 changes: 31 additions & 0 deletions libtss-dotnet/src/Libtss/KeyShareHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,37 @@ public unsafe byte[] GroupVerifyingKey
}
}

/// <summary>
/// The 32-byte BIP-32 chain code paired with <see cref="GroupVerifyingKey"/>.
/// </summary>
/// <remarks>
/// <para>
/// Only the BIP-32-capable ciphersuites carry a chain code:
/// <see cref="Ciphersuite.Secp256k1ECDSA"/>, <see cref="Ciphersuite.Secp256r1ECDSA"/>
/// and <see cref="Ciphersuite.Secp256k1Taproot"/>. Every other suite throws a
/// <see cref="TssException"/> with <see cref="TssStatus.ProtoMismatch"/>.
/// </para>
/// <para>
/// Pair with <see cref="GroupVerifyingKey"/> to build a BIP-32 extended public key (xpub)
/// whose off-MPC child derivation matches <see cref="DeriveOperations.DeriveChild"/>.
/// </para>
/// </remarks>
public unsafe byte[] ChainCode
{
get
{
ThrowIfDisposed();
var buf = new NativeTssBuffer();
int status;
fixed (TssBuffer* p = &buf.Raw)
{
status = Native.tss_handle_chain_code(_handle, p);
}
TssException.ThrowIfError(status);
return buf.ToArray();
}
}

/// <summary>
/// The public verification key for this specific share as raw bytes.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions libtss-dotnet/src/Libtss/Native.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ public static extern int tss_refresh_next(
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_handle_group_key(ulong handle, TssBuffer* @out);

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_handle_chain_code(ulong handle, TssBuffer* @out);

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_handle_pubkey_package(ulong handle, TssBuffer* @out);

Expand Down
63 changes: 63 additions & 0 deletions libtss-dotnet/tests/Libtss.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,69 @@ public void SplitKey_ZerosInputSecretKey()
Assert.All(secretKey, b => Assert.Equal(0, b));
}

[Fact]
public void ChainCode_Taproot_IsSha256OfGroupKey()
{
RequireNativeLibrary();
Tss.Init();

var (handles, _) = FrostOperations.GenerateDealer(
Ciphersuite.Secp256k1Taproot, maxSigners: 3, minSigners: 2);
try
{
byte[] chainCode = handles[0].ChainCode;
Assert.Equal(32, chainCode.Length);
Assert.Equal(SHA256.HashData(handles[0].GroupVerifyingKey), chainCode);
}
finally
{
foreach (var h in handles)
h.Dispose();
}
}

[Fact]
public void ChainCode_ChangesAfterDeriveChild()
{
RequireNativeLibrary();
Tss.Init();

var (handles, _) = FrostOperations.GenerateDealer(
Ciphersuite.Secp256k1Taproot, maxSigners: 3, minSigners: 2);
try
{
using var child = DeriveOperations.DeriveChild(handles[0], childNumber: 7);
byte[] childChainCode = child.ChainCode;
Assert.Equal(32, childChainCode.Length);
Assert.NotEqual(handles[0].ChainCode, childChainCode);
}
finally
{
foreach (var h in handles)
h.Dispose();
}
}

[Fact]
public void ChainCode_UnsupportedSuite_Throws()
{
RequireNativeLibrary();
Tss.Init();

var (handles, _) = FrostOperations.GenerateDealer(
Ciphersuite.Ed25519, maxSigners: 3, minSigners: 2);
try
{
var ex = Assert.Throws<TssException>(() => handles[0].ChainCode);
Assert.Equal(TssStatus.ProtoMismatch, ex.Code);
}
finally
{
foreach (var h in handles)
h.Dispose();
}
}

[Fact]
public void WipeBytes_ZerosArray()
{
Expand Down
3 changes: 3 additions & 0 deletions libtss-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ zeroize = { workspace = true }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
hmac = "0.12"
sha2 = "0.10"
k256 = { workspace = true }

[build-dependencies]
cbindgen = "0.27"
Expand Down
57 changes: 57 additions & 0 deletions libtss-ffi/libtss.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ TssStatus tss_handle_verifying_share(TssHandle handle, struct TssBuffer *out);

TssStatus tss_handle_group_key(TssHandle handle, struct TssBuffer *out);

// Return the 32-byte BIP-32 chain code paired with the group key.
//
// Supported for the BIP-32-capable ciphersuites only (Secp256k1ECDSA,
// Secp256r1ECDSA, Secp256k1Taproot); every other suite returns
// `TSS_ERR_PROTO_MISMATCH`. Pair with `tss_handle_group_key` to build an
// extended public key whose off-MPC child derivation matches `tss_derive_child`.
TssStatus tss_handle_chain_code(TssHandle handle, struct TssBuffer *out);

TssStatus tss_handle_pubkey_package(TssHandle handle, struct TssBuffer *out);

uint8_t tss_handle_ciphersuite(TssHandle handle);
Expand Down Expand Up @@ -235,6 +243,41 @@ TssStatus tss_message_build(struct TssBuffer *buf,
const uint8_t *data,
size_t data_len);

// Compute this helper's masked DKLs23 repair term for `target`.
//
// `helpers` lists every helper (this party included, the target excluded);
// all helpers must pass the identical list and `session_id`. `out_term`
// receives a 32-byte scalar to unicast to the target; free it with
// `tss_buffer_free`. A share missing a zero-share pairing with any helper
// (a hollow, not yet refreshed share) returns `TSS_ERR_INVALID_SHARE`
// rather than an unmasked term.
TssStatus tss_repair_helper_term(TssHandle key_share,
const uint16_t *helpers,
size_t helpers_len,
uint16_t target,
const uint8_t *session_id,
size_t session_id_len,
struct TssBuffer *out_term);

// Assemble helpers' repair terms into a hollow DKLs23 key share for `target`.
//
// `pubkey_package` is the serialized group package and `chain_code` its
// 32-byte BIP-32 chain code, both as the helpers report them; `terms` holds
// at least `min_signers` outputs of `tss_repair_helper_term`. The sum of the
// terms is checked against the target's verifying share and a mismatch
// returns `TSS_ERR_INVALID_SHARE` without creating a handle. The returned
// share cannot sign until a `tss_refresh_new_dkls` refresh has run across the
// whole group. Free it with `tss_handle_free`.
TssStatus tss_repair_assemble(uint8_t suite,
struct TssSlice pubkey_package,
struct TssSlice chain_code,
uint16_t target,
const struct TssSlice *terms,
size_t terms_len,
const uint8_t *session_id,
size_t session_id_len,
TssHandle *out_key_share);

TssStatus tss_dkg_new(uint8_t suite,
uint16_t self_id,
uint16_t max_signers,
Expand Down Expand Up @@ -272,6 +315,20 @@ TssStatus tss_refresh_new(TssHandle key_share,
TssHandle *out_session,
struct TssBuffer *out_messages);

// Create a DKLs23 complete refresh session for a secp256k1 key share.
//
// Every party of the group must run this under the same fresh `session_id`;
// the refreshed shares are salted with it and cannot co-sign with any other
// generation. Advance with `tss_refresh_next` three times. `key_share` stays
// valid; the completed session yields a new handle. Non-DKLs23 shares return
// `TSS_ERR_PROTO_MISMATCH`; an empty `session_id` returns
// `TSS_ERR_INVALID_CONFIG`.
TssStatus tss_refresh_new_dkls(TssHandle key_share,
const uint8_t *session_id,
size_t session_id_len,
TssHandle *out_session,
struct TssBuffer *out_messages);

TssStatus tss_refresh_receiver(TssHandle key_share, TssHandle *out_session);

TssStatus tss_refresh_next(TssHandle session,
Expand Down
17 changes: 17 additions & 0 deletions libtss-ffi/src/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ pub extern "C" fn tss_handle_group_key(handle: TssHandle, out: *mut TssBuffer) -
})
}

/// Return the 32-byte BIP-32 chain code paired with the group key.
///
/// Supported for the BIP-32-capable ciphersuites only (Secp256k1ECDSA,
/// Secp256r1ECDSA, Secp256k1Taproot); every other suite returns
/// `TSS_ERR_PROTO_MISMATCH`. Pair with `tss_handle_group_key` to build an
/// extended public key whose off-MPC child derivation matches `tss_derive_child`.
#[no_mangle]
pub extern "C" fn tss_handle_chain_code(handle: TssHandle, out: *mut TssBuffer) -> TssStatus {
ffi_entry!({
let out = unsafe { out.as_mut() }
.ok_or_else(|| libtss::TssError::InvalidConfig("out is null".into()))?;
let ks = borrow_key_share(handle)?;
*out = TssBuffer::from_vec(ks.chain_code()?);
Ok(())
})
}

#[no_mangle]
pub extern "C" fn tss_handle_pubkey_package(handle: TssHandle, out: *mut TssBuffer) -> TssStatus {
ffi_entry!({
Expand Down
1 change: 1 addition & 0 deletions libtss-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod init;
pub mod memory;
pub mod messages;
pub mod registry;
pub mod repair;
pub mod session;
pub mod types;

Expand Down
98 changes: 98 additions & 0 deletions libtss-ffi/src/repair.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use crate::ffi_entry;
use crate::messages::checked_slice;
use crate::session::{borrow_key_share, checked_optional_slice};
use crate::types::{TssBuffer, TssHandle, TssSlice, TssStatus};

/// Compute this helper's masked DKLs23 repair term for `target`.
///
/// `helpers` lists every helper (this party included, the target excluded);
/// all helpers must pass the identical list and `session_id`. `out_term`
/// receives a 32-byte scalar to unicast to the target; free it with
/// `tss_buffer_free`. A share missing a zero-share pairing with any helper
/// (a hollow, not yet refreshed share) returns `TSS_ERR_INVALID_SHARE`
/// rather than an unmasked term.
#[no_mangle]
pub extern "C" fn tss_repair_helper_term(
key_share: TssHandle,
helpers: *const u16,
helpers_len: usize,
target: u16,
session_id: *const u8,
session_id_len: usize,
out_term: *mut TssBuffer,
) -> TssStatus {
ffi_entry!({
let out_term = unsafe { out_term.as_mut() }
.ok_or_else(|| libtss::TssError::InvalidConfig("out_term is null".into()))?;
*out_term = TssBuffer::empty();

let helper_ids: Vec<libtss::Identifier> = if helpers.is_null() || helpers_len == 0 {
Vec::new()
} else {
let raw = unsafe { core::slice::from_raw_parts(helpers, helpers_len) };
raw.iter()
.map(|&id| libtss::Identifier::new(id))
.collect::<Result<Vec<_>, _>>()?
};
let target = libtss::Identifier::new(target)?;
let sid = checked_optional_slice(session_id, session_id_len)?.ok_or_else(|| {
libtss::TssError::InvalidConfig("repair requires a session_id".into())
})?;

let handle = borrow_key_share(key_share)?;
let term = libtss::repair_helper_term(&handle, &helper_ids, target, sid)?;
*out_term = TssBuffer::from_vec(term.to_vec());
Ok(())
})
}

/// Assemble helpers' repair terms into a hollow DKLs23 key share for `target`.
///
/// `pubkey_package` is the serialized group package and `chain_code` its
/// 32-byte BIP-32 chain code, both as the helpers report them; `terms` holds
/// at least `min_signers` outputs of `tss_repair_helper_term`. The sum of the
/// terms is checked against the target's verifying share and a mismatch
/// returns `TSS_ERR_INVALID_SHARE` without creating a handle. The returned
/// share cannot sign until a `tss_refresh_new_dkls` refresh has run across the
/// whole group. Free it with `tss_handle_free`.
#[allow(clippy::too_many_arguments)]
#[no_mangle]
pub extern "C" fn tss_repair_assemble(
suite: u8,
pubkey_package: TssSlice,
chain_code: TssSlice,
target: u16,
terms: *const TssSlice,
terms_len: usize,
session_id: *const u8,
session_id_len: usize,
out_key_share: *mut TssHandle,
) -> TssStatus {
ffi_entry!({
let out_key_share = unsafe { out_key_share.as_mut() }
.ok_or_else(|| libtss::TssError::InvalidConfig("out_key_share is null".into()))?;
*out_key_share = 0;

let suite = libtss::Ciphersuite::try_from(suite)?;
let package = libtss::PublicKeyPackage::deserialize(checked_slice(pubkey_package)?)?;
let chain_code = checked_slice(chain_code)?;
let target = libtss::Identifier::new(target)?;
let term_slices: Vec<&[u8]> = if terms.is_null() || terms_len == 0 {
Vec::new()
} else {
let raw = unsafe { core::slice::from_raw_parts(terms, terms_len) };
raw.iter()
.map(|term| checked_slice(*term))
.collect::<Result<Vec<_>, _>>()?
};
let sid = checked_optional_slice(session_id, session_id_len)?.ok_or_else(|| {
libtss::TssError::InvalidConfig("repair requires a session_id".into())
})?;

let handle =
libtss::repair_assemble(suite, &package, chain_code, target, &term_slices, sid)?;
*out_key_share = handle.handle_id();
std::mem::forget(handle);
Ok(())
})
}
Loading
Loading