Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ECDSA over secp256k1 signatures and verification #490

Merged
merged 52 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f43a97c
[zoo] add generator for secp256k1
Vindaar Dec 8, 2024
6434eba
[ECDSA] add initial ECDSA signing / verifying implementation
Vindaar Dec 8, 2024
e8540b6
[ecdsa] fix imports
Vindaar Dec 12, 2024
41d502d
[ecdsa] export Secp256k1 as `C` for convenience
Vindaar Dec 12, 2024
2760af4
[ecdsa] export `toDER` proc
Vindaar Dec 12, 2024
9f31c38
[ecdsa] use `isZero` instead of old zero comparison
Vindaar Dec 12, 2024
5862d43
[ecdsa] rename private key generator & add private -> public key
Vindaar Dec 12, 2024
4fd34e2
[tests] add test cases for ECDSA signature verification
Vindaar Dec 12, 2024
a4ca057
[ecdsa] handle some `.noinit.` cases
Vindaar Dec 21, 2024
22bd57b
[ecdsa] turn `toBytes`, `arrayWith` into in-place procedures
Vindaar Dec 21, 2024
4e17514
[ecdsa] clean up comment about Fp -> Fr conversion
Vindaar Dec 21, 2024
46d8f92
[ecdsa] replace toPemPrivateKey/PublicKey by in-place array variants
Vindaar Dec 21, 2024
7164f07
[ecdsa] replace `toDER` by non allocating variant
Vindaar Dec 21, 2024
890b185
[ecdsa] replace out-of-place arithmetic by in-place
Vindaar Dec 23, 2024
50de116
[ecdsa] move ECDSA implementation to ~signatures~ directory
Vindaar Dec 23, 2024
931044d
[ecdsa] remove dependence on explicit SHA256 hash function
Vindaar Dec 24, 2024
fe8a8aa
[ecdsa] make DERSignature generic under curve by having static size
Vindaar Dec 24, 2024
bec1536
[ecdsa] turn more procs generic over curve and hash function
Vindaar Dec 24, 2024
34442fa
[ecdsa] replace sign/verify API by one matching BLS signatures
Vindaar Dec 24, 2024
06f7a5f
[ecdsa] remove global curve & generator constants
Vindaar Dec 24, 2024
ad16403
[ecdsa] correctly handle truncation of digests > Fr BigInts
Vindaar Dec 24, 2024
e9174e6
create file for common signature ops, `derivePubkey` for ECDSA & BLS
Vindaar Dec 24, 2024
4d72a6b
create file specifically for ECDSA over secp256k1
Vindaar Dec 24, 2024
a8ecd59
[ecdsa] add `fromDER` to split DER encoded signature back into r, s a…
Vindaar Dec 26, 2024
828189c
[tests] add OpenSSL wrapper intended for test cases
Vindaar Dec 26, 2024
722fa37
[tests] first step towards OpenSSL tests
Vindaar Dec 26, 2024
64130a1
[tests] fully avoid JSON intermediary files for ECDSA tests
Vindaar Dec 27, 2024
e9387e8
[tests] rename file back to test case name, add DERSigSize tests
Vindaar Dec 27, 2024
0d24f6a
[tests] also test our DER encoder
Vindaar Dec 27, 2024
5229551
[tests] extend OpenSSL wrapper for required functionality
Vindaar Dec 28, 2024
1d05da4
[tests] move openssl wrapper to root of tests to share between tests
Vindaar Dec 28, 2024
c0b3806
[tests] add test case to verify PEM file writer
Vindaar Dec 28, 2024
87bc887
[ecdsa] clean up and fix PEM file writers
Vindaar Dec 28, 2024
8000567
[tests] [bench] use shared OpenSSL wrapper where appropriate
Vindaar Dec 28, 2024
04ce1c8
[codecs] move serialization logic to ecdsa secp256k1 submodule
Vindaar Dec 28, 2024
0c5195f
[codecs] move DER signature serialization to codecs_ecdsa submodule
Vindaar Dec 28, 2024
89688ba
[ecdsa] adjust ECDSA secp256k1 API & test cases
Vindaar Dec 28, 2024
5011fe3
[ecdsa] add mini docstring for `verify`
Vindaar Dec 28, 2024
fa5a5eb
[codecs] clean up imports in `codecs_ecdsa.nim`
Vindaar Dec 28, 2024
2f6a897
[ecdsa] clean up imports of `ecdsa_secp256k1.nim`
Vindaar Dec 28, 2024
2693aec
[ecdsa] do not export `raw` field in ecdsa_secp256k1
Vindaar Dec 30, 2024
1b44a8f
[CI] fix CI failures by including OpenSSL wrapper instead of import
Vindaar Dec 30, 2024
c2c39af
[bench] disable OpenSSL bench for sha256 on windows
Vindaar Dec 31, 2024
fa9e0ab
[nimble] add ECDSA signature test to nimble task
Vindaar Dec 31, 2024
4cac2ec
[ecdsa] replace brainfart using pointer size for bits in byte
Vindaar Dec 31, 2024
30285ff
[ecdsa] fix final related brainfart :)
Vindaar Dec 31, 2024
901597e
[tests] when the brainfart infects the test cases too! 🤯
Vindaar Dec 31, 2024
6c09fe1
replace DERSig* by DerSig*
Vindaar Jan 1, 2025
0bbc839
replace `toPemFile` by simply `toPem`
Vindaar Jan 1, 2025
20057e0
rename `common_signature_ops` to `ecc_sig_ops`
Vindaar Jan 1, 2025
9642ca6
[tests] disable ECDSA test for Windows
Vindaar Jan 1, 2025
2fd2bb2
[ecdsa] avoid awkward arrayWith declaration & call
Vindaar Jan 4, 2025
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
45 changes: 11 additions & 34 deletions benchmarks/bench_h_sha256.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,16 @@ import
helpers/prng_unsafe,
./bench_blueprint

