From 67c9e351e3555a45c34b57872b6bf74e078a322d Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Mon, 27 Jun 2022 14:07:42 -0400 Subject: [PATCH] Support Secp256r1 curve --- pyteal/ast/ecdsa.py | 73 +++++++++++++++++------------- pyteal/ast/ecdsa_test.py | 96 ++++++++++++++++++++++++---------------- 2 files changed, 101 insertions(+), 68 deletions(-) diff --git a/pyteal/ast/ecdsa.py b/pyteal/ast/ecdsa.py index b85acf2c1..e32974279 100644 --- a/pyteal/ast/ecdsa.py +++ b/pyteal/ast/ecdsa.py @@ -2,7 +2,7 @@ from typing import Tuple, TYPE_CHECKING from pyteal.ast import Expr, MultiValue -from pyteal.errors import TealTypeError, verifyTealVersion +from pyteal.errors import TealTypeError, verifyFieldVersion, verifyTealVersion from pyteal.ir import Op, TealBlock, TealOp from pyteal.types import TealType, require_type @@ -17,6 +17,7 @@ class EcdsaCurve(Enum): """Enum representing an elliptic curve specification used in ECDSA.""" Secp256k1 = (0, "Secp256k1", 5) + Secp256r1 = (1, "Secp256r1", 7) def __init__(self, id: int, name: str, min_version: int) -> None: self.id = id @@ -52,11 +53,13 @@ def __init__( def __teal__(self, options: "CompileOptions"): verifyTealVersion( - max(self.op.min_version, self.curve.min_version), + self.op.min_version, options.version, "TEAL version too low to use op {}".format(self.op), ) + verifyFieldVersion(self.curve.arg_name, self.curve.min_version, options.version) + return TealBlock.FromOp( options, TealOp(self, self.op, self.curve.arg_name), *self.args ) @@ -110,8 +113,7 @@ def EcdsaVerify( return EcdsaVerifyExpr(curve, data, sigA, sigB, pubkey[0], pubkey[1]) - -def EcdsaDecompress(curve: EcdsaCurve, compressed_pk: Expr) -> MultiValue: +class EcdsaDecompress(MultiValue): """Decompress an ECDSA public key. Args: @@ -119,25 +121,29 @@ def EcdsaDecompress(curve: EcdsaCurve, compressed_pk: Expr) -> MultiValue: compressed_pk: The compressed public key. Must be 33 bytes long and big endian encoded. Returns: - A MultiValue expression representing the two components of the public key, big endian + A MultiValue instance representing the two components of the public key, big endian encoded. """ + def __init__(self, curve: EcdsaCurve, compressed_pk: Expr): + if not isinstance(curve, EcdsaCurve): + raise TealTypeError(curve, EcdsaCurve) - if not isinstance(curve, EcdsaCurve): - raise TealTypeError(curve, EcdsaCurve) + self.curve = curve - require_type(compressed_pk, TealType.bytes) - return MultiValue( - Op.ecdsa_pk_decompress, - EcdsaPubkey, - immediate_args=[curve.arg_name], - args=[compressed_pk], - ) + require_type(compressed_pk, TealType.bytes) + super().__init__( + Op.ecdsa_pk_decompress, + EcdsaPubkey, + immediate_args=[curve.arg_name], + args=[compressed_pk], + ) + + def __teal__(self, options: "CompileOptions"): + verifyFieldVersion(self.curve.arg_name, self.curve.min_version, options.version) + return super().__teal__(options) -def EcdsaRecover( - curve: EcdsaCurve, data: Expr, recovery_id: Expr, sigA: Expr, sigB: Expr -) -> MultiValue: +class EcdsaRecover(MultiValue): """Reover an ECDSA public key from a signature. All byte arguments must be big endian encoded. @@ -150,20 +156,27 @@ def EcdsaRecover( sigB: Second component of the signature. Must evaluate to bytes. Returns: - A MultiValue expression representing the two components of the public key, big endian + A MultiValue instance representing the two components of the public key, big endian encoded. """ + def __init__(self, curve: EcdsaCurve, data: Expr, recovery_id: Expr, sigA: Expr, sigB: Expr): + if not isinstance(curve, EcdsaCurve): + raise TealTypeError(curve, EcdsaCurve) - if not isinstance(curve, EcdsaCurve): - raise TealTypeError(curve, EcdsaCurve) + self.curve = curve + + require_type(data, TealType.bytes) + require_type(recovery_id, TealType.uint64) + require_type(sigA, TealType.bytes) + require_type(sigB, TealType.bytes) + super().__init__( + Op.ecdsa_pk_recover, + EcdsaPubkey, + immediate_args=[curve.arg_name], + args=[data, recovery_id, sigA, sigB], + ) + + def __teal__(self, options: "CompileOptions"): + verifyFieldVersion(self.curve.arg_name, self.curve.min_version, options.version) - require_type(data, TealType.bytes) - require_type(recovery_id, TealType.uint64) - require_type(sigA, TealType.bytes) - require_type(sigB, TealType.bytes) - return MultiValue( - Op.ecdsa_pk_recover, - EcdsaPubkey, - immediate_args=[curve.arg_name], - args=[data, recovery_id, sigA, sigB], - ) + return super().__teal__(options) diff --git a/pyteal/ast/ecdsa_test.py b/pyteal/ast/ecdsa_test.py index 681755a65..d39debcfa 100644 --- a/pyteal/ast/ecdsa_test.py +++ b/pyteal/ast/ecdsa_test.py @@ -4,18 +4,19 @@ teal4Options = pt.CompileOptions(version=4) teal5Options = pt.CompileOptions(version=5) +teal7Options = pt.CompileOptions(version=7) - -def test_ecdsa_decompress(): +@pytest.mark.parametrize("curve", [pt.EcdsaCurve.Secp256k1, pt.EcdsaCurve.Secp256r1]) +def test_ecdsa_decompress(curve: pt.EcdsaCurve): compressed_pubkey = pt.Bytes("XY") - pubkey = pt.EcdsaDecompress(pt.EcdsaCurve.Secp256k1, compressed_pubkey) + pubkey = pt.EcdsaDecompress(curve, compressed_pubkey) assert pubkey.type_of() == pt.TealType.none expected = pt.TealSimpleBlock( [ pt.TealOp(compressed_pubkey, pt.Op.byte, '"XY"'), pt.TealOp( - pubkey, pt.Op.ecdsa_pk_decompress, pt.EcdsaCurve.Secp256k1.arg_name + pubkey, pt.Op.ecdsa_pk_decompress, curve.arg_name ), pt.TealOp( pubkey.output_slots[1].store(), pt.Op.store, pubkey.output_slots[1] @@ -26,7 +27,7 @@ def test_ecdsa_decompress(): ] ) - actual, _ = pubkey.__teal__(teal5Options) + actual, _ = pubkey.__teal__(teal7Options) actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) @@ -34,13 +35,17 @@ def test_ecdsa_decompress(): assert actual == expected # compile without errors this is necessary so assembly is also tested - pt.compileTeal(pt.Seq(pubkey, pt.Approve()), pt.Mode.Application, version=5) + pt.compileTeal(pt.Seq(pubkey, pt.Approve()), pt.Mode.Application, version=curve.min_version) + + with pytest.raises(pt.TealInputError): + pt.compileTeal(pt.Seq(pubkey, pt.Approve()), pt.Mode.Application, version=curve.min_version - 1) -def test_ecdsa_recover(): +@pytest.mark.parametrize("curve", [pt.EcdsaCurve.Secp256k1, pt.EcdsaCurve.Secp256r1]) +def test_ecdsa_recover(curve: pt.EcdsaCurve): args = [pt.Bytes("data"), pt.Int(1), pt.Bytes("sigA"), pt.Bytes("sigB")] pubkey = pt.EcdsaRecover( - pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], args[3] + curve, args[0], args[1], args[2], args[3] ) assert pubkey.type_of() == pt.TealType.none @@ -50,7 +55,7 @@ def test_ecdsa_recover(): pt.TealOp(args[1], pt.Op.int, 1), pt.TealOp(args[2], pt.Op.byte, '"sigA"'), pt.TealOp(args[3], pt.Op.byte, '"sigB"'), - pt.TealOp(pubkey, pt.Op.ecdsa_pk_recover, pt.EcdsaCurve.Secp256k1.arg_name), + pt.TealOp(pubkey, pt.Op.ecdsa_pk_recover, curve.arg_name), pt.TealOp( pubkey.output_slots[1].store(), pt.Op.store, pubkey.output_slots[1] ), @@ -60,7 +65,7 @@ def test_ecdsa_recover(): ] ) - actual, _ = pubkey.__teal__(teal5Options) + actual, _ = pubkey.__teal__(teal7Options) actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) @@ -68,13 +73,16 @@ def test_ecdsa_recover(): assert actual == expected # compile without errors this is necessary so assembly is also tested - pt.compileTeal(pt.Seq(pubkey, pt.Approve()), pt.Mode.Application, version=5) + pt.compileTeal(pt.Seq(pubkey, pt.Approve()), pt.Mode.Application, version=curve.min_version) + with pytest.raises(pt.TealInputError): + pt.compileTeal(pt.Seq(pubkey, pt.Approve()), pt.Mode.Application, version=curve.min_version - 1) -def test_ecdsa_verify_basic(): +@pytest.mark.parametrize("curve", [pt.EcdsaCurve.Secp256k1, pt.EcdsaCurve.Secp256r1]) +def test_ecdsa_verify_basic(curve: pt.EcdsaCurve): args = [pt.Bytes("data"), pt.Bytes("sigA"), pt.Bytes("sigB")] pubkey = (pt.Bytes("X"), pt.Bytes("Y")) - expr = pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], pubkey) + expr = pt.EcdsaVerify(curve, args[0], args[1], args[2], pubkey) assert expr.type_of() == pt.TealType.uint64 expected = pt.TealSimpleBlock( @@ -84,32 +92,36 @@ def test_ecdsa_verify_basic(): pt.TealOp(args[2], pt.Op.byte, '"sigB"'), pt.TealOp(pubkey[0], pt.Op.byte, '"X"'), pt.TealOp(pubkey[1], pt.Op.byte, '"Y"'), - pt.TealOp(expr, pt.Op.ecdsa_verify, pt.EcdsaCurve.Secp256k1.arg_name), + pt.TealOp(expr, pt.Op.ecdsa_verify, curve.arg_name), ] ) - actual, _ = expr.__teal__(teal5Options) + actual, _ = expr.__teal__(teal7Options) actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) assert actual == expected # compile without errors this is necessary so assembly is also tested - pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=5) + pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=curve.min_version) + + with pytest.raises(pt.TealInputError): + pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=curve.min_version - 1) -def test_ecdsa_verify_compressed_pk(): +@pytest.mark.parametrize("curve", [pt.EcdsaCurve.Secp256k1, pt.EcdsaCurve.Secp256r1]) +def test_ecdsa_verify_compressed_pk(curve: pt.EcdsaCurve): args = [pt.Bytes("data"), pt.Bytes("sigA"), pt.Bytes("sigB")] compressed_pubkey = pt.Bytes("XY") - pubkey = pt.EcdsaDecompress(pt.EcdsaCurve.Secp256k1, compressed_pubkey) - expr = pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], pubkey) + pubkey = pt.EcdsaDecompress(curve, compressed_pubkey) + expr = pt.EcdsaVerify(curve, args[0], args[1], args[2], pubkey) assert expr.type_of() == pt.TealType.uint64 expected = pt.TealSimpleBlock( [ pt.TealOp(compressed_pubkey, pt.Op.byte, '"XY"'), pt.TealOp( - pubkey, pt.Op.ecdsa_pk_decompress, pt.EcdsaCurve.Secp256k1.arg_name + pubkey, pt.Op.ecdsa_pk_decompress, curve.arg_name ), pt.TealOp( pubkey.output_slots[1].store(), pt.Op.store, pubkey.output_slots[1] @@ -126,11 +138,11 @@ def test_ecdsa_verify_compressed_pk(): pt.TealOp( pubkey.output_slots[1].load(), pt.Op.load, pubkey.output_slots[1] ), - pt.TealOp(expr, pt.Op.ecdsa_verify, pt.EcdsaCurve.Secp256k1.arg_name), + pt.TealOp(expr, pt.Op.ecdsa_verify, curve.arg_name), ] ) - actual, _ = expr.__teal__(teal5Options) + actual, _ = expr.__teal__(teal7Options) actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) @@ -138,15 +150,19 @@ def test_ecdsa_verify_compressed_pk(): assert actual == expected # compile without errors this is necessary so assembly is also tested - pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=5) + pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=curve.min_version) + + with pytest.raises(pt.TealInputError): + pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=curve.min_version - 1) -def test_ecdsa_verify_recovered_pk(): +@pytest.mark.parametrize("curve", [pt.EcdsaCurve.Secp256k1, pt.EcdsaCurve.Secp256r1]) +def test_ecdsa_verify_recovered_pk(curve: pt.EcdsaCurve): args = [pt.Bytes("data"), pt.Int(1), pt.Bytes("sigA"), pt.Bytes("sigB")] pubkey = pt.EcdsaRecover( - pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], args[3] + curve, args[0], args[1], args[2], args[3] ) - expr = pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[2], args[3], pubkey) + expr = pt.EcdsaVerify(curve, args[0], args[2], args[3], pubkey) assert expr.type_of() == pt.TealType.uint64 expected = pt.TealSimpleBlock( @@ -155,7 +171,7 @@ def test_ecdsa_verify_recovered_pk(): pt.TealOp(args[1], pt.Op.int, 1), pt.TealOp(args[2], pt.Op.byte, '"sigA"'), pt.TealOp(args[3], pt.Op.byte, '"sigB"'), - pt.TealOp(pubkey, pt.Op.ecdsa_pk_recover, pt.EcdsaCurve.Secp256k1.arg_name), + pt.TealOp(pubkey, pt.Op.ecdsa_pk_recover, curve.arg_name), pt.TealOp( pubkey.output_slots[1].store(), pt.Op.store, pubkey.output_slots[1] ), @@ -171,11 +187,11 @@ def test_ecdsa_verify_recovered_pk(): pt.TealOp( pubkey.output_slots[1].load(), pt.Op.load, pubkey.output_slots[1] ), - pt.TealOp(expr, pt.Op.ecdsa_verify, pt.EcdsaCurve.Secp256k1.arg_name), + pt.TealOp(expr, pt.Op.ecdsa_verify, curve.arg_name), ] ) - actual, _ = expr.__teal__(teal5Options) + actual, _ = expr.__teal__(teal7Options) actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) @@ -183,26 +199,30 @@ def test_ecdsa_verify_recovered_pk(): assert actual == expected # compile without errors this is necessary so assembly is also tested - pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=5) + pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=curve.min_version) + + with pytest.raises(pt.TealInputError): + pt.compileTeal(pt.Seq(pt.Pop(expr), pt.Approve()), pt.Mode.Application, version=curve.min_version - 1) -def test_ecdsa_invalid(): +@pytest.mark.parametrize("curve", [pt.EcdsaCurve.Secp256k1, pt.EcdsaCurve.Secp256r1]) +def test_ecdsa_invalid(curve: pt.EcdsaCurve): with pytest.raises(pt.TealTypeError): args = [pt.Bytes("data"), pt.Bytes("1"), pt.Bytes("sigA"), pt.Bytes("sigB")] - pt.EcdsaRecover(pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], args[3]) + pt.EcdsaRecover(curve, args[0], args[1], args[2], args[3]) with pytest.raises(pt.TealTypeError): - pt.EcdsaDecompress(pt.EcdsaCurve.Secp256k1, pt.Int(1)) + pt.EcdsaDecompress(curve, pt.Int(1)) with pytest.raises(pt.TealTypeError): args = [pt.Bytes("data"), pt.Bytes("sigA"), pt.Bytes("sigB")] pubkey = (pt.Bytes("X"), pt.Int(1)) - pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], pubkey) + pt.EcdsaVerify(curve, args[0], args[1], args[2], pubkey) with pytest.raises(pt.TealTypeError): args = [pt.Bytes("data"), pt.Int(1), pt.Bytes("sigB")] pubkey = (pt.Bytes("X"), pt.Bytes("Y")) - pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], pubkey) + pt.EcdsaVerify(curve, args[0], args[1], args[2], pubkey) with pytest.raises(pt.TealTypeError): args = [pt.Bytes("data"), pt.Bytes("sigA"), pt.Bytes("sigB")] @@ -210,16 +230,16 @@ def test_ecdsa_invalid(): pubkey = pt.MultiValue( pt.Op.ecdsa_pk_decompress, [pt.TealType.uint64, pt.TealType.bytes], - immediate_args=[pt.EcdsaCurve.Secp256k1], + immediate_args=[curve], args=[compressed_pk], ) - pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], pubkey) + pt.EcdsaVerify(curve, args[0], args[1], args[2], pubkey) with pytest.raises(pt.TealInputError): args = [pt.Bytes("data"), pt.Bytes("sigA"), pt.Bytes("sigB")] pubkey = (pt.Bytes("X"), pt.Bytes("Y")) expr = pt.EcdsaVerify( - pt.EcdsaCurve.Secp256k1, args[0], args[1], args[2], pubkey + curve, args[0], args[1], args[2], pubkey ) expr.__teal__(teal4Options)