Skip to content
Draft
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ CPPFLAGS += --std=c++20 -fPIC -O3 $(INCLUDES)
plugin-c/%.o: plugin-c/%.cpp $(PREFIX)/libcryptopp/lib/libcryptopp.a $(PREFIX)/libff/lib/libff.a $(PREFIX)/c-kzg-4844/lib/libckzg.a $(PREFIX)/c-kzg-4844/lib/libblst.a
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) -o $@ $<

$(PREFIX)/plugin/lib/plugin.a: plugin-c/crypto.o plugin-c/hash_ext.o plugin-c/kzg.o plugin-c/json.o plugin-c/k.o plugin-c/plugin_util.o
$(PREFIX)/plugin/lib/plugin.a: plugin-c/crypto.o plugin-c/hash_ext.o plugin-c/kzg.o plugin-c/json.o plugin-c/k.o plugin-c/plugin_util.o plugin-c/p256verify.o
mkdir -p $(dir $@)
ar r $@ $^

Expand Down
2 changes: 1 addition & 1 deletion krypto/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test-unit: poetry-install
$(POETRY_RUN) pytest src/tests/unit --maxfail=1 --verbose $(TEST_ARGS)

test-integration: poetry-install
$(POETRY_RUN) pytest src/tests/integration --maxfail=1 --verbose --durations=0 --numprocesses=4 --dist=worksteal $(TEST_ARGS)
$(POETRY_RUN) pytest src/tests/integration --maxfail=1 --verbose --durations=0 --numprocesses=8 --dist=worksteal $(TEST_ARGS)


# Coverage
Expand Down
15 changes: 15 additions & 0 deletions krypto/src/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,18 @@ def _krypto_kompile(**kwargs: Any) -> Path:
return kompile(**args)

return _krypto_kompile


@pytest.fixture(scope='session')
def definition_dir(krypto_kompile: Callable[..., Path]) -> Path:
definition = """
requires "plugin/krypto.md"

module TEST
imports BOOL
imports KRYPTO
syntax Pgm ::= Bool | Bytes | String | G1Point | G2Point
configuration <k> $PGM:Pgm </k>
endmodule
"""
return krypto_kompile(definition=definition, main_module='TEST', syntax_module='TEST')
5,469 changes: 5,469 additions & 0 deletions krypto/src/tests/integration/test-data/test-vectors.json

Large diffs are not rendered by default.

21 changes: 5 additions & 16 deletions krypto/src/tests/integration/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,13 @@
from .utils import hex2bytes, run

if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path
from typing import Final


x01_32B: Final = hex2bytes(31 * '00' + '01') # noqa: N816


@pytest.fixture(scope='session')
def definition_dir(krypto_kompile: Callable[..., Path]) -> Path:
definition = """
requires "plugin/krypto.md"

module TEST
imports BOOL
imports KRYPTO
syntax Pgm ::= Bool | Bytes | String | G1Point | G2Point
configuration <k> $PGM:Pgm </k>
endmodule
"""
return krypto_kompile(definition=definition, main_module='TEST', syntax_module='TEST')


HOOK_TEST_DATA: Final = (
(
'Keccak256',
Expand Down Expand Up @@ -169,6 +153,11 @@ def definition_dir(krypto_kompile: Callable[..., Path]) -> Path:
'isValidPoint((0x0, 0x0))',
'true',
),
(
'p256verify',
f'P256Verify({hex2bytes("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca6050232ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e184cd60b855d442f5b3c7b11eb6c4e0ae7525fe710fab9aa7c77a67f79e6fadd762927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e")})',
'b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01"',
),
)


Expand Down
59 changes: 59 additions & 0 deletions krypto/src/tests/integration/test_p256verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import Final

import pytest

from .utils import hex2bytes, run

# EIP-7951 test vectors URL
# wget 'https://raw.githubusercontent.com/ethereum/EIPs/d386b29b5a31bd5cfd8d21bbf4e8a0c87734085e/assets/eip-7951/test-vectors.json'
TEST_VECTORS_FILE: Final = Path(__file__).parent / 'test-data' / 'test-vectors.json'
P256VERIFY_SUCCESS: Final = (
'b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01"'
)
P256VERIFY_FAILURE: Final = 'b""'


def load_test_vectors() -> list[tuple[str, str, str]]:
"""Load and parse EIP-7951 test vectors.

Returns:
List of (test_name, input_hex, expected_hex) tuples
"""
with TEST_VECTORS_FILE.open('r') as f:
test_data = json.load(f)

test_vectors = []
for test in test_data:
test_name = test['Name']
input_hex = test['Input']
expected_hex = test['Expected']

expected_output = P256VERIFY_SUCCESS if expected_hex else P256VERIFY_FAILURE

test_vectors.append((test_name, input_hex, expected_output))

return test_vectors


P256VERIFY_TEST_DATA = load_test_vectors()


@pytest.mark.parametrize(
'test_id,input_hex,expected_output',
P256VERIFY_TEST_DATA,
ids=[test_id for test_id, *_ in P256VERIFY_TEST_DATA],
)
def test_p256verify_hook(definition_dir: Path, test_id: str, input_hex: str, expected_output: str) -> None:
# Given
pgm = f'P256Verify({hex2bytes(input_hex)})'
expected = f'<k>\n {expected_output} ~> .K\n</k>'

