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

Update to IETF BLS draft 04 and add some input validations #103

Merged
merged 2 commits into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pip install py_ecc

## BLS Signatures

`py_ecc` implements the [IETF BLS draft standard v3](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-03) as per the inter-blockchain standardization agreement. The BLS standards specify [different ciphersuites](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-03#section-4) which each have different functionality to accommodate various use cases. The following ciphersuites are available from this library:
`py_ecc` implements the [IETF BLS draft standard v4](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-04) as per the inter-blockchain standardization agreement. The BLS standards specify [different ciphersuites](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-04#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_XMD:SHA-256_SSWU_RO_NUL_`
- `G2MessageAugmentation` also known as `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_`
Expand Down
183 changes: 153 additions & 30 deletions py_ecc/bls/ciphersuites.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,100 @@
)


Z1_PUBKEY = G1_to_pubkey(Z1)
Z2_SIGNATURE = G2_to_signature(Z2)


def is_Z1_pubkey(PK: bytes) -> bool:
"""
Check if PK is point at infinity.
"""
return PK == Z1_PUBKEY


class BaseG2Ciphersuite(abc.ABC):
DST = b''
xmd_hash_function = sha256

#
# Input validation helpers
#
@staticmethod
def SkToPk(privkey: int) -> BLSPubkey:
return G1_to_pubkey(multiply(G1, privkey))
def _is_valid_privkey(privkey: int) -> bool:
return isinstance(privkey, int) and privkey > 0 and privkey < curve_order

@staticmethod
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, key_info + i2osp(l, 2), l)
x = os2ip(okm) % curve_order
return x
def _is_valid_pubkey(pubkey: bytes) -> bool:
# SV: minimal-pubkey-size
return isinstance(pubkey, bytes) and len(pubkey) == 48

@staticmethod
def _is_valid_message(message: bytes) -> bool:
return isinstance(message, bytes)

@staticmethod
def _is_valid_signature(signature: bytes) -> bool:
# SV: minimal-pubkey-size
return isinstance(signature, bytes) and len(signature) == 96

#
# APIs
#
@classmethod
def SkToPk(cls, privkey: int) -> BLSPubkey:
"""
The SkToPk algorithm takes a secret key SK and outputs the
corresponding public key PK.

Raise `ValidationError` when there is input validation error.
"""
try:
# Inputs validation
assert cls._is_valid_privkey(privkey)
except Exception as e:
raise ValidationError(e)

# Procedure
return G1_to_pubkey(multiply(G1, privkey))

@classmethod
def KeyGen(cls, IKM: bytes, key_info: bytes = b'') -> int:
salt = b'BLS-SIG-KEYGEN-SALT-'
SK = 0
while SK == 0:
salt = cls.xmd_hash_function(salt).digest()
prk = hkdf_extract(salt, IKM + b'\x00')
l = ceil((1.5 * ceil(log2(curve_order))) / 8) # noqa: E741
okm = hkdf_expand(prk, key_info + i2osp(l, 2), l)
SK = os2ip(okm) % curve_order
return SK

@staticmethod
def KeyValidate(PK: BLSPubkey) -> bool:
try:
pubkey_to_G1(PK)
if is_Z1_pubkey(PK):
return False
pubkey_to_G1(PK) # pubkey_to_G1 includes subgroup check
except ValidationError:
return False
return True

@classmethod
def _CoreSign(cls, SK: int, message: bytes, DST: bytes) -> BLSSignature:
"""
The CoreSign algorithm computes a signature from SK, a secret key,
and message, an octet string.

Raise `ValidationError` when there is input validation error.
"""
try:
# Inputs validation
assert cls._is_valid_privkey(SK)
assert cls._is_valid_message(message)
except Exception as e:
raise ValidationError(e)

# Procedure
message_point = hash_to_G2(message, DST, cls.xmd_hash_function)
signature_point = multiply(message_point, SK)
return G2_to_signature(signature_point)
Expand All @@ -77,7 +145,13 @@ 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)
# Inputs validation
assert cls._is_valid_pubkey(PK)
assert cls._is_valid_message(message)
assert cls._is_valid_signature(signature)

