Skip to content

Commit

Permalink
Support Secp256r1 curve
Browse files Browse the repository at this point in the history
  • Loading branch information
jdtzmn committed Jun 27, 2022
1 parent d0f9d8d commit 67c9e35
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 68 deletions.
73 changes: 43 additions & 30 deletions pyteal/ast/ecdsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -110,34 +113,37 @@ 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:
curve: Enum representing the ECDSA curve used for the public key
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.
Expand All @@ -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)
96 changes: 58 additions & 38 deletions pyteal/ast/ecdsa_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -26,21 +27,25 @@ def test_ecdsa_decompress():
]
)

actual, _ = pubkey.__teal__(teal5Options)
actual, _ = pubkey.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
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

Expand All @@ -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]
),
Expand All @@ -60,21 +65,24 @@ def test_ecdsa_recover():
]
)

actual, _ = pubkey.__teal__(teal5Options)
actual, _ = pubkey.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
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(
Expand All @@ -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]
Expand All @@ -126,27 +138,31 @@ 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)

with pt.TealComponent.Context.ignoreExprEquality():
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(
Expand All @@ -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]
),
Expand All @@ -171,55 +187,59 @@ 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)

with pt.TealComponent.Context.ignoreExprEquality():
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")]
compressed_pk = pt.Bytes("XY")
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)
Expand Down

0 comments on commit 67c9e35

Please sign in to comment.