From ed72e393ab2b76a26ab6ff4821e9085b3d706bd9 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Thu, 29 Jan 2026 14:14:33 +0100 Subject: [PATCH] 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 => _