From dca3a8559b094523966a2d1f57ff05609226760e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 5 Jan 2025 15:08:02 +0100 Subject: [PATCH] Add ECDSA over secp256k1 signatures and verification (#490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- benchmarks/bench_h_sha256.nim | 45 +-- constantine.nimble | 3 + constantine/ecdsa_secp256k1.nim | 68 ++++ .../named/constants/secp256k1_generators.nim | 26 ++ constantine/named/zoo_generators.nim | 3 +- constantine/serialization/codecs_ecdsa.nim | 162 +++++++++ .../serialization/codecs_ecdsa_secp256k1.nim | 163 +++++++++ constantine/signatures/bls_signatures.nim | 19 +- constantine/signatures/ecc_sig_ops.nim | 26 ++ constantine/signatures/ecdsa.nim | 311 ++++++++++++++++++ tests/ecdsa/t_ecdsa_verify_openssl.nim | 184 +++++++++++ tests/openssl_wrapper.nim | 155 +++++++++ tests/t_hash_sha256_vs_openssl.nim | 49 +-- 13 files changed, 1123 insertions(+), 91 deletions(-) create mode 100644 constantine/ecdsa_secp256k1.nim create mode 100644 constantine/named/constants/secp256k1_generators.nim create mode 100644 constantine/serialization/codecs_ecdsa.nim create mode 100644 constantine/serialization/codecs_ecdsa_secp256k1.nim create mode 100644 constantine/signatures/ecc_sig_ops.nim create mode 100644 constantine/signatures/ecdsa.nim create mode 100644 tests/ecdsa/t_ecdsa_verify_openssl.nim create mode 100644 tests/openssl_wrapper.nim diff --git a/benchmarks/bench_h_sha256.nim b/benchmarks/bench_h_sha256.nim index c912bf8c4..dbb9740db 100644 --- a/benchmarks/bench_h_sha256.nim +++ b/benchmarks/bench_h_sha256.nim @@ -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) # -------------------------------------------------------------------- @@ -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() diff --git a/constantine.nimble b/constantine.nimble index 82c0be0b7..ecf0fcea4 100644 --- a/constantine.nimble +++ b/constantine.nimble @@ -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), diff --git a/constantine/ecdsa_secp256k1.nim b/constantine/ecdsa_secp256k1.nim new file mode 100644 index 000000000..bc28b188a --- /dev/null +++ b/constantine/ecdsa_secp256k1.nim @@ -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) diff --git a/constantine/named/constants/secp256k1_generators.nim b/constantine/named/constants/secp256k1_generators.nim new file mode 100644 index 000000000..efd17908b --- /dev/null +++ b/constantine/named/constants/secp256k1_generators.nim @@ -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" +) diff --git a/constantine/named/zoo_generators.nim b/constantine/named/zoo_generators.nim index d462bf6af..d858d9a19 100644 --- a/constantine/named/zoo_generators.nim +++ b/constantine/named/zoo_generators.nim @@ -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".} diff --git a/constantine/serialization/codecs_ecdsa.nim b/constantine/serialization/codecs_ecdsa.nim new file mode 100644 index 000000000..a804c6f57 --- /dev/null +++ b/constantine/serialization/codecs_ecdsa.nim @@ -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) diff --git a/constantine/serialization/codecs_ecdsa_secp256k1.nim b/constantine/serialization/codecs_ecdsa_secp256k1.nim new file mode 100644 index 000000000..c92f84b5e --- /dev/null +++ b/constantine/serialization/codecs_ecdsa_secp256k1.nim @@ -0,0 +1,163 @@ +# 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 serialization of ECDSA public and private keys over Secp256k1 +in ASN.1 DER encoded PEM files following SEC1: + +https://www.secg.org/sec1-v2.pdf + +(See appendix C) + +Further, RFC 7468 + +https://www.rfc-editor.org/rfc/rfc7468 + +describes the PEM file format itself. + +The code below could be made generic under the curve relatively easily. +That requires to adjust the array sizes & length bytes for the scalar fields +/ elliptic curve points in use and needs the OIDs for the curves. + + +*NOTE*: Currently this relies on the Nim stdlib `base64` encoder. + +TODO: Write custom `base64` encoder and add tests for existing `base64` +decoder in `codecs.nim` +]## + +import + constantine/named/algebras, + constantine/platforms/primitives, + constantine/math/arithmetic/finite_fields, + constantine/math/elliptic/ec_shortweierstrass_affine, + constantine/math/io/io_bigints, + constantine/ecdsa_secp256k1 + +import std / [strutils, base64, math, importutils] + +const C = Secp256k1 + +proc toBytes[Name: static Algebra; N: static int](res: var array[N, byte], x: FF[Name]) = + discard res.marshal(x.toBig(), bigEndian) + +proc toPemPrivateKey*(res: var array[48, byte], privateKey: Fr[C]) = + ## Encodes a private key as ASN.1 DER encoded private keys. + ## + ## See: https://www.secg.org/sec1-v2.pdf appendix C.4 + ## + ## TODO: Adjust to support different curves. + # Start with SEQUENCE + res.rawCopy(0, [byte(0x30), byte(0x2E)], 0, 2) + + # Version (always 1) + res.rawCopy(2, [byte(0x02), 1, 1], 0, 3) + + # Private key as octet string + var secKeyBytes {.noinit.}: array[32, byte] + secKeyBytes.toBytes(privateKey) + + res.rawCopy(5, [byte(0x04), byte(secKeyBytes.len)], 0, 2) + res.rawCopy(7, secKeyBytes, 0, 32) ## XXX: array size + + # Parameters (secp256k1 OID: 1.3.132.0.10) + const Secp256k1Oid = [byte(0xA0), byte(7), byte(6), byte(5), + byte(0x2B), byte(0x81), byte(0x04), byte(0x00), byte(0x0A)] + res.rawCopy(39, Secp256k1Oid, 0, 9) + +proc toPemPublicKey*(res: var array[88, byte], publicKey: EC_ShortW_Aff[Fp[C], G1]) = + ## Encodes a public key as ASN.1 DER encoded public keys. + ## + ## See: https://www.secg.org/sec1-v2.pdf appendix C.3 + ## + ## TODO: Adjust to support different curves. + # Start with SEQUENCE + res.rawCopy(0, [byte(0x30), byte(0x56)], 0, 2) + + # Algorithm identifier + const algoId = [ + byte(0x30), byte(0x10), # SEQUENCE + byte(0x06), byte(0x07), # OID for EC + byte(0x2A), byte(0x86), byte(0x48), # 1.2.840.10045.2.1 + byte(0xCE), byte(0x3D), byte(0x02), byte(0x01), + byte(0x06), byte(0x05), # OID for secp256k1 + byte(0x2B), byte(0x81), byte(0x04), byte(0x00), byte(0x0A) # 1.3.132.0.10 + ] + + res.rawCopy(2, algoId, 0, algoId.len) # algoId.len == 18 + + # Public key as bit string + const encoding = [byte(0x03), byte(0x42)] # [BIT-STRING, 2+32+32 prefix & coordinates] + const prefix = [ + byte(0x00), # DER BIT STRING: number of unused bits (always 0 for keys) + byte(0x04) # SEC1: uncompressed point format marker + ] + + template toByteArray(x: Fp[C] | Fr[C]): untyped = + var a: array[32, byte] + a.toBytes(x) + a + + res.rawCopy(20, encoding, 0, 2) + res.rawCopy(22, prefix, 0, 2) + res.rawCopy(24, publicKey.x.toByteArray(), 0, 32) + res.rawCopy(56, publicKey.y.toByteArray(), 0, 32) + +## NOTE: +## The below procs / code is currently "unsuited" for Constantine in the sense that +## it currently still contains stdlib dependencies. Most of those are trivial, with the +## exception of a base64 encoder. +## Having a ANS1.DER encoder (and maybe decoder in the future) for SEC1 private and +## public keys would be nice to have in CTT, I think (at least for the curves that +## we support for the related operations; secp256k1 at the moment). + +proc wrap(s: string, maxLineWidth = 64): string = + ## Wrap the given string at `maxLineWidth` over multiple lines + let lines = s.len.ceilDiv maxLineWidth + result = newStringOfCap(s.len + lines) + for i in 0 ..< lines: + let frm = i * maxLineWidth + let to = min(s.len, (i+1) * maxLineWidth) + result.add s[frm ..< to] + if i < lines-1: + result.add "\n" + +proc toPem*(publicKey: PublicKey): string = + ## Convert a given private key to data in PEM format following SEC1 + ## + ## RFC 7468 describes the textual encoding of these files: + ## https://www.rfc-editor.org/rfc/rfc7468#section-10 + # 1. Convert public key to ASN.1 DER + var derB: array[88, byte] + privateAccess(PublicKey) # for `raw` access + derB.toPemPublicKey(publicKey.raw) + # 2. Encode bytes in base64 + let der64 = derB.encode().wrap() + # 3. Wrap in begin/end public key template + result = "-----BEGIN PUBLIC KEY-----\n" + result.add der64 & "\n" + result.add "-----END PUBLIC KEY-----\n" + +proc toPem*(privateKey: SecretKey): string = + ## XXX: For now using `std/base64` but will need to write base64 encoder + ## & add tests for CTT base64 decoder! + ## Convert a given private key to data in PEM format following SEC1 + ## + ## RFC 7468 describes the textual encoding of these files: + ## https://www.rfc-editor.org/rfc/rfc7468#section-13 + # 1. Convert private key to ASN.1 DER encoding + var derB {.noinit.}: array[48, byte] + privateAccess(SecretKey) # for `raw` access + derB.toPemPrivateKey(privateKey.raw) + # 2. Encode bytes in base64 + let der64 = derB.encode().wrap() + # 3. Wrap in begin/end private key template + result = "-----BEGIN EC PRIVATE KEY-----\n" + result.add der64 & "\n" + result.add "-----END EC PRIVATE KEY-----\n" diff --git a/constantine/signatures/bls_signatures.nim b/constantine/signatures/bls_signatures.nim index 153f54e1f..1a3ecf2f6 100644 --- a/constantine/signatures/bls_signatures.nim +++ b/constantine/signatures/bls_signatures.nim @@ -15,7 +15,10 @@ import constantine/named/algebras, constantine/hash_to_curve/[hash_to_curve, h2c_hash_to_field], constantine/hashes, - constantine/platforms/views + constantine/platforms/views, + constantine/signatures/ecc_sig_ops # for `derivePubkey` + +export ecc_sig_ops # ############################################################ # @@ -34,20 +37,6 @@ import {.push raises: [].} # No exceptions allowed in core cryptographic operations {.push checks: off.} # No defects due to array bound checking or signed integer overflow allowed -func derivePubkey*[Pubkey, SecKey](pubkey: var Pubkey, seckey: SecKey) = - ## Generates the public key associated with the input secret key. - ## - ## The secret key MUST be in range (0, curve order) - ## 0 is INVALID - const Group = Pubkey.G - type Field = Pubkey.F - const EC = Field.Name - - var pk {.noInit.}: EC_ShortW_Jac[Field, Group] - pk.setGenerator() - pk.scalarMul(seckey) - pubkey.affine(pk) - func coreSign*[Sig, SecKey]( signature: var Sig, secretKey: SecKey, diff --git a/constantine/signatures/ecc_sig_ops.nim b/constantine/signatures/ecc_sig_ops.nim new file mode 100644 index 000000000..6b5cb1f7c --- /dev/null +++ b/constantine/signatures/ecc_sig_ops.nim @@ -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/math/[ec_shortweierstrass], + constantine/named/zoo_generators, + constantine/named/algebras + +func derivePubkey*[Pubkey, SecKey](pubkey: var Pubkey, seckey: SecKey) = + ## Generates the public key associated with the input secret key. + ## + ## The secret key MUST be in range (0, curve order) + ## 0 is INVALID + const Group = Pubkey.G + type Field = Pubkey.F + const EC = Field.Name + + var pk {.noInit.}: EC_ShortW_Jac[Field, Group] + pk.setGenerator() + pk.scalarMul(seckey) + pubkey.affine(pk) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim new file mode 100644 index 000000000..088c8845c --- /dev/null +++ b/constantine/signatures/ecdsa.nim @@ -0,0 +1,311 @@ +# 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/hashes, + constantine/named/algebras, + constantine/math/io/[io_bigints, io_fields, io_ec], + constantine/math/elliptic/[ec_shortweierstrass_affine, ec_shortweierstrass_jacobian, ec_scalar_mul, ec_multi_scalar_mul], + constantine/math/[arithmetic, ec_shortweierstrass], + constantine/platforms/[abstractions, views], + constantine/serialization/codecs, # for fromHex and (in the future) base64 encoding + constantine/mac/mac_hmac, # for deterministic nonce generation via RFC 6979 + constantine/named/zoo_generators, # for generator + constantine/csprngs/sysrand, + constantine/signatures/ecc_sig_ops # for `derivePubkey` + +import std / macros # for `update` convenience helper + +export ecc_sig_ops + +type + ## Decides the type of sampler we use for the nonce. By default + ## a simple uniform random sampler. Alternatively a deterministic + ## sampler based on message hash and private key. + NonceSampler* = enum + nsRandom, ## pure uniform random sampling + nsRfc6979 ## deterministic according to RFC 6979 + +proc toBytes[Name: static Algebra; N: static int](res: var array[N, byte], x: FF[Name]) = + discard res.marshal(x.toBig(), bigEndian) + +func fromDigest[Name: static Algebra; N: static int]( + dst: var Fr[Name], src: array[N, byte], + truncateInput: static bool): bool {.discardable.} = + ## Convert a hash function digest to an element in the scalar field `Fr[Name]`. + ## The proc returns a boolean indicating whether the data in `src` is + ## smaller than the field modulus. It is discardable, because in some + ## use cases this is fine (e.g. constructing a field element from a hash), + ## but invalid in the nonce generation following RFC6979. + ## + ## The `truncateInput` argument handles how `src` arrays larger than the BigInt + ## underlying `Fr[Name]` are handled. If it is `false` we will simply throw + ## an assertion error on `unmarshal` (used in RFC6979 nonce generation where + ## the array size cannot be larger than `Fr[Name]`). If it is `true`, we truncate + ## the digest array to the left most bits of up to the number of bits underlying + ## the BigInt of `Fr[Name]` following SEC1v2 [0] (page 45, 5.1-5.4). + ## + ## [0]: https://www.secg.org/sec1-v2.pdf + var scalar {.noInit.}: matchingOrderBigInt(Name) + when truncateInput: # for signature & verification + # If the `src` array is larger than the BigInt underlying `Fr[Name]`, need + # to truncate the `src`. + const OctetWidth = 8 + const FrBytes = Fr[Name].bits.ceildiv_vartime(OctetWidth) + # effectively: `scalar ~ array[0 ..< scalar.len]` + scalar.unmarshal(toOpenArray[byte](src, 0, FrBytes-1), bigEndian) + # Now still need to right shift potential individual bits. + # e.g. 381 bit BigInt fits into 384 bit (48 bytes), so need to + # right shift 3 bits to truncate correctly. + const toShift = FrBytes * OctetWidth - Fr[Name].bits + when toShift > 0: + scalar.shiftRight(toShift) + else: # for RFC 6979 nonce sampling. If larger than modulus, sample again + scalar.unmarshal(src, bigEndian) + scalar.unmarshal(src, bigEndian) + # `true` if smaller than modulus + result = bool(scalar < Fr[Name].getModulus()) + dst.fromBig(scalar) + +proc randomFieldElement[FF](): FF = + ## random element in ~Fp[T]/Fr[T]~ + let m = FF.getModulus() + var b: matchingBigInt(FF.Name) + + while b.isZero().bool or (b > m).bool: + ## XXX: raise / what else to do if `sysrand` call fails? + doAssert b.limbs.sysrand() + + result.fromBig(b) + +proc byteArrayWith(N: static int, val: byte): array[N, byte] {.noinit, inline.} = + for i in 0 ..< N: + result[i] = val + +macro update[T](hmac: var HMAC[T], args: varargs[untyped]): untyped = + ## Mini helper to allow HMAC to act on multiple arguments in succession + result = newStmtList() + for arg in args: + result.add quote do: + `hmac`.update(`arg`) + +template round(hmac, input, output: typed, args: varargs[untyped]): untyped = + ## Perform a full 'round' of HMAC. Pre-shared secret is `input`, the + ## result will be stored in `output`. All `args` are fed into the HMAC + ## in the order they are given. + hmac.init(input) + hmac.update(args) + hmac.finish(output) + +proc nonceRfc6979[Name: static Algebra]( + msgHash, privateKey: Fr[Name], + H: type CryptoHash): Fr[Name] {.noinit.} = + ## Generate deterministic nonce according to RFC 6979. + ## + ## Spec: + ## https://datatracker.ietf.org/doc/html/rfc6979#section-3.2 + + const OctetWidth = 8 + const N = Fr[Name].bits.ceilDiv_vartime(OctetWidth) + + # Step a: `h1 = H(m)` hash message (already done, input is hash), convert to array of bytes + var msgHashBytes {.noinit.}: array[N, byte] + msgHashBytes.toBytes(msgHash) + # Piece of step d: Conversion of the private key to a byte array. + # No need for `bits2octets`, because the private key is already a valid + # scalar in the field `Fr[C]` and thus < p-1 (`bits2octets` converts + # `r` bytes to a BigInt, reduces modulo prime order `p` and converts to + # a byte array). + var privKeyBytes {.noinit.}: array[N, byte] + privKeyBytes.toBytes(privateKey) + + # Initial values + # Step b: `V = 0x01 0x01 0x01 ... 0x01` + var v = byteArrayWith(N, byte 0x01) + # Step c: `K = 0x00 0x00 0x00 ... 0x00` + var k = byteArrayWith(N, byte 0x00) + + # Create HMAC contexts + var hmac {.noinit.}: HMAC[H] + + # Step d: `K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1))` + hmac.round(k, k, v, [byte 0x00], privKeyBytes, msgHashBytes) + # Step e: `V = HMAC_K(V)` + hmac.round(k, v, v) + # Step f: `K = HMAC_K(V || 0x01 || int2octets(x) || bits2octets(h1))` + hmac.round(k, k, v, [byte 0x01], privKeyBytes, msgHashBytes) + # Step g: `V = HMAC_K(V)` + hmac.round(k, v, v) + # Step h: Loop until valid nonce found + while true: + # Step h.1 (init T to zero) and h.2: + # `V = HMAC_K(V)` + # `T = T || V` + # We do not need to accumulate a `T`, because we use SHA256 as a hash + # function (256 bits) and Secp256k1 as a curve (also 256 big int). + hmac.round(k, v, v) # v becomes T + + # Step h.3: `k = bits2int(T)` + var candidate {.noinit.}: Fr[Name] + # `fromDigest` returns `false` if the array is larger than the field modulus, + # important for uniform sampling in valid range `[1, q-1]`! + let smaller = candidate.fromDigest(v, truncateInput = false) # do not truncate! + + if not bool(candidate.isZero()) and smaller: + return candidate + + # Step h.3 failure state: + # `K = HMAC_K(V || 0x00)` + # `V = HMAC_K(V)` + # Try again if invalid + hmac.round(k, k, v, [byte 0x00]) + hmac.round(k, v, v) + +proc generateNonce[Name: static Algebra]( + kind: NonceSampler, msgHash, privateKey: Fr[Name], + H: type CryptoHash): Fr[Name] {.noinit.} = + case kind + of nsRandom: randomFieldElement[Fr[Name]]() + of nsRfc6979: nonceRfc6979(msgHash, privateKey, H) + +proc signImpl[Name: static Algebra; Sig]( + sig: var Sig, + secretKey: Fr[Name], + message: openArray[byte], + H: type CryptoHash, + nonceSampler: NonceSampler = nsRandom) = + ## Sign a given `message` using the `secretKey`. + ## + ## By default we use a purely random nonce (uniform random number), + ## but passing `nonceSampler = nsRfc6979` uses RFC 6979 to compute + ## a deterministic nonce (and thus deterministic signature) given + ## the message and private key as base. + # 1. hash the message in big endian order + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var message_hash: Fr[Name] + # if `dgst` uses more bytes than + message_hash.fromDigest(dgst, truncateInput = true) + + # Generator of the curve + const G = Name.getGenerator($G1) + + # loop until we found a valid (non zero) signature + while true: + # Generate random nonce + var k = generateNonce(nonceSampler, message_hash, secretKey, H) + + var R {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] + # Calculate r (x-coordinate of kG) + # `r = kĀ·G (mod n)` + R.scalarMul(k, G) + # get x coordinate of the point `r` *in affine coordinates* + let rx = R.getAffine().x + let r = Fr[Name].fromBig(rx.toBig()) # convert to `Fr` + + if bool(r.isZero()): + continue # try again + + # Calculate s + # `s = (kā»Ā¹ Ā· (h + r Ā· p)) (mod n)` + # with `h`: message hash as `Fr[C]` (if we didn't use SHA256 w/ 32 byte output + # we'd need to truncate to N bits for N being bits in modulo `n`) + var s {.noinit.}: Fr[Name] + s.prod(r, secretKey) # `r * secretKey` + s += message_hash # `message_hash + r * secretKey` + k.inv() # `k := kā»Ā¹` + s *= k # `kā»Ā¹ * (message_hash + r * secretKey)` + # get inversion of `s` for 'lower-s normalization' + var sneg = s # inversion of `s` + sneg.neg() # q - s + # conditionally assign result based on BigInt comparison + let mask = s.toBig() > sneg.toBig() # if true, `s` is in upper half, need `sneg` + ccopy(s, sneg, mask) + + if bool(s.isZero()): + continue # try again + + # Set output and return + sig.r = r + sig.s = s + return + +proc coreSign*[Sig, SecKey]( + signature: var Sig, + secretKey: SecKey, + message: openArray[byte], + H: type CryptoHash, + nonceSampler: NonceSampler = nsRandom) {.genCharAPI.} = + ## Computes a signature for the message from the specified secret key. + ## + ## Output: + ## - `signature` is overwritten with `message` signed with `secretKey` + ## + ## Inputs: + ## - `Hash` a cryptographic hash function. + ## - `Hash` MAY be `sha256` + ## - `Hash` MAY be `keccak` + ## - Otherwise, H MUST be a hash function that has been proved + ## indifferentiable from a random oracle [MRH04] under a reasonable + ## cryptographic assumption. + ## - `message` is the message to hash + signature.signImpl(secretKey, message, H, nonceSampler) + +proc verifyImpl[Name: static Algebra; Sig]( + publicKey: EC_ShortW_Aff[Fp[Name], G1], + signature: Sig, # tuple[r, s: Fr[Name]], + message: openArray[byte], + H: type CryptoHash, +): bool = + ## Verify a given `signature` for a `message` using the given `publicKey`. + # 1. Hash the message (same as in signing) + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var e {.noinit.}: Fr[Name] + e.fromDigest(dgst, truncateInput = true) + + # 2. Compute w = sā»Ā¹ + var w = signature.s + w.inv() # w = sā»Ā¹ + + # 3. Compute uā‚ = ew and uā‚‚ = rw + var + u1 {.noinit.}: Fr[Name] + u2 {.noinit.}: Fr[Name] + u1.prod(e, w) + u2.prod(signature.r, w) + + # 4. Compute uā‚G + uā‚‚Q + var + point1 {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] + point2 {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] + # Generator of the curve + const G = publicKey.F.Name.getGenerator($publicKey.G) + point1.scalarMul(u1, G) + point2.scalarMul(u2, publicKey) + var R {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] + R.sum(point1, point2) + + # 5. Get x coordinate (in `Fp`) and convert to `Fr` (like in signing) + let x = R.getAffine().x + let r_computed = Fr[Name].fromBig(x.toBig()) + + # 6. Verify r_computed equals provided r + result = bool(r_computed == signature.r) + +func coreVerify*[Pubkey, Sig]( + pubkey: Pubkey, + message: openarray[byte], + signature: Sig, + H: type CryptoHash): bool {.genCharAPI.} = + ## Check that a signature is valid + ## for a message under the provided public key + ## This assumes that the PublicKey and Signatures + ## have been pre-checked for non-infinity and being in the correct subgroup + ## (likely on deserialization) + result = pubKey.verifyImpl(signature, message, H) diff --git a/tests/ecdsa/t_ecdsa_verify_openssl.nim b/tests/ecdsa/t_ecdsa_verify_openssl.nim new file mode 100644 index 000000000..8f52066a4 --- /dev/null +++ b/tests/ecdsa/t_ecdsa_verify_openssl.nim @@ -0,0 +1,184 @@ +##[ +This is a helper program to generate ECDSA signatures using OpenSSL as a +set of test vectors for our implementation. + +We generate test vectors following these cases: +- same message, different nonces -> different signature +- random message, random nonces + +Further, generate signatures using Constantine, which we verify +with OpenSSL. +]## + +import + constantine/csprngs/sysrand, + constantine/named/algebras, + constantine/math/io/[io_bigints, io_fields, io_ec], + constantine/serialization/[codecs, codecs_ecdsa, codecs_ecdsa_secp256k1], + constantine/math/arithmetic/[bigints, finite_fields], + constantine/platforms/abstractions, + constantine/ecdsa_secp256k1 + +when not defined(windows): + # Windows (at least in GH actions CI) does not provide, among others `BN_new` + # so we disable this test for Windows for the time being. + import ../openssl_wrapper + +import + std / [os, osproc, strutils, strformat, unittest, importutils] + +const C = Secp256k1 + +proc randomFieldElement[FF](): FF = + ## random element in ~Fp[T]/Fr[T]~ + let m = FF.getModulus() + var b: matchingBigInt(FF.Name) + + while b.isZero().bool or (b > m).bool: + ## XXX: raise / what else to do if `sysrand` call fails? + doAssert b.limbs.sysrand() + + result.fromBig(b) + +proc generatePrivateKey(): SecretKey {.noinit.} = + ## Generate a new private key using a cryptographic random number generator. + privateAccess(SecretKey) + result = SecretKey(raw: randomFieldElement[Fr[C]]()) + +proc generateMessage(len: int): string = + ## Returns a randomly generated message of `len` bytes as a + ## string of hex bytes. + let len = min(1024, len) # maximum length, to fit into our array + var buf: array[1024, byte] + doAssert sysrand(buf) + # Convert raw bytes to hex + result = buf.toOpenArray[:byte](0, len - 1).toHex() + +proc toHex(s: string): string = + result = s.toOpenArrayByte(0, s.len-1).toHex() + +proc toBytes[Name: static Algebra; N: static int](res: var array[N, byte], x: FF[Name]) = + discard res.marshal(x.toBig(), bigEndian) + +func getPublicKey(secKey: SecretKey): PublicKey {.noinit.} = + result.derive_pubkey(secKey) + +template toOA(x: string): untyped = toOpenArrayByte(x, 0, x.len-1) + +when not defined(windows): # see above + proc signAndVerify(num: int, msg = "", nonceSampler = nsRandom) = + ## Generates `num` signatures and verify them against OpenSSL. + ## + ## If `msg` is given, use a fixed message. Otherwise will generate a message with + ## a length up to 1024 bytes. + ## + ## As a side effect it also verifies our `fromDER` parser and as an additional + ## sanity check our `toDER` converter. + for i in 0 ..< num: + let msg = if msg.len > 0: msg else: generateMessage(64) # 64 byte long messages + let secKey = generatePrivateKey() + let pubKey = getPublicKey(secKey) + + # Get bytes of private key & initialize an OpenSSL key + var skBytes: array[32, byte] + privateAccess(SecretKey) # access to `raw` + skBytes.toBytes(secKey.raw) + var osSecKey: EVP_PKEY + osSecKey.initPrivateKeyOpenSSL(skBytes) + + # Sign the message using OpenSSL + var osSig: array[72, byte] + osSig.signMessageOpenSSL(msg.toOpenArrayByte(0, msg.len-1), osSecKey) + # Destructure the DER encoded signature into two arrays + var rOSL: array[32, byte] + var sOSL: array[32, byte] + # And turn into hex strings + check fromRawDER(rOSL, sOSL, osSig) + let (r, s) = (rOSL.toHex(), sOSL.toHex()) + # Convert to scalar and verify signature + let (rOslFr, sOslFr) = (Fr[C].fromHex(r), Fr[C].fromHex(s)) + privateAccess(Signature) # make `r`, `s` accessible in scope + let sigOsl = Signature(r: rOslFr, s: sOslFr) + check pubKey.verify(toOA msg, sigOsl) + # Now also sign with CTT and verify + var sigCTT {.noinit.}: Signature + sigCTT.sign(secKey, toOA msg) + check pubKey.verify(toOA msg, sigCTT) + + # Verify that we can generate a DER signature again from the OpenSSL + # data and it is equivalent to original + var derSig: DerSignature[DerSigSize(Secp256k1)] + derSig.toDER(rOslFr, sOslFr) + check derSig.data == osSig + + proc verifyPemWriter(num: int, msg = "") = + ## We verify our PEM writers in a bit of a roundabout way. + ## + ## TODO: Ideally we would simply write a given raw private and public key + ## using the C API of OpenSSL and compare writing the same key using + ## our serialization logic. + let dir = getTempDir() + # temp filename for private key PEM file + let pubKeyFile = dir / "public_key.pem" + let secKeyFile = dir / "private_key.pem" + let sigFile = dir / "msg.sig" + for i in 0 ..< num: + let msg = if msg.len > 0: msg else: generateMessage(64) # 64 byte long messages + let secKey = generatePrivateKey() + let pubKey = getPublicKey(secKey) + + writeFile(secKeyFile, toPem(secKey)) + writeFile(pubKeyFile, toPem(pubKey)) + + # Write a PEM file for public and private key using CTT and use it + # to sign and verify a message. + # NOTE: I tried using OpenSSL's C API, but couldn't get it to work + # 1. Sign using the private key and message + let sign = &"echo -n '{msg}' | openssl dgst -sha256 -sign {secKeyFile} -out {sigFile}" + let (resS, errS) = execCmdEx(sign) + check errS == 0 + + # 2. Verify using public key + let verify = &"echo -n '{msg}' | openssl dgst -sha256 -verify {pubKeyFile} -signature {sigFile}" + let (resV, errV) = execCmdEx(verify) + check errV == 0 + + proc signRfc6979(msg: string, num = 10) = + ## Signs the given message with a randomly generated private key `num` times + ## using deterministic nonce generation and verifies the signature comes out + ## identical each time. + var derSig: DerSignature[DerSigSize(Secp256k1)] + + let secKey = generatePrivateKey() + var sig {.noinit.}: Signature + sig.sign(secKey, toOA msg, nonceSampler = nsRfc6979) + for i in 0 ..< num: + var sig2 {.noinit.}: Signature + sig2.sign(secKey, toOA msg, nonceSampler = nsRfc6979) + check signatures_are_equal(sig, sig2) + + suite "General ECDSA related tests": + test "DERSigSize correctly computes maximum size of DER encoded signature": + # Check that `DerSigSize` correctly computes the maximum DER encoded signature + # based on the size of the scalar + check DerSigSize(Secp256k1) == 72 # 256 bit subgroup order -> 32 byte scalars + check DerSigSize(P256) == 72 # 256 bit subgroup order + check DerSigSize(Edwards25519) == 72 # 253 bits subgroup order, fits 256 bit BigInt + check DerSigSize(BLS12_381) == 72 # not commonly used, but larger modulo but *same* subgroup order + check DerSigSize(P224) == 64 # 224 bit subgroup order -> 28 byte scalars + check DerSigSize(BW6_761) == 104 # not commonly used, but larger modulo with *larger* subgroup order + # 377 bit subgroup order -> 384 BigInt -> 48 byte scalars + + suite "ECDSA over secp256k1": + test "Verify OpenSSL generated signatures from a fixed message (different nonces)": + signAndVerify(100, "Hello, Constantine!") # fixed message + + test "Verify OpenSSL generated signatures for different messages": + signAndVerify(100) # randomly generated message + + test "Verify deterministic nonce generation via RFC6979 yields deterministic signatures": + signRfc6979("Hello, Constantine!") + signRfc6979("Foobar is 42") + + test "Verify PEM file serialization for public and private keys": + verifyPemWriter(100) diff --git a/tests/openssl_wrapper.nim b/tests/openssl_wrapper.nim new file mode 100644 index 000000000..a5be82681 --- /dev/null +++ b/tests/openssl_wrapper.nim @@ -0,0 +1,155 @@ +# Deal with platform mess +# -------------------------------------------------------------------- +when defined(windows): + when sizeof(int) == 8: + const DLL_SSL_Name* = "(libssl-1_1-x64|ssleay64|libssl64).dll" + else: + const DLL_SSL_Name* = "(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|)" + + when defined(macosx) or defined(macos) or defined(ios): + const DLL_SSL_Name* = "libssl" & versions & ".dylib" + elif defined(genode): + const DLL_SSL_Name* = "libssl.lib.so" + else: + const DLL_SSL_Name* = "libssl.so" & versions + +# OpenSSL wrapper +# -------------------------------------------------------------------- + +# OpenSSL removed direct use of their SHA256 function. https://github.com/openssl/openssl/commit/4d49b68504cc494e552bce8e0b82ec8b501d5abe +# It isn't accessible anymore in Windows CI on Github Action. +# But the new API isn't expose on Linux :/ + +# TODO: fix Windows +when not defined(windows): + proc SHA256[T: byte|char]( + msg: openarray[T], + digest: ptr array[32, byte] = nil + ): ptr array[32, byte] {.noconv, dynlib: DLL_SSL_Name, importc.} + + # proc EVP_Q_digest[T: byte|char]( + # ossl_libctx: pointer, + # algoName: cstring, + # propq: cstring, + # data: openArray[T], + # digest: var array[32, byte], + # size: ptr uint): int32 {.noconv, dynlib: DLL_SSL_Name, importc.} + + proc SHA256_OpenSSL*[T: byte|char]( + digest: var array[32, byte], + s: openArray[T]) = + discard SHA256(s, digest.addr) + # discard EVP_Q_digest(nil, "SHA256", nil, s, digest, nil) + +type + BIGNUM_Obj* = object + EC_KEY_Obj* = object + EC_GROUP_Obj* = object + + EVP_PKEY_Obj* = object + EVP_MD_CTX_Obj* = object + EVP_PKEY_CTX_Obj* = object + + EVP_PKEY* = ptr EVP_PKEY_Obj + EVP_MD_CTX* = ptr EVP_MD_CTX_Obj + EVP_PKEY_CTX* = ptr EVP_PKEY_CTX_Obj + + BIGNUM* = ptr BIGNUM_Obj + EC_KEY* = ptr EC_KEY_Obj + EC_GROUP* = ptr EC_GROUP_Obj + + BIO_Obj* = object + BIO* = ptr BIO_Obj + + OSSL_ENCODER_CTX_Obj* = object + OSSL_ENCODER_CTX* = ptr OSSL_ENCODER_CTX_Obj + + OSSL_LIB_CTX_Obj* = object + OSSL_PROVIDER_Obj* = object + OSSL_LIB_CTX* = ptr OSSL_LIB_CTX_Obj + OSSL_PROVIDER* = ptr OSSL_PROVIDER_Obj + OSSL_PARAM_Obj* = object + OSSL_PARAM* = ptr OSSL_PARAM_Obj + + OSSL_PARAM_BLD_Obj* = object + OSSL_PARAM_BLD* = ptr OSSL_PARAM_BLD_Obj + +## Push the pragmas to clean up the code a bit +{.push noconv, importc, dynlib: DLL_SSL_Name.} +proc EVP_MD_CTX_new*(): EVP_MD_CTX +proc EVP_MD_CTX_free*(ctx: EVP_MD_CTX) + +proc EVP_sha256*(): pointer + +proc EVP_DigestSignInit*(ctx: EVP_MD_CTX, + pctx: ptr EVP_PKEY_CTX, + typ: pointer, + e: pointer, + pkey: EVP_PKEY): cint + +proc EVP_DigestSign*(ctx: EVP_MD_CTX, + sig: ptr byte, + siglen: ptr uint, + tbs: ptr byte, + tbslen: uint): cint + +proc BN_bin2bn*(s: ptr byte, len: cint, ret: BIGNUM): BIGNUM +proc BN_new*(): BIGNUM +proc BN_free*(bn: BIGNUM) + +proc EC_KEY_new_by_curve_name*(nid: cint): EC_KEY +proc EC_KEY_set_private_key*(key: EC_KEY, priv: BIGNUM): cint + +proc EC_KEY_get0_group*(key: EC_KEY): EC_GROUP +proc EC_KEY_generate_key*(key: EC_KEY): cint + +proc EVP_PKEY_new*(): EVP_PKEY + +## NOTE: This is _also_ now outdated and one should use the `EVP_PKEY_fromdata` function +## in theory: +## https://docs.openssl.org/master/man3/EVP_PKEY_fromdata/ +proc EVP_PKEY_set1_EC_KEY*(pkey: EVP_PKEY, key: EC_KEY): cint + +proc BIO_new_file*(filename: cstring, mode: cstring): BIO +proc BIO_free*(bio: BIO): cint +{.pop.} + +proc initPrivateKeyOpenSSL*(pkey: var EVP_PKEY, rawKey: openArray[byte]) = + ## Initializes an OpenSSL private key of `EVP_PKEY` type from a given + ## raw private key in bytes. + let bn = BN_new() + discard BN_bin2bn(unsafeAddr rawKey[0], rawKey.len.cint, bn) + + let eckey = EC_KEY_new_by_curve_name(714) # NID_secp256k1 + discard EC_KEY_set_private_key(eckey, bn) + + pkey = EVP_PKEY_new() + discard EVP_PKEY_set1_EC_KEY(pkey, eckey) + + BN_free(bn) + +proc signMessageOpenSSL*(sig: var array[72, byte], msg: openArray[byte], key: EVP_PKEY) = + ## Sign a message with OpenSSL and return the resulting DER encoded signature in `sig`. + let ctx = EVP_MD_CTX_new() + var pctx: EVP_PKEY_CTX + + if EVP_DigestSignInit(ctx, addr pctx, EVP_sha256(), nil, key) <= 0: + raise newException(Exception, "Signing init failed") + + # Get required signature length + var sigLen: uint + if EVP_DigestSign(ctx, nil, addr sigLen, nil, 0.uint) <= 0: + raise newException(Exception, "Getting sig length failed") + + doAssert sigLen.int == 72 + + if EVP_DigestSign(ctx, addr sig[0], addr sigLen, + unsafeAddr msg[0], msg.len.uint) <= 0: + raise newException(Exception, "Signing failed") + + EVP_MD_CTX_free(ctx) diff --git a/tests/t_hash_sha256_vs_openssl.nim b/tests/t_hash_sha256_vs_openssl.nim index 6c0025b14..8df742b92 100644 --- a/tests/t_hash_sha256_vs_openssl.nim +++ b/tests/t_hash_sha256_vs_openssl.nim @@ -5,47 +5,14 @@ import # Helpers helpers/prng_unsafe -# 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|)" - - 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 -# -------------------------------------------------------------------- - -# OpenSSL removed direct use of their SHA256 function. https://github.com/openssl/openssl/commit/4d49b68504cc494e552bce8e0b82ec8b501d5abe -# It isn't accessible anymore in Windows CI on Github Action. -# But the new API EVP_Q_digest isn't accesible either -# TODO: fix Windows -when not defined(windows): - proc EVP_Q_digest[T: byte|char]( - ossl_libctx: pointer, - algoName: cstring, - propq: cstring, - data: openArray[T], - digest: var array[32, byte], - size: ptr uint): int32 {.noconv, dynlib: DLLSSLName, importc.} - - proc SHA256_OpenSSL[T: byte|char]( - digest: var array[32, byte], - s: openArray[T]) = - # discard SHA256(s, digest.addr) - discard EVP_Q_digest(nil, "SHA256", nil, s, digest, nil) +## 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 ./openssl_wrapper # Test cases # --------------------------------------------------------------------