From 4d9674bd714aae3a7c6a5f3d87ae1b20dda4bbdf Mon Sep 17 00:00:00 2001 From: mratsim Date: Mon, 26 Oct 2020 18:04:29 +0100 Subject: [PATCH] Rework BLSv4 API https://github.com/ethereum/eth2.0-specs/pull/2080 and address https://github.com/status-im/nim-blscurve/issues/76 https://github.com/status-im/nim-blscurve/issues/77 --- blscurve/bls_backend.nim | 4 +- blscurve/eth2_keygen/hkdf_mod_r_miracl.nim | 8 +- blscurve/miracl/bls_sig_io.nim | 16 +++ blscurve/miracl/bls_signature_scheme.nim | 124 +++++++++++++++++---- blscurve/miracl/common.nim | 4 + tests/priv_to_pub.nim | 8 +- 6 files changed, 133 insertions(+), 31 deletions(-) diff --git a/blscurve/bls_backend.nim b/blscurve/bls_backend.nim index 11b9882..43870ee 100644 --- a/blscurve/bls_backend.nim +++ b/blscurve/bls_backend.nim @@ -48,7 +48,7 @@ else: SecretKey, PublicKey, Signature, ProofOfPossession, AggregateSignature, `==`, - init, aggregate, finish, + init, aggregate, finish, aggregateAll, sign, verify, aggregateVerify, fastAggregateVerify, - privToPub, + publicFromSecret, isZero, fromHex, fromBytes, toHex, serialize, exportRaw diff --git a/blscurve/eth2_keygen/hkdf_mod_r_miracl.nim b/blscurve/eth2_keygen/hkdf_mod_r_miracl.nim index c71320c..dfb840a 100644 --- a/blscurve/eth2_keygen/hkdf_mod_r_miracl.nim +++ b/blscurve/eth2_keygen/hkdf_mod_r_miracl.nim @@ -56,7 +56,7 @@ func hkdf_mod_r*(secretKey: var SecretKey, ikm: openArray[byte], key_info: strin # The cast is a workaround for private field access BIG_384_dmod(cast[ptr BIG_384](secretKey.addr)[], dseckey, CURVE_Order) - if bool cast[ptr BIG_384](secretKey.addr)[].BIG_384_iszilch(): + if secretKey.isZero(): salt = sha256.digest(salt0) else: return true @@ -120,11 +120,13 @@ func keyGen*(ikm: openarray[byte], publicKey: var PublicKey, secretKey: var Secr if ikm.len < 32: return false - let ok = secretKey.hkdf_mod_r(ikm, key_info) + var ok = secretKey.hkdf_mod_r(ikm, key_info) if not ok: return false # 4. xP = x * P # 6. PK = point_to_pubkey(xP) - publicKey = privToPub(secretKey) + ok = publicKey.publicFromSecret(secretKey) + if not ok: + return false return true diff --git a/blscurve/miracl/bls_sig_io.nim b/blscurve/miracl/bls_sig_io.nim index 481e6ee..a75ae48 100644 --- a/blscurve/miracl/bls_sig_io.nim +++ b/blscurve/miracl/bls_sig_io.nim @@ -22,10 +22,18 @@ func fromHex*[T: SecretKey|PublicKey|Signature|ProofOfPossession]( ## Initialize a BLS signature scheme object from ## its hex raw bytes representation. ## Returns true on a success and false otherwise + ## For secret key deserialization + ## A zero key is invalid when obj is SecretKey: result = obj.intVal.fromHex(hexStr) + if obj.intVal.isZilch(): + return false else: result = obj.point.fromHex(hexStr) + when obj is PublicKey: + # KeyValidate + if obj.point.isInf(): + result = false func fromBytes*[T: SecretKey|PublicKey|Signature|ProofOfPossession]( obj: var T, @@ -34,10 +42,18 @@ func fromBytes*[T: SecretKey|PublicKey|Signature|ProofOfPossession]( ## Initialize a BLS signature scheme object from ## its raw bytes representation. ## Returns true on success and false otherwise + ## For secret key deserialization + ## A zero key is invalid when obj is SecretKey: result = obj.intVal.fromBytes(raw) + if obj.intVal.isZilch(): + return false else: result = obj.point.fromBytes(raw) + when obj is PublicKey: + # KeyValidate + if obj.point.isInf(): + result = false func toHex*(obj: SecretKey|PublicKey|Signature|ProofOfPossession): string {.inline.} = ## Return the hex representation of a BLS signature scheme object diff --git a/blscurve/miracl/bls_signature_scheme.nim b/blscurve/miracl/bls_signature_scheme.nim index 7bd9026..201a6fa 100644 --- a/blscurve/miracl/bls_signature_scheme.nim +++ b/blscurve/miracl/bls_signature_scheme.nim @@ -41,6 +41,9 @@ import ./hash_to_curve type SecretKey* = object ## A secret key in the BLS (Boneh-Lynn-Shacham) signature scheme. + ## + ## This SecretKey is non-zero by construction. + ## ## This secret key SHOULD be protected against: ## - side-channel attacks: ## implementation must perform exactly the same memory access @@ -103,55 +106,103 @@ func subgroupCheck(P: GroupG1 or GroupG2): bool = rP.mul(CURVE_Order) result = rP.isInf() -func secretKeyToPublickey*(secretKey: SecretKey): PublicKey {.noInit.} = +func isZero*(seckey: SecretKey): bool = + ## Returns true if the secret key is zero + ## Those are invalid + result = seckey.intVal.iszilch() + +func publicFromSecret*(pubkey: var PublicKey, seckey: SecretKey): bool {.noInit.} = ## Generates a public key from a secret key - # Inputs: - # - SK, a secret integer such that 0 <= SK < r. - # - # Outputs: - # - PK, a public key encoded as an octet string. + ## Inputs: + ## - SK, a secret integer such that 1 <= SK < r. + ## + ## Outputs: + ## - PK, a public key encoded as an octet string. + ## + ## Returns: + ## - false is secret key is invalid (SK == 0), true otherwise + ## + ## Side-channel/Constant-time considerations: + ## The SK content is not revealed unless its value + ## is exactly 0 # # Procedure: # 1. xP = SK * P # 2. PK = point_to_pubkey(xP) # 3. return PK - result.point = generator1() - result.point.mul(secretKey.intVal) -func privToPub*(secretKey: SecretKey): PublicKey {.noInit, inline, deprecated: "Use secretKeyToPublickey instead".} = - secretKeyToPublickey(secretKey) + + # Always != 0: + # keyGen, deriveChild_secretKey, fromHex, fromBytes guarantee that. + if seckey.isZero(): + return false + pubkey.point = generator1() + pubkey.point.mul(secKey.intVal) + return true # Aggregate # ---------------------------------------------------------------------- +# 2.8. Aggregate (BLSv4) +# +# The Aggregate algorithm aggregates multiple signatures into one. +# signature = Aggregate((signature_1, ..., signature_n)) +# +# Inputs: +# - signature_1, ..., signature_n, octet strings output by +# either CoreSign or Aggregate. +# +# Outputs: +# - signature, an octet string encoding a aggregated signature +# that combines all inputs; or INVALID. +# +# Precondition: n >= 1, otherwise return INVALID. +# +# Procedure: +# 1. aggregate = signature_to_point(signature_1) +# 2. If aggregate is INVALID, return INVALID +# 3. for i in 2, ..., n: +# 4. next = signature_to_point(signature_i) +# 5. If next is INVALID, return INVALID +# 6. aggregate = aggregate + next +# 7. signature = point_to_signature(aggregate) +# +# Comments: +# - This does not require signatures to be non-zero func init*(agg: var AggregateSignature, sig: Signature) {.inline.} = ## Initialize an aggregate signature with a signature agg = AggregateSignature(sig) proc aggregate*(agg: var AggregateSignature, sig: Signature) {.inline.} = - ## Aggregates signature ``sig2`` into ``sig1``. + ## Returns the aggregate signature of ``sig1`` + ``sig2``. + # Precondition n >= 1 is respected agg.point.add(sig.point) proc aggregate*(agg: var AggregateSignature, sigs: openarray[Signature]) = - ## Aggregates an array of signatures `sigs` into a signature `sig` + ## Returns the aggregate signature of ``sig1`` + ``sigs[0..= 1 is respected even if sigs.len == 0 for s in sigs: - agg.point.add(s.point) + agg.aggregate(s) proc finish*(sig: var Signature, agg: AggregateSignature) {.inline.} = ## Canonicalize the AggregateSignature into a Signature sig = Signature(agg) -proc aggregate*(sigs: openarray[Signature]): Signature = - ## Aggregates array of signatures ``sigs`` - ## and return aggregated signature. - ## +proc aggregateAll*(dst: var Signature, sigs: openarray[Signature]): bool = + ## Returns the aggregate signature of ``sigs[0.. 0) - result = sigs[0] + ## + ## Returns false if `sigs` is the empty array + ## and true otherwise + if len(sigs) == 0: + return false + dst = sigs[0] for i in 1 ..< sigs.len: - result.point.add(sigs[i].point) + dst.point.add(sigs[i].point) + return true # Core operations # ---------------------------------------------------------------------- @@ -175,6 +226,11 @@ func coreSign[T: byte|char]( domainSepTag: static string): GroupG2 = ## Computes a signature or proof-of-possession ## from a secret key and a message + ## + ## The SecretKey MUST be directly created via + ## `keyGen` or `derive_child_secretKey` + ## or deserialized from `fromBytes` or `fromHex`. + ## This ensures the precondition that it's not a zero key. # Spec # 1. Q = hash_to_point(message) # 2. R = SK * Q @@ -190,6 +246,10 @@ func coreVerify[T: byte|char]( domainSepTag: static string): bool = ## Check that a signature (or proof-of-possession) is valid ## for a message (or serialized publickey) under the provided public key + ## + ## PublicKey MUST be non-zero + ## `publicFromSecret`, `fromHex`, `fromBytes` ensure that. + ## # Spec # 1. R = signature_to_point(signature) # 2. If R is INVALID, return INVALID @@ -213,6 +273,7 @@ func coreVerify[T: byte|char]( if not subgroupCheck(sig_or_proof.point): return false # 4. If KeyValidate(PK) is INVALID, return INVALID + # We assumes PK is not 0 if not subgroupCheck(publicKey.point): return false let Q = hashToG2(message, domainSepTag) @@ -331,7 +392,9 @@ func popProve*(secretKey: SecretKey): ProofOfPossession = # 4. R = SK * Q # 5. proof = point_to_signature(R) # 6. return proof - let pubkey = privToPub(secretKey) + var pubkey {.noInit.}: PublicKey + let ok {.used.} = pubkey.publicFromSecret(secretKey) + assert ok, "The secret key is INVALID, it should be initialized non-zero with keyGen or derive_child_secretKey" result = popProve(secretKey, pubkey) func popVerify*(publicKey: PublicKey, proof: ProofOfPossession): bool = @@ -351,6 +414,11 @@ func popVerify*(publicKey: PublicKey, proof: ProofOfPossession): bool = func sign*[T: byte|char](secretKey: SecretKey, message: openarray[T]): Signature = ## Computes a signature ## from a secret key and a message + ## + ## The SecretKey MUST be directly created via + ## `keyGen` or `derive_child_secretKey` + ## or deserialized from `fromBytes` or `fromHex`. + ## This ensures the precondition that it's not a zero key. result.point = secretKey.coreSign(message, DST) func verify*[T: byte|char]( @@ -364,6 +432,11 @@ func verify*[T: byte|char]( ## ## Compared to the IETF spec API, it is modified to ## enforce proper usage of the proof-of-possession + ## + ## The PublicKey MUST be directly created via + ## `publicFromPrivate` + ## or deserialized from `fromBytes` or `fromHex`. + ## This ensures the precondition that it's not a zero key. if not publicKey.popVerify(proof): return false return publicKey.coreVerify(message, signature, DST) @@ -379,6 +452,11 @@ func verify*[T: byte|char]( ## The proof-of-possession MUST be verified before calling this function. ## It is recommended to use the overload that accepts a proof-of-possession ## to enforce correct usage. + ## + ## The PublicKey MUST be directly created via + ## `publicFromPrivate` + ## or deserialized from `fromBytes` or `fromHex`. + ## This ensures the precondition that it's not a zero key. return publicKey.coreVerify(message, signature, DST) func aggregateVerify*( diff --git a/blscurve/miracl/common.nim b/blscurve/miracl/common.nim index 429e0ac..3e20f96 100644 --- a/blscurve/miracl/common.nim +++ b/blscurve/miracl/common.nim @@ -155,6 +155,10 @@ proc cmp*(a: BIG_384, b: BIG_384): int {.inline.} = ## Returns ``-1`` if ``a < b``, ``0`` if ``a == b``, ``1`` if ``a > b`` result = int(BIG_384_comp(a, b)) +proc iszilch*(a: BIG_384): bool {.inline.} = + ## Returns ``true`` if ``a`` is zero. + result = bool(BIG_384_iszilch(a)) + proc iszilch*(a: FP_BLS12381): bool {.inline.} = ## Returns ``true`` if ``a`` is zero. result = (FP_BLS12381_iszilch(unsafeAddr a) == 1) diff --git a/tests/priv_to_pub.nim b/tests/priv_to_pub.nim index b250bab..4dcc9b5 100644 --- a/tests/priv_to_pub.nim +++ b/tests/priv_to_pub.nim @@ -19,11 +19,13 @@ echo "----------------------------------\n" proc test_sk_to_pk(seckey, pubkey: string) = - var sk{.noInit.}: SecretKey + var + sk{.noInit.}: SecretKey + pk{.noInit.}: PublicKey let ok = sk.fromHex(seckey) doAssert ok - let pk = sk.privToPub() - + let ok2 = pk.publicFromSecret(sk) + doAssert ok2 doAssert pk.toHex() == pubkey, "\ncomputed: " & pk.toHex() & "\nexpected: " & pubkey & '\n' echo "SUCCESS"