# Procedure
assert cls.KeyValidate(PK)
signature_point = signature_to_G2(signature)
final_exponentiation = final_exponentiate(
pairing(
Expand All @@ -94,13 +168,24 @@ def _CoreVerify(cls, PK: BLSPubkey, message: bytes,
except (ValidationError, ValueError, AssertionError):
return False

@staticmethod
def Aggregate(signatures: Sequence[BLSSignature]) -> BLSSignature:
if len(signatures) < 1:
raise ValidationError(
'Insufficient number of signatures: should be greater than'
' or equal to 1, got %d' % len(signatures)
)
@classmethod
def Aggregate(cls, signatures: Sequence[BLSSignature]) -> BLSSignature:
"""
The Aggregate algorithm aggregates multiple signatures into one.

Raise `ValidationError` when there is input validation error.
"""
try:
# Inputs validation
for signature in signatures:
assert cls._is_valid_signature(signature)

# Preconditions
assert len(signatures) >= 1
except Exception as e:
raise ValidationError(e)

# Procedure
aggregate = Z2 # Seed with the point at infinity
for signature in signatures:
signature_point = signature_to_G2(signature)
Expand All @@ -111,19 +196,22 @@ def Aggregate(signatures: Sequence[BLSSignature]) -> BLSSignature:
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): got len(PKs)=%s, len(messages)=%s'
% (len(PKs), len(messages))
)
if len(PKs) < 1:
raise ValidationError(
'Insufficient number of PKs: should be greater than'
' or equal to 1, got %d' % len(PKs)
)
# Inputs validation
for pk in PKs:
assert cls._is_valid_pubkey(pk)
for message in messages:
assert cls._is_valid_message(message)
assert len(PKs) == len(messages)
assert cls._is_valid_signature(signature)

# Preconditions
assert len(PKs) >= 1

# Procedure
signature_point = signature_to_G2(signature)
aggregate = FQ12.one()
for pk, message in zip(PKs, messages):
assert cls.KeyValidate(pk)
pubkey_point = pubkey_to_G1(pk)
message_point = hash_to_G2(message, DST, cls.xmd_hash_function)
aggregate *= pairing(message_point, pubkey_point, final_exponentiate=False)
Expand Down Expand Up @@ -173,6 +261,8 @@ def Verify(cls, PK: BLSPubkey, message: bytes, signature: BLSSignature) -> bool:
@classmethod
def AggregateVerify(cls, PKs: Sequence[BLSPubkey],
messages: Sequence[bytes], signature: BLSSignature) -> bool:
if len(PKs) != len(messages):
return False
messages = [pk + msg for pk, msg in zip(PKs, messages)]
return cls._CoreAggregateVerify(PKs, messages, signature, cls.DST)

Expand All @@ -181,6 +271,19 @@ class G2ProofOfPossession(BaseG2Ciphersuite):
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 _is_valid_pubkey(cls, pubkey: bytes) -> bool:
"""
Note: PopVerify is a precondition for -Verify APIs
However, it's difficult to verify it with the API interface in runtime.
To ensure `is_Z1_pubkey` is checked in `FastAggregateVerify`,
we check it in the input validation.
See https://github.com/cfrg/draft-irtf-cfrg-bls-signature/issues/27 for the discussion.
"""
if is_Z1_pubkey(pubkey):
return False
return super()._is_valid_pubkey(pubkey)

@classmethod
def AggregateVerify(cls, PKs: Sequence[BLSPubkey],
messages: Sequence[bytes], signature: BLSSignature) -> bool:
Expand All @@ -197,7 +300,16 @@ def PopVerify(cls, PK: BLSPubkey, proof: BLSSignature) -> bool:

@staticmethod
def _AggregatePKs(PKs: Sequence[BLSPubkey]) -> BLSPubkey:
assert len(PKs) >= 1, 'Insufficient number of PKs. (n < 1)'
"""
Aggregate the public keys.

Raise `ValidationError` when there is input validation error.
"""
try:
assert len(PKs) >= 1, 'Insufficient number of PKs. (n < 1)'
except Exception as e:
raise ValidationError(e)

aggregate = Z1 # Seed with the point at infinity
for pk in PKs:
pubkey_point = pubkey_to_G1(pk)
Expand All @@ -208,7 +320,18 @@ def _AggregatePKs(PKs: Sequence[BLSPubkey]) -> BLSPubkey:
def FastAggregateVerify(cls, PKs: Sequence[BLSPubkey],
message: bytes, signature: BLSSignature) -> bool:
try:
# Inputs validation
for pk in PKs:
assert cls._is_valid_pubkey(pk)
assert cls._is_valid_message(message)
assert cls._is_valid_signature(signature)

# Preconditions
assert len(PKs) >= 1

# Procedure
aggregate_pubkey = cls._AggregatePKs(PKs)
except AssertionError:
except (ValidationError, AssertionError):
return False
return cls.Verify(aggregate_pubkey, message, signature)
else:
return cls.Verify(aggregate_pubkey, message, signature)
72 changes: 67 additions & 5 deletions tests/bls/ciphersuites/test_g2_basic.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,78 @@
import pytest

from eth_utils import (
ValidationError,
)

from py_ecc.bls import G2Basic
from py_ecc.bls.ciphersuites import (
Z1_PUBKEY,
Z2_SIGNATURE,
)


@pytest.mark.parametrize(
'SKs,messages,success',
'SKs,messages,result',
[
(range(10), range(10), True),
(range(3), (b'42', b'69', b'42'), False), # Test duplicate messages fail
(list(range(1, 11)), list(range(1, 11)), True),
(list(range(1, 4)), (b'42', b'69', b'42'), False), # Test duplicate messages fail
]
)
def test_aggregate_verify(SKs, messages, success):
def test_aggregate_verify(SKs, messages, result):
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(PKs, messages, aggregate_signature) == success
assert G2Basic.AggregateVerify(PKs, messages, aggregate_signature) == result


@pytest.mark.parametrize(
'privkey, success',
[
(1, True),
(0, False),
('hello', False), # wrong type
]
)
def test_sk_to_pk(privkey, success):
if success:
G2Basic.SkToPk(privkey)
else:
with pytest.raises(ValidationError):
G2Basic.SkToPk(privkey)


@pytest.mark.parametrize(
'privkey, message, success',
[
(1, b'message', True),
(0, b'message', False),
('hello', b'message', False), # wrong type privkey
(1, 123, False), # wrong type message
]
)
def test_sign(privkey, message, success):
if success:
G2Basic.Sign(privkey, message)
else:
with pytest.raises(ValidationError):
G2Basic.Sign(privkey, message)



@pytest.mark.parametrize(
'signatures, success',
[
([G2Basic.Sign(1, b'helloworld')], True),
([G2Basic.Sign(1, b'helloworld'), G2Basic.Sign(2, b'helloworld')], True),
([Z2_SIGNATURE], True),
(['hello'], False),
([], False),
]
)
def test_aggregate(signatures, success):
if success:
G2Basic.Aggregate(signatures)
else:
with pytest.raises(ValidationError):
G2Basic.Aggregate(signatures)
5 changes: 2 additions & 3 deletions tests/bls/ciphersuites/test_g2_message_augmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
(735),
(127409812145),
(90768492698215092512159),
(0),
]
)
def test_sign_verify(privkey):
Expand All @@ -24,12 +23,12 @@ def test_sign_verify(privkey):
@pytest.mark.parametrize(
'SKs,messages',
[
(range(10), range(10)),
(list(range(1, 11)), list(range(1, 11)))
]
)
def test_aggregate_verify(SKs, messages):
PKs = [G2MessageAugmentation.SkToPk(SK) for SK in SKs]
messages = [bytes(msg) + PK for msg, PK in zip(messages, PKs)]
signatures = [G2MessageAugmentation.Sign(SK, msg) for SK, msg in zip(SKs, messages)]
aggregate_signature = G2MessageAugmentation.Aggregate(signatures)
assert G2MessageAugmentation.AggregateVerify(PKs, messages, aggregate_signature)
assert G2MessageAugmentation.AggregateVerify(PKs, messages, aggregate_signature)
Loading