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

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a322b87
[zoo] add generator for secp256k1
Vindaar Dec 8, 2024
83b0c6f
[ECDSA] add initial ECDSA signing / verifying implementation
Vindaar Dec 8, 2024
177fa84
[ecdsa] fix imports
Vindaar Dec 12, 2024
e4144f5
[ecdsa] export Secp256k1 as `C` for convenience
Vindaar Dec 12, 2024
f7cbc18
[ecdsa] export `toDER` proc
Vindaar Dec 12, 2024
f38aa2e
[ecdsa] use `isZero` instead of old zero comparison
Vindaar Dec 12, 2024
ab420c5
[ecdsa] rename private key generator & add private -> public key
Vindaar Dec 12, 2024
aadaa6b
[tests] add test cases for ECDSA signature verification
Vindaar Dec 12, 2024
64375f9
[ecdsa] handle some `.noinit.` cases
Vindaar Dec 21, 2024
fb4b414
[ecdsa] turn `toBytes`, `arrayWith` into in-place procedures
Vindaar Dec 21, 2024
a145ec5
[ecdsa] clean up comment about Fp -> Fr conversion
Vindaar Dec 21, 2024
de6205d
[ecdsa] replace toPemPrivateKey/PublicKey by in-place array variants
Vindaar Dec 21, 2024
5ea2d19
[ecdsa] replace `toDER` by non allocating variant
Vindaar Dec 21, 2024
93ac7bd
[ecdsa] replace out-of-place arithmetic by in-place
Vindaar Dec 23, 2024
d9fb203
[ecdsa] move ECDSA implementation to ~signatures~ directory
Vindaar Dec 23, 2024
2dac4c6
[ecdsa] remove dependence on explicit SHA256 hash function
Vindaar Dec 24, 2024
9269cef
[ecdsa] make DERSignature generic under curve by having static size
Vindaar Dec 24, 2024
2c6decf
[ecdsa] turn more procs generic over curve and hash function
Vindaar Dec 24, 2024
e8967ce
[ecdsa] replace sign/verify API by one matching BLS signatures
Vindaar Dec 24, 2024
b893080
[ecdsa] remove global curve & generator constants
Vindaar Dec 24, 2024
d54908b
[ecdsa] correctly handle truncation of digests > Fr BigInts
Vindaar Dec 24, 2024
18425cf
create file for common signature ops, `derivePubkey` for ECDSA & BLS
Vindaar Dec 24, 2024
6df2c10
create file specifically for ECDSA over secp256k1
Vindaar Dec 24, 2024
dd58bee
[ecdsa] add `fromDER` to split DER encoded signature back into r, s a…
Vindaar Dec 26, 2024
133098e
[tests] add OpenSSL wrapper intended for test cases
Vindaar Dec 26, 2024
c0f9d5c
[tests] first step towards OpenSSL tests
Vindaar Dec 26, 2024
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
356 changes: 356 additions & 0 deletions constantine/ecdsa/ecdsa.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
import
Copy link
Owner

Choose a reason for hiding this comment

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

ECDSA should be in the constantine/signatures folder

../hashes/h_sha256,
Copy link
Owner

Choose a reason for hiding this comment

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

It should be hash agnostic, with it's actual instantiation using a predefined hash.

See the function signature of BLS signatures