# When
actual = run(definition_dir, pgm)

# Then
assert expected == actual
128 changes: 128 additions & 0 deletions plugin-c/p256verify.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#include "p256verify.h"
#include "plugin_util.h"

extern "C" {

// Helper: Create public key EVP_PKEY object
static EVP_PKEY *p256_pubkey_from_coords(const unsigned char *qx, const unsigned char *qy);
// Helper: Build signature from raw (r, s) components
static unsigned char *p256_sig_to_der(const unsigned char *r_bytes, const unsigned char *s_bytes, int *out_len);
// Helper: Perform ECDSA verification with pre-hashed data
static int p256_verify_prehash(EVP_PKEY *pkey, const unsigned char *sig_der, int sig_len, const unsigned char *hash, size_t hash_len);

struct string *hook_KRYPTO_p256verify(struct string *input) {
// The precompile expects exactly 160 bytes: h || r || s || qx || qy
if (len(input) != 160) {
return allocString(0);
}

// 32-bytes slices
const unsigned char *data = (unsigned char *)input->data;
const unsigned char *h = data;
const unsigned char *r_bytes = data + 32;
const unsigned char *s_bytes = data + 64;
const unsigned char *qx = data + 96;
const unsigned char *qy = data + 128;

EVP_PKEY *pkey = p256_pubkey_from_coords(qx, qy);
if (!pkey) {
return allocString(0);
}

int sig_der_len = 0;
unsigned char *sig_der = p256_sig_to_der(r_bytes, s_bytes, &sig_der_len);
if (!sig_der) {
EVP_PKEY_free(pkey);
return allocString(0);
}

int valid = p256_verify_prehash(pkey, sig_der, sig_der_len, h, 32);

OPENSSL_free(sig_der);
EVP_PKEY_free(pkey);

if (valid) {
unsigned char result[32] = {0};
result[31] = 1;
return raw(result, 32);
}
return allocString(0);
}

static EVP_PKEY *p256_pubkey_from_coords(const unsigned char *qx, const unsigned char *qy) {
// Build SEC1 uncompressed format: 0x04 || x || y
unsigned char pubkey_uncompressed[65];
pubkey_uncompressed[0] = 0x04;
memcpy(pubkey_uncompressed + 1, qx, 32);
memcpy(pubkey_uncompressed + 33, qy, 32);

EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL);
if (!pctx) {
return NULL;
}

if (EVP_PKEY_fromdata_init(pctx) <= 0) {
EVP_PKEY_CTX_free(pctx);
return NULL;
}

OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, (char *)"prime256v1", 0),
OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_PUB_KEY, pubkey_uncompressed, 65),
OSSL_PARAM_construct_end()
};

EVP_PKEY *pkey = NULL;
if (EVP_PKEY_fromdata(pctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) {
EVP_PKEY_CTX_free(pctx);
return NULL;
}

EVP_PKEY_CTX_free(pctx);
return pkey;
}

static unsigned char *p256_sig_to_der(const unsigned char *r_bytes, const unsigned char *s_bytes, int *out_len) {
ECDSA_SIG *sig = ECDSA_SIG_new();
if (!sig) {
return NULL;
}

BIGNUM *bn_r = BN_bin2bn(r_bytes, 32, NULL);
BIGNUM *bn_s = BN_bin2bn(s_bytes, 32, NULL);

if (!bn_r || !bn_s || !ECDSA_SIG_set0(sig, bn_r, bn_s)) {
// Note: ECDSA_SIG_set0 takes ownership on success, so only free on failure
if (bn_r) BN_free(bn_r);
if (bn_s) BN_free(bn_s);
ECDSA_SIG_free(sig);
return NULL;
}

unsigned char *sig_der = NULL;
*out_len = i2d_ECDSA_SIG(sig, &sig_der);
ECDSA_SIG_free(sig);

if (*out_len <= 0) {
return NULL;
}

return sig_der;
}

static int p256_verify_prehash(EVP_PKEY *pkey, const unsigned char *sig_der, int sig_len, const unsigned char *hash, size_t hash_len) {
EVP_PKEY_CTX *vctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!vctx) {
return 0;
}

int result = 0;
if (EVP_PKEY_verify_init(vctx) > 0) {
result = (EVP_PKEY_verify(vctx, sig_der, sig_len, hash, hash_len) == 1);
}

EVP_PKEY_CTX_free(vctx);
return result;
}

}
17 changes: 17 additions & 0 deletions plugin-c/p256verify.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef P256VERIFY_H
#define P256VERIFY_H

#include <openssl/evp.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <openssl/param_build.h>
#include <openssl/core_names.h>

#include "plugin_util.h"

extern "C" {
struct string *hook_KRYPTO_p256verify(struct string *input);
}

#endif
5 changes: 5 additions & 0 deletions plugin/krypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,10 @@ functions evaluate to `#False`.
syntax Int ::= "BLS12_FIELD_MODULUS" [alias]
rule BLS12_FIELD_MODULUS => 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787
// -----------------------------------------------------------------------------------------------------
```

```k
syntax Bytes ::= P256Verify ( Bytes ) [function, hook(KRYPTO.p256verify)]
// -------------------------------------------------------------------------
endmodule
```