exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 1a136d2b43ad2ab00bd14de4c8e1ff863013e9fa
parent d3dcb110d998d37a6532f0c22f960104f6528459
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sat,  1 Aug 2026 14:18:50 +0200

new otp codes as part of dd97

Diffstat:
Mdebian/control | 2+-
Msrc/include/taler/taler_crypto_lib.h | 203++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/json/json_helper.c | 4++++
Msrc/util/crypto_confirmation.c | 11+++++++++++
Asrc/util/crypto_ecdsa.c | 455+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/crypto_signatures.c | 361+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/meson.build | 3+++
Asrc/util/test_crypto_confirmation.c | 964+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 2001 insertions(+), 2 deletions(-)

diff --git a/debian/control b/debian/control @@ -9,7 +9,7 @@ Build-Depends: gcc, debhelper-compat (= 12), gettext, - libgnunet-dev (>=0.28.0~dev5), + libgnunet-dev (>=0.28.1~dev2), libcurl4-gnutls-dev (>=7.35.0) | libcurl4-openssl-dev (>= 7.35.0), libgcrypt20-dev (>=1.8), libgnutls28-dev (>=3.2.12), diff --git a/src/include/taler/taler_crypto_lib.h b/src/include/taler/taler_crypto_lib.h @@ -84,7 +84,17 @@ enum TALER_MerchantConfirmationAlgorithm /** * Purchase confirmation with payment */ - TALER_MCA_WITH_PRICE = 2 + TALER_MCA_WITH_PRICE = 2, + + /** + * ECDSA (NIST P-256) signature over the order's challenge + */ + TALER_MCA_ECDSA_CHALLENGE = 3, + + /** + * EdDSA (Ed25519) signature over the order's challenge + */ + TALER_MCA_EDDSA_CHALLENGE = 4 }; @@ -1269,9 +1279,137 @@ TALER_rfc3548_base32decode (const char *val, size_t key_len); +/* ************** ECDSA over NIST P-256 ***************** */ + +/** + * @brief Private key for ECDSA over NIST P-256. + * + * Note that this is a different curve from the one behind + * #GNUNET_CRYPTO_EcdsaPrivateKey, which uses Ed25519. + */ +struct TALER_EcdsaP256PrivateKeyP +{ + /** + * The private scalar. + */ + unsigned char d[32]; +}; + + +/** + * @brief Public key for ECDSA over NIST P-256, as a compressed SEC1 + * point: a 0x02 or 0x03 prefix encoding the parity of Y, followed by + * the 32 bytes of X. + */ +struct TALER_EcdsaP256PublicKeyP +{ + /** + * The compressed public point. + */ + unsigned char q[33]; +}; + + +/** + * @brief Signature for ECDSA over NIST P-256: the scalars r and s, + * each zero-padded to a fixed 32 bytes. Only the canonical "low-s" + * form is produced and accepted. + */ +struct TALER_EcdsaP256SignatureP +{ + /** + * The scalars r and s, in that order. + */ + unsigned char r_s[64]; +}; + + +/** + * Create a fresh ECDSA key pair over NIST P-256. + * + * @param[out] priv set to the private key + * @param[out] pub set to the corresponding public key + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TALER_ecdsa_p256_key_create ( + struct TALER_EcdsaP256PrivateKeyP *priv, + struct TALER_EcdsaP256PublicKeyP *pub); + + +/** + * Sign the already-hashed @a hash with @a priv. + * + * @param priv private key to sign with + * @param hash 256-bit digest of the message to sign + * @param[out] sig set to the signature, in canonical low-s form + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TALER_ecdsa_p256_sign ( + const struct TALER_EcdsaP256PrivateKeyP *priv, + const struct GNUNET_ShortHashCode *hash, + struct TALER_EcdsaP256SignatureP *sig); + + +/** + * Verify @a sig over the already-hashed @a hash under @a pub. + * Signatures that are not in canonical low-s form are rejected, as + * are public keys that do not decode to a point on the curve. + * + * @param pub public key to verify against + * @param hash 256-bit digest of the signed message + * @param sig signature to check + * @return #GNUNET_OK if @a sig is valid + */ +enum GNUNET_GenericReturnValue +TALER_ecdsa_p256_verify ( + const struct TALER_EcdsaP256PublicKeyP *pub, + const struct GNUNET_ShortHashCode *hash, + const struct TALER_EcdsaP256SignatureP *sig); + + +/* ********* POS confirmations signing a challenge ******** */ + +/** + * Length of a challenge generated by an offline verifier. + */ +#define TALER_POS_CHALLENGE_LENGTH 32 + + +/** + * @brief Challenge generated by an offline verifier (such as an + * unattended appliance or an electronic tag) and signed by the + * merchant backend once the corresponding order was paid. + */ +struct TALER_PosChallengeP +{ + /** + * Unpredictable value chosen by the offline verifier. + */ + unsigned char challenge[TALER_POS_CHALLENGE_LENGTH]; +}; + + +/** + * Domain separation prefix for challenge-signature POS confirmations. + * The signed message is this prefix (without the terminator) directly + * followed by the 32 raw challenge bytes. + * + * The trailing version is part of the separation: offline verifiers + * hard-code this construction, so any future change to what gets + * signed must bump it rather than reuse it, leaving signatures of the + * two constructions mutually unacceptable. + */ +#define TALER_POS_CHALLENGE_SALT "taler-pos-challenge-v1" + + /** * @brief Builds POS confirmation token to verify payment. * + * Only for the time-based (TOTP) algorithms; the challenge-signature + * algorithms are handled by #TALER_build_pos_confirmation_sig(). + * * @param pos_key encoded key for verification payment * @param pos_alg algorithm to compute the payment verification * @param total of the order paid @@ -1287,6 +1425,69 @@ TALER_build_pos_confirmation ( /** + * @brief Builds a POS confirmation that signs the order's challenge. + * + * The counterpart to #TALER_build_pos_confirmation() for the + * challenge-signature algorithms, which are bound to a challenge from + * the offline verifier instead of to the current time. + * + * @param pos_key Crockford base32-encoded private key of the device + * @param pos_alg algorithm to use, must be + * #TALER_MCA_ECDSA_CHALLENGE or #TALER_MCA_EDDSA_CHALLENGE + * @param challenge challenge to bind the signature to + * @return Crockford base32-encoded signature, NULL otherwise + */ +char * +TALER_build_pos_confirmation_sig ( + const char *pos_key, + enum TALER_MerchantConfirmationAlgorithm pos_alg, + const struct TALER_PosChallengeP *challenge); + + +/** + * @brief Generate a key pair for a challenge-signature OTP device. + * + * The private key never leaves the merchant backend; only @a pos_pub + * is handed to the merchant to configure the offline verifier. + * + * @param pos_alg algorithm to generate the key pair for, must be + * #TALER_MCA_ECDSA_CHALLENGE or #TALER_MCA_EDDSA_CHALLENGE + * @param[out] pos_key set to the Crockford base32-encoded private key, + * to be freed by the caller + * @param[out] pos_pub set to the Crockford base32-encoded public key, + * to be freed by the caller + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TALER_otp_device_key_create ( + enum TALER_MerchantConfirmationAlgorithm pos_alg, + char **pos_key, + char **pos_pub); + + +/** + * @brief Verify a challenge-signature POS confirmation. + * + * This is what an offline verifier does; the merchant backend itself + * only ever signs. Provided here so that both sides of the protocol + * can be tested against one implementation. + * + * @param pos_pub Crockford base32-encoded public key of the device + * @param pos_alg algorithm the device uses, must be + * #TALER_MCA_ECDSA_CHALLENGE or #TALER_MCA_EDDSA_CHALLENGE + * @param challenge challenge the confirmation should be bound to + * @param pos_confirmation Crockford base32-encoded signature to check + * @return #GNUNET_OK if @a pos_confirmation is valid + */ +enum GNUNET_GenericReturnValue +TALER_check_pos_confirmation_sig ( + const char *pos_pub, + enum TALER_MerchantConfirmationAlgorithm pos_alg, + const struct TALER_PosChallengeP *challenge, + const char *pos_confirmation); + + +/** * Set of the fees applying to a denomination. */ struct TALER_DenomFeeSet diff --git a/src/json/json_helper.c b/src/json/json_helper.c @@ -1772,6 +1772,10 @@ parse_otp_type (void *cls, .val = TALER_MCA_WITHOUT_PRICE }, { .name = "TOTP_WITH_PRICE", .val = TALER_MCA_WITH_PRICE }, + { .name = "ECDSA_CHALLENGE", + .val = TALER_MCA_ECDSA_CHALLENGE }, + { .name = "EDDSA_CHALLENGE", + .val = TALER_MCA_EDDSA_CHALLENGE }, { .name = NULL, .val = TALER_MCA_NONE }, }; diff --git a/src/util/crypto_confirmation.c b/src/util/crypto_confirmation.c @@ -244,6 +244,13 @@ TALER_build_pos_confirmation (const char *pos_key, GNUNET_break (0); GNUNET_free (key); return NULL; + case TALER_MCA_ECDSA_CHALLENGE: + case TALER_MCA_EDDSA_CHALLENGE: + /* Challenge-signature confirmations are not time-based; they + are computed in crypto_signatures.c. */ + GNUNET_break (0); + GNUNET_free (key); + return NULL; case TALER_MCA_WITHOUT_PRICE: /* and 30s */ /* Return all T-OTP codes in range separated by new lines, e.g. "12345678 @@ -267,6 +274,7 @@ TALER_build_pos_confirmation (const char *pos_key, TALER_amount_is_valid (total) ) ) { GNUNET_break_op (0); + GNUNET_free (key); return NULL; } TALER_amount_hton (&ntotal, @@ -289,3 +297,6 @@ TALER_build_pos_confirmation (const char *pos_key, GNUNET_break (0); return NULL; } + + +/* end of crypto_confirmation.c */ diff --git a/src/util/crypto_ecdsa.c b/src/util/crypto_ecdsa.c @@ -0,0 +1,455 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/crypto_ecdsa.c + * @brief ECDSA over NIST P-256 + * @author Bohdan Potuzhnyi + * @author Volodymyr Potuzhnyi + * + * GNUnet's ECDSA is over Ed25519, so P-256 is implemented here on top + * of libgcrypt. This module knows nothing about what is being + * signed: callers pass a hash and receive raw fixed-width scalars. + */ +#include "platform.h" +#include "taler/taler_util.h" +#include <gcrypt.h> + + +/** + * Curve name libgcrypt uses. + */ +#define P256_CURVE "NIST P-256" + +/** + * Size of a scalar: the private key, and each of r and s. + */ +#define P256_SCALAR_LEN 32 + +/** + * Order of the group, used to canonicalize signatures to low-s. + */ +#define P256_ORDER_HEX \ + "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551" + + +/** + * Write @a v into @a buf, zero-padded on the left to exactly @a len + * bytes. Needed because libgcrypt returns MPIs without leading + * zeros, while the wire format uses fixed-width scalars. + * + * @param v value to write + * @param[out] buf where to write the value + * @param len number of bytes to write + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +mpi_to_fixed (gcry_mpi_t v, + unsigned char *buf, + size_t len) +{ + size_t nbytes; + + if (0 != gcry_mpi_print (GCRYMPI_FMT_USG, + NULL, + 0, + &nbytes, + v)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (nbytes > len) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + memset (buf, + 0, + len); + if (0 != gcry_mpi_print (GCRYMPI_FMT_USG, + &buf[len - nbytes], + nbytes, + &nbytes, + v)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Extract the MPI @a token from the s-expression @a sexp. + * + * @param sexp s-expression to search + * @param token name of the token to extract + * @return the value, or NULL on error; caller must release + */ +static gcry_mpi_t +sexp_extract_mpi (gcry_sexp_t sexp, + const char *token) +{ + gcry_sexp_t t; + gcry_mpi_t ret; + + t = gcry_sexp_find_token (sexp, + token, + 0); + if (NULL == t) + { + GNUNET_break (0); + return NULL; + } + ret = gcry_sexp_nth_mpi (t, + 1, + GCRYMPI_FMT_USG); + gcry_sexp_release (t); + if (NULL == ret) + GNUNET_break (0); + return ret; +} + + +/** + * Is @a s in the upper half of the group order? ECDSA accepts both + * @a s and n-s; only the low variant is canonical. + * + * @param s scalar to check + * @param[out] high set to true if @a s is the high variant + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +s_is_high (gcry_mpi_t s, + bool *high) +{ + gcry_mpi_t n; + gcry_mpi_t half; + + if (0 != gcry_mpi_scan (&n, + GCRYMPI_FMT_HEX, + P256_ORDER_HEX, + 0, + NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + half = gcry_mpi_new (256); + gcry_mpi_rshift (half, + n, + 1); + *high = (gcry_mpi_cmp (s, + half) > 0); + gcry_mpi_release (half); + gcry_mpi_release (n); + return GNUNET_OK; +} + + +/** + * Replace @a s by n-s, canonicalizing a signature to low-s. + * + * @param[in,out] s scalar to negate modulo the group order + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +s_to_low (gcry_mpi_t s) +{ + gcry_mpi_t n; + + if (0 != gcry_mpi_scan (&n, + GCRYMPI_FMT_HEX, + P256_ORDER_HEX, + 0, + NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + gcry_mpi_sub (s, + n, + s); + gcry_mpi_release (n); + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_ecdsa_p256_key_create ( + struct TALER_EcdsaP256PrivateKeyP *priv, + struct TALER_EcdsaP256PublicKeyP *pub) +{ + gcry_sexp_t params = NULL; + gcry_sexp_t keypair = NULL; + gcry_mpi_t d = NULL; + gcry_mpi_t q = NULL; + unsigned char qbuf[2 * P256_SCALAR_LEN + 1]; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + + if (0 != gcry_sexp_build (&params, + NULL, + "(genkey(ecc(curve \"" P256_CURVE "\")))")) + { + GNUNET_break (0); + goto cleanup; + } + if (0 != gcry_pk_genkey (&keypair, + params)) + { + GNUNET_break (0); + goto cleanup; + } + d = sexp_extract_mpi (keypair, + "d"); + q = sexp_extract_mpi (keypair, + "q"); + if ( (NULL == d) || + (NULL == q) ) + goto cleanup; + if (GNUNET_OK != + mpi_to_fixed (d, + priv->d, + sizeof (priv->d))) + goto cleanup; + /* libgcrypt hands us the uncompressed point 0x04|X|Y; we store the + compressed form, whose prefix encodes the parity of Y */ + if (GNUNET_OK != + mpi_to_fixed (q, + qbuf, + sizeof (qbuf))) + goto cleanup; + if (0x04 != qbuf[0]) + { + GNUNET_break (0); + goto cleanup; + } + pub->q[0] = (qbuf[sizeof (qbuf) - 1] & 1) + ? 0x03 + : 0x02; + memcpy (&pub->q[1], + &qbuf[1], + P256_SCALAR_LEN); + ret = GNUNET_OK; +cleanup: + if (NULL != d) + gcry_mpi_release (d); + if (NULL != q) + gcry_mpi_release (q); + if (NULL != keypair) + gcry_sexp_release (keypair); + if (NULL != params) + gcry_sexp_release (params); + return ret; +} + + +enum GNUNET_GenericReturnValue +TALER_ecdsa_p256_sign ( + const struct TALER_EcdsaP256PrivateKeyP *priv, + const struct GNUNET_ShortHashCode *hash, + struct TALER_EcdsaP256SignatureP *sig) +{ + gcry_sexp_t skey = NULL; + gcry_sexp_t data = NULL; + gcry_sexp_t sigs = NULL; + gcry_mpi_t hm = NULL; + gcry_mpi_t r = NULL; + gcry_mpi_t s = NULL; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + bool high; + + if (0 != gcry_sexp_build (&skey, + NULL, + "(private-key(ecc(curve \"" P256_CURVE "\")" + "(d %b)))", + (int) sizeof (priv->d), + (const char *) priv->d)) + { + GNUNET_break (0); + goto cleanup; + } + if (0 != gcry_mpi_scan (&hm, + GCRYMPI_FMT_USG, + hash, + sizeof (*hash), + NULL)) + { + GNUNET_break (0); + goto cleanup; + } + if (0 != gcry_sexp_build (&data, + NULL, + "(data(flags raw)(value %m))", + hm)) + { + GNUNET_break (0); + goto cleanup; + } + if (0 != gcry_pk_sign (&sigs, + data, + skey)) + { + GNUNET_break (0); + goto cleanup; + } + r = sexp_extract_mpi (sigs, + "r"); + s = sexp_extract_mpi (sigs, + "s"); + if ( (NULL == r) || + (NULL == s) ) + goto cleanup; + if (GNUNET_OK != + s_is_high (s, + &high)) + goto cleanup; + if (high && + (GNUNET_OK != + s_to_low (s)) ) + goto cleanup; + if ( (GNUNET_OK != + mpi_to_fixed (r, + sig->r_s, + P256_SCALAR_LEN)) || + (GNUNET_OK != + mpi_to_fixed (s, + &sig->r_s[P256_SCALAR_LEN], + P256_SCALAR_LEN)) ) + goto cleanup; + ret = GNUNET_OK; +cleanup: + if (NULL != r) + gcry_mpi_release (r); + if (NULL != s) + gcry_mpi_release (s); + if (NULL != hm) + gcry_mpi_release (hm); + if (NULL != sigs) + gcry_sexp_release (sigs); + if (NULL != data) + gcry_sexp_release (data); + if (NULL != skey) + gcry_sexp_release (skey); + return ret; +} + + +enum GNUNET_GenericReturnValue +TALER_ecdsa_p256_verify ( + const struct TALER_EcdsaP256PublicKeyP *pub, + const struct GNUNET_ShortHashCode *hash, + const struct TALER_EcdsaP256SignatureP *sig) +{ + gcry_sexp_t pkey = NULL; + gcry_sexp_t data = NULL; + gcry_sexp_t sigs = NULL; + gcry_mpi_t hm = NULL; + gcry_mpi_t r = NULL; + gcry_mpi_t s = NULL; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + bool high; + + if (0 != gcry_mpi_scan (&r, + GCRYMPI_FMT_USG, + sig->r_s, + P256_SCALAR_LEN, + NULL)) + { + GNUNET_break_op (0); + goto cleanup; + } + if (0 != gcry_mpi_scan (&s, + GCRYMPI_FMT_USG, + &sig->r_s[P256_SCALAR_LEN], + P256_SCALAR_LEN, + NULL)) + { + GNUNET_break_op (0); + goto cleanup; + } + /* only the canonical low-s form is accepted, so that a signature + cannot be mauled into a second valid encoding */ + if (GNUNET_OK != + s_is_high (s, + &high)) + goto cleanup; + if (high) + { + GNUNET_break_op (0); + goto cleanup; + } + if (0 != gcry_mpi_scan (&hm, + GCRYMPI_FMT_USG, + hash, + sizeof (*hash), + NULL)) + { + GNUNET_break (0); + goto cleanup; + } + if (0 != gcry_sexp_build (&data, + NULL, + "(data(flags raw)(value %m))", + hm)) + { + GNUNET_break (0); + goto cleanup; + } + /* libgcrypt decompresses the point and rejects anything that is not + on the curve, so an invalid public key fails here */ + if (0 != gcry_sexp_build (&pkey, + NULL, + "(public-key(ecc(curve \"" P256_CURVE "\")" + "(q %b)))", + (int) sizeof (pub->q), + (const char *) pub->q)) + { + GNUNET_break_op (0); + goto cleanup; + } + if (0 != gcry_sexp_build (&sigs, + NULL, + "(sig-val(ecdsa(r %m)(s %m)))", + r, + s)) + { + GNUNET_break (0); + goto cleanup; + } + ret = (0 == gcry_pk_verify (sigs, + data, + pkey)) + ? GNUNET_OK + : GNUNET_SYSERR; +cleanup: + if (NULL != r) + gcry_mpi_release (r); + if (NULL != s) + gcry_mpi_release (s); + if (NULL != hm) + gcry_mpi_release (hm); + if (NULL != sigs) + gcry_sexp_release (sigs); + if (NULL != data) + gcry_sexp_release (data); + if (NULL != pkey) + gcry_sexp_release (pkey); + return ret; +} + + +/* end of crypto_ecdsa.c */ diff --git a/src/util/crypto_signatures.c b/src/util/crypto_signatures.c @@ -0,0 +1,361 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/crypto_signatures.c + * @brief POS confirmations that sign a challenge (DD 97) + * @author Bohdan Potuzhnyi + * @author Volodymyr Potuzhnyi + * + * The counterpart to the time-based confirmations in + * crypto_confirmation.c. An offline verifier picks an unpredictable + * challenge, the merchant backend signs it once the order was paid, + * and the verifier checks that signature against a public key it was + * configured with. Only the public key ever leaves the backend. + * + * This file owns the message that gets signed, the encoding of keys + * and signatures, and which curve each algorithm uses. The signature + * operations themselves come from TALER_ecdsa_p256_*() and from + * GNUnet's EdDSA. + */ +#include "platform.h" +#include "taler/taler_util.h" +#include <gcrypt.h> + + +/** + * Size of the message that gets signed. + */ +#define CHALLENGE_MSG_SIZE \ + (sizeof (TALER_POS_CHALLENGE_SALT) - 1 + TALER_POS_CHALLENGE_LENGTH) + + +/** + * Assemble the message a confirmation signs: the domain separation + * salt directly followed by the raw challenge bytes. An offline + * verifier reproduces exactly this. + * + * @param challenge challenge to bind the signature to + * @param[out] msg where to write #CHALLENGE_MSG_SIZE bytes + */ +static void +build_message (const struct TALER_PosChallengeP *challenge, + unsigned char *msg) +{ + memcpy (msg, + TALER_POS_CHALLENGE_SALT, + sizeof (TALER_POS_CHALLENGE_SALT) - 1); + memcpy (&msg[sizeof (TALER_POS_CHALLENGE_SALT) - 1], + challenge->challenge, + TALER_POS_CHALLENGE_LENGTH); +} + + +/** + * Hash the challenge message, as the ECDSA algorithm signs a digest. + * + * @param challenge challenge to bind the signature to + * @param[out] hash set to the SHA-256 of the challenge message + */ +static void +hash_message (const struct TALER_PosChallengeP *challenge, + struct GNUNET_ShortHashCode *hash) +{ + unsigned char msg[CHALLENGE_MSG_SIZE]; + + build_message (challenge, + msg); + gcry_md_hash_buffer (GCRY_MD_SHA256, + hash, + msg, + sizeof (msg)); +} + + +enum GNUNET_GenericReturnValue +TALER_otp_device_key_create ( + enum TALER_MerchantConfirmationAlgorithm pos_alg, + char **pos_key, + char **pos_pub) +{ + *pos_key = NULL; + *pos_pub = NULL; + switch (pos_alg) + { + case TALER_MCA_ECDSA_CHALLENGE: + { + struct TALER_EcdsaP256PrivateKeyP priv; + struct TALER_EcdsaP256PublicKeyP pub; + + if (GNUNET_OK != + TALER_ecdsa_p256_key_create (&priv, + &pub)) + return GNUNET_SYSERR; + *pos_key = GNUNET_STRINGS_data_to_string_alloc (&priv, + sizeof (priv)); + *pos_pub = GNUNET_STRINGS_data_to_string_alloc (&pub, + sizeof (pub)); + GNUNET_CRYPTO_zero_keys (&priv, + sizeof (priv)); + break; + } + case TALER_MCA_EDDSA_CHALLENGE: + { + struct GNUNET_CRYPTO_EddsaPrivateKey priv; + struct GNUNET_CRYPTO_EddsaPublicKey pub; + + GNUNET_CRYPTO_eddsa_key_create (&priv); + GNUNET_CRYPTO_eddsa_key_get_public (&priv, + &pub); + *pos_key = GNUNET_STRINGS_data_to_string_alloc (&priv, + sizeof (priv)); + *pos_pub = GNUNET_STRINGS_data_to_string_alloc (&pub, + sizeof (pub)); + GNUNET_CRYPTO_eddsa_key_clear (&priv); + break; + } + case TALER_MCA_NONE: + case TALER_MCA_WITHOUT_PRICE: + case TALER_MCA_WITH_PRICE: + /* TOTP keys are supplied by the merchant, not generated here */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + if ( (NULL == *pos_key) || + (NULL == *pos_pub) ) + { + GNUNET_break (0); + GNUNET_free (*pos_key); + GNUNET_free (*pos_pub); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Sign @a challenge with the NIST P-256 private key @a pos_key. + * + * @param pos_key Crockford base32-encoded P-256 private scalar + * @param challenge challenge to bind the signature to + * @return Crockford base32-encoded r|s signature, or NULL on error + */ +static char * +sign_ecdsa (const char *pos_key, + const struct TALER_PosChallengeP *challenge) +{ + struct TALER_EcdsaP256PrivateKeyP priv; + struct TALER_EcdsaP256SignatureP sig; + struct GNUNET_ShortHashCode hash; + char *ret; + + if ( (NULL == pos_key) || + (NULL == challenge) ) + { + GNUNET_break (0); + return NULL; + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (pos_key, + strlen (pos_key), + &priv, + sizeof (priv))) + { + GNUNET_break (0); + return NULL; + } + hash_message (challenge, + &hash); + if (GNUNET_OK != + TALER_ecdsa_p256_sign (&priv, + &hash, + &sig)) + { + GNUNET_break (0); + GNUNET_CRYPTO_zero_keys (&priv, + sizeof (priv)); + return NULL; + } + GNUNET_CRYPTO_zero_keys (&priv, + sizeof (priv)); + ret = GNUNET_STRINGS_data_to_string_alloc (&sig, + sizeof (sig)); + return ret; +} + + +/** + * Sign @a challenge with the Ed25519 private key @a pos_key. + * + * @param pos_key Crockford base32-encoded Ed25519 private key + * @param challenge challenge to bind the signature to + * @return Crockford base32-encoded signature, or NULL on error + */ +static char * +sign_eddsa (const char *pos_key, + const struct TALER_PosChallengeP *challenge) +{ + struct GNUNET_CRYPTO_EddsaPrivateKey priv; + struct GNUNET_CRYPTO_EddsaSignature sig; + unsigned char msg[CHALLENGE_MSG_SIZE]; + char *ret; + + if ( (NULL == pos_key) || + (NULL == challenge) ) + { + GNUNET_break (0); + return NULL; + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (pos_key, + strlen (pos_key), + &priv, + sizeof (priv))) + { + GNUNET_break (0); + return NULL; + } + build_message (challenge, + msg); + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_sign_raw (&priv, + msg, + sizeof (msg), + &sig)) + { + GNUNET_break (0); + GNUNET_CRYPTO_eddsa_key_clear (&priv); + return NULL; + } + GNUNET_CRYPTO_eddsa_key_clear (&priv); + ret = GNUNET_STRINGS_data_to_string_alloc (&sig, + sizeof (sig)); + return ret; +} + + +char * +TALER_build_pos_confirmation_sig ( + const char *pos_key, + enum TALER_MerchantConfirmationAlgorithm pos_alg, + const struct TALER_PosChallengeP *challenge) +{ + if ( (NULL == pos_key) || + (NULL == challenge) ) + { + GNUNET_break (0); + return NULL; + } + switch (pos_alg) + { + case TALER_MCA_ECDSA_CHALLENGE: + return sign_ecdsa (pos_key, + challenge); + case TALER_MCA_EDDSA_CHALLENGE: + return sign_eddsa (pos_key, + challenge); + case TALER_MCA_NONE: + case TALER_MCA_WITHOUT_PRICE: + case TALER_MCA_WITH_PRICE: + /* time-based algorithms are TALER_build_pos_confirmation()'s job */ + GNUNET_break (0); + return NULL; + } + GNUNET_break (0); + return NULL; +} + + +enum GNUNET_GenericReturnValue +TALER_check_pos_confirmation_sig ( + const char *pos_pub, + enum TALER_MerchantConfirmationAlgorithm pos_alg, + const struct TALER_PosChallengeP *challenge, + const char *pos_confirmation) +{ + if ( (NULL == pos_pub) || + (NULL == challenge) || + (NULL == pos_confirmation) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + switch (pos_alg) + { + case TALER_MCA_ECDSA_CHALLENGE: + { + struct TALER_EcdsaP256PublicKeyP pub; + struct TALER_EcdsaP256SignatureP sig; + struct GNUNET_ShortHashCode hash; + + if ( (GNUNET_OK != + GNUNET_STRINGS_string_to_data (pos_pub, + strlen (pos_pub), + &pub, + sizeof (pub))) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (pos_confirmation, + strlen (pos_confirmation), + &sig, + sizeof (sig))) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + hash_message (challenge, + &hash); + return TALER_ecdsa_p256_verify (&pub, + &hash, + &sig); + } + case TALER_MCA_EDDSA_CHALLENGE: + { + struct GNUNET_CRYPTO_EddsaPublicKey pub; + struct GNUNET_CRYPTO_EddsaSignature sig; + unsigned char msg[CHALLENGE_MSG_SIZE]; + + if ( (GNUNET_OK != + GNUNET_STRINGS_string_to_data (pos_pub, + strlen (pos_pub), + &pub, + sizeof (pub))) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (pos_confirmation, + strlen (pos_confirmation), + &sig, + sizeof (sig))) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + build_message (challenge, + msg); + return GNUNET_CRYPTO_eddsa_verify_raw (msg, + sizeof (msg), + &sig, + &pub); + } + case TALER_MCA_NONE: + case TALER_MCA_WITHOUT_PRICE: + case TALER_MCA_WITH_PRICE: + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_break (0); + return GNUNET_SYSERR; +} + + +/* end of crypto_signatures.c */ diff --git a/src/util/meson.build b/src/util/meson.build @@ -45,6 +45,8 @@ libtalerutil_src = [ 'config.c', 'crypto.c', 'crypto_confirmation.c', + 'crypto_ecdsa.c', + 'crypto_signatures.c', 'crypto_contract.c', 'crypto_helper_common.c', 'crypto_helper_rsa.c', @@ -148,6 +150,7 @@ talerutil_tests = [ 'test_age_restriction', 'test_amount', 'test_crypto', + 'test_crypto_confirmation', 'test_payto', 'test_url', ] diff --git a/src/util/test_crypto_confirmation.c b/src/util/test_crypto_confirmation.c @@ -0,0 +1,964 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/test_crypto_confirmation.c + * @brief tests for POS confirmation computation + * @author Bohdan Potuzhnyi + * @author Volodymyr Potuzhnyi + */ +#include "platform.h" +#include "taler/taler_util.h" + + +/** + * Number of checks that failed. + */ +static unsigned int fails; + + +/** + * Record the outcome of a single check. + * + * @param ok true if the check passed + * @param label human-readable description of the check + */ +static void +check (bool ok, + const char *label) +{ + if (! ok) + { + fprintf (stderr, + "FAIL %s\n", + label); + fails++; + return; + } + fprintf (stderr, + "ok %s\n", + label); +} + + +/** + * Check that @a enc decodes to exactly @a len bytes. + * + * @param enc Crockford base32-encoded value + * @param len expected number of bytes + * @return true if the length matches + */ +static bool +decodes_to (const char *enc, + size_t len) +{ + unsigned char buf[128]; + + GNUNET_assert (len <= sizeof (buf)); + return (GNUNET_OK == + GNUNET_STRINGS_string_to_data (enc, + strlen (enc), + buf, + len)); +} + + +/** + * Run the full challenge-signature test suite for one algorithm. + * + * @param alg algorithm to exercise + * @param name human-readable name of @a alg + * @param pub_len expected public key length in bytes + */ +static void +test_challenge_alg (enum TALER_MerchantConfirmationAlgorithm alg, + const char *name, + size_t pub_len) +{ + char *priv = NULL; + char *pub = NULL; + char *priv2 = NULL; + char *pub2 = NULL; + char *conf; + char *conf2; + struct TALER_PosChallengeP ch; + struct TALER_PosChallengeP ch2; + char label[256]; + +#define LABEL(what) \ + GNUNET_snprintf (label, sizeof (label), "%s: %s", name, what) + + fprintf (stderr, + "-- %s --\n", + name); + LABEL ("keygen"); + check (GNUNET_OK == + TALER_otp_device_key_create (alg, + &priv, + &pub), + label); + if ( (NULL == priv) || + (NULL == pub) ) + return; + LABEL ("public key has the documented length"); + check (decodes_to (pub, + pub_len), + label); + + GNUNET_CRYPTO_random_block (&ch, + sizeof (ch)); + conf = TALER_build_pos_confirmation_sig (priv, + alg, + &ch); + LABEL ("sign"); + check (NULL != conf, + label); + if (NULL == conf) + return; + LABEL ("signature is 64 bytes"); + check (decodes_to (conf, + 64), + label); + LABEL ("verify accepts a fresh confirmation"); + check (GNUNET_OK == + TALER_check_pos_confirmation_sig (pub, + alg, + &ch, + conf), + label); + + /* DD 97: modifying the challenge must invalidate the confirmation */ + ch2 = ch; + ch2.challenge[0] ^= 0x01; + LABEL ("mutated challenge is rejected"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (pub, + alg, + &ch2, + conf), + label); + + /* DD 97: another device's key must not accept the confirmation */ + check (GNUNET_OK == + TALER_otp_device_key_create (alg, + &priv2, + &pub2), + "second keygen"); + LABEL ("confirmation is rejected under a different key"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (pub2, + alg, + &ch, + conf), + label); + + conf2 = TALER_build_pos_confirmation_sig (priv2, + alg, + &ch); + LABEL ("a different key yields a different confirmation"); + check ( (NULL != conf2) && + (0 != strcmp (conf, + conf2)), + label); + + { + char *bad = GNUNET_strdup (conf); + + bad[0] = ('A' == bad[0]) ? 'B' : 'A'; + LABEL ("mauled confirmation is rejected"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (pub, + alg, + &ch, + bad), + label); + GNUNET_free (bad); + } + + /* DD 97: must not return a confirmation we cannot compute */ + LABEL ("missing challenge fails closed"); + check (NULL == + TALER_build_pos_confirmation_sig (priv, + alg, + NULL), + label); +#undef LABEL + GNUNET_free (conf); + GNUNET_free (conf2); + GNUNET_free (priv); + GNUNET_free (pub); + GNUNET_free (priv2); + GNUNET_free (pub2); +} + + +/** + * Known-answer vectors. These pin the on-the-wire format an offline + * verifier depends on: a round-trip test would happily follow us if + * the salt or the field order ever changed. + */ +#define KAT_EDDSA_PRIV "041061050R3GG28A1C60T3GF208H44RM2MB1E60S38DHR78Y3WG0" +#define KAT_EDDSA_PUB "F6TNCBMFWSAFJG3RP49EHACBMY81Z19TWTAVXNZ0WE8GQB84JSJ0" +#define KAT_EDDSA_SIG \ + "VKTQA94GZCQ6R5AWV51X51X4P6Q4EE8F0MXWWZXMGRKJKR2KV2BRBXJG8YV4E0VY56GG" \ + "CBCG5XAMEV1C1AD9FNXHDBXK4JXNZ008E08" +#define KAT_ECDSA_PUB "0CZV95QR7R1GKGFFDAW2N4ZMAMXTWYF4AHX7XR87F3XW5KZV9C922" +#define KAT_ECDSA_SIG \ + "03Q7RNNBBRPKG4YNH5JH92H557JHHGV92M7MMFXD9MH0MQFJG46QADH7GXNFNGTPD6MD" \ + "80Y4CC3ESKTRM7AF9PRBXVN4FMYEY7JWMM8" + + +/** + * Check the confirmations we produce against fixed vectors, so that a + * change to the signed message is caught instead of silently breaking + * every already-deployed offline verifier. + */ +static void +test_known_answers (void) +{ + struct TALER_PosChallengeP ch; + char *conf; + + fprintf (stderr, + "-- known-answer vectors --\n"); + for (unsigned int i = 0; i < sizeof (ch.challenge); i++) + ch.challenge[i] = (unsigned char) (0xF0 - i); + + /* EdDSA signing is deterministic, so the exact bytes are pinned */ + conf = TALER_build_pos_confirmation_sig (KAT_EDDSA_PRIV, + TALER_MCA_EDDSA_CHALLENGE, + &ch); + check ( (NULL != conf) && + (0 == strcmp (conf, + KAT_EDDSA_SIG)), + "EdDSA confirmation matches the known-answer vector"); + GNUNET_free (conf); + check (GNUNET_OK == + TALER_check_pos_confirmation_sig (KAT_EDDSA_PUB, + TALER_MCA_EDDSA_CHALLENGE, + &ch, + KAT_EDDSA_SIG), + "EdDSA known-answer vector verifies"); + + /* ECDSA signing is randomized, so only verification can be pinned */ + check (GNUNET_OK == + TALER_check_pos_confirmation_sig (KAT_ECDSA_PUB, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + KAT_ECDSA_SIG), + "ECDSA known-answer vector verifies"); +} + + +/** + * Re-encode @a raw as a confirmation string. + * + * @param raw 64 raw signature bytes + * @return encoded signature, to be freed by the caller + */ +static char * +encode_sig (const unsigned char *raw) +{ + return GNUNET_STRINGS_data_to_string_alloc (raw, + 64); +} + + +/** + * Decode the confirmation @a enc into its 64 raw bytes. + * + * @param enc encoded signature + * @param[out] raw where to write the raw signature + */ +static void +decode_sig (const char *enc, + unsigned char *raw) +{ + GNUNET_assert (GNUNET_OK == + GNUNET_STRINGS_string_to_data (enc, + strlen (enc), + raw, + 64)); +} + + +/** + * Add the Ed25519 group order L to the little-endian scalar S in + * place. The result is a different encoding of the same scalar + * modulo L, which a verifier that omits the canonicality check would + * wrongly accept. + * + * @param[in,out] s the 32 little-endian bytes of S + */ +static void +add_ed25519_order (unsigned char *s) +{ + static const unsigned char L[32] = { + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, + 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 + }; + unsigned int carry = 0; + + for (unsigned int i = 0; i < 32; i++) + { + unsigned int v = s[i] + L[i] + carry; + + s[i] = (unsigned char) (v & 0xff); + carry = v >> 8; + } +} + + +/** + * Replace the big-endian P-256 scalar @a s by n-s in place. Both + * (r,s) and (r,n-s) are mathematically valid ECDSA signatures; we + * accept only the smaller one. + * + * @param[in,out] s the 32 big-endian bytes of s + */ +static void +negate_p256_scalar (unsigned char *s) +{ + static const unsigned char n[32] = { + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, + 0xf3, 0xb9, 0xca, 0xc2, 0xfc, 0x63, 0x25, 0x51 + }; + int borrow = 0; + + for (int i = 31; i >= 0; i--) + { + int v = (int) n[i] - (int) s[i] - borrow; + + if (v < 0) + { + v += 256; + borrow = 1; + } + else + { + borrow = 0; + } + s[i] = (unsigned char) v; + } +} + + +/** + * A confirmation must not be transformable into a second confirmation + * that a verifier would also accept: an offline device that tracked + * spent confirmations instead of spent challenges would otherwise be + * replayable. + */ +static void +test_malleability (void) +{ + char *priv; + char *pub; + char *conf; + char *mauled; + struct TALER_PosChallengeP ch; + unsigned char raw[64]; + + fprintf (stderr, + "-- malleability --\n"); + GNUNET_CRYPTO_random_block (&ch, + sizeof (ch)); + + /* Ed25519: S+L encodes the same scalar; RFC 8032 requires S < L */ + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_EDDSA_CHALLENGE, + &priv, + &pub)); + conf = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_EDDSA_CHALLENGE, + &ch); + GNUNET_assert (NULL != conf); + decode_sig (conf, + raw); + add_ed25519_order (&raw[32]); + mauled = encode_sig (raw); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (pub, + TALER_MCA_EDDSA_CHALLENGE, + &ch, + mauled), + "EdDSA non-canonical S (S+L) is rejected"); + GNUNET_free (mauled); + GNUNET_free (conf); + GNUNET_free (priv); + GNUNET_free (pub); + + /* P-256: (r, n-s) is a valid signature that we reject by policy */ + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_ECDSA_CHALLENGE, + &priv, + &pub)); + conf = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_ECDSA_CHALLENGE, + &ch); + GNUNET_assert (NULL != conf); + decode_sig (conf, + raw); + negate_p256_scalar (&raw[32]); + mauled = encode_sig (raw); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (pub, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + mauled), + "ECDSA high-s variant is rejected"); + GNUNET_free (mauled); + GNUNET_free (conf); + GNUNET_free (priv); + GNUNET_free (pub); +} + + +/** + * Every ECDSA signature we emit must be the canonical low-s one, and + * the public key must be a compressed point. Runs enough iterations + * that a scalar with a leading zero byte -- which the fixed-width + * encoding has to pad -- is hit with near certainty. + */ +static void +test_ecdsa_encoding (void) +{ + unsigned int low_s_failures = 0; + unsigned int prefix_failures = 0; + unsigned int verify_failures = 0; + unsigned int short_scalars = 0; + + fprintf (stderr, + "-- ECDSA encoding (300 iterations) --\n"); + for (unsigned int i = 0; i < 300; i++) + { + char *priv; + char *pub; + char *conf; + struct TALER_PosChallengeP ch; + unsigned char raw[64]; + unsigned char pubraw[33]; + unsigned char neg[32]; + + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_ECDSA_CHALLENGE, + &priv, + &pub)); + GNUNET_CRYPTO_random_block (&ch, + sizeof (ch)); + conf = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_ECDSA_CHALLENGE, + &ch); + GNUNET_assert (NULL != conf); + GNUNET_assert (GNUNET_OK == + GNUNET_STRINGS_string_to_data (pub, + strlen (pub), + pubraw, + sizeof (pubraw))); + if ( (0x02 != pubraw[0]) && + (0x03 != pubraw[0]) ) + prefix_failures++; + decode_sig (conf, + raw); + /* s is low exactly when it is smaller than n-s */ + memcpy (neg, + &raw[32], + sizeof (neg)); + negate_p256_scalar (neg); + if (memcmp (&raw[32], + neg, + sizeof (neg)) >= 0) + low_s_failures++; + /* count fixed-width scalars that needed zero padding */ + if ( (0x00 == raw[0]) || + (0x00 == raw[32]) ) + short_scalars++; + if (GNUNET_OK != + TALER_check_pos_confirmation_sig (pub, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + conf)) + verify_failures++; + GNUNET_free (conf); + GNUNET_free (priv); + GNUNET_free (pub); + } + check (0 == prefix_failures, + "every public key is a compressed point (0x02/0x03)"); + check (0 == low_s_failures, + "every signature uses the canonical low-s form"); + check (0 == verify_failures, + "every signature verifies"); + fprintf (stderr, + " (%u/300 signatures had a scalar needing zero padding)\n", + short_scalars); +} + + +/** + * Ed25519 signing is deterministic while ECDSA is randomized; a + * repeated ECDSA nonce would be catastrophic, so the signatures must + * differ. + */ +static void +test_signing_determinism (void) +{ + char *priv; + char *pub; + char *a; + char *b; + struct TALER_PosChallengeP ch; + + fprintf (stderr, + "-- signing determinism --\n"); + GNUNET_CRYPTO_random_block (&ch, + sizeof (ch)); + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_EDDSA_CHALLENGE, + &priv, + &pub)); + a = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_EDDSA_CHALLENGE, + &ch); + b = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_EDDSA_CHALLENGE, + &ch); + check ( (NULL != a) && + (NULL != b) && + (0 == strcmp (a, + b)), + "EdDSA signing is deterministic"); + GNUNET_free (a); + GNUNET_free (b); + GNUNET_free (priv); + GNUNET_free (pub); + + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_ECDSA_CHALLENGE, + &priv, + &pub)); + a = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_ECDSA_CHALLENGE, + &ch); + b = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_ECDSA_CHALLENGE, + &ch); + check ( (NULL != a) && + (NULL != b) && + (0 != strcmp (a, + b)), + "ECDSA signing uses a fresh nonce each time"); + check ( (GNUNET_OK == + TALER_check_pos_confirmation_sig (pub, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + a)) && + (GNUNET_OK == + TALER_check_pos_confirmation_sig (pub, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + b)), + "both ECDSA signatures verify"); + GNUNET_free (a); + GNUNET_free (b); + GNUNET_free (priv); + GNUNET_free (pub); +} + + +/** + * Is the compressed point @a raw refused as the public key for the + * confirmation @a sig? + * + * @param raw 33 bytes to offer as a public key + * @param ch challenge the confirmation is bound to + * @param sig a confirmation that is valid under the real key + * @return true if verification refused @a raw + */ +static bool +pub_rejected (const unsigned char *raw, + const struct TALER_PosChallengeP *ch, + const char *sig) +{ + char *enc; + enum GNUNET_GenericReturnValue r; + + enc = GNUNET_STRINGS_data_to_string_alloc (raw, + 33); + r = TALER_check_pos_confirmation_sig (enc, + TALER_MCA_ECDSA_CHALLENGE, + ch, + sig); + GNUNET_free (enc); + return (GNUNET_OK != r); +} + + +/** + * A public key that is not a valid curve point must be refused. The + * compressed encoding bounds what can even be expressed, so the cases + * are an X with no square root, an X at or beyond the field prime, + * and an undefined prefix byte. libgcrypt refuses all of them today + * and this pins that, so that changing how the point is handed to the + * backend cannot silently start accepting garbage. + */ +static void +test_ecdsa_invalid_points (void) +{ + char *priv; + char *pub; + char *conf; + struct TALER_PosChallengeP ch; + unsigned char real[33]; + unsigned char raw[33]; + unsigned int accepted = 0; + + fprintf (stderr, + "-- ECDSA invalid public points --\n"); + GNUNET_CRYPTO_random_block (&ch, + sizeof (ch)); + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_ECDSA_CHALLENGE, + &priv, + &pub)); + conf = TALER_build_pos_confirmation_sig (priv, + TALER_MCA_ECDSA_CHALLENGE, + &ch); + GNUNET_assert (NULL != conf); + GNUNET_assert (GNUNET_OK == + GNUNET_STRINGS_string_to_data (pub, + strlen (pub), + real, + sizeof (real))); + + /* X = 1 has no square root modulo p, so these 33 bytes do not + encode a point at all (cross-checked against OpenSSL) */ + memset (raw, + 0, + sizeof (raw)); + raw[0] = 0x02; + raw[32] = 1; + check (pub_rejected (raw, + &ch, + conf), + "an X with no valid Y is rejected"); + + /* X = 5 is on the curve, but it is not our key: this must fail as a + signature check rather than as a decoding error */ + memset (raw, + 0, + sizeof (raw)); + raw[0] = 0x02; + raw[32] = 5; + check (pub_rejected (raw, + &ch, + conf), + "a valid but unrelated point is rejected"); + + /* X beyond the field prime */ + raw[0] = 0x02; + memset (&raw[1], + 0xFF, + 32); + check (pub_rejected (raw, + &ch, + conf), + "an X beyond the field prime is rejected"); + + /* undefined or wrong prefix bytes on an otherwise real key */ + memcpy (raw, + real, + sizeof (raw)); + raw[0] = 0x00; + check (pub_rejected (raw, + &ch, + conf), + "prefix 0x00 is rejected"); + memcpy (raw, + real, + sizeof (raw)); + raw[0] = 0x04; + check (pub_rejected (raw, + &ch, + conf), + "the uncompressed-point prefix 0x04 is rejected"); + memcpy (raw, + real, + sizeof (raw)); + raw[0] = 0x05; + check (pub_rejected (raw, + &ch, + conf), + "an undefined prefix is rejected"); + + /* flipping the parity bit selects the other point with the same X, + which is a valid point but the wrong public key */ + memcpy (raw, + real, + sizeof (raw)); + raw[0] = (0x02 == real[0]) ? 0x03 : 0x02; + check (pub_rejected (raw, + &ch, + conf), + "flipping the point parity is rejected"); + + /* Small X values, both prefixes. Some are points that are simply + not our key; the rest have no square root modulo p and do not + decode at all. None of them may verify. The prefix picks between + +Y and -Y, so it does not affect whether a Y exists. + + on the curve: 5, 6, 8, 9, 12, 13, 17 + off the curve: 1, 2, 3, 4, 7, 10, 11, 14, 15, 16, 18, 19, 20 */ + for (unsigned int x = 1; x <= 20; x++) + { + for (unsigned int parity = 0; parity < 2; parity++) + { + memset (raw, + 0, + sizeof (raw)); + raw[0] = (0 == parity) ? 0x02 : 0x03; + raw[32] = (unsigned char) x; + if (! pub_rejected (raw, + &ch, + conf)) + accepted++; + } + } + check (0 == accepted, + "40 small compressed points are all rejected"); + + GNUNET_free (conf); + GNUNET_free (priv); + GNUNET_free (pub); +} + + +/** + * Malformed and mismatched inputs must be refused rather than + * misinterpreted. + */ +static void +test_bad_inputs (void) +{ + char *ed_priv; + char *ed_pub; + char *ec_priv; + char *ec_pub; + char *ed_sig; + char *ec_sig; + struct TALER_PosChallengeP ch; + + fprintf (stderr, + "-- malformed and mismatched inputs --\n"); + GNUNET_CRYPTO_random_block (&ch, + sizeof (ch)); + + /* key generation is only defined for the challenge algorithms */ + { + char *k = NULL; + char *p = NULL; + + check (GNUNET_OK != + TALER_otp_device_key_create (TALER_MCA_NONE, + &k, + &p), + "keygen refuses NONE"); + check (GNUNET_OK != + TALER_otp_device_key_create (TALER_MCA_WITHOUT_PRICE, + &k, + &p), + "keygen refuses TOTP_WITHOUT_PRICE"); + check (GNUNET_OK != + TALER_otp_device_key_create (TALER_MCA_WITH_PRICE, + &k, + &p), + "keygen refuses TOTP_WITH_PRICE"); + } + + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_EDDSA_CHALLENGE, + &ed_priv, + &ed_pub)); + GNUNET_assert (GNUNET_OK == + TALER_otp_device_key_create (TALER_MCA_ECDSA_CHALLENGE, + &ec_priv, + &ec_pub)); + ed_sig = TALER_build_pos_confirmation_sig (ed_priv, + TALER_MCA_EDDSA_CHALLENGE, + &ch); + ec_sig = TALER_build_pos_confirmation_sig (ec_priv, + TALER_MCA_ECDSA_CHALLENGE, + &ch); + GNUNET_assert ( (NULL != ed_sig) && + (NULL != ec_sig) ); + + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (ed_pub, + TALER_MCA_WITH_PRICE, + &ch, + ed_sig), + "verify refuses a TOTP algorithm"); + /* an Ed25519 key is 32 bytes and a P-256 key 33, so the encodings + are not interchangeable even before the curve differs */ + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (ed_pub, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + ed_sig), + "an EdDSA confirmation does not verify as ECDSA"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (ec_pub, + TALER_MCA_EDDSA_CHALLENGE, + &ch, + ec_sig), + "an ECDSA confirmation does not verify as EdDSA"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (ec_pub, + TALER_MCA_ECDSA_CHALLENGE, + &ch, + ed_sig), + "a confirmation made with the wrong key is rejected"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig ("!!!not base32!!!", + TALER_MCA_EDDSA_CHALLENGE, + &ch, + ed_sig), + "a malformed public key is rejected"); + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (ed_pub, + TALER_MCA_EDDSA_CHALLENGE, + &ch, + "!!!not base32!!!"), + "a malformed confirmation is rejected"); + { + char *truncated = GNUNET_strdup (ed_sig); + + truncated[strlen (truncated) - 4] = '\0'; + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (ed_pub, + TALER_MCA_EDDSA_CHALLENGE, + &ch, + truncated), + "a truncated confirmation is rejected"); + GNUNET_free (truncated); + } + { + char *truncated = GNUNET_strdup (ed_pub); + + truncated[strlen (truncated) - 4] = '\0'; + check (GNUNET_OK != + TALER_check_pos_confirmation_sig (truncated, + TALER_MCA_EDDSA_CHALLENGE, + &ch, + ed_sig), + "a truncated public key is rejected"); + GNUNET_free (truncated); + } + /* signing must refuse a key that is not the right size */ + check (NULL == + TALER_build_pos_confirmation_sig ("TOOSHORT", + TALER_MCA_EDDSA_CHALLENGE, + &ch), + "signing refuses a short EdDSA key"); + check (NULL == + TALER_build_pos_confirmation_sig ("TOOSHORT", + TALER_MCA_ECDSA_CHALLENGE, + &ch), + "signing refuses a short ECDSA key"); + + GNUNET_free (ed_sig); + GNUNET_free (ec_sig); + GNUNET_free (ed_priv); + GNUNET_free (ed_pub); + GNUNET_free (ec_priv); + GNUNET_free (ec_pub); +} + + +/** + * The challenge algorithms must not have disturbed the TOTP + * algorithms, which stay bit-for-bit as they were. + */ +static void +test_totp_unchanged (void) +{ + /* RFC 3548 base32, as a TOTP application would provide it */ + const char *pos_key = "JBSWY3DPEHPK3PXP"; + struct TALER_Amount total; + char *code; + + fprintf (stderr, + "-- TOTP regression --\n"); + code = TALER_build_pos_confirmation (pos_key, + TALER_MCA_WITHOUT_PRICE, + NULL, + GNUNET_TIME_UNIT_ZERO_TS); + check (NULL != code, + "TOTP_WITHOUT_PRICE still produces a code"); + GNUNET_free (code); + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:1.5", + &total)); + code = TALER_build_pos_confirmation (pos_key, + TALER_MCA_WITH_PRICE, + &total, + GNUNET_TIME_UNIT_ZERO_TS); + check (NULL != code, + "TOTP_WITH_PRICE still produces a code"); + GNUNET_free (code); + + /* an invalid amount must fail closed rather than sign nothing */ + code = TALER_build_pos_confirmation (pos_key, + TALER_MCA_WITH_PRICE, + NULL, + GNUNET_TIME_UNIT_ZERO_TS); + check (NULL == code, + "TOTP_WITH_PRICE without an amount fails closed"); + GNUNET_free (code); +} + + +int +main (int argc, + char *const *argv) +{ + (void) argc; + (void) argv; + GNUNET_log_setup ("test-crypto-confirmation", + "WARNING", + NULL); + test_challenge_alg (TALER_MCA_EDDSA_CHALLENGE, + "EDDSA_CHALLENGE", + 32); + test_challenge_alg (TALER_MCA_ECDSA_CHALLENGE, + "ECDSA_CHALLENGE", + 33); + test_known_answers (); + test_malleability (); + test_ecdsa_encoding (); + test_signing_determinism (); + test_ecdsa_invalid_points (); + test_bad_inputs (); + test_totp_unchanged (); + return (0 == fails) ? 0 : 1; +} + + +/* end of test_crypto_confirmation.c */