proc separator*() = separator(69)

# Deal with platform mess
# --------------------------------------------------------------------
when defined(windows):
when sizeof(int) == 8:
const DLLSSLName* = "(libssl-1_1-x64|ssleay64|libssl64).dll"
else:
const DLLSSLName* = "(libssl-1_1|ssleay32|libssl32).dll"
else:
when defined(macosx) or defined(macos) or defined(ios):
const versions = "(.1.1|.38|.39|.41|.43|.44|.45|.46|.47|.48|.10|.1.0.2|.1.0.1|.1.0.0|.0.9.9|.0.9.8|)"
else:
const versions = "(.1.1|.1.0.2|.1.0.1|.1.0.0|.0.9.9|.0.9.8|.48|.47|.46|.45|.44|.43|.41|.39|.38|.10|)"
## NOTE: For a reason that evades me at the moment, if we only `import`
## the wrapper, we get a linker error of the form:
##
## @mopenssl_wrapper.nim.c:(.text+0x110): undefined reference to `Dl_1073742356_'
## /usr/bin/ld: warning: creating DT_TEXTREL in a PIE
##
## So for the moment, we just include the wrapper.
include ../tests/openssl_wrapper

when defined(macosx) or defined(macos) or defined(ios):
const DLLSSLName* = "libssl" & versions & ".dylib"
elif defined(genode):
const DLLSSLName* = "libssl.lib.so"
else:
const DLLSSLName* = "libssl.so" & versions

# OpenSSL wrapper
# --------------------------------------------------------------------

proc SHA256[T: byte|char](
msg: openarray[T],
digest: ptr array[32, byte] = nil
): ptr array[32, byte] {.noconv, dynlib: DLLSSLName, importc.}

proc SHA256_OpenSSL[T: byte|char](
digest: var array[32, byte],
s: openarray[T]) =
discard SHA256(s, digest.addr)
proc separator*() = separator(69)

# --------------------------------------------------------------------

Expand Down Expand Up @@ -80,6 +56,7 @@ when isMainModule:
let msg = rng.random_byte_seq(s)
let iters = int(target_cycles div (s.int64 * worst_cycles_per_bytes))
benchSHA256_constantine(msg, $s & "B", iters)
benchSHA256_openssl(msg, $s & "B", iters)
when not defined(windows): # not available on Windows in GH actions atm
benchSHA256_openssl(msg, $s & "B", iters)