func coreSign*[Sig, SecKey](
signature: var Sig,
secretKey: SecKey,
message: openArray[byte],
H: type CryptoHash,
k: static int,
augmentation: openArray[byte],
domainSepTag: openArray[byte]) {.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 a Merkle-Damgaard hash function like SHA-2
## - `Hash` MAY be a sponge-based hash function like SHA-3 or BLAKE2
## - Otherwise, H MUST be a hash function that has been proved
## indifferentiable from a random oracle [MRH04] under a reasonable
## cryptographic assumption.
## - k the security parameter of the suite in bits (for example 128)
## - `output`, an elliptic curve point that will be overwritten.
## - `augmentation`, an optional augmentation to the message. This will be prepended,
## prior to hashing.
## This is used for building the "message augmentation" variant of BLS signatures
## https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#section-3.2
## which requires `CoreSign(SK, PK || message)`
## and `CoreVerify(PK, PK || message, signature)`
## - `message` is the message to hash
## - `domainSepTag` is the protocol domain separation tag (DST).
type ECP_Jac = EC_ShortW_Jac[Sig.F, Sig.G]
var sig {.noInit.}: ECP_Jac
H.hashToCurve(k, sig, augmentation, message, domainSepTag)
sig.scalarMul(secretKey)
signature.affine(sig)

../named/algebras,
../math/io/[io_bigints, io_fields, io_ec],
../math/elliptic/[ec_shortweierstrass_affine, ec_shortweierstrass_jacobian, ec_scalar_mul],
../math/arithmetic,
../platforms/abstractions,
../serialization/codecs, # for fromHex and (in the future) base64 encoding
../mac/mac_hmac, # for deterministic nonce generation via RFC 6979
../named/zoo_generators, # for generator
../csprngs/sysrand

import std / macros # for `update` convenience helper

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

# For easier readibility, define the curve and generator
# as globals in this file
const C* = Secp256k1
const G = Secp256k1.getGenerator("G1")
Copy link
Owner

Choose a reason for hiding this comment

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

The signature scheme itself should be generic in constantine/signatures and concrete implementations should appear in constantine/my_protocol.nim

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup, fair enough. I wrote it by defining the curve like this so that the code itself is already an (almost) valid generic / static proc. Didn't know if we want to make it fully generic immediately or start with secp256k1 only. Will address it.


proc hashMessage(message: string): array[32, byte] =
# Hash a given message
var h {.noinit.}: sha256
h.init()
h.update(message)
h.finish(result)
Copy link
Owner

Choose a reason for hiding this comment

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

This duplicates constantine/hashes.nim

type
CryptoHash* = concept h, var ctx, type H
## Interface of a cryptographic hash function
##
## - digestSizeInBytes is the hash output size in bytes
## - internalBlockSize, in bits:
## hash functions are supposed to ingest fixed block size
## that are padded if necessary
## - SHA256 block size is 64 bits
## - SHA512 block size is 128 bits
## - SHA3-512 block size is 72 bits
# should we avoid int to avoid exception? But they are compile-time
H.digestSize is static int
H.internalBlockSize is static int
# Context
# -------------------------------------------
ctx.init()
ctx.update(openarray[byte])
ctx.finish(var array[H.digestSize, byte])
ctx.clear()
func hash*[DigestSize: static int](
HashKind: type CryptoHash,
digest: var array[DigestSize, byte],
message: openArray[byte],
clearMem = false) {.genCharAPI.} =
## Produce a digest from a message
static: doAssert DigestSize == HashKind.type.digestSize
var ctx {.noInit.}: HashKind
ctx.init()
ctx.update(message)
ctx.finish(digest)
if clearMem:
ctx.clear()


proc toBytes(x: Fr[C] | Fp[C]): array[32, byte] =
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
proc toBytes(x: Fr[C] | Fp[C]): array[32, byte] =
proc toBytes(x: Fr[C] | Fp[C]): array[32, byte] {.noInit.} =

I assume this is temporary for debugging? Returning big arrays tends to generate bad code:

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, that either slipped my mind or I just wasn't aware of it! Good to know. Will change it to an in-place variant.

let bi = x.toBig()
discard result.marshal(bi, bigEndian)

proc toDER*(r, s: Fr[C]): seq[byte] =
Copy link
Owner

Choose a reason for hiding this comment

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

There should be no allocations for signature algorithms.

Also DER should likely go in constantine/serialization

Copy link
Collaborator Author

@Vindaar Vindaar Dec 13, 2024

Choose a reason for hiding this comment

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

Well, toDER is mainly for testing / convenience as also mentioned in its docstring. That's why I went with the simple seq approach here. Mainly because we cannot determine at compile time what length the resulting DER encoded data has (due to prefix 0 byte or dropped leading zero bytes). Otherwise I assume one needs to always use an array that can fit the longest variant and truncate later.

Copy link
Owner

Choose a reason for hiding this comment

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

I would defer then to how libsecp256k1 (https://github.com/bitcoin-core/secp256k1) or BearSSL (https://bearssl.org/#download-and-installation) do it

libsecp256k1 is blockchain-focused so some TLS oriented serialization is not available.
Neither allocates.

But it's fine to put a note for now with TODO: zero-alloc

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed the implementation to something inspired by Bitcoin's implementation. Now have a DERSignature type which has a 72 byte array buffer & an actual length and fill up to len bytes.

(We could of course also just have the proc take sig: var array[72, byte], outlen: var int parameters instead)

## Converts the given signature `(r, s)` into a signature in
## ASN.1 DER encoding following SEC1.
##
## Note that the implementation is not written for efficiency
## and should be viewed as a convenience tool for the time being.
# Convert signature to DER format
result = @[byte(0x30)] # sequence marker

# Convert r and s to big-endian bytes
var rBytes = @(r.toBytes())
var sBytes = @(s.toBytes())

# Add padding if needed (if high bit is set)
if (rBytes[0] and 0x80) != 0:
rBytes = @[byte(0)] & rBytes
if (sBytes[0] and 0x80) != 0:
sBytes = @[byte(0)] & sBytes

# Add integer markers and lengths
let rEncoded = @[byte(0x02), byte(rBytes.len)] & rBytes
let sEncoded = @[byte(0x02), byte(sBytes.len)] & sBytes

# Total length
let totalLen = rEncoded.len + sEncoded.len
result.add(byte(totalLen))

# Add r and s encodings
result.add(rEncoded)
result.add(sEncoded)

func fromDigest(dst: var Fr[C], src: array[32, byte]): bool {.discardable.} =
## Convert a SHA256 digest to an element in the scalar field `Fr[Secp256k1]`.
## 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.
var scalar {.noInit.}: BigInt[256]
scalar.unmarshal(src, bigEndian)
# `true` if smaller than modulus
result = bool(scalar < Fr[C].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()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In case this were to fail, do we want a different failure mode?

Copy link
Owner

Choose a reason for hiding this comment

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

For now it's fine not to address this, AFAIK most protocols use RFC6979 to avoid adding the RNG to the attack surface. If the RNG fails to produce data, I'm not even sure what it means at the OS level, no more entropy maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure either to be honest, but given that we do return a bool I didn't just want to discard it.


result.fromBig(b)

proc arrayWith[N: static int](val: byte): array[N, byte] =
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
proc arrayWith[N: static int](val: byte): array[N, byte] =
proc arrayWith[N: static int](val: byte): array[N, byte] {.noInit.}=

Same remark on returning big values

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(msgHash, privateKey: Fr[C]): Fr[C] =
## Generate deterministic nonce according to RFC 6979.
##
## Spec:
## https://datatracker.ietf.org/doc/html/rfc6979#section-3.2
# Step a: `h1 = H(m)` hash message (already done, input is hash), convert to array of bytes
let msgHashBytes = msgHash.toBytes()
# 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).
let privKeyBytes = privateKey.toBytes()

# Initial values
# Step b: `V = 0x01 0x01 0x01 ... 0x01`
var v = arrayWith[32](byte 0x01)
# Step c: `K = 0x00 0x00 0x00 ... 0x00`
var k = arrayWith[32](byte 0x00)

# Create HMAC contexts
var hmac {.noinit.}: HMAC[sha256]

# 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: Fr[C]
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
var candidate: Fr[C]
var candidate {.noinit.}: Fr[C]

# `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)

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(kind: NonceSampler, msgHash, privateKey: Fr[C]): Fr[C] =
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
proc generateNonce(kind: NonceSampler, msgHash, privateKey: Fr[C]): Fr[C] =
proc generateNonce(kind: NonceSampler, msgHash, privateKey: Fr[C]): Fr[C] {.noinit.} =

case kind
of nsRandom: randomFieldElement[Fr[C]]()
of nsRfc6979: nonceRfc6979(msgHash, privateKey)

proc signMessage*(message: string, privateKey: Fr[C],
nonceSampler: NonceSampler = nsRandom): tuple[r, s: Fr[C]] =
Copy link
Owner

Choose a reason for hiding this comment

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

You can reuse the API

func coreSign*[Sig, SecKey](
signature: var Sig,
secretKey: SecKey,
message: openArray[byte],
H: type CryptoHash,

## Sign a given `message` using the `privateKey`.
##
## 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
let h = hashMessage(message)
var message_hash: Fr[C]
message_hash.fromDigest(h)

# loop until we found a valid (non zero) signature
while true:
# Generate random nonce
var k = generateNonce(nonceSampler, message_hash, privateKey)

# Calculate r (x-coordinate of kG)
# `r = k·G (mod n)`
let r_point = k * G
# get x coordinate of the point `r` *in affine coordinates*
let rx = r_point.getAffine().x # element of Fp
## XXX: smarter way for this?
let r = Fr[C].fromBig(rx.toBig())
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need to convert the coordinate in Fp to a scalar in Fr. The "best" way I could come up with is this to big / from big conversion. Is there a cleaner way?

Copy link
Owner

Choose a reason for hiding this comment

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

This is the best generic conversion.

In theory for secp256k1, you could take mod r because Fp/Fr can be both use canonical representation and still have a fast reduction (#445) however at the moment only Fp uses canonical repr and fast reduction while Fr uses Montgomery representation and Montgomery reduction.


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`)
k.inv()
var s = (k * (message_hash + r * privateKey))
Copy link
Owner

@mratsim mratsim Dec 12, 2024

Choose a reason for hiding this comment

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

It might be faster to do k*privateKey and then a multiscalarmul([k, k*privkey], [message_hash, r]).

Note that the out-of-place functions are for convenience when debugging/testing/developping but not for production code (#413)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I understand. Still hasn't fully sunk in (intuitively) to avoid out-of-place procs! My bad. 😁

# 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

return (r: r, s: s)

proc verifySignature*(
message: string,
signature: tuple[r, s: Fr[C]],
publicKey: EC_ShortW_Aff[Fp[C], G1]
): bool =
## Verify a given `signature` for a `message` using the given `publicKey`.
# 1. Hash the message (same as in signing)
let h = hashMessage(message)
var e: Fr[C]
e.fromDigest(h)

# 2. Compute w = s⁻¹
var w = signature.s
w.inv() # w = s⁻¹

# 3. Compute u₁ = ew and u₂ = rw
let u1 = e * w
let u2 = signature.r * w

# 4. Compute u₁G + u₂Q
let point1 = u1 * G
let point2 = u2 * publicKey
let R = point1 + point2

# 5. Get x coordinate and convert to Fr (like in signing)
let x = R.getAffine().x
let r_computed = Fr[C].fromBig(x.toBig())

# 6. Verify r_computed equals provided r
result = bool(r_computed == signature.r)

proc generatePrivateKey*(): Fr[C] =
Copy link
Owner

Choose a reason for hiding this comment

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

This is not needed at this level, except maybe for testing.

Blockchain protocols have special ways to generate a private key usually from a mnemonic or via a HD derivation path (hierarchical deterministic) like BIP32 https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki or EIP-2333 (https://github.com/mratsim/constantine/blob/13d5bb7/constantine/ethereum_eip2333_bls12381_key_derivation.nim)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I assumed having a way to generate a random private key might be useful. For testing we can just move it to a test file of course.

## Generate a new private key using a cryptographic random number generator.
result = randomFieldElement[Fr[C]]()

proc getPublicKey*(pk: Fr[C]): EC_ShortW_Aff[Fp[C], G1] =
## Derives the public key from a given private key,
## `privateKey · G` in affine coordinates.
result = (pk * G).getAffine()
Copy link
Owner

Choose a reason for hiding this comment

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

Can reuse

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)


proc toPemPrivateKey(privateKey: Fr[C]): seq[byte] =
# Start with SEQUENCE
result = @[byte(0x30)]

# Version (always 1)
let version = @[byte(0x02), byte(1), byte(1)]

# Private key as octet string
let privKeyBytes = privateKey.toBytes()
let privKeyEncoded = @[byte(0x04), byte(privKeyBytes.len)] & @privKeyBytes

# Parameters (secp256k1 OID: 1.3.132.0.10)
let parameters = @[byte(0xA0), byte(7), byte(6), byte(5),
byte(0x2B), byte(0x81), byte(0x04), byte(0x00), byte(0x0A)]

# Combine all parts
let contents = version & privKeyEncoded & parameters
result.add(byte(contents.len))
result.add(contents)

proc toPemPublicKey(publicKey: EC_ShortW_Aff[Fp[C], G1]): seq[byte] =
# Start with SEQUENCE
result = @[byte(0x30)]

# Algorithm identifier
let 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
]

# Public key as bit string
let pubKeyBytes = @[
byte(0x00), # DER BIT STRING: number of unused bits (always 0 for keys)
byte(0x04) # SEC1: uncompressed point format marker
] & @(publicKey.x.toBytes()) & @(publicKey.y.toBytes()) # x & y coordinates

let pubKeyEncoded = @[byte(0x03), byte(pubKeyBytes.len)] & pubKeyBytes

# Combine all parts
let contents = algoId & pubKeyEncoded
result.add(byte(contents.len))
result.add(contents)
Copy link
Owner

Choose a reason for hiding this comment

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

For constantine/serialization and without dynamic alloc


## 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).

## XXX: Might also need to replace this by header / tail approach to avoid
## stdlib `%`!
import std / [strutils, base64, math]
const PrivateKeyTmpl = """-----BEGIN EC PRIVATE KEY-----
$#
-----END EC PRIVATE KEY-----
"""
const PublicKeyTmpl = """-----BEGIN PUBLIC KEY-----
$#
-----END PUBLIC KEY-----
"""

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 toPemFile*(publicKey: EC_ShortW_Aff[Fp[C], G1]): string =
## Convert a given private key to data in PEM format following SEC1
# 1. Convert public key to ASN.1 DER
let derB = publicKey.toPemPublicKey()
# 2. Encode bytes in base64
let der64 = derB.encode().wrap()
# 3. Wrap in begin/end public key template
result = PublicKeyTmpl % [der64]

proc toPemFile*(privateKey: Fr[C]): 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
# 1. Convert private key to ASN.1 DER encoding
let derB = toPemPrivateKey(privateKey)
# 2. Encode bytes in base64
let der64 = derB.encode().wrap()
# 3. Wrap in begin/end private key template
result = PrivateKeyTmpl % [der64]
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
Loading
Loading