Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add ECDSA over secp256k1 signatures and verification #490

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[ECDSA] add initial ECDSA signing / verifying implementation
Vindaar committed Dec 30, 2024
commit 6434eba37d05a58a480868461f79a3d995b81665
351 changes: 351 additions & 0 deletions constantine/ecdsa/ecdsa.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
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] =
## Converts the given signature `(r, s)` into a signature in
## ASN.1 DER encoding.
##
## 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 == zero):
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 getPrivateKey*(): Fr[C] =
## Generate a new private key using a cryptographic random number generator.
result = randomFieldElement[Fr[C]]()

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]