main()
3 changes: 3 additions & 0 deletions constantine.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,9 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[
("tests/t_ethereum_verkle_primitives.nim", false),
("tests/t_ethereum_verkle_ipa_primitives.nim", false),

# Signatures
("tests/ecdsa/t_ecdsa_verify_openssl.nim", false),

# Proof systems
# ----------------------------------------------------------
("tests/proof_systems/t_r1cs_parser.nim", false),
Expand Down
68 changes: 68 additions & 0 deletions constantine/ecdsa_secp256k1.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import
constantine/zoo_exports,
constantine/signatures/ecdsa,
constantine/hashes/h_sha256,
constantine/named/algebras,
constantine/math/elliptic/[ec_shortweierstrass_affine],
constantine/math/[arithmetic, ec_shortweierstrass],
constantine/platforms/[abstractions, views]

export NonceSampler

const prefix_ffi = "ctt_ecdsa_secp256k1_"
type
SecretKey* {.byref, exportc: prefix_ffi & "seckey".} = object
## A Secp256k1 secret key
raw: Fr[Secp256k1]

PublicKey* {.byref, exportc: prefix_ffi & "pubkey".} = object
## A Secp256k1 public key for ECDSA signatures
raw: EC_ShortW_Aff[Fp[Secp256k1], G1]

Signature* {.byref, exportc: prefix_ffi & "signature".} = object
## A Secp256k1 signature for ECDSA signatures
r: Fr[Secp256k1]
s: Fr[Secp256k1]

func pubkey_is_zero*(pubkey: PublicKey): bool {.libPrefix: prefix_ffi.} =
## Returns true if input is 0
bool(pubkey.raw.isNeutral())

func pubkeys_are_equal*(a, b: PublicKey): bool {.libPrefix: prefix_ffi.} =
## Returns true if inputs are equal
bool(a.raw == b.raw)

func signatures_are_equal*(a, b: Signature): bool {.libPrefix: prefix_ffi.} =
## Returns true if inputs are equal
bool(a.r == b.r and a.s == b.s)

proc sign*(sig: var Signature,
secretKey: SecretKey,
message: openArray[byte],
nonceSampler: NonceSampler = nsRandom) {.libPrefix: prefix_ffi, genCharAPI.} =
## Sign `message` using `secretKey` and store the signature in `sig`. The nonce
## will either be randomly sampled `nsRandom` or deterministically calculated according
## to RFC6979 (`nsRfc6979`)
sig.coreSign(secretKey.raw, message, sha256, nonceSampler)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's Bitcoin protocol that uses SHA256?

We can rename the file btc_ecdsa_secp256k1.


proc verify*(
publicKey: PublicKey,
message: openArray[byte],
signature: Signature
): bool {.libPrefix: prefix_ffi, genCharAPI.} =
## Verify `signature` using `publicKey` for `message`.
result = publicKey.raw.coreVerify(message, signature, sha256)

func derive_pubkey*(public_key: var PublicKey, secret_key: SecretKey) {.libPrefix: prefix_ffi.} =
## Derive the public key matching with a secret key
##
## The secret_key MUST be validated
public_key.raw.derivePubkey(secret_key.raw)
26 changes: 26 additions & 0 deletions constantine/named/constants/secp256k1_generators.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import
constantine/named/algebras,
constantine/math/elliptic/ec_shortweierstrass_affine,
constantine/math/io/[io_fields, io_extfields]

{.used.}

# Generators
# -----------------------------------------------------------------
# https://www.secg.org/sec2-v2.pdf page 9 (13 of PDF), sec. 2.4.1

# The group G_1 (== G) is defined on the curve Y^2 = X^3 + 7 over the field F_p
# with p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
# with generator:
const Secp256k1_generator_G1* = EC_ShortW_Aff[Fp[Secp256k1], G1](
x: Fp[Secp256k1].fromHex"0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
y: Fp[Secp256k1].fromHex"0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"
)
3 changes: 2 additions & 1 deletion constantine/named/zoo_generators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import
./constants/bls12_381_generators,
./constants/bn254_snarks_generators,
./constants/bandersnatch_generators,
./constants/banderwagon_generators
./constants/banderwagon_generators,
./constants/secp256k1_generators

{.experimental: "dynamicbindsym".}

Expand Down
162 changes: 162 additions & 0 deletions constantine/serialization/codecs_ecdsa.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

##[

Performs (de-)serialization of ECDSA signatures into ASN.1 DER encoded
data following SEC1:

https://www.secg.org/sec1-v2.pdf

In contrast to `codecs_ecdsa_secp256k1.nim` this file is generic under the choice
of elliptic curve.
]##

import
constantine/named/algebras,
constantine/math/io/[io_bigints, io_fields],
constantine/math/elliptic/[ec_shortweierstrass_affine],
constantine/platforms/[abstractions, views],
constantine/serialization/codecs # for fromHex and (in the future) base64 encoding

type
## Helper type for ASN.1 DER signatures to avoid allocation.
## Has a `data` buffer of 72 bytes (maximum possible size for
## a signature for `secp256k1`) and `len` of actually used data.
## `data[0 ..< len]` is the actual signature.
DerSignature*[N: static int] = object
data*: array[N, byte] # Max size: 6 bytes overhead + 33 bytes each for r,s
len*: int # Actual length used

