From 425dc5be241eff7fd7f838850d15d0382b743665 Mon Sep 17 00:00:00 2001 From: Saam Tehrani Date: Sat, 4 Jan 2025 06:48:21 +0000 Subject: [PATCH] add sign/verify, swap openssl use with bitcoin implementation --- apps/arweave/c_src/secp256k1/secp256k1_nif.c | 215 +++++++++++++++++-- apps/arweave/src/ar_wallet.erl | 6 +- apps/arweave/src/ec_secp256k1.erl | 95 ++++++-- apps/arweave/src/secp256k1_nif.erl | 8 +- 4 files changed, 280 insertions(+), 44 deletions(-) diff --git a/apps/arweave/c_src/secp256k1/secp256k1_nif.c b/apps/arweave/c_src/secp256k1/secp256k1_nif.c index f0fd0c550..123156ae3 100644 --- a/apps/arweave/c_src/secp256k1/secp256k1_nif.c +++ b/apps/arweave/c_src/secp256k1/secp256k1_nif.c @@ -4,49 +4,228 @@ #define SECP256K1_PUBKEY_UNCOMPRESSED_SIZE 65 #define SECP256K1_PUBKEY_COMPRESSED_SIZE 33 +#define SECP256K1_SIGNATURE_SIZE 64 #define SECP256K1_PRIVKEY_SIZE 32 #define SECP256K1_CONTEXT_SEED_SIZE 32 +#define SECP256K1_DIGEST_SIZE 32 static int secp256k1_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info) { return 0; } +#if defined(_MSC_VER) +// For SecureZeroMemory +#include +#endif +/* Cleanses memory to prevent leaking sensitive info. Won't be optimized out. */ +static void secure_erase(void *ptr, size_t len) { +#if defined(_MSC_VER) + /* SecureZeroMemory is guaranteed not to be optimized out by MSVC. */ + SecureZeroMemory(ptr, len); +#elif defined(__GNUC__) + /* We use a memory barrier that scares the compiler away from optimizing out the memset. + * + * Quoting Adam Langley in commit ad1907fe73334d6c696c8539646c21b11178f20f + * in BoringSSL (ISC License): + * As best as we can tell, this is sufficient to break any optimisations that + * might try to eliminate "superfluous" memsets. + * This method used in memzero_explicit() the Linux kernel, too. Its advantage is that it is + * pretty efficient, because the compiler can still implement the memset() efficiently, + * just not remove it entirely. See "Dead Store Elimination (Still) Considered Harmful" by + * Yang et al. (USENIX Security 2017) for more background. + */ + memset(ptr, 0, len); + __asm__ __volatile__("" : : "r"(ptr) : "memory"); +#else + void *(*volatile const volatile_memset)(void *, int, size_t) = memset; + volatile_memset(ptr, 0, len); +#endif +} + static ERL_NIF_TERM generate_key(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + char *error = NULL; unsigned char seed[SECP256K1_CONTEXT_SEED_SIZE]; - if (!RAND_bytes(seed, sizeof(seed))) { - return error_tuple(env, "Failed to generate random seed for context."); - } + unsigned char privbytes[SECP256K1_PRIVKEY_SIZE]; + unsigned char pubbytes[SECP256K1_PUBKEY_UNCOMPRESSED_SIZE]; + secp256k1_pubkey pubkey; secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + if (!RAND_priv_bytes(seed, sizeof(seed))) { + error = "Failed to generate random seed for context."; + goto cleanup; + } + if (!secp256k1_context_randomize(ctx, seed)) { - return error_tuple(env, "Failed to randomize context."); + error = "Failed to randomize context."; + goto cleanup; } - unsigned char privbytes[SECP256K1_PRIVKEY_SIZE]; - if (!RAND_bytes(privbytes, sizeof(privbytes))) { - return error_tuple(env, "Failed to generate random key."); + if (!RAND_priv_bytes(privbytes, sizeof(privbytes))) { + error = "Failed to generate random key."; + goto cleanup; } + if (!secp256k1_ec_seckey_verify(ctx, privbytes)) { - return error_tuple(env, "Generated secret secp256k1 key is invalid."); + error = "Generated secret secp256k1 key is invalid."; + goto cleanup; } - secp256k1_pubkey pubkey; if(!secp256k1_ec_pubkey_create(ctx, &pubkey, privbytes)) { - return error_tuple(env, "Failed to populate public key."); + error = "Failed to populate public key."; + goto cleanup; } - - unsigned char pubbytes[SECP256K1_PUBKEY_UNCOMPRESSED_SIZE]; - size_t l = sizeof(pubbytes); + size_t l = SECP256K1_PUBKEY_UNCOMPRESSED_SIZE; if (!secp256k1_ec_pubkey_serialize(ctx, pubbytes, &l, &pubkey, SECP256K1_EC_UNCOMPRESSED)) { - return error_tuple(env, "Failed to serialize public key."); + error = "Failed to serialize public key."; + goto cleanup; + } + + ERL_NIF_TERM privkey_term = make_output_binary(env, privbytes, SECP256K1_PRIVKEY_SIZE); + ERL_NIF_TERM pubkey_term = make_output_binary(env, pubbytes, SECP256K1_PUBKEY_UNCOMPRESSED_SIZE); + +cleanup: + secp256k1_context_destroy(ctx); + secure_erase(seed, sizeof(seed)); + secure_erase(privbytes, sizeof(privbytes)); + memset(pubbytes, 0, SECP256K1_PUBKEY_UNCOMPRESSED_SIZE); + + if (error) { + return error_tuple(env, error); + } + return ok_tuple2(env, privkey_term, pubkey_term); + +} + +static ERL_NIF_TERM sign(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + if (argc != 2) { + return enif_make_badarg(env); + } + ErlNifBinary Digest, PrivateBytes; + if (!enif_inspect_binary(env, argv[0], &Digest)) { + return enif_make_badarg(env); + } + if (Digest.size != SECP256K1_DIGEST_SIZE) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[1], &PrivateBytes)) { + return enif_make_badarg(env); + } + if (PrivateBytes.size != SECP256K1_PRIVKEY_SIZE) { + return enif_make_badarg(env); + } + + char *error = NULL; + unsigned char seed[SECP256K1_CONTEXT_SEED_SIZE]; + unsigned char digest[SECP256K1_DIGEST_SIZE]; + unsigned char privbytes[SECP256K1_PRIVKEY_SIZE]; + unsigned char signature[SECP256K1_SIGNATURE_SIZE]; + secp256k1_ecdsa_signature s; + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + memcpy(digest, Digest.data, SECP256K1_DIGEST_SIZE); + memcpy(privbytes, PrivateBytes.data, SECP256K1_PRIVKEY_SIZE); + + if (!RAND_priv_bytes(seed, sizeof(seed))) { + error = "Failed to generate random seed for context."; + goto cleanup; + } + + if (!secp256k1_context_randomize(ctx, seed)) { + error = "Failed to randomize context."; + goto cleanup; + } + + if(!secp256k1_ecdsa_sign(ctx, &s, digest, privbytes, NULL, NULL)) { + error = "Failed to create signature."; + goto cleanup; + } + + if(!secp256k1_ecdsa_signature_serialize_compact(ctx, signature, &s)) { + error = "Failed to serialize signature."; + goto cleanup; + } + + ERL_NIF_TERM signature_term = make_output_binary(env, signature, SECP256K1_SIGNATURE_SIZE); + +cleanup: + secp256k1_context_destroy(ctx); + secure_erase(seed, sizeof(seed)); + secure_erase(privbytes, sizeof(privbytes)); + memset(signature, 0, SECP256K1_SIGNATURE_SIZE); + + if (error) { + return error_tuple(env, error); + } + return ok_tuple(env, signature_term); +} + +static ERL_NIF_TERM verify(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + if (argc != 3) { + return enif_make_badarg(env); + } + ErlNifBinary Digest, Signature, PublicBytes; + if (!enif_inspect_binary(env, argv[0], &Digest)) { + return enif_make_badarg(env); + } + if (Digest.size != SECP256K1_DIGEST_SIZE) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[1], &Signature)) { + return enif_make_badarg(env); + } + if (Signature.size != SECP256K1_SIGNATURE_SIZE) { + return enif_make_badarg(env); + } + if (!enif_inspect_binary(env, argv[2], &PublicBytes)) { + return enif_make_badarg(env); + } + if (PublicBytes.size != SECP256K1_PUBKEY_COMPRESSED_SIZE && PublicBytes.size != SECP256K1_PUBKEY_UNCOMPRESSED_SIZE) { + return enif_make_badarg(env); + } + + char *error = NULL; + unsigned char digest[SECP256K1_DIGEST_SIZE]; + unsigned char signature[SECP256K1_SIGNATURE_SIZE]; + unsigned char pubbytes[PublicBytes.size]; + secp256k1_ecdsa_signature s; + secp256k1_pubkey pubkey; + + memcpy(digest, Digest.data, SECP256K1_DIGEST_SIZE); + memcpy(signature, Signature.data, SECP256K1_SIGNATURE_SIZE); + memcpy(pubbytes, PublicBytes.data, PublicBytes.size); + + if (!secp256k1_ecdsa_signature_parse_compact(secp256k1_context_static, &s, signature)) { + error = "Failed to deserialize/parse signature."; + goto cleanup; + } + + if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, pubbytes, PublicBytes.size)) { + error = "Failed to deserialize/parse public key."; + goto cleanup; } - ERL_NIF_TERM privkey_bin = make_output_binary(env, privbytes, SECP256K1_PRIVKEY_SIZE); - ERL_NIF_TERM pubkey_bin = make_output_binary(env, pubbytes, SECP256K1_PUBKEY_UNCOMPRESSED_SIZE); - return ok_tuple2(env, privkey_bin, pubkey_bin); + int is_valid = secp256k1_ecdsa_verify(secp256k1_context_static, &s, digest, &pubkey); + +cleanup: + memset(digest, 0, SECP256K1_DIGEST_SIZE); + memset(pubbytes, 0, PublicBytes.size); + memset(signature, 0, SECP256K1_SIGNATURE_SIZE); + + if (error) { + return error_tuple(env, error); + } + if (is_valid) { + return enif_make_atom(env, "true"); + } + return enif_make_atom(env, "false"); } static ErlNifFunc nif_funcs[] = { - {"generate_key", 0, generate_key} + {"generate_key", 0, generate_key}, + {"sign", 2, sign}, + {"verify", 3, verify}, }; ERL_NIF_INIT(secp256k1_nif, nif_funcs, secp256k1_load, NULL, NULL, NULL) diff --git a/apps/arweave/src/ar_wallet.erl b/apps/arweave/src/ar_wallet.erl index 7c910a405..18b57e8ea 100644 --- a/apps/arweave/src/ar_wallet.erl +++ b/apps/arweave/src/ar_wallet.erl @@ -180,7 +180,7 @@ sign({{KeyAlg, PublicExpnt}, Priv, Pub}, Data) } ); sign({_, #'ECPrivateKey'{} = PrivateKey, _}, Data)-> - ec_secp256k1:sign(Data, sha256, PrivateKey). + ec_secp256k1:sign(Data, PrivateKey). %% @doc Verify that a signature is correct. verify({_, Identifier}, Data, Sig) when is_binary(Identifier) -> @@ -191,11 +191,11 @@ verify(Identifier, Data, Sig) when is_binary(Identifier) -> rsa_pss:verify( Data, sha256, Sig, rsa_pss:from_identifier(Identifier)); {?ECDSA_SIGN_ALG, secp256k1} -> - ec_secp256k1:verify(Data, sha256, Sig, ec_secp256k1:from_identifier(Identifier)); + ec_secp256k1:verify(Data, Sig, ec_secp256k1:from_identifier(Identifier)); {invalid_prefix, _} -> invalid_identifier end; verify({_, {#'ECPoint'{}, {namedCurve, secp256k1}} = PublicKey}, Data, Sig) -> - ec_secp256k1:verify(Data, sha256, Sig, PublicKey). + ec_secp256k1:verify(Data, Sig, PublicKey). -spec from_identifier(Identifier :: binary()) -> public_key:rsa_public_key() | public_key:ecdsa_public_key(). from_identifier(Identifier) -> diff --git a/apps/arweave/src/ec_secp256k1.erl b/apps/arweave/src/ec_secp256k1.erl index d9ecdf368..8ef5bb61e 100644 --- a/apps/arweave/src/ec_secp256k1.erl +++ b/apps/arweave/src/ec_secp256k1.erl @@ -1,9 +1,9 @@ -module(ec_secp256k1). - +-include("ar.hrl"). -include_lib("public_key/include/public_key.hrl"). -include_lib("eunit/include/eunit.hrl"). --export([new/0, sign/3, verify/4, to_public/1, serialize/2, deserializePublic/2, deserializePrivate/2, identifier/1, from_identifier/1]). +-export([new/0, sign/2, verify/3, to_public/1, serialize/2, deserializePublic/2, deserializePrivate/2, identifier/1, from_identifier/1]). -type ecdsa_digest_type() :: sha256. -type serialization_formats() :: raw | jwk. @@ -16,7 +16,19 @@ -spec new() -> PrivateKey :: public_key:ecdsa_private_key(). new() -> - public_key:generate_key({namedCurve, secp256k1}). + case secp256k1_nif:generate_key() of + {error, Reason} -> + ?LOG_ERROR([{event, secp256k1_generate_key}, {reason, Reason}]), + erlang:halt(); + {ok, PrivBytes, PubBytes} -> + #'ECPrivateKey'{ + version=1, + privateKey=PrivBytes, + parameters={namedCurve, secp256k1}, + publicKey=PubBytes, + attributes=asn1_NOVALUE + } + end. -spec to_public(PrivateKey :: public_key:ecdsa_private_key()) -> public_key:ecdsa_public_key(). @@ -24,31 +36,48 @@ to_public({_, _, _, _, PubBytes, _}) -> ECPoint = #'ECPoint'{point=PubBytes}, {ECPoint, {namedCurve, secp256k1}}. --spec sign(DigestOrPlainText :: binary() | {digest, binary()}, DigestType :: ecdsa_digest_type(), PrivateKey :: public_key:ecdsa_private_key()) -> binary(). -sign(DigestOrPlainText, DigestType, {_, _, PrivBytes, _, _, _}) -> - DERSignature = crypto:sign(ecdsa, DigestType, DigestOrPlainText, [PrivBytes, secp256k1]), - case check_low_s(DERSignature) of - {valid, _, _} -> DERSignature; - {invalid, R, S} -> public_key:der_encode('ECDSA-Sig-Value', #'ECDSA-Sig-Value'{ r = R, s = ?SigDiv - S}) - end. +-spec sign(DigestOrPlainText :: binary() | {digest, binary()}, PrivateKey :: public_key:ecdsa_private_key()) -> binary(). +sign({digest, Digest}, {_, _, PrivBytes, _, _, _}) -> + {ok, Signature} = secp256k1_nif:sign(Digest, PrivBytes), + case check_low_s(Signature) of + {valid, _, _} -> Signature; + {invalid, R, S} -> + RBin = int_to_bin(R), + SBin = int_to_bin(?SigDiv - S), + <> + end; +sign(PlainText, PrivateKey) -> + Digest = crypto:hash(sha256, PlainText), + sign({digest, Digest}, PrivateKey). --spec verify(Message :: binary() | {digest, binary()}, DigestType :: ecdsa_digest_type(), Signature :: binary(), PublicKey :: public_key:ecdsa_public_key()) -> boolean(). -verify(DigestOrPlainText, DigestType, Signature, {#'ECPoint'{point=PubBytes}, {namedCurve, secp256k1}}) when byte_size(Signature) == 64 -> +-spec verify(DigestOrPlainText :: binary() | {digest, binary()}, Signature :: binary(), PublicKey :: public_key:ecdsa_public_key()) -> boolean(). +verify({digest, Digest}, Signature, {#'ECPoint'{point=PubBytes}, {namedCurve, secp256k1}}) when byte_size(Signature) == 64 -> case check_low_s(Signature) of - {valid, R, S} -> - DERSignature = public_key:der_encode('ECDSA-Sig-Value', #'ECDSA-Sig-Value'{ r = R, s = S}), - crypto:verify(ecdsa, DigestType, DigestOrPlainText, DERSignature, [PubBytes, secp256k1]); - {invalid, _, _} -> - false - end; -verify(DigestOrPlainText, DigestType, DERSignature, {#'ECPoint'{point=PubBytes}, {namedCurve, secp256k1}}) -> - case check_low_s(DERSignature) of {valid, _, _} -> - crypto:verify(ecdsa, DigestType, DigestOrPlainText, DERSignature, [PubBytes, secp256k1]); + secp256k1_nif:verify(Digest, Signature, PubBytes); {invalid, _, _} -> false - end. + end; +verify(PlainText, Signature, {#'ECPoint'{}, {namedCurve, secp256k1}} = PublicKey) when byte_size(Signature) == 64 -> + Digest = crypto:hash(sha256, PlainText), + verify({digest, Digest}, Signature, PublicKey); +verify({digest, Digest}, DERSignature, {#'ECPoint'{point=PubBytes}, {namedCurve, secp256k1}}) -> + case catch public_key:der_decode('ECDSA-Sig-Value', DERSignature) of + {'EXIT', _} -> false; + #'ECDSA-Sig-Value'{ r = R, s = S } -> + case check_low_s(R, S) of + {valid, R, S} -> + RBin = int_to_bin(R), + SBin = int_to_bin(S), + Sig = <>, + secp256k1_nif:verify(Digest, Sig, PubBytes); + {invalid, _, _} -> false + end + end; +verify(PlainText, DERSignature, {#'ECPoint'{}, {namedCurve, secp256k1}} = PublicKey) -> + Digest = crypto:hash(sha256, PlainText), + verify({digest, Digest}, DERSignature, PublicKey). -spec serialize(Format :: serialization_formats(), PublicKey :: public_key:ecdsa_public_key() | public_key:ecdsa_private_key()) -> binary(). @@ -151,6 +180,20 @@ check_low_s(R, S) -> {invalid, R, S} end. +%% @private +int_to_bin(X) when X < 0 -> int_to_bin_neg(X, []); +int_to_bin(X) -> int_to_bin_pos(X, []). + +int_to_bin_pos(0,Ds=[_|_]) -> + list_to_binary(Ds); +int_to_bin_pos(X,Ds) -> + int_to_bin_pos(X bsr 8, [(X band 255)|Ds]). + +int_to_bin_neg(-1, Ds=[MSB|_]) when MSB >= 16#80 -> + list_to_binary(Ds); +int_to_bin_neg(X,Ds) -> + int_to_bin_neg(X bsr 8, [(X band 255)|Ds]). + %% @doc Ensure that parsing of core command line options functions correctly. de_serialization_test() -> SK = new(), @@ -163,3 +206,11 @@ de_serialization_test() -> ?assertEqual(serialize(raw, deserializePrivate(jwk, SKJWK)), SKRaw), PKJWK = serialize(jwk, PK), ?assertEqual(serialize(raw, deserializePublic(jwk, PKJWK)), PKRaw). + +sign_verify_test() -> + SK = new(), + PK = to_public(SK), + Msg = <<"This is a test message!">>, + Sig = sign(Msg, SK), + ?assertEqual(byte_size(Sig), 64), + ?assert(verify(Msg, Sig, PK)). diff --git a/apps/arweave/src/secp256k1_nif.erl b/apps/arweave/src/secp256k1_nif.erl index 98c07aa05..5cbdc0fb5 100644 --- a/apps/arweave/src/secp256k1_nif.erl +++ b/apps/arweave/src/secp256k1_nif.erl @@ -1,5 +1,5 @@ -module(secp256k1_nif). --export([generate_key/0]). +-export([generate_key/0, sign/2, verify/3]). -on_load(init/0). @@ -9,3 +9,9 @@ init() -> generate_key() -> erlang:nif_error(not_loaded). + +sign(_Digest, _PrivateBytes) -> + erlang:nif_error(nif_not_loaded). + +verify(_Digest, _Signature, _PublicBytes) -> + erlang:nif_error(nif_not_loaded).