Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
996a2cf
AWS_KMS: Added aws-sdk vcpkg.
brianjohnson5972 May 8, 2026
cea6a81
AWS_KMS: updated the baseline.
brianjohnson5972 May 8, 2026
3588d04
AWS_KMS: kms signature
brianjohnson5972 May 12, 2026
c210bfc
AWS_KMS: documentation update
brianjohnson5972 May 14, 2026
2f3df3e
AWS_KMS: New baseline from vcpkg and documentation
brianjohnson5972 May 14, 2026
0231753
AWS_KMS: Updated version
brianjohnson5972 May 14, 2026
ec7cc07
AWS_KMS: Removed accidental file add
brianjohnson5972 May 15, 2026
adf55fc
AWS_KMS: added standalone test readme
brianjohnson5972 May 16, 2026
f6624ac
AWS_KMS: added to standalone test readme
brianjohnson5972 May 16, 2026
38441cf
AWS_KMS: Fixed comments and removed design doc for PR
brianjohnson5972 May 20, 2026
57b3021
AWS_KMS: Added error support for non-AWS kms keys
brianjohnson5972 May 20, 2026
ca6d142
AWS_KMS: No `GetPublicKey` pinning
brianjohnson5972 May 20, 2026
c3822f5
AWS_KMS: startup credential check
brianjohnson5972 May 20, 2026
8e292e4
AWS_KMS: Comment cleanup
brianjohnson5972 May 20, 2026
f3c4bb9
AWS_KMS: Better error reporting for transient aws-kms
brianjohnson5972 May 20, 2026
5b8f0ed
AWS_KMS: eth_client_signer integration gap fixed
brianjohnson5972 May 20, 2026
fbcf59d
AWS_KMS: replaced includes with forward declarations
brianjohnson5972 May 20, 2026
84263fd
Merge remote-tracking branch 'origin/master' into feature/aws-kms-sig…
brianjohnson5972 May 21, 2026
8b42c61
AWS_KMS: Updated baseline and reference for vcpkg
brianjohnson5972 May 21, 2026
af87e68
AWS_KMS: Fixed claude identified issues
brianjohnson5972 May 21, 2026
0ea9446
AWS_KMS: Fixed KMS ARNs
brianjohnson5972 May 22, 2026
2e43baf
AWS_KMS: KMS store after provider
brianjohnson5972 May 22, 2026
1161e4b
AWS_KMS: Fix signing interface for sha256 and keccak256
brianjohnson5972 May 22, 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
2 changes: 2 additions & 0 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Elected via Appointed Proof of Stake (APoS). They validate transactions and prod
- Building and submitting outbound envelopes to external chains
- Responding to challenges if disputed

Signing keys for outbound submissions can be held locally (`KEY:`), in a `kiod` daemon (`KIOD:`), or in AWS KMS (`KMS:` — secp256k1 asymmetric keys). See `plugins/signature_provider_manager_plugin/test/README.md` for KMS key setup, IAM requirements, the `KMS:` spec format, and operational notes.

