From ed72e393ab2b76a26ab6ff4821e9085b3d706bd9 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Thu, 29 Jan 2026 14:14:33 +0100 Subject: [PATCH 1/2] 1st commit: add MLDSA sig verification --- rebar.config | 2 + rebar.lock | 22 ++++---- src/so_ast_infer_types.erl | 3 ++ src/so_ast_to_fcode.erl | 4 ++ test/contracts/qr_auth.aes | 21 ++++++++ test/contracts/qr_auth_tx.aes | 76 +++++++++++++++++++++++++++ test/contracts/unapplied_builtins.aes | 3 ++ 7 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 test/contracts/qr_auth.aes create mode 100644 test/contracts/qr_auth_tx.aes diff --git a/rebar.config b/rebar.config index 58f1a97..1d6ac99 100644 --- a/rebar.config +++ b/rebar.config @@ -1,5 +1,7 @@ %% -*- mode: erlang; indent-tabs-mode: nil -*- +{minimum_otp_vsn, "28.1"}. + {erl_opts, [debug_info]}. {deps, [ {gmbytecode, diff --git a/rebar.lock b/rebar.lock index 2bd61be..dd741ca 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,13 +1,4 @@ -{"1.2.0", -[{<<"gmbytecode">>, - {git,"https://git.qpq.swiss/QPQ-AG/gmbytecode.git", - {ref, "97cea33be8f3a35d26055664da7aa59531ff5537"}}, - 0}, - {<<"gmserialization">>, - {git,"https://git.qpq.swiss/QPQ-AG/gmserialization.git", - {ref,"ac64e01b0f675c1a34c70a827062f381920742db"}}, - 1}, - {<<"base58">>, +[{<<"base58">>, {git,"https://git.qpq.swiss/QPQ-AG/erl-base58.git", {ref,"e6aa62eeae3d4388311401f06e4b939bf4e94b9c"}}, 2}, @@ -23,8 +14,15 @@ {git,"https://git.qpq.swiss/QPQ-AG/getopt.git", {ref,"dbab6262a2430809430deda9d8650f58f9d80898"}}, 1}, + {<<"gmbytecode">>, + {git,"https://git.qpq.swiss/QPQ-AG/gmbytecode.git", + {ref,"97cea33be8f3a35d26055664da7aa59531ff5537"}}, + 0}, + {<<"gmserialization">>, + {git,"https://git.qpq.swiss/QPQ-AG/gmserialization.git", + {ref,"ac64e01b0f675c1a34c70a827062f381920742db"}}, + 1}, {<<"jsx">>, {git,"https://github.com/talentdeficit/jsx.git", {ref,"3074d4865b3385a050badf7828ad31490d860df5"}}, - 0}]}. - + 0}]. diff --git a/src/so_ast_infer_types.erl b/src/so_ast_infer_types.erl index d8e982b..1c7ee1d 100644 --- a/src/so_ast_infer_types.erl +++ b/src/so_ast_infer_types.erl @@ -781,6 +781,9 @@ global_env() -> {"verify_sig_secp256k1", Fun([Hash, Bytes(64), SignId], Bool)}, {"ecverify_secp256k1", Fun([Hash, Bytes(20), Bytes(65)], Bool)}, {"ecrecover_secp256k1", Fun([Hash, Bytes(65)], Option(Bytes(20)))}, + {"verify_sig_mldsa44", Fun([Hash, Bytes(any), SignId], Bool)}, + {"verify_sig_mldsa65", Fun([Hash, Bytes(any), SignId], Bool)}, + {"verify_sig_mldsa87", Fun([Hash, Bytes(any), SignId], Bool)}, {"sha3", Fun1(A, Hash)}, {"sha256", Fun1(A, Hash)}, {"blake2b", Fun1(A, Hash)}, diff --git a/src/so_ast_to_fcode.erl b/src/so_ast_to_fcode.erl index a90fb35..62b0de4 100644 --- a/src/so_ast_to_fcode.erl +++ b/src/so_ast_to_fcode.erl @@ -40,6 +40,7 @@ contract_to_address | address_to_contract | crypto_verify_sig | crypto_verify_sig_secp256k1 | crypto_sha3 | crypto_sha256 | crypto_blake2b | crypto_poseidon | crypto_ecverify_secp256k1 | crypto_ecrecover_secp256k1 | + crypto_verify_sig_mldsa44 | crypto_verify_sig_mldsa65 | crypto_verify_sig_mldsa87 | mcl_bls12_381_g1_neg | mcl_bls12_381_g1_norm | mcl_bls12_381_g1_valid | mcl_bls12_381_g1_is_zero | mcl_bls12_381_g1_add | mcl_bls12_381_g1_mul | mcl_bls12_381_g2_neg | mcl_bls12_381_g2_norm | mcl_bls12_381_g2_valid | @@ -285,6 +286,8 @@ builtins() -> {"lookup_default", 3}, {"delete", 2}, {"member", 2}, {"size", 1}]}, {["Crypto"], [{"verify_sig", 3}, {"verify_sig_secp256k1", 3}, {"ecverify_secp256k1", 3}, {"ecrecover_secp256k1", 2}, + {"verify_sig_mldsa44", 3}, {"verify_sig_mldsa65", 3}, + {"verify_sig_mldsa87", 3}, {"sha3", 1}, {"sha256", 1}, {"blake2b", 1}, {"poseidon", 2}]}, {["MCL_BLS12_381"], [{"g1_neg", 1}, {"g1_norm", 1}, {"g1_valid", 1}, {"g1_is_zero", 1}, {"g1_add", 2}, {"g1_mul", 2}, {"g2_neg", 1}, {"g2_norm", 1}, {"g2_valid", 1}, {"g2_is_zero", 1}, {"g2_add", 2}, {"g2_mul", 2}, @@ -1163,6 +1166,7 @@ op_builtins() -> int_to_str, int_to_bytes, int_mulmod, address_to_str, address_to_bytes, address_to_contract, crypto_verify_sig, crypto_verify_sig_secp256k1, crypto_sha3, crypto_sha256, crypto_blake2b, + crypto_verify_sig_mldsa44, crypto_verify_sig_mldsa65, crypto_verify_sig_mldsa87, crypto_poseidon, crypto_ecverify_secp256k1, crypto_ecrecover_secp256k1, mcl_bls12_381_g1_neg, mcl_bls12_381_g1_norm, mcl_bls12_381_g1_valid, mcl_bls12_381_g1_is_zero, mcl_bls12_381_g1_add, mcl_bls12_381_g1_mul, diff --git a/test/contracts/qr_auth.aes b/test/contracts/qr_auth.aes new file mode 100644 index 0000000..10e8406 --- /dev/null +++ b/test/contracts/qr_auth.aes @@ -0,0 +1,21 @@ +// Contract using Quantum-Resistant signing (MLDSA65) +contract QrAuth = + record state = { nonce : int, owner : address, owner_pub : bytes } + + entrypoint init(pub : bytes) = { nonce = 1 + , owner = Call.caller + , owner_pub = pub } + + stateful entrypoint authorize(n : int, s : signature) : bool = + require(n >= state.nonce, "Nonce too low") + require(n =< state.nonce, "Nonce too high") + put(state{ nonce = n + 1 }) + switch(Auth.tx_hash) + None => abort("Not in Auth context") + Some(tx_hash) => Crypto.verify_sig_mldsa65(to_sign(tx_hash, n), state.owner_pub, s) + + entrypoint to_sign(h : hash, n : int) = + Crypto.blake2b((h, n)) + + entrypoint weird_string() : string = + "\x19Weird String\x42\nMore\n" diff --git a/test/contracts/qr_auth_tx.aes b/test/contracts/qr_auth_tx.aes new file mode 100644 index 0000000..1c1a360 --- /dev/null +++ b/test/contracts/qr_auth_tx.aes @@ -0,0 +1,76 @@ +// namespace Chain = +// record tx = { paying_for : option(Chain.paying_for_tx) +// , ga_metas : list(Chain.ga_meta_tx) +// , actor : address +// , fee : int +// , ttl : int +// , tx : Chain.base_tx } + +// datatype ga_meta_tx = GAMetaTx(address, int) +// datatype paying_for_tx = PayingForTx(address, int) +// datatype base_tx = SpendTx(address, int, string) +// | OracleRegisterTx | OracleQueryTx | OracleResponseTx | OracleExtendTx +// | NamePreclaimTx | NameClaimTx(hash) | NameUpdateTx(string) +// | NameRevokeTx(hash) | NameTransferTx(address, string) +// | ChannelCreateTx(address) | ChannelDepositTx(address, int) | ChannelWithdrawTx(address, int) | +// | ChannelForceProgressTx(address) | ChannelCloseMutualTx(address) | ChannelCloseSoloTx(address) +// | ChannelSlashTx(address) | ChannelSettleTx(address) | ChannelSnapshotSoloTx(address) +// | ContractCreateTx(int) | ContractCallTx(address, int) +// | GAAttachTx + + +// Contract implementing Quantum-resistant (MLDSA65) authentication +contract QrAuthTx = + record state = { nonce : int, owner : address, owner_pub : bytes } + datatype foo = Bar | Baz() + + entrypoint init(pub : bytes) = { nonce = 1 + , owner = Call.caller + , owner_pub = pub } + + stateful entrypoint authorize(n : int, s : signature) : bool = + require(n >= state.nonce, "Nonce too low") + require(n =< state.nonce, "Nonce too high") + put(state{ nonce = n + 1 }) + switch(Auth.tx_hash) + None => abort("Not in Auth context") + Some(tx_hash) => + let Some(tx0) = Auth.tx + let x : option(Chain.paying_for_tx) = tx0.paying_for + let x : list(Chain.ga_meta_tx) = tx0.ga_metas + let x : int = tx0.fee + tx0.ttl + let x : address = tx0.actor + let x : Chain.tx = { tx = Chain.NamePreclaimTx, paying_for = None, ga_metas = [], + fee = 123, ttl = 0, actor = Call.caller } + switch(tx0.tx) + Chain.SpendTx(receiver, amount, payload) => verify(tx_hash, n, s) + Chain.OracleRegisterTx => false + Chain.OracleQueryTx => false + Chain.OracleResponseTx => false + Chain.OracleExtendTx => false + Chain.NamePreclaimTx => false + Chain.NameClaimTx(name) => false + Chain.NameUpdateTx(name) => false + Chain.NameRevokeTx(name) => false + Chain.NameTransferTx(to, name) => false + Chain.ChannelCreateTx(other_party) => false + Chain.ChannelDepositTx(channel, amount) => false + Chain.ChannelWithdrawTx(channel, amount) => false + Chain.ChannelForceProgressTx(channel) => false + Chain.ChannelCloseMutualTx(channel) => false + Chain.ChannelCloseSoloTx(channel) => false + Chain.ChannelSlashTx(channel) => false + Chain.ChannelSettleTx(channel) => false + Chain.ChannelSnapshotSoloTx(channel) => false + Chain.ContractCreateTx(amount) => false + Chain.ContractCallTx(ct_address, amount) => false + Chain.GAAttachTx => false + + function verify(tx_hash, n, s) = + Crypto.verify_sig_mldsa65(to_sign(tx_hash, n), state.owner_pub, s) + + entrypoint to_sign(h : hash, n : int) = + Crypto.blake2b((h, n)) + + entrypoint weird_string() : string = + "\x19Weird String\x42\nMore\n" diff --git a/test/contracts/unapplied_builtins.aes b/test/contracts/unapplied_builtins.aes index de53a9f..5cd5d25 100644 --- a/test/contracts/unapplied_builtins.aes +++ b/test/contracts/unapplied_builtins.aes @@ -29,6 +29,9 @@ contract UnappliedBuiltins = function crypto_verify_sig_secp256k1() = Crypto.verify_sig_secp256k1 function crypto_ecverify_secp256k1() = Crypto.ecverify_secp256k1 function crypto_ecrecover_secp256k1() = Crypto.ecrecover_secp256k1 + function crypto_verify_sig_mldsa44() = Crypto.verify_sig_mldsa44 + function crypto_verify_sig_mldsa65() = Crypto.verify_sig_mldsa65 + function crypto_verify_sig_mldsa87() = Crypto.verify_sig_mldsa87 function crypto_sha3() = Crypto.sha3 : t => _ function crypto_sha256() = Crypto.sha256 : t => _ function crypto_blake2b() = Crypto.blake2b : t => _ -- 2.30.2 From 4f6529ed9d5dcaf9fda96ae45fb7242c60883800 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Mon, 2 Feb 2026 20:05:07 +0100 Subject: [PATCH 2/2] update deps, add mldsa-related byte codes --- rebar.config | 3 +-- rebar.lock | 25 ++++++++++++++----------- src/so_ast_infer_types.erl | 6 +++--- src/so_fcode_to_fate.erl | 6 ++++++ 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/rebar.config b/rebar.config index 1d6ac99..88873b9 100644 --- a/rebar.config +++ b/rebar.config @@ -5,8 +5,7 @@ {erl_opts, [debug_info]}. {deps, [ {gmbytecode, - {git, "https://git.qpq.swiss/QPQ-AG/gmbytecode.git", - {ref, "97cea33be8f3a35d26055664da7aa59531ff5537"}}} + {git, "https://git.qpq.swiss/QPQ-AG/gmbytecode.git", {ref, "54dc140629"}}} , {eblake2, "1.0.0"} , {jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}} ]}. diff --git a/rebar.lock b/rebar.lock index dd741ca..8e1c93d 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,28 +1,31 @@ +{"1.2.0", [{<<"base58">>, {git,"https://git.qpq.swiss/QPQ-AG/erl-base58.git", {ref,"e6aa62eeae3d4388311401f06e4b939bf4e94b9c"}}, 2}, - {<<"eblake2">>, - {git,"https://git.qpq.swiss/QPQ-AG/eblake2.git", - {ref,"b29d585b8760746142014884007eb8441a3b6a14"}}, - 0}, + {<<"eblake2">>,{pkg,<<"eblake2">>,<<"1.0.0">>},0}, {<<"enacl">>, {git,"https://git.qpq.swiss/QPQ-AG/enacl.git", {ref,"4eb7ec70084ba7c87b1af8797c4c4e90c84f95a2"}}, 2}, - {<<"getopt">>, - {git,"https://git.qpq.swiss/QPQ-AG/getopt.git", - {ref,"dbab6262a2430809430deda9d8650f58f9d80898"}}, - 1}, + {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},1}, {<<"gmbytecode">>, {git,"https://git.qpq.swiss/QPQ-AG/gmbytecode.git", - {ref,"97cea33be8f3a35d26055664da7aa59531ff5537"}}, + {ref,"54dc140629b3af5de2043f52ba9dc36aff6b949d"}}, 0}, {<<"gmserialization">>, {git,"https://git.qpq.swiss/QPQ-AG/gmserialization.git", - {ref,"ac64e01b0f675c1a34c70a827062f381920742db"}}, + {ref,"9d2ecc00d32ea295309563e54a81636ecb597e96"}}, 1}, {<<"jsx">>, {git,"https://github.com/talentdeficit/jsx.git", {ref,"3074d4865b3385a050badf7828ad31490d860df5"}}, - 0}]. + 0}]}. +[ +{pkg_hash,[ + {<<"eblake2">>, <<"EC8AD20E438AAB3F2E8D5D118C366A0754219195F8A0F536587440F8F9BCF2EF">>}, + {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}]}, +{pkg_hash_ext,[ + {<<"eblake2">>, <<"3C4D300A91845B25D501929A26AC2E6F7157480846FAB2347A4C11AE52E08A99">>}, + {<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>}]} +]. diff --git a/src/so_ast_infer_types.erl b/src/so_ast_infer_types.erl index 1c7ee1d..cc3bb20 100644 --- a/src/so_ast_infer_types.erl +++ b/src/so_ast_infer_types.erl @@ -781,9 +781,9 @@ global_env() -> {"verify_sig_secp256k1", Fun([Hash, Bytes(64), SignId], Bool)}, {"ecverify_secp256k1", Fun([Hash, Bytes(20), Bytes(65)], Bool)}, {"ecrecover_secp256k1", Fun([Hash, Bytes(65)], Option(Bytes(20)))}, - {"verify_sig_mldsa44", Fun([Hash, Bytes(any), SignId], Bool)}, - {"verify_sig_mldsa65", Fun([Hash, Bytes(any), SignId], Bool)}, - {"verify_sig_mldsa87", Fun([Hash, Bytes(any), SignId], Bool)}, + {"verify_sig_mldsa44", Fun([Hash, Bytes(1312), Bytes(2420)], Bool)}, + {"verify_sig_mldsa65", Fun([Hash, Bytes(1952), Bytes(3309)], Bool)}, + {"verify_sig_mldsa87", Fun([Hash, Bytes(2592), Bytes(4627)], Bool)}, {"sha3", Fun1(A, Hash)}, {"sha256", Fun1(A, Hash)}, {"blake2b", Fun1(A, Hash)}, diff --git a/src/so_fcode_to_fate.erl b/src/so_fcode_to_fate.erl index 43dfe10..c986577 100644 --- a/src/so_fcode_to_fate.erl +++ b/src/so_fcode_to_fate.erl @@ -693,6 +693,9 @@ op_to_scode(crypto_verify_sig) -> gmb_fate_ops:verify_sig(?a, ?a, ?a, op_to_scode(crypto_verify_sig_secp256k1) -> gmb_fate_ops:verify_sig_secp256k1(?a, ?a, ?a, ?a); op_to_scode(crypto_ecverify_secp256k1) -> gmb_fate_ops:ecverify_secp256k1(?a, ?a, ?a, ?a); op_to_scode(crypto_ecrecover_secp256k1) -> gmb_fate_ops:ecrecover_secp256k1(?a, ?a, ?a); +op_to_scode(crypto_verify_sig_mldsa44) -> gmb_fate_ops:verify_sig_mldsa44(?a, ?a, ?a, ?a); +op_to_scode(crypto_verify_sig_mldsa65) -> gmb_fate_ops:verify_sig_mldsa65(?a, ?a, ?a, ?a); +op_to_scode(crypto_verify_sig_mldsa87) -> gmb_fate_ops:verify_sig_mldsa87(?a, ?a, ?a, ?a); op_to_scode(crypto_sha3) -> gmb_fate_ops:sha3(?a, ?a); op_to_scode(crypto_sha256) -> gmb_fate_ops:sha256(?a, ?a); op_to_scode(crypto_blake2b) -> gmb_fate_ops:blake2b(?a, ?a); @@ -1033,6 +1036,9 @@ attributes(I) -> {'POSEIDON', A, B, C} -> Pure(A, [B, C]); {'VERIFY_SIG', A, B, C, D} -> Pure(A, [B, C, D]); {'VERIFY_SIG_SECP256K1', A, B, C, D} -> Pure(A, [B, C, D]); + {'VERIFY_SIG_MLDSA44', A, B, C, D} -> Pure(A, [B, C, D]); + {'VERIFY_SIG_MLDSA65', A, B, C, D} -> Pure(A, [B, C, D]); + {'VERIFY_SIG_MLDSA87', A, B, C, D} -> Pure(A, [B, C, D]); {'ECVERIFY_SECP256K1', A, B, C, D} -> Pure(A, [B, C, D]); {'ECRECOVER_SECP256K1', A, B, C} -> Pure(A, [B, C]); {'CONTRACT_TO_ADDRESS', A, B} -> Pure(A, [B]); -- 2.30.2