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:
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 (¶ms,
+ 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 */