### Underwriters
Independent operators running the `underwriter_plugin`. They provide collateral-backed liquidity for cross-chain swaps:
1. Monitor `sysio.msgch` for `SWAP` attestations
Expand Down
2 changes: 2 additions & 0 deletions libraries/chain/include/sysio/chain/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ namespace sysio { namespace chain {
3110006, "Incorrect plugin configuration" )
FC_DECLARE_DERIVED_EXCEPTION( missing_trace_api_plugin_exception, plugin_exception,
3110007, "Missing Trace API Plugin" )
FC_DECLARE_DERIVED_EXCEPTION( signing_transient_exception, plugin_exception,
3110008, "Transient signing-provider failure; the operation should be retried" )


FC_DECLARE_DERIVED_EXCEPTION( wallet_exception, chain_exception,
Expand Down
33 changes: 31 additions & 2 deletions libraries/libfc/include/fc/crypto/signature_provider.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
#pragma once
#include <fc/crypto/private_key.hpp>
#include <fc/crypto/public_key.hpp>
#include <fc/crypto/sha256.hpp>
#include <fc/crypto/keccak256.hpp>
#include <fc/exception/exception.hpp>

#include <span>

namespace fc::crypto {
using signature_provider_id_t = std::variant<std::string, fc::crypto::public_key>;

/// Wire default signing function (sha256 digest)
using sign_fn = std::function<fc::crypto::signature(const sha256&)>;
/// A 32-byte digest tagged with the hash algorithm that produced it. A signing
/// closure receives one of these and can dispatch on the active alternative --
/// e.g. an Ethereum remote signer expects `keccak256`, block signing `sha256`.
using hash256 = std::variant<sha256, keccak256>;

/// Signing closure stored on every `signature_provider_t`. Takes the digest by
/// value: a `hash256` is two 32-byte hashes in a variant -- trivially cheap to
/// copy -- and the closure owns its copy.
using sign_fn = std::function<fc::crypto::signature(hash256)>;

/// The raw 32 digest bytes of whichever hash `h` carries. For closures that
/// sign an opaque 32-byte digest (AWS KMS, kiod) regardless of hash algorithm.
inline std::span<const uint8_t, 32> digest_span(const hash256& h) {
return std::visit([](const auto& d) -> std::span<const uint8_t, 32> {
return d.to_uint8_span();
}, h);
}

/// The `sha256` alternative of `h`. Throws if `h` carries a `keccak256`. For
/// closures that feed a SHA-256-only API (`private_key::sign`) and are only
/// ever handed a SHA-256 digest -- the assertion catches a wrong-hash bug.
inline const sha256& as_sha256(const hash256& h) {
const sha256* s = std::get_if<sha256>(&h);
FC_ASSERT(s, "expected a sha256 digest, but the hash256 carries a keccak256");
return *s;
}

/**
* `signature_provider_entry` constructed provider
Expand Down
97 changes: 81 additions & 16 deletions libraries/libfc/include/fc/crypto/signer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,70 @@
#include <fc/crypto/keccak256.hpp>
#include <fc/crypto/sha256.hpp>

#include <magic_enum/magic_enum.hpp>

#include <span>

namespace fc::crypto {

// ===========================================================================
// signer_traits compile-time signing dispatch per (TargetChain, KeyType)
// signer_traits -- compile-time signing dispatch per (TargetChain, KeyType)
// ===========================================================================

/// Primary template must be specialized for each valid (TargetChain, KeyType) pair
/// Primary template -- must be specialized for each valid (TargetChain, KeyType) pair
template<chain_kind_t TargetChain, chain_key_type_t KeyType>
struct signer_traits;

namespace detail {

/// Sign a 32-byte keccak digest with an ethereum (secp256k1) key, transparently
/// supporting both `signature_provider_t` shapes:
///
/// - A provider with a local `em` private key signs in-process.
/// - A provider with no local key -- a remote signer such as AWS KMS -- has no
/// key to sign with directly, so the digest is handed to its `sign` closure
/// as a `hash256` carrying the `keccak256` alternative; the closure raw-signs
/// those 32 bytes.
///
/// Digest preparation (plain keccak vs. EIP-191 framing) is the caller's job --
/// it has already happened in `signer_traits::prepare` -- so the remote signer
/// must raw-sign the digest as given. The remote path self-verifies this:
/// it recovers the public key from the returned signature and asserts it
/// matches the provider's key. A remote signer that applied its own message
/// framing instead of raw-signing, or that signed with the wrong key, recovers
/// to a different key and is rejected here -- before an invalid transaction can
/// be emitted -- rather than yielding a silently bad signature.
inline signature em_sign_keccak(const signature_provider_t& p, const keccak256& digest) {
if (p.private_key) {
FC_ASSERT(p.private_key->contains<em::private_key_shim>(),
"ETH signing requires an EM private key");
const auto& em_key = p.private_key->get<em::private_key_shim>();
return signature(signature::storage_type(em_key.sign_keccak256(digest)));
}

FC_ASSERT(static_cast<bool>(p.sign),
"signature provider has neither a local private key nor a sign function");

// Hand the keccak digest to the remote signer's closure, correctly typed as
// the keccak256 alternative of hash256 -- no byte reinterpretation.
const signature sig = p.sign(digest);

// Self-verify: the remote signer must have raw-signed `digest` with the
// provider's key. Recover and compare; reject anything else loudly.
FC_ASSERT(sig.contains<em::signature_shim>(),
"remote signer returned a non-Ethereum signature");
const auto recovered = public_key(public_key::storage_type(
sig.get<em::signature_shim>().recover_eth(digest)));
FC_ASSERT(recovered == p.public_key,
"remote signer produced a signature that does not recover to the "
"provider's public key -- the signer's key or digest framing is wrong");
return sig;
}

} // namespace detail

// ---------------------------------------------------------------------------
// (ethereum, ethereum) ETH client transaction signing
// (ethereum, ethereum) -- ETH client transaction signing
// Signs: keccak256(raw_bytes) via EM (secp256k1)
// ---------------------------------------------------------------------------
template<>
Expand All @@ -34,10 +84,8 @@ struct signer_traits<chain_kind_ethereum, chain_key_type_ethereum> {
}

static signature do_sign(const signature_provider_t& p, const prepared_type& digest) {
FC_ASSERT(p.private_key, "ETH signing requires a local private key");
FC_ASSERT(p.private_key->contains<em::private_key_shim>(), "ETH signing requires an EM private key");
auto& em_key = p.private_key->get<em::private_key_shim>();
return signature(signature::storage_type(em_key.sign_keccak256(digest)));
// Sign with a local em key, or delegate to a remote signer's closure.
return detail::em_sign_keccak(p, digest);
}

static public_key do_recover(const signature& sig, const prepared_type& digest) {
Expand All @@ -48,7 +96,7 @@ struct signer_traits<chain_kind_ethereum, chain_key_type_ethereum> {
};

// ---------------------------------------------------------------------------
// (wire, ethereum) Wire transactions signed via MetaMask / personal_sign
// (wire, ethereum) -- Wire transactions signed via MetaMask / personal_sign
// Signs: keccak256(EIP-191 prefix + sha256_raw) via EM (secp256k1)
// ---------------------------------------------------------------------------
template<>
Expand All @@ -62,10 +110,8 @@ struct signer_traits<chain_kind_wire, chain_key_type_ethereum> {
}

static signature do_sign(const signature_provider_t& p, const prepared_type& digest) {
FC_ASSERT(p.private_key, "ETH signing requires a local private key");
FC_ASSERT(p.private_key->contains<em::private_key_shim>(), "ETH signing requires an EM private key");
auto& em_key = p.private_key->get<em::private_key_shim>();
return signature(signature::storage_type(em_key.sign_keccak256(digest)));
// Sign with a local em key, or delegate to a remote signer's closure.
return detail::em_sign_keccak(p, digest);
}

static public_key do_recover(const signature& sig, const prepared_type& digest) {
Expand All @@ -76,7 +122,7 @@ struct signer_traits<chain_kind_wire, chain_key_type_ethereum> {
};

// ---------------------------------------------------------------------------
// (solana, solana) Solana client transaction signing
// (solana, solana) -- Solana client transaction signing
// Signs: raw_bytes via ED25519
// ---------------------------------------------------------------------------
template<>
Expand Down Expand Up @@ -106,7 +152,7 @@ struct signer_traits<chain_kind_solana, chain_key_type_solana> {
};

// ===========================================================================
// signer<TargetChain, KeyType> typed cross-chain signing wrapper
// signer<TargetChain, KeyType> -- typed cross-chain signing wrapper
// ===========================================================================

template<chain_kind_t TargetChain, chain_key_type_t KeyType>
Expand All @@ -118,7 +164,7 @@ struct signer {
explicit signer(const signature_provider_t& p) : provider(p) {
FC_ASSERT(p.key_type == KeyType,
"signer: provider key_type mismatch (expected {}, got {})",
static_cast<int>(KeyType), static_cast<int>(p.key_type));
magic_enum::enum_name(KeyType), magic_enum::enum_name(p.key_type));
}

signature sign(typename traits::input_type data) const {
Expand All @@ -144,10 +190,29 @@ struct signer {
};

// ===========================================================================
// wire_signer key-type agnostic Wire transaction signing
// wire_signer -- key-type agnostic Wire transaction signing
// Passes through to provider.sign(sha256); handles K1/EM/ED polymorphically
// ===========================================================================

/**
* @brief Key-type-agnostic Wire transaction signer.
*
* Hands the 32-byte digest straight to `provider.sign(...)`, which dispatches
* polymorphically over K1 / EM / ED keys inside the provider's closure.
*
* Unlike `signer<>` (and its `wire_eth_signer` alias), `wire_signer` has no
* `prepare()` hook: it raw-signs exactly the 32 bytes it is given and applies
* no EIP-191 framing. For an Ethereum key that framing
* (`ethereum::hash_user_message`) lives in
* `signer_traits<chain_kind_wire, chain_key_type_ethereum>::prepare`, not in
* the signature provider -- a `KEY:` provider's `em::private_key_shim` and a
* `KMS:` provider alike raw-sign the digest as handed in. Consequently
* `wire_signer` and `wire_eth_signer` are NOT interchangeable even for an
* identical provider: swapping one for the other changes which bytes are
* signed. Use `wire_eth_signer` when the Wire transaction must carry a
* MetaMask-compatible `personal_sign` signature; use `wire_signer` only when
* the caller has already produced the exact digest to be signed.
*/
struct wire_signer {
const signature_provider_t& provider;

Expand Down
8 changes: 7 additions & 1 deletion libraries/libfc/src/network/ethereum/ethereum_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ fc::variant ethereum_client::execute_contract_tx_fn(const eip1559_tx& source_tx,
auto& tx_sig_data = tx_sig.get<fc::em::signature_shim>().serialize();
std::copy_n(tx_sig_data.begin(), 32, tx.r.begin());
std::copy_n(tx_sig_data.begin() + 32, 32, tx.s.begin());
tx.v = tx_sig_data[64] - 27; // recovery id
// Byte 64 of the recoverable signature is the Ethereum `v`, encoded
// pre-EIP-155 as `27 + recovery_id` (Yellow Paper Appendix F) by every
// signing path -- local `em` keys and the AWS KMS signer alike (cf.
// `eth_v_offset` in kms_signature_provider.cpp). EIP-1559 typed
// transactions carry the bare recovery id, so strip the 27 offset here;
// this is the exact inverse of the packing done at signing time.
tx.v = tx_sig_data[64] - 27; // recovery id (pre-EIP-155 27 offset removed)
tx_encoded = rlp::encode_eip1559_signed_typed(tx);
}

Expand Down
106 changes: 105 additions & 1 deletion libraries/libfc/test/crypto/test_cypher_suites.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ signature_provider_t make_provider(const private_key& key, chain_key_type_t key_
p.key_type = key_type;
p.public_key = key.get_public_key();
p.private_key = key;
p.sign = [key](const sha256& d) { return key.sign(d); };
p.sign = [key](hash256 d) { return key.sign(as_sha256(d)); };
return p;
}

Expand Down Expand Up @@ -645,4 +645,108 @@ BOOST_AUTO_TEST_CASE(test_signer_rejects_wrong_key_type) try {
BOOST_CHECK_THROW(eth_client_signer{provider}, fc::exception);
} FC_LOG_AND_RETHROW();

// ===========================================================================
// Remote-signer support — eth_client_signer / wire_eth_signer driving a
// signature_provider_t that has no local private key (e.g. an AWS KMS key),
// where the `sign` closure is the signer.
// ===========================================================================

BOOST_AUTO_TEST_CASE(test_eth_client_signer_signs_via_closure_without_private_key) try {
// A provider with no local private key — its `sign` closure does the
// signing, mirroring an AWS KMS-backed provider.
auto key = private_key::generate(private_key::key_type::em);
auto pub = key.get_public_key();
auto em_key = key.get<em::private_key_shim>();

auto payload = ethereum::to_uint8_span("eth transaction bytes");
auto kc = keccak256::hash(payload);

signature_provider_t remote;
remote.key_type = chain_key_type_ethereum;
remote.public_key = pub;
// remote.private_key intentionally left empty.
remote.sign = [em_key, kc](hash256 digest) {
// A KMS-style raw signer: a recoverable signature over the 32-byte
// digest it is handed. The eth signing path now delivers a keccak256-
// tagged hash256 -- verify the typed argument arrives correctly.
BOOST_REQUIRE(std::holds_alternative<keccak256>(digest));
BOOST_CHECK(std::get<keccak256>(digest) == kc);
return signature(signature::storage_type(em_key.sign_keccak256(kc)));
};
BOOST_REQUIRE(!remote.private_key.has_value());

eth_client_signer s(remote);
auto sig = s.sign(payload);
auto recovered = s.recover(sig, payload);
BOOST_CHECK_EQUAL(recovered.to_string({}), pub.to_string({}));
} FC_LOG_AND_RETHROW();

BOOST_AUTO_TEST_CASE(test_eth_client_signer_closure_matches_local_key) try {
// The remote-signer path must produce the identical signature the local-key
// path produces — libsecp256k1 ECDSA is RFC-6979 deterministic.
auto key = private_key::generate(private_key::key_type::em);
auto em_key = key.get<em::private_key_shim>();

auto payload = ethereum::to_uint8_span("eth transaction bytes");
auto kc = keccak256::hash(payload);

auto local = make_provider(key, chain_key_type_ethereum); // has private_key

signature_provider_t remote;
remote.key_type = chain_key_type_ethereum;
remote.public_key = key.get_public_key();
remote.sign = [em_key, kc](hash256) {
return signature(signature::storage_type(em_key.sign_keccak256(kc)));
};

auto local_sig = eth_client_signer(local).sign(payload);
auto remote_sig = eth_client_signer(remote).sign(payload);
BOOST_CHECK_EQUAL(local_sig.to_string({}), remote_sig.to_string({}));
} FC_LOG_AND_RETHROW();

BOOST_AUTO_TEST_CASE(test_eth_client_signer_rejects_remote_signature_for_wrong_key) try {
// A remote signer whose closure signs with a different key than the provider
// advertises must be rejected by the self-verifying recover check, rather
// than emitting a transaction with an invalid signature.
auto key_a = private_key::generate(private_key::key_type::em);
auto key_b = private_key::generate(private_key::key_type::em);
auto em_b = key_b.get<em::private_key_shim>();

auto payload = ethereum::to_uint8_span("eth transaction bytes");
auto kc = keccak256::hash(payload);

signature_provider_t bad;
bad.key_type = chain_key_type_ethereum;
bad.public_key = key_a.get_public_key(); // provider advertises key A
bad.sign = [em_b, kc](hash256) { // but the closure signs with key B
return signature(signature::storage_type(em_b.sign_keccak256(kc)));
};

eth_client_signer s(bad);
BOOST_CHECK_THROW(s.sign(payload), fc::exception);
} FC_LOG_AND_RETHROW();

BOOST_AUTO_TEST_CASE(test_wire_eth_signer_signs_via_closure_without_private_key) try {
// wire_eth_signer shares the same remote-signer path; its EIP-191 framing is
// applied in `prepare`, so the closure still just raw-signs the digest.
auto key = private_key::generate(private_key::key_type::em);
auto pub = key.get_public_key();
auto em_key = key.get<em::private_key_shim>();

auto digest = sha256::hash("wire transaction digest");
auto prepared = ethereum::hash_user_message(digest.to_uint8_span());

signature_provider_t remote;
remote.key_type = chain_key_type_ethereum;
remote.public_key = pub;
remote.sign = [em_key, prepared](hash256) {
return signature(signature::storage_type(em_key.sign_keccak256(prepared)));
};

wire_eth_signer s(remote);
auto sig = s.sign(digest);
auto recovered = s.recover(sig, digest);
BOOST_CHECK_EQUAL(recovered.to_string({}), pub.to_string({}));
} FC_LOG_AND_RETHROW();

BOOST_AUTO_TEST_SUITE_END()
6 changes: 4 additions & 2 deletions libraries/testing/include/sysio/testing/bls_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ inline std::pair<fc::crypto::bls::private_key, fc::crypto::signature_provider_pt
key_name,
fc::crypto::public_key{fc::crypto::bls::public_key_shim{privkey.get_public_key().serialize()}},
fc::crypto::private_key{fc::crypto::bls::private_key_shim{privkey.get_secret()}},
[privkey](const fc::sha256& digest) {
return fc::crypto::signature{bls::signature_shim{privkey.sign_sha256(digest).serialize()}};
[privkey](fc::crypto::hash256 digest) {
// BLS block-signing key: always signs a SHA-256 digest.
return fc::crypto::signature{bls::signature_shim{
privkey.sign_sha256(fc::crypto::as_sha256(digest)).serialize()}};
}
));
}
Expand Down
4 changes: 3 additions & 1 deletion plugins/producer_plugin/src/producer_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,9 @@ class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin

auto sig_provider = _signature_providers.at(key);

return sig_provider->sign(digest);
// Block signing uses a SHA-256 digest; wrap it as the sha256
// alternative of the provider closure's hash256 argument.
return sig_provider->sign(fc::crypto::hash256{digest});
} else {
return chain::signature_type();
}
Expand Down
Loading