Skip to content

Commit

Permalink
Updates BLS spec from v0 -> v2
Browse files Browse the repository at this point in the history
Implement @hwwhww's feedback. (Missed v0 -> v1 updates.)

Implements OS2IP and I2OSP for int <-> bytes conversions

Typing for i2osp & os2ip

Add in missing key_info type

Check number of sig/messages in aggregates.

Handles impossible case of _Aggregate_PKs returning True (for MyPy's sake).

Adds docstrings to i2osp & os2ip

Co-Authored-By: Hsiao-Wei Wang <[email protected]>

@hwwhww's nitpicks

Properly handle n < 1 cases in aggregates with assertions.
  • Loading branch information
CarlBeek authored and hwwhww committed May 6, 2020
1 parent 8981f54 commit c196fe5
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 102 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ pip install py_ecc

## BLS Signatures

`py_ecc` implements the [IETF BLS draft standard v0](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-00) with [hash-to-curve v6](https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-06) as per the inter-blockchain standardization agreement. The BLS standards specify [different ciphersuites](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-00#section-4.2) which each have different functionality to accommodate various use cases. The following ciphersuites are availible from this library:
`py_ecc` implements the [IETF BLS draft standard v0](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02) with [hash-to-curve v6](https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-06) as per the inter-blockchain standardization agreement. The BLS standards specify [different ciphersuites](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-4) which each have different functionality to accommodate various use cases. The following ciphersuites are available from this library:

- `G2Basic` also known as `BLS_SIG_BLS12381G2-SHA256-SSWU-RO-_NUL_`
- `G2MessageAugmentation` also known as `BLS_SIG_BLS12381G2-SHA256-SSWU-RO-_AUG_`
- `G2ProofOfPossession` also known as `BLS_SIG_BLS12381G2-SHA256-SSWU-RO-_POP_`
- `G2Basic` also known as `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_`
- `G2MessageAugmentation` also known as `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_`
- `G2ProofOfPossession` also known as `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_`

### Basic Usage

```python
from py_ecc.bls import G2ProofOfPossession as bls_pop

private_key = 5566
public_key = bls_pop.PrivToPub(private_key)
public_key = bls_pop.SkToPk(private_key)

message = b'\xab' * 32 # The message to be signed

Expand All @@ -39,7 +39,7 @@ assert bls_pop.Verify(public_key, message, signature)

```python
private_keys = [3, 14, 159]
public_keys = [bls_pop.PrivToPub(key) for key in private_keys]
public_keys = [bls_pop.SkToPk(key) for key in private_keys]
signatures = [bls_pop.Sign(key, message) for key in private_keys]

# Aggregating
Expand Down
91 changes: 49 additions & 42 deletions py_ecc/bls/ciphersuites.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import (
Sequence,
Tuple,
)
from math import (
ceil,
Expand All @@ -12,7 +11,6 @@
BLSSignature,
)
from eth_utils import (
big_endian_to_int,
ValidationError,
)
from hashlib import sha256
Expand All @@ -33,6 +31,8 @@
from .hash import (
hkdf_expand,
hkdf_extract,
i2osp,
os2ip,
)
from .hash_to_curve import hash_to_G2
from .g2_primatives import (
Expand All @@ -48,16 +48,16 @@ class BaseG2Ciphersuite(abc.ABC):
xmd_hash_function = sha256

@staticmethod
def PrivToPub(privkey: int) -> BLSPubkey:
def SkToPk(privkey: int) -> BLSPubkey:
return G1_to_pubkey(multiply(G1, privkey))

@staticmethod
def KeyGen(IKM: bytes) -> Tuple[BLSPubkey, int]:
prk = hkdf_extract(b'BLS-SIG-KEYGEN-SALT-', IKM)
def KeyGen(IKM: bytes, key_info: bytes = b'') -> int:
prk = hkdf_extract(b'BLS-SIG-KEYGEN-SALT-', IKM + b'\x00')
l = ceil((1.5 * ceil(log2(curve_order))) / 8) # noqa: E741
okm = hkdf_expand(prk, b'', l)
x = big_endian_to_int(okm) % curve_order
return (BaseG2Ciphersuite.PrivToPub(x), x)
okm = hkdf_expand(prk, key_info + i2osp(l, 2), l)
x = os2ip(okm) % curve_order
return x

@staticmethod
def KeyValidate(PK: BLSPubkey) -> bool:
Expand All @@ -77,6 +77,7 @@ def _CoreSign(cls, SK: int, message: bytes, DST: bytes) -> BLSSignature:
def _CoreVerify(cls, PK: BLSPubkey, message: bytes,
signature: BLSSignature, DST: bytes) -> bool:
try:
assert BaseG2Ciphersuite.KeyValidate(PK)
signature_point = signature_to_G2(signature)
final_exponentiation = final_exponentiate(
pairing(
Expand All @@ -95,24 +96,29 @@ def _CoreVerify(cls, PK: BLSPubkey, message: bytes,

@staticmethod
def Aggregate(signatures: Sequence[BLSSignature]) -> BLSSignature:
accumulator = Z2 # Seed with the point at infinity
assert len(signatures) >= 1, 'Insufficient number of signatures. (n < 1)'
aggregate = Z2 # Seed with the point at infinity
for signature in signatures:
signature_point = signature_to_G2(signature)
accumulator = add(accumulator, signature_point)
return G2_to_signature(accumulator)
aggregate = add(aggregate, signature_point)
return G2_to_signature(aggregate)

@classmethod
def _CoreAggregateVerify(cls, pairs: Sequence[Tuple[BLSPubkey, bytes]],
def _CoreAggregateVerify(cls, PKs: Sequence[BLSPubkey], messages: Sequence[bytes],
signature: BLSSignature, DST: bytes) -> bool:
try:
if len(PKs) != len(messages):
raise ValidationError('len(PKs) != len(messages)')
if len(PKs) < 1:
raise ValidationError('Insufficient number of signatures. (n < 1)')
signature_point = signature_to_G2(signature)
accumulator = FQ12.one()
for pk, message in pairs:
aggregate = FQ12.one()
for pk, message in zip(PKs, messages):
pubkey_point = pubkey_to_G1(pk)
message_point = hash_to_G2(message, DST, cls.xmd_hash_function)
accumulator *= pairing(message_point, pubkey_point, final_exponentiate=False)
accumulator *= pairing(signature_point, neg(G1), final_exponentiate=False)
return final_exponentiate(accumulator) == FQ12.one()
aggregate *= pairing(message_point, pubkey_point, final_exponentiate=False)
aggregate *= pairing(signature_point, neg(G1), final_exponentiate=False)
return final_exponentiate(aggregate) == FQ12.one()

except (ValidationError, ValueError, AssertionError):
return False
Expand All @@ -126,56 +132,53 @@ def Verify(cls, PK: BLSPubkey, message: bytes, signature: BLSSignature) -> bool:
return cls._CoreVerify(PK, message, signature, cls.DST)

@abc.abstractclassmethod
def AggregateVerify(cls, pairs: Sequence[Tuple[BLSPubkey, bytes]],
signature: BLSSignature) -> bool:
def AggregateVerify(cls, PKs: Sequence[BLSPubkey],
messages: Sequence[bytes], signature: BLSSignature) -> bool:
...


class G2Basic(BaseG2Ciphersuite):
DST = b'BLS_SIG_BLS12381G2-SHA256-SSWU-RO-_NUL_'
DST = b'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'

@classmethod
def AggregateVerify(cls, pairs: Sequence[Tuple[BLSPubkey, bytes]],
signature: BLSSignature) -> bool:
pairs = list(pairs)
_, messages = zip(*pairs)
def AggregateVerify(cls, PKs: Sequence[BLSPubkey],
messages: Sequence[bytes], signature: BLSSignature) -> bool:
if len(messages) != len(set(messages)): # Messages are not unique
return False
return cls._CoreAggregateVerify(pairs, signature, cls.DST)
return cls._CoreAggregateVerify(PKs, messages, signature, cls.DST)


class G2MessageAugmentation(BaseG2Ciphersuite):
DST = b'BLS_SIG_BLS12381G2-SHA256-SSWU-RO-_AUG_'
DST = b'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_'

@classmethod
def Sign(cls, SK: int, message: bytes) -> BLSSignature:
PK = cls.PrivToPub(SK)
PK = cls.SkToPk(SK)
return cls._CoreSign(SK, PK + message, cls.DST)

@classmethod
def Verify(cls, PK: BLSPubkey, message: bytes, signature: BLSSignature) -> bool:
return cls._CoreVerify(PK, PK + message, signature, cls.DST)

@classmethod
def AggregateVerify(cls, pairs: Sequence[Tuple[BLSPubkey, bytes]],
signature: BLSSignature) -> bool:
pairs = list(pairs)
pairs = [(pk, pk + msg) for pk, msg in pairs]
return cls._CoreAggregateVerify(pairs, signature, cls.DST)
def AggregateVerify(cls, PKs: Sequence[BLSPubkey],
messages: Sequence[bytes], signature: BLSSignature) -> bool:
messages = [pk + msg for pk, msg in zip(PKs, messages)]
return cls._CoreAggregateVerify(PKs, messages, signature, cls.DST)


class G2ProofOfPossession(BaseG2Ciphersuite):
DST = b'BLS_SIG_BLS12381G2-SHA256-SSWU-RO-_POP_'
POP_TAG = b'BLS_POP_BLS12381G2-SHA256-SSWU-RO-_POP_'
DST = b'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_'
POP_TAG = b'BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_'

@classmethod
def AggregateVerify(cls, pairs: Sequence[Tuple[BLSPubkey, bytes]],
signature: BLSSignature) -> bool:
return cls._CoreAggregateVerify(pairs, signature, cls.DST)
def AggregateVerify(cls, PKs: Sequence[BLSPubkey],
messages: Sequence[bytes], signature: BLSSignature) -> bool:
return cls._CoreAggregateVerify(PKs, messages, signature, cls.DST)

@classmethod
def PopProve(cls, SK: int) -> BLSSignature:
pubkey = cls.PrivToPub(SK)
pubkey = cls.SkToPk(SK)
return cls._CoreSign(SK, pubkey, cls.POP_TAG)

@classmethod
Expand All @@ -184,14 +187,18 @@ def PopVerify(cls, PK: BLSPubkey, proof: BLSSignature) -> bool:

@staticmethod
def _AggregatePKs(PKs: Sequence[BLSPubkey]) -> BLSPubkey:
accumulator = Z1 # Seed with the point at infinity
assert len(PKs) >= 1, 'Insufficient number of PKs. (n < 1)'
aggregate = Z1 # Seed with the point at infinity
for pk in PKs:
pubkey_point = pubkey_to_G1(pk)
accumulator = add(accumulator, pubkey_point)
return G1_to_pubkey(accumulator)
aggregate = add(aggregate, pubkey_point)
return G1_to_pubkey(aggregate)

@classmethod
def FastAggregateVerify(cls, PKs: Sequence[BLSPubkey],
message: bytes, signature: BLSSignature) -> bool:
aggregate_pubkey = cls._AggregatePKs(PKs)
try:
aggregate_pubkey = cls._AggregatePKs(PKs)
except AssertionError:
return False
return cls.Verify(aggregate_pubkey, message, signature)
4 changes: 0 additions & 4 deletions py_ecc/bls/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
POW_2_382 = 2**382
POW_2_383 = 2**383


# Store all the possible single bytes for faster access in hash-to-field
ALL_BYTES = tuple(bytes([i]) for i in range(256))

# Paramaters for hashing to the field as specified in:
# https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-06#section-8.7
HASH_TO_FIELD_L = 64
17 changes: 8 additions & 9 deletions py_ecc/bls/g2_primatives.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
BLSPubkey,
)
from eth_utils import (
big_endian_to_int,
ValidationError,
)

Expand All @@ -14,6 +13,10 @@
)
from py_ecc.typing import Optimized_Point3D

from .hash import (
i2osp,
os2ip,
)
from .point_compression import (
compress_G1,
decompress_G1,
Expand All @@ -34,15 +37,11 @@ def subgroup_check(P: Optimized_Point3D) -> bool:

def G2_to_signature(pt: G2Uncompressed) -> BLSSignature:
z1, z2 = compress_G2(pt)
return BLSSignature(
z1.to_bytes(48, "big") + z2.to_bytes(48, "big")
)
return BLSSignature(i2osp(z1, 48) + i2osp(z2, 48))


def signature_to_G2(signature: BLSSignature) -> G2Uncompressed:
p = G2Compressed(
(big_endian_to_int(signature[:48]), big_endian_to_int(signature[48:]))
)
p = G2Compressed((os2ip(signature[:48]), os2ip(signature[48:])))
signature_point = decompress_G2(p)
if not subgroup_check(signature_point):
raise ValidationError('Signature is not a part of the E2 subgroup.')
Expand All @@ -51,11 +50,11 @@ def signature_to_G2(signature: BLSSignature) -> G2Uncompressed:

def G1_to_pubkey(pt: G1Uncompressed) -> BLSPubkey:
z = compress_G1(pt)
return BLSPubkey(z.to_bytes(48, "big"))
return BLSPubkey(i2osp(z, 48))


def pubkey_to_G1(pubkey: BLSPubkey) -> G1Uncompressed:
z = big_endian_to_int(pubkey)
z = os2ip(pubkey)
pubkey_point = decompress_G1(G1Compressed(z))
if not subgroup_check(pubkey_point):
raise ValidationError('Pubkey is not a part of the E1 subgroup.')
Expand Down
39 changes: 29 additions & 10 deletions py_ecc/bls/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@
from typing import (
Union,
)
from hashlib import sha256
import hashlib
from _hashlib import HASH

from .constants import (
ALL_BYTES,
)


def hkdf_extract(salt: Union[bytes, bytearray], ikm: Union[bytes, bytearray]) -> bytes:
"""
HKDF-Extract
https://tools.ietf.org/html/rfc5869
"""
return hmac.new(salt, ikm, sha256).digest()
return hmac.new(salt, ikm, hashlib.sha256).digest()


def hkdf_expand(prk: Union[bytes, bytearray], info: Union[bytes, bytearray], length: int) -> bytes:
Expand All @@ -37,13 +33,35 @@ def hkdf_expand(prk: Union[bytes, bytearray], info: Union[bytes, bytearray], len
text = previous + info + bytes([i + 1])

# T(i + 1) = HMAC(T(i) || info || i)
previous = bytearray(hmac.new(prk, text, sha256).digest())
previous = bytearray(hmac.new(prk, text, hashlib.sha256).digest())
okm.extend(previous)

# Return first `length` bytes.
return okm[:length]


def i2osp(x: int, xlen: int) -> bytes:
"""
Convert a nonnegative integer `x` to an octet string of a specified length `xlen`.
https://tools.ietf.org/html/rfc8017#section-4.1
"""
return x.to_bytes(xlen, byteorder='big', signed=False)


def os2ip(x: bytes) -> int:
"""
Convert an octet string `x` to a nonnegative integer.
https://tools.ietf.org/html/rfc8017#section-4.2
"""
return int.from_bytes(x, byteorder='big', signed=False)


def sha256(x: bytes) -> bytes:
m = hashlib.sha256
m.update(x)
return m.digest()


def xor(a: bytes, b: bytes) -> bytes:
return bytes(_a ^ _b for _a, _b in zip(a, b))

Expand All @@ -56,12 +74,13 @@ def expand_message_xmd(msg: bytes, DST: bytes, len_in_bytes: int, hash_function:
ell = math.ceil(len_in_bytes / b_in_bytes)
if ell > 255:
raise ValueError('invalid len in bytes for hash function')
DST_prime = ALL_BYTES[len(DST)] + DST # Prepend the length if the DST as a single byte

DST_prime = i2osp(len(DST), 1) + DST
Z_pad = b'\x00' * r_in_bytes
l_i_b_str = len_in_bytes.to_bytes(2, 'big')
l_i_b_str = i2osp(len_in_bytes, 2)
b_0 = hash_function(Z_pad + msg + l_i_b_str + b'\x00' + DST_prime).digest()
b = [hash_function(b_0 + b'\x01' + DST_prime).digest()]
for i in range(2, ell + 1):
b.append(hash_function(xor(b_0, b[i - 2]) + ALL_BYTES[i] + DST_prime).digest())
b.append(hash_function(xor(b_0, b[i - 2]) + i2osp(i, 1) + DST_prime).digest())
pseudo_random_bytes = b''.join(b)
return pseudo_random_bytes[:len_in_bytes]
7 changes: 5 additions & 2 deletions py_ecc/bls/hash_to_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
)

from .constants import HASH_TO_FIELD_L
from .hash import expand_message_xmd
from .hash import (
expand_message_xmd,
os2ip,
)
from .typing import G2Uncompressed


Expand Down Expand Up @@ -56,7 +59,7 @@ def hash_to_field_FQ2(message: bytes, count: int,
for j in range(0, M):
elem_offset = HASH_TO_FIELD_L * (j + i * M)
tv = pseudo_random_bytes[elem_offset: elem_offset + HASH_TO_FIELD_L]
e.append(int.from_bytes(tv, 'big') % field_modulus)
e.append(os2ip(tv) % field_modulus)
u.append(FQ2(e))
return tuple(u)

Expand Down
4 changes: 2 additions & 2 deletions tests/bls/ciphersuites/test_g2_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
]
)
def test_aggregate_verify(SKs, messages, success):
PKs = [G2Basic.PrivToPub(SK) for SK in SKs]
PKs = [G2Basic.SkToPk(SK) for SK in SKs]
messages = [bytes(msg) for msg in messages]
signatures = [G2Basic.Sign(SK, msg) for SK, msg in zip(SKs, messages)]
aggregate_signature = G2Basic.Aggregate(signatures)
assert G2Basic.AggregateVerify(zip(PKs, messages), aggregate_signature) == success
assert G2Basic.AggregateVerify(PKs, messages, aggregate_signature) == success
Loading

0 comments on commit c196fe5

Please sign in to comment.