-
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ECDSA over secp256k1 signatures and verification (#490)
* [zoo] add generator for secp256k1 * [ECDSA] add initial ECDSA signing / verifying implementation * [ecdsa] fix imports * [ecdsa] export Secp256k1 as `C` for convenience (We might want to take this out before we merge?) * [ecdsa] export `toDER` proc * [ecdsa] use `isZero` instead of old zero comparison * [ecdsa] rename private key generator & add private -> public key * [tests] add test cases for ECDSA signature verification Adds a test vector generator and a test case (and test vectors) for verification of OpenSSL generated signatures. * [ecdsa] handle some `.noinit.` cases * [ecdsa] turn `toBytes`, `arrayWith` into in-place procedures * [ecdsa] clean up comment about Fp -> Fr conversion * [ecdsa] replace toPemPrivateKey/PublicKey by in-place array variants * [ecdsa] replace `toDER` by non allocating variant Inspired by Bitcoin's implementation: https://github.com/bitcoin-core/secp256k1/blob/f79f46c70386c693ff4e7aef0b9e7923ba284e56/src/ecdsa_impl.h#L171-L193 Essentially, we just have a 72 byte buffer and only fill it up to `len` bytes. * [ecdsa] replace out-of-place arithmetic by in-place * [ecdsa] move ECDSA implementation to ~signatures~ directory * [ecdsa] remove dependence on explicit SHA256 hash function * [ecdsa] make DERSignature generic under curve by having static size The size is now a static argument of the type, depending on the curve. `DERSigSize` can be used to calculate the size required for the given curve. * [ecdsa] turn more procs generic over curve and hash function * [ecdsa] replace sign/verify API by one matching BLS signatures * [ecdsa] remove global curve & generator constants * [ecdsa] correctly handle truncation of digests > Fr BigInts * create file for common signature ops, `derivePubkey` for ECDSA & BLS Also cleans up the imports of the ECDSA file and adds the copyright header * create file specifically for ECDSA over secp256k1 * [ecdsa] add `fromDER` to split DER encoded signature back into r, s arrays Written for OpenSSL interop for test suite (to be moved to serialization with the other related procs). * [tests] add OpenSSL wrapper intended for test cases * [tests] first step towards OpenSSL tests Next is to merge this into the actual test case and avoid the JSON intermediate stage altogether. * [tests] fully avoid JSON intermediary files for ECDSA tests * [tests] rename file back to test case name, add DERSigSize tests * [tests] also test our DER encoder * [tests] extend OpenSSL wrapper for required functionality * [tests] move openssl wrapper to root of tests to share between tests * [tests] add test case to verify PEM file writer * [ecdsa] clean up and fix PEM file writers The total length of the public key was wrong. Needs to be 86 bytes and not 88. I counted the SEQUENCE and total length bytes by accident. * [tests] [bench] use shared OpenSSL wrapper where appropriate * [codecs] move serialization logic to ecdsa secp256k1 submodule * [codecs] move DER signature serialization to codecs_ecdsa submodule * [ecdsa] adjust ECDSA secp256k1 API & test cases * [ecdsa] add mini docstring for `verify` * [codecs] clean up imports in `codecs_ecdsa.nim` * [ecdsa] clean up imports of `ecdsa_secp256k1.nim` * [ecdsa] do not export `raw` field in ecdsa_secp256k1 * [CI] fix CI failures by including OpenSSL wrapper instead of import Not sure what's happening here. Never seen that with any C wrappers. * [bench] disable OpenSSL bench for sha256 on windows API not currently available in the OpenSSL version on GH actions * [nimble] add ECDSA signature test to nimble task * [ecdsa] replace brainfart using pointer size for bits in byte ... * [ecdsa] fix final related brainfart :) * [tests] when the brainfart infects the test cases too! 🤯 * replace DERSig* by DerSig* * replace `toPemFile` by simply `toPem` * rename `common_signature_ops` to `ecc_sig_ops` to clarify that these are only for EC based signatures. * [tests] disable ECDSA test for Windows * [ecdsa] avoid awkward arrayWith declaration & call
- Loading branch information
Showing
13 changed files
with
1,123 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.