template DerSigSize*(Name: static Algebra): int =
const OctetWidth = 8
6 + 2 * (Fr[Name].bits.ceilDiv_vartime(OctetWidth) + 1)

proc toDER*[Name: static Algebra; N: static int](derSig: var DerSignature[N], r, s: Fr[Name]) =
## Converts signature (r,s) to DER format without allocation.
## Max size is 72 bytes (for Secp256k1 or any curve with 32 byte scalars in `Fr`):
## 6 bytes overhead + up to 32+1 bytes each for r,s.
## 6 byte 'overhead' for:
## - `0x30` byte SEQUENCE designator
## - total length of the array
## - integer type designator `0x02` (before `r` and `s`)
## - length of `r` and `s`
##
## Implementation follows ideas of Bitcoin's secp256k1 implementation:
## https://github.com/bitcoin-core/secp256k1/blob/f79f46c70386c693ff4e7aef0b9e7923ba284e56/src/ecdsa_impl.h#L171-L193
const OctetWidth = 8
const N = Fr[Name].bits.ceilDiv_vartime(OctetWidth) # 32 for `secp256k1`

template toByteArray(x: Fr[Name]): untyped =
## Convert to a 33 byte array. Leading zero byte required if
## first real byte (idx 1) highest bit set (> 0x80).
var a: array[N+1, byte]
discard toOpenArray[byte](a, 1, N).marshal(x.toBig(), bigEndian)
a

# 1. Prepare the data & determine required sizes

# Convert r,s to big-endian bytes
var rBytes = r.toByteArray()
var sBytes = s.toByteArray()
var rLen = N + 1
var sLen = N + 1

# Skip leading zeros but ensure high bit constraint
var rPos = 0
while rLen > 1 and rBytes[rPos] == 0 and (rBytes[rPos+1] < 0x80.byte):
dec rLen
inc rPos
var sPos = 0
while sLen > 1 and sBytes[sPos] == 0 and (sBytes[sPos+1] < 0x80.byte):
dec sLen
inc sPos

# Set total length
derSig.len = 6 + rLen + sLen


# 2. Write the actual data
var pos = 0
template setInc(val: byte): untyped =
# Set `val` at `pos` and increase `pos`
derSig.data[pos] = val
inc pos

# Write DER structure, global
setInc 0x30 # sequence
setInc (4 + rLen + sLen).byte # total length

# `r` prefix
setInc 0x02 # integer
setInc rLen.byte # length of `r`
# Write `r` bytes in valid region
derSig.data.rawCopy(pos, rBytes, rPos, rLen)
inc pos, rLen

# `s` prefix
setInc 0x02 # integer
setInc sLen.byte # length of `s`
# Write `s` bytes in valid region
derSig.data.rawCopy(pos, sBytes, sPos, sLen)
inc pos, sLen

assert derSig.len == pos

proc fromRawDER*(r, s: var array[32, byte], sig: openArray[byte]): bool =
## Extracts the `r` and `s` values from a given DER signature.
##
## Returns `true` if the input is a valid DER encoded signature
## for `secp256k1` (or any curve with 32 byte scalars).
var pos = 0

template checkInc(val: untyped): untyped =
if pos > sig.high or sig[pos] != val:
# Invalid signature
return false
inc pos
template readInc(val: untyped): untyped =
if pos > sig.high:
return false
val = sig[pos]
inc pos

checkInc(0x30) # SEQUENCE
var totalLen: byte; readInc(totalLen)

template parseElement(el: var array[32, byte]): untyped =
var eLen: byte; readInc(eLen) # len of `r`
if pos + eLen.int > sig.len: # would need more data than available
return false
# read `r` into *last* `rLen` bytes
var eStart = el.len - eLen.int
if eStart < 0: # indicates prefix 0 due to first byte >= 0x80 (highest bit set)
doAssert eLen == 33
inc pos # skip first byte
eStart = 0 # start from 0 in `el`
dec eLen # decrease eLen by 1
el.rawCopy(eStart, sig, pos, eLen.int)
inc pos, eLen.int

# `r`
checkInc(0x02) # INTEGER
parseElement(r)

# `s`
checkInc(0x02) # INTEGER
parseElement(s)

# NOTE: `totalLen` does not include the prefix [0x30, totalLen] 2 bytes. Hence -2.
assert pos - 2 == totalLen.int, "Pos = " & $pos & ", totalLen = " & $totalLen

result = true

proc fromDER*(r, s: var array[32, byte], derSig: DerSignature) =
## Splits a given `DerSignature` back into the `r` and `s` elements as
## raw byte arrays.
fromRawDER(r, s, derSig.data)
Loading
Loading