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

Support Secp256r1 curve #423

Merged
merged 6 commits into from
Jun 30, 2022
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
18 changes: 11 additions & 7 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 @@ -113,11 +116,9 @@ def EcdsaVerify(

def EcdsaDecompress(curve: EcdsaCurve, compressed_pk: Expr) -> 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
encoded.
Expand All @@ -132,23 +133,23 @@ def EcdsaDecompress(curve: EcdsaCurve, compressed_pk: Expr) -> MultiValue:
EcdsaPubkey,
immediate_args=[curve.arg_name],
args=[compressed_pk],
compile_check=lambda options: verifyFieldVersion(
curve.arg_name, curve.min_version, options.version
),
)


def EcdsaRecover(
curve: EcdsaCurve, data: Expr, recovery_id: Expr, sigA: Expr, sigB: Expr
) -> MultiValue:
"""Reover an ECDSA public key from a signature.
michaeldiamant marked this conversation as resolved.
Show resolved Hide resolved

All byte arguments must be big endian encoded.

Args:
curve: Enum representing the ECDSA curve used for the public key
data: Hash value of the signed data. Must be 32 bytes long.
recovery_id: value used to extract public key from signature. Must evaluate to uint.
sigA: First component of the signature. Must evaluate to bytes.
sigB: Second component of the signature. Must evaluate to bytes.

Returns:
A MultiValue expression representing the two components of the public key, big endian
encoded.
Expand All @@ -166,4 +167,7 @@ def EcdsaRecover(
EcdsaPubkey,
immediate_args=[curve.arg_name],
args=[data, recovery_id, sigA, sigB],
compile_check=lambda options: verifyFieldVersion(
curve.arg_name, curve.min_version, options.version
),
)
162 changes: 112 additions & 50 deletions pyteal/ast/ecdsa_test.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import pytest
from typing import Union, List, cast

import pyteal as pt

teal4Options = pt.CompileOptions(version=4)
teal5Options = pt.CompileOptions(version=5)
teal7Options = pt.CompileOptions(version=7)

curve_options_map = {
pt.EcdsaCurve.Secp256k1: teal5Options,
pt.EcdsaCurve.Secp256r1: teal7Options,
}

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
),
pt.TealOp(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,22 +32,30 @@ def test_ecdsa_decompress():
]
)

actual, _ = pubkey.__teal__(teal5Options)
actual, _ = pubkey.__teal__(curve_options_map[curve])
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]
)
pubkey = pt.EcdsaRecover(curve, args[0], args[1], args[2], args[3])
assert pubkey.type_of() == pt.TealType.none

expected = pt.TealSimpleBlock(
Expand All @@ -50,7 +64,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 +74,31 @@ def test_ecdsa_recover():
]
)

actual, _ = pubkey.__teal__(teal5Options)
actual, _ = pubkey.__teal__(curve_options_map[curve])
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,33 +108,43 @@ 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__(curve_options_map[curve])
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
),
pt.TealOp(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 +160,37 @@ 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__(curve_options_map[curve])
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]
)
expr = pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, args[0], args[2], args[3], pubkey)
pubkey = pt.EcdsaRecover(curve, args[0], args[1], args[2], args[3])
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 +199,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,60 +215,78 @@ 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__(curve_options_map[curve])
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])
args: List[Union[pt.Bytes, pt.Int]] = [
pt.Bytes("data"),
pt.Bytes("1"),
pt.Bytes("sigA"),
pt.Bytes("sigB"),
]
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)
pubkey: Union[tuple[pt.Bytes, Union[pt.Int, pt.Bytes]], pt.MultiValue] = (
pt.Bytes("X"),
pt.Int(1),
)
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.__str__()],
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
)
expr = pt.EcdsaVerify(curve, args[0], args[1], args[2], pubkey)

expr.__teal__(teal4Options)

with pytest.raises(pt.TealTypeError):
args = [pt.Bytes("data"), pt.Bytes("sigA"), pt.Bytes("sigB")]
pubkey = (pt.Bytes("X"), pt.Bytes("Y"))
expr = pt.EcdsaVerify(5, args[0], args[1], args[2], pubkey)
expr = pt.EcdsaVerify(cast(pt.EcdsaCurve, 5), args[0], args[1], args[2], pubkey)
Loading