-
-
Notifications
You must be signed in to change notification settings - Fork 47
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
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 6434eba
[ECDSA] add initial ECDSA signing / verifying implementation
Vindaar e8540b6
[ecdsa] fix imports
Vindaar 41d502d
[ecdsa] export Secp256k1 as `C` for convenience
Vindaar 2760af4
[ecdsa] export `toDER` proc
Vindaar 9f31c38
[ecdsa] use `isZero` instead of old zero comparison
Vindaar 5862d43
[ecdsa] rename private key generator & add private -> public key
Vindaar 4fd34e2
[tests] add test cases for ECDSA signature verification
Vindaar a4ca057
[ecdsa] handle some `.noinit.` cases
Vindaar 22bd57b
[ecdsa] turn `toBytes`, `arrayWith` into in-place procedures
Vindaar 4e17514
[ecdsa] clean up comment about Fp -> Fr conversion
Vindaar 46d8f92
[ecdsa] replace toPemPrivateKey/PublicKey by in-place array variants
Vindaar 7164f07
[ecdsa] replace `toDER` by non allocating variant
Vindaar 890b185
[ecdsa] replace out-of-place arithmetic by in-place
Vindaar 50de116
[ecdsa] move ECDSA implementation to ~signatures~ directory
Vindaar 931044d
[ecdsa] remove dependence on explicit SHA256 hash function
Vindaar fe8a8aa
[ecdsa] make DERSignature generic under curve by having static size
Vindaar bec1536
[ecdsa] turn more procs generic over curve and hash function
Vindaar 34442fa
[ecdsa] replace sign/verify API by one matching BLS signatures
Vindaar 06f7a5f
[ecdsa] remove global curve & generator constants
Vindaar ad16403
[ecdsa] correctly handle truncation of digests > Fr BigInts
Vindaar e9174e6
create file for common signature ops, `derivePubkey` for ECDSA & BLS
Vindaar 4d72a6b
create file specifically for ECDSA over secp256k1
Vindaar a8ecd59
[ecdsa] add `fromDER` to split DER encoded signature back into r, s a…
Vindaar 828189c
[tests] add OpenSSL wrapper intended for test cases
Vindaar 722fa37
[tests] first step towards OpenSSL tests
Vindaar 64130a1
[tests] fully avoid JSON intermediary files for ECDSA tests
Vindaar e9387e8
[tests] rename file back to test case name, add DERSigSize tests
Vindaar 0d24f6a
[tests] also test our DER encoder
Vindaar 5229551
[tests] extend OpenSSL wrapper for required functionality
Vindaar 1d05da4
[tests] move openssl wrapper to root of tests to share between tests
Vindaar c0b3806
[tests] add test case to verify PEM file writer
Vindaar 87bc887
[ecdsa] clean up and fix PEM file writers
Vindaar 8000567
[tests] [bench] use shared OpenSSL wrapper where appropriate
Vindaar 04ce1c8
[codecs] move serialization logic to ecdsa secp256k1 submodule
Vindaar 0c5195f
[codecs] move DER signature serialization to codecs_ecdsa submodule
Vindaar 89688ba
[ecdsa] adjust ECDSA secp256k1 API & test cases
Vindaar 5011fe3
[ecdsa] add mini docstring for `verify`
Vindaar fa5a5eb
[codecs] clean up imports in `codecs_ecdsa.nim`
Vindaar 2f6a897
[ecdsa] clean up imports of `ecdsa_secp256k1.nim`
Vindaar 2693aec
[ecdsa] do not export `raw` field in ecdsa_secp256k1
Vindaar 1b44a8f
[CI] fix CI failures by including OpenSSL wrapper instead of import
Vindaar c2c39af
[bench] disable OpenSSL bench for sha256 on windows
Vindaar fa9e0ab
[nimble] add ECDSA signature test to nimble task
Vindaar 4cac2ec
[ecdsa] replace brainfart using pointer size for bits in byte
Vindaar 30285ff
[ecdsa] fix final related brainfart :)
Vindaar 901597e
[tests] when the brainfart infects the test cases too! 🤯
Vindaar 6c09fe1
replace DERSig* by DerSig*
Vindaar 0bbc839
replace `toPemFile` by simply `toPem`
Vindaar 20057e0
rename `common_signature_ops` to `ecc_sig_ops`
Vindaar 9642ca6
[tests] disable ECDSA test for Windows
Vindaar 2fd2bb2
[ecdsa] avoid awkward arrayWith declaration & call
Vindaar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.