anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

commit 6af6cd9c8e60bd0a581279bd503c838914a28275
parent 902b2e7ca5946190de0163ee56ff90247a9df19a
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 29 Jul 2026 18:38:11 +0200

propagate decryption failures from the crypto wrappers instead of asserting

Diffstat:
Mmeson.build | 2+-
Msrc/backend/anastasis-httpd_truth-challenge.c | 28+++++++++++++++-------------
Msrc/backend/anastasis-httpd_truth-solve.c | 32++++++++++++++++++--------------
Msrc/include/anastasis.h | 14+++++++++++++-
Msrc/include/anastasis_crypto_lib.h | 17+++++++++++++----
Msrc/lib/anastasis_recovery.c | 63+++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/reducer/anastasis_api_recovery_redux.c | 10++++++++++
Msrc/testing/testing_api_cmd_truth_store.c | 16+++++++++++-----
Msrc/util/anastasis_crypto.c | 114++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/util/test_anastasis_crypto.c | 67++++++++++++++++++++++++++++++++++++++++++++++---------------------
10 files changed, 243 insertions(+), 120 deletions(-)

diff --git a/meson.build b/meson.build @@ -310,7 +310,7 @@ if not get_option('only-doc') libltversions = [ ['libanastasis', '0:0:0'], - ['libanastasisutil', '0:0:0'], + ['libanastasisutil', '1:0:0'], ['libanastasisauthorization', '0:0:0'], ['libanastasiseufin', '0:0:0'], ['libanastasisdb', '3:0:0'], diff --git a/src/backend/anastasis-httpd_truth-challenge.c b/src/backend/anastasis-httpd_truth-challenge.c @@ -1232,21 +1232,23 @@ AH_handler_truth_challenge ( /* We've been paid, now validate response */ { + enum GNUNET_GenericReturnValue res; + /* decrypt encrypted_truth */ - ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, - encrypted_truth, - encrypted_truth_size, - &decrypted_truth, - &decrypted_truth_size); + res = ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, + encrypted_truth, + encrypted_truth_size, + &decrypted_truth, + &decrypted_truth_size); GNUNET_free (encrypted_truth); - } - if (NULL == decrypted_truth) - { - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, - NULL); + if (GNUNET_OK != res) + { + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, + NULL); + } } /* Not security question and no answer: use plugin to check if diff --git a/src/backend/anastasis-httpd_truth-solve.c b/src/backend/anastasis-httpd_truth-solve.c @@ -1373,21 +1373,25 @@ AH_handler_truth_solve ( /* We've been paid, now validate the response */ /* decrypt encrypted_truth */ - ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, - encrypted_truth, - encrypted_truth_size, - &decrypted_truth, - &decrypted_truth_size); - GNUNET_free (encrypted_truth); - if (NULL == decrypted_truth) { - /* most likely, the decryption key is simply wrong */ - GNUNET_break_op (0); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, - NULL); + enum GNUNET_GenericReturnValue res; + + res = ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, + encrypted_truth, + encrypted_truth_size, + &decrypted_truth, + &decrypted_truth_size); + GNUNET_free (encrypted_truth); + if (GNUNET_OK != res) + { + /* most likely, the decryption key is simply wrong */ + GNUNET_break_op (0); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, + NULL); + } } /* Special case for secure question: we do not generate a numeric challenge, diff --git a/src/include/anastasis.h b/src/include/anastasis.h @@ -686,7 +686,19 @@ enum ANASTASIS_RecoveryStatus /** * The Anastasis server reported that the account is unknown. */ - ANASTASIS_RS_POLICY_UNKNOWN + ANASTASIS_RS_POLICY_UNKNOWN, + + /** + * The recovery document could not be decrypted with the key derived + * from the identity attributes. + */ + ANASTASIS_RS_POLICY_DECRYPTION_FAILED, + + /** + * The core secret could not be reassembled from the key shares we + * obtained; at least one of them must have been wrong. + */ + ANASTASIS_RS_CORE_SECRET_RECOVERY_FAILED }; diff --git a/src/include/anastasis_crypto_lib.h b/src/include/anastasis_crypto_lib.h @@ -385,8 +385,10 @@ ANASTASIS_CRYPTO_recovery_document_encrypt ( * @param[out] rec_doc return from the result, which contains the encrypted recovery document * and the nonce and iv used for the encryption as Additional Data * @param[out] rd_size size of the result + * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a enc_rec_doc could not + * be decrypted (it comes from an untrusted provider) */ -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_recovery_document_decrypt ( const struct ANASTASIS_CRYPTO_UserIdentifierP *id, const void *enc_rec_doc, @@ -456,8 +458,11 @@ ANASTASIS_CRYPTO_keyshare_encrypt ( * @param id the user identification which is the entropy source for the key generation * @param xsalt answer to security question, otherwise NULL; used as extra salt in KDF * @param[out] key_share the result of decryption + * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a enc_key_share could not + * be decrypted or had the wrong size (it comes from an untrusted + * provider) */ -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_keyshare_decrypt ( const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *enc_key_share, const struct ANASTASIS_CRYPTO_UserIdentifierP *id, @@ -498,8 +503,10 @@ ANASTASIS_CRYPTO_truth_encrypt ( * @param ect_size size of the truth data * @param truth return from the result, which contains the truth * @param truth_size size of the result + * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a enc_truth could not be + * decrypted */ -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_truth_decrypt ( const struct ANASTASIS_CRYPTO_TruthKeyP *truth_enc_key, const void *enc_truth, @@ -577,8 +584,10 @@ ANASTASIS_CRYPTO_destroy_encrypted_core_secret ( * @param encrypted_core_secret_size size of the encrypted core secret * @param[out] core_secret decrypted core secret will be returned * @param[out] core_secret_size size of core secret + * @return #GNUNET_OK on success, #GNUNET_SYSERR if the master key or the core + * secret could not be decrypted; @a core_secret is then NULL */ -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_core_secret_recover ( const void *encrypted_master_key, size_t encrypted_master_key_size, diff --git a/src/lib/anastasis_recovery.c b/src/lib/anastasis_recovery.c @@ -380,10 +380,19 @@ truth_solve_cb (void *cls, ANASTASIS_CRYPTO_user_identifier_derive (recovery->id_data, &c->provider_salt, &id); - ANASTASIS_CRYPTO_keyshare_decrypt (&tsr->details.success.eks, - &id, - c->answer, - &c->key_share); + if (GNUNET_OK != + ANASTASIS_CRYPTO_keyshare_decrypt (&tsr->details.success.eks, + &id, + c->answer, + &c->key_share)) + { + /* the provider answered, but with a key share we cannot use */ + GNUNET_break_op (0); + csr.cs = ANASTASIS_CHALLENGE_ANSWER_STATUS_SERVER_FAILURE; + c->af (c->af_cls, + &csr); + return; + } recovery->solved_challenges[recovery->solved_challenge_pos++] = c; c->ci.solved = true; csr.cs = ANASTASIS_CHALLENGE_ANSWER_STATUS_SOLVED; @@ -442,13 +451,25 @@ truth_solve_cb (void *cls, &policy_key); GNUNET_assert (NULL != rdps->emk); GNUNET_assert (rdps->emk_size > 0); - ANASTASIS_CRYPTO_core_secret_recover (rdps->emk, - rdps->emk_size, - &policy_key, - recovery->enc_core_secret, - recovery->enc_core_secret_size, - &core_secret, - &core_secret_size); + if (GNUNET_OK != + ANASTASIS_CRYPTO_core_secret_recover (rdps->emk, + rdps->emk_size, + &policy_key, + recovery->enc_core_secret, + recovery->enc_core_secret_size, + &core_secret, + &core_secret_size)) + { + /* do NOT hand a garbage buffer to the application as if it were + the recovered secret */ + GNUNET_break_op (0); + recovery->csc (recovery->csc_cls, + ANASTASIS_RS_CORE_SECRET_RECOVERY_FAILED, + NULL, + 0); + ANASTASIS_recovery_abort (recovery); + return; + } recovery->csc (recovery->csc_cls, ANASTASIS_RS_SUCCESS, core_secret, @@ -725,11 +746,21 @@ policy_lookup_cb (void *cls, ANASTASIS_recovery_abort (r); return; } - ANASTASIS_CRYPTO_recovery_document_decrypt (&r->id, - dd->details.ok.policy, - dd->details.ok.policy_size, - &plaintext, - &size_plaintext); + if (GNUNET_OK != + ANASTASIS_CRYPTO_recovery_document_decrypt (&r->id, + dd->details.ok.policy, + dd->details.ok.policy_size, + &plaintext, + &size_plaintext)) + { + GNUNET_break_op (0); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DECRYPTION_FAILED, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } if (size_plaintext < sizeof (uint32_t)) { GNUNET_break_op (0); diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c @@ -367,6 +367,16 @@ error_by_status (enum ANASTASIS_RecoveryStatus rc, *msg = gettext_noop ("account unknown to Anastasis server"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; break; + case ANASTASIS_RS_POLICY_DECRYPTION_FAILED: + *msg = gettext_noop ( + "failed to decrypt policy document with the given identity attributes"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_CORE_SECRET_RECOVERY_FAILED: + *msg = gettext_noop ( + "failed to reassemble the core secret from the recovered key shares"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; } return ec; } diff --git a/src/testing/testing_api_cmd_truth_store.c b/src/testing/testing_api_cmd_truth_store.c @@ -247,11 +247,17 @@ truth_store_run (void *cls, void *t; size_t t_size; - ANASTASIS_CRYPTO_truth_decrypt (&tss->key, - encrypted_truth, - size_encrypted_truth, - &t, - &t_size); + if (GNUNET_OK != + ANASTASIS_CRYPTO_truth_decrypt (&tss->key, + encrypted_truth, + size_encrypted_truth, + &t, + &t_size)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } if ( (t_size != tss->truth_data_size) || (0 != memcmp (tss->truth_data, t, diff --git a/src/util/anastasis_crypto.c b/src/util/anastasis_crypto.c @@ -201,6 +201,7 @@ anastasis_decrypt (const void *key, { GNUNET_break (0); GNUNET_free (*res); + *res = NULL; *res_size = 0; return GNUNET_SYSERR; } @@ -288,7 +289,7 @@ ANASTASIS_CRYPTO_recovery_document_encrypt ( } -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_recovery_document_decrypt ( const struct ANASTASIS_CRYPTO_UserIdentifierP *id, const void *enc_rec_doc, @@ -298,13 +299,13 @@ ANASTASIS_CRYPTO_recovery_document_decrypt ( { const char *salt = "erd"; - anastasis_decrypt (id, - sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), - enc_rec_doc, - erd_size, - salt, - rec_doc, - rd_size); + return anastasis_decrypt (id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), + enc_rec_doc, + erd_size, + salt, + rec_doc, + rd_size); } @@ -339,7 +340,7 @@ ANASTASIS_CRYPTO_keyshare_encrypt ( } -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_keyshare_decrypt ( const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *enc_key_share, const struct ANASTASIS_CRYPTO_UserIdentifierP *id, @@ -350,19 +351,29 @@ ANASTASIS_CRYPTO_keyshare_decrypt ( size_t ks_size = 0; void *ks = NULL; - anastasis_decrypt (id, - sizeof (*id), - enc_key_share, - sizeof (*enc_key_share), - (NULL == xsalt) ? salt : xsalt, - &ks, - &ks_size); - GNUNET_assert (ks_size == - sizeof (struct ANASTASIS_CRYPTO_KeyShareP)); + if (GNUNET_OK != + anastasis_decrypt (id, + sizeof (*id), + enc_key_share, + sizeof (*enc_key_share), + (NULL == xsalt) ? salt : xsalt, + &ks, + &ks_size)) + return GNUNET_SYSERR; + if (ks_size != + sizeof (struct ANASTASIS_CRYPTO_KeyShareP)) + { + /* the ciphertext came from an untrusted provider, so a wrong + plaintext size must not be an assertion */ + GNUNET_break_op (0); + GNUNET_free (ks); + return GNUNET_SYSERR; + } memcpy (key_share, ks, sizeof (struct ANASTASIS_CRYPTO_KeyShareP)); GNUNET_free (ks); + return GNUNET_OK; } @@ -388,7 +399,7 @@ ANASTASIS_CRYPTO_truth_encrypt ( } -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_truth_decrypt ( const struct ANASTASIS_CRYPTO_TruthKeyP *truth_enc_key, const void *enc_truth, @@ -398,13 +409,13 @@ ANASTASIS_CRYPTO_truth_decrypt ( { const char *salt = "ect"; - anastasis_decrypt (truth_enc_key, - sizeof (struct ANASTASIS_CRYPTO_TruthKeyP), - enc_truth, - ect_size, - salt, - truth, - truth_size); + return anastasis_decrypt (truth_enc_key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP), + enc_truth, + ect_size, + salt, + truth, + truth_size); } @@ -494,7 +505,7 @@ ANASTASIS_CRYPTO_core_secret_encrypt ( } -void +enum GNUNET_GenericReturnValue ANASTASIS_CRYPTO_core_secret_recover ( const void *encrypted_master_key, size_t encrypted_master_key_size, @@ -507,34 +518,47 @@ ANASTASIS_CRYPTO_core_secret_recover ( void *master_key; size_t master_key_size; - *core_secret = GNUNET_malloc (encrypted_core_secret_size); - anastasis_decrypt (&policy_key->key, - sizeof (struct GNUNET_HashCode), - encrypted_master_key, - encrypted_master_key_size, - "emk", - &master_key, - &master_key_size); - GNUNET_break (NULL != master_key); + *core_secret = NULL; + *core_secret_size = 0; + if (GNUNET_OK != + anastasis_decrypt (&policy_key->key, + sizeof (struct GNUNET_HashCode), + encrypted_master_key, + encrypted_master_key_size, + "emk", + &master_key, + &master_key_size)) + { + /* the key shares came from providers we do not trust, so failing to + reassemble the master key is an expected outcome, not a bug */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "At %s:%d encrypted core secret is %s-%llu b\n", __FILE__, __LINE__, TALER_b2s (encrypted_core_secret, encrypted_core_secret_size), (unsigned long long) encrypted_core_secret_size); - anastasis_decrypt (master_key, - master_key_size, - encrypted_core_secret, - encrypted_core_secret_size, - "cse", - core_secret, - core_secret_size); - GNUNET_break (NULL != *core_secret); + if (GNUNET_OK != + anastasis_decrypt (master_key, + master_key_size, + encrypted_core_secret, + encrypted_core_secret_size, + "cse", + core_secret, + core_secret_size)) + { + GNUNET_break_op (0); + GNUNET_free (master_key); + return GNUNET_SYSERR; + } + GNUNET_free (master_key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "At %s:%d decrypted core secret is %s-%llu b\n", __FILE__, __LINE__, TALER_b2s (*core_secret, *core_secret_size), (unsigned long long) *core_secret_size); - GNUNET_assert (GNUNET_SYSERR != *core_secret_size); + return GNUNET_OK; } diff --git a/src/util/test_anastasis_crypto.c b/src/util/test_anastasis_crypto.c @@ -117,11 +117,12 @@ test_recovery_document (void) &ciphertext, &size_ciphertext); - ANASTASIS_CRYPTO_recovery_document_decrypt (&id, - ciphertext, - size_ciphertext, - &plaintext, - &size_plaintext); + GNUNET_assert (GNUNET_OK == + ANASTASIS_CRYPTO_recovery_document_decrypt (&id, + ciphertext, + size_ciphertext, + &plaintext, + &size_plaintext)); GNUNET_assert (strlen (test) == size_plaintext); ret = strncmp (plaintext, test, strlen (test)); json_decref (id_data); @@ -156,10 +157,11 @@ test_key_share (void) &id, NULL, &ciphertext); - ANASTASIS_CRYPTO_keyshare_decrypt (&ciphertext, - &id, - NULL, - &plaintext); + GNUNET_assert (GNUNET_OK == + ANASTASIS_CRYPTO_keyshare_decrypt (&ciphertext, + &id, + NULL, + &plaintext)); return GNUNET_memcmp (&key_share, &plaintext); } @@ -192,11 +194,12 @@ test_truth (void) &ciphertext, &size_ciphertext); - ANASTASIS_CRYPTO_truth_decrypt (&truth_enc_key, - ciphertext, - size_ciphertext, - &plaintext, - &size_plaintext); + GNUNET_assert (GNUNET_OK == + ANASTASIS_CRYPTO_truth_decrypt (&truth_enc_key, + ciphertext, + size_ciphertext, + &plaintext, + &size_plaintext)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "TRUTH_AFTER: %s\n", TALER_b2s (plaintext, size_plaintext)); @@ -262,13 +265,15 @@ test_core_secret (void) void *dec_core_secret; size_t core_secret_size; - ANASTASIS_CRYPTO_core_secret_recover (cser->enc_master_keys[k], - cser->enc_master_key_sizes[k], - &policy_keys[k], - cser->enc_core_secret, - cser->enc_core_secret_size, - &dec_core_secret, - &core_secret_size); + GNUNET_assert (GNUNET_OK == + ANASTASIS_CRYPTO_core_secret_recover ( + cser->enc_master_keys[k], + cser->enc_master_key_sizes[k], + &policy_keys[k], + cser->enc_core_secret, + cser->enc_core_secret_size, + &dec_core_secret, + &core_secret_size)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "CORE_SECRET_AFTER_%i: %s\n", k, @@ -281,6 +286,26 @@ test_core_secret (void) test))); GNUNET_free (dec_core_secret); } + /* recovering with a key that does not belong to the ciphertext must fail + instead of returning garbage as if it had succeeded */ + { + void *dec_core_secret; + size_t core_secret_size; + struct ANASTASIS_CRYPTO_PolicyKeyP wrong_key; + + GNUNET_CRYPTO_random_block (&wrong_key, + sizeof (wrong_key)); + GNUNET_assert (GNUNET_OK != + ANASTASIS_CRYPTO_core_secret_recover ( + cser->enc_master_keys[0], + cser->enc_master_key_sizes[0], + &wrong_key, + cser->enc_core_secret, + cser->enc_core_secret_size, + &dec_core_secret, + &core_secret_size)); + GNUNET_assert (NULL == dec_core_secret); + } ANASTASIS_CRYPTO_destroy_encrypted_core_secret (cser); return 0; }