anastasis

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

commit 4fefdf6262bdffa6a4cfedd5fd25e24821787def
parent b54b60be71dbac974e247b9688186dbc3d896c74
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 29 Jul 2026 10:44:31 +0200

refactor library to separate mere parsing from starting network activities

Diffstat:
Msrc/include/anastasis.h | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/lib/anastasis_recovery.c | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/reducer/anastasis_api_recovery_redux.c | 68++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/reducer/meson.build | 29+++++++++++++++++++++++++++++
4 files changed, 225 insertions(+), 41 deletions(-)

diff --git a/src/include/anastasis.h b/src/include/anastasis.h @@ -81,6 +81,18 @@ struct ANASTASIS_ChallengeDetails */ bool async; + /** + * true if @e answer_pin is set. + */ + bool have_answer_pin; + + /** + * Answer code the user must eventually confirm for an asynchronous + * challenge; only valid if @e have_answer_pin. Persisted across + * serialization so that a suspended recovery can resume polling. + */ + uint64_t answer_pin; + }; @@ -95,6 +107,21 @@ ANASTASIS_challenge_get_details (struct ANASTASIS_Challenge *challenge); /** + * Mark @a challenge as awaiting asynchronous resolution by the user, + * remembering the @a answer_pin the user must eventually confirm. + * + * Both values are part of the serialized form of the recovery + * operation, so they survive suspending and resuming a recovery. + * + * @param[in,out] challenge the challenge to mark + * @param answer_pin answer code to remember + */ +void +ANASTASIS_challenge_set_async (struct ANASTASIS_Challenge *challenge, + uint64_t answer_pin); + + +/** * Possible outcomes of trying to start a challenge operation. */ enum ANASTASIS_ChallengeStartStatus @@ -720,21 +747,67 @@ ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r); /** * Deserialize recovery operation. * + * The returned handle is inert: no callbacks are installed and no + * network activity is started. Call #ANASTASIS_recovery_resume() to + * continue the recovery, or simply #ANASTASIS_recovery_serialize() it + * again and #ANASTASIS_recovery_abort() it if all you needed was to + * inspect it. + * * @param ctx context for making HTTP requests * @param input result from #ANASTASIS_recovery_serialize() + * @return recovery operation handle, NULL if @a input is malformed + */ +struct ANASTASIS_Recovery * +ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, + const json_t *input); + + +/** + * Resume a recovery operation obtained from + * #ANASTASIS_recovery_deserialize(): install the callbacks and start + * the policy lookup (or, if the policies are already known, arrange + * for @a pc to be called from the scheduler). + * + * May be called at most once per handle. + * + * @param[in,out] r recovery operation to resume * @param pc opens the policy call back which holds the downloaded version and the policies * @param pc_cls closure for callback * @param csc core secret callback is opened, with this the core secert is passed to the client after the authentication * @param csc_cls handle for the callback - * @return recovery operation handle + * @return #GNUNET_OK on success, #GNUNET_SYSERR if the lookup could + * not be started */ -struct ANASTASIS_Recovery * -ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, - const json_t *input, - ANASTASIS_PolicyCallback pc, - ANASTASIS_POLICY_RESULT_CLOSURE *pc_cls, - ANASTASIS_CoreSecretCallback csc, - ANASTASIS_CORE_SECRET_RESULT_CLOSURE *csc_cls); +enum GNUNET_GenericReturnValue +ANASTASIS_recovery_resume (struct ANASTASIS_Recovery *r, + ANASTASIS_PolicyCallback pc, + ANASTASIS_POLICY_RESULT_CLOSURE *pc_cls, + ANASTASIS_CoreSecretCallback csc, + ANASTASIS_CORE_SECRET_RESULT_CLOSURE *csc_cls); + + +/** + * Obtain the recovery information (policies and challenges) known for + * @a r. Valid until @a r is aborted. + * + * @param r recovery operation to inspect + * @return recovery information, never NULL + */ +const struct ANASTASIS_RecoveryInformation * +ANASTASIS_recovery_get_info (const struct ANASTASIS_Recovery *r); + + +/** + * Find the challenge with the given @a uuid in @a r. + * + * @param r recovery operation to search + * @param uuid UUID of the challenge to find + * @return NULL if @a r has no such challenge + */ +struct ANASTASIS_Challenge * +ANASTASIS_recovery_get_challenge ( + const struct ANASTASIS_Recovery *r, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid); /** diff --git a/src/lib/anastasis_recovery.c b/src/lib/anastasis_recovery.c @@ -466,6 +466,41 @@ ANASTASIS_challenge_get_details (struct ANASTASIS_Challenge *challenge) } +void +ANASTASIS_challenge_set_async (struct ANASTASIS_Challenge *challenge, + uint64_t answer_pin) +{ + challenge->ci.async = true; + challenge->ci.have_answer_pin = true; + challenge->ci.answer_pin = answer_pin; +} + + +const struct ANASTASIS_RecoveryInformation * +ANASTASIS_recovery_get_info (const struct ANASTASIS_Recovery *r) +{ + return &r->ri; +} + + +struct ANASTASIS_Challenge * +ANASTASIS_recovery_get_challenge ( + const struct ANASTASIS_Recovery *r, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) +{ + for (unsigned int i = 0; i < r->ri.cs_len; i++) + { + struct ANASTASIS_Challenge *c = r->ri.cs[i]; + + if (0 == + GNUNET_memcmp (uuid, + &c->ci.uuid)) + return c; + } + return NULL; +} + + enum GNUNET_GenericReturnValue ANASTASIS_challenge_start (struct ANASTASIS_Challenge *c, const struct ANASTASIS_PaymentSecretP *psp, @@ -1107,7 +1142,11 @@ ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r) GNUNET_JSON_pack_bool ("solved", c->ci.solved), GNUNET_JSON_pack_bool ("async", - c->ci.async)); + c->ci.async), + GNUNET_JSON_pack_conditional ( + c->ci.have_answer_pin, + GNUNET_JSON_pack_uint64 ("answer-pin", + c->ci.answer_pin))); GNUNET_assert (0 == json_array_append_new (cs_arr, cs)); @@ -1177,6 +1216,7 @@ parse_cs_array (struct ANASTASIS_Recovery *r, const char *url; const char *escrow_type; bool no_key_share; + bool no_answer_pin; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("uuid", &c->ci.uuid), @@ -1197,6 +1237,10 @@ parse_cs_array (struct ANASTASIS_Recovery *r, &c->ci.async), NULL), GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("answer-pin", + &c->ci.answer_pin), + &no_answer_pin), + GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("key_share", &c->key_share), &no_key_share), @@ -1219,6 +1263,7 @@ parse_cs_array (struct ANASTASIS_Recovery *r, c->instructions = GNUNET_strdup (instructions); c->ci.instructions = c->instructions; c->ci.provider_url = c->url; + c->ci.have_answer_pin = ! no_answer_pin; if (! no_key_share) { c->ci.solved = true; @@ -1369,19 +1414,11 @@ run_async_pc (void *cls) struct ANASTASIS_Recovery * ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, - const json_t *input, - ANASTASIS_PolicyCallback pc, - void *pc_cls, - ANASTASIS_CoreSecretCallback csc, - void *csc_cls) + const json_t *input) { struct ANASTASIS_Recovery *r; r = GNUNET_new (struct ANASTASIS_Recovery); - r->csc = csc; - r->csc_cls = csc_cls; - r->pc = pc; - r->pc_cls = pc_cls; r->ctx = ctx; { const char *err_json_name; @@ -1457,6 +1494,24 @@ ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, } GNUNET_JSON_parse_free (spec); } + return r; +} + + +enum GNUNET_GenericReturnValue +ANASTASIS_recovery_resume (struct ANASTASIS_Recovery *r, + ANASTASIS_PolicyCallback pc, + void *pc_cls, + ANASTASIS_CoreSecretCallback csc, + void *csc_cls) +{ + GNUNET_assert (NULL == r->pc); + GNUNET_assert (NULL == r->plo); + GNUNET_assert (NULL == r->do_async); + r->csc = csc; + r->csc_cls = csc_cls; + r->pc = pc; + r->pc_cls = pc_cls; if (0 == r->ri.dps_len) { struct ANASTASIS_CRYPTO_AccountPublicKeyP pub_key; @@ -1483,8 +1538,7 @@ ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, if (NULL == r->plo) { GNUNET_break (0); - ANASTASIS_recovery_abort (r); - return NULL; + return GNUNET_SYSERR; } } else @@ -1492,7 +1546,7 @@ ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, r->do_async = GNUNET_SCHEDULER_add_now (&run_async_pc, r); } - return r; + return GNUNET_OK; } diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c @@ -1545,11 +1545,18 @@ solve_challenge (json_t *state, sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, - rd, - &solve_challenge_cb, - sctx, - &core_secret_cb, - sctx); + rd); + if ( (NULL != sctx->r) && + (GNUNET_OK != + ANASTASIS_recovery_resume (sctx->r, + &solve_challenge_cb, + sctx, + &core_secret_cb, + sctx)) ) + { + ANASTASIS_recovery_abort (sctx->r); + sctx->r = NULL; + } if (NULL == sctx->r) { json_decref (sctx->state); @@ -1606,11 +1613,18 @@ poll_challenges (json_t *state, sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, - rd, - &solve_challenge_cb, - sctx, - &core_secret_cb, - sctx); + rd); + if ( (NULL != sctx->r) && + (GNUNET_OK != + ANASTASIS_recovery_resume (sctx->r, + &solve_challenge_cb, + sctx, + &core_secret_cb, + sctx)) ) + { + ANASTASIS_recovery_abort (sctx->r); + sctx->r = NULL; + } if (NULL == sctx->r) { json_decref (sctx->state); @@ -1711,11 +1725,18 @@ pay_challenge (json_t *state, sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, - rd, - &pay_challenge_cb, - sctx, - &core_secret_cb, - sctx); + rd); + if ( (NULL != sctx->r) && + (GNUNET_OK != + ANASTASIS_recovery_resume (sctx->r, + &pay_challenge_cb, + sctx, + &core_secret_cb, + sctx)) ) + { + ANASTASIS_recovery_abort (sctx->r); + sctx->r = NULL; + } if (NULL == sctx->r) { json_decref (sctx->state); @@ -1995,11 +2016,18 @@ select_challenge (json_t *state, sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, - rd, - &select_challenge_cb, - sctx, - &core_secret_cb, - sctx); + rd); + if ( (NULL != sctx->r) && + (GNUNET_OK != + ANASTASIS_recovery_resume (sctx->r, + &select_challenge_cb, + sctx, + &core_secret_cb, + sctx)) ) + { + ANASTASIS_recovery_abort (sctx->r); + sctx->r = NULL; + } if (NULL == sctx->r) { json_decref (sctx->state); diff --git a/src/reducer/meson.build b/src/reducer/meson.build @@ -3,6 +3,10 @@ libanastasisredux_SOURCES = [ 'anastasis_api_discovery.c', 'anastasis_api_providers.c', 'anastasis_api_redux.c', + 'anastasis_api_redux_state.c', + 'anastasis_api_redux_parse.c', + 'anastasis_api_redux_serialize.c', + 'anastasis_api_redux_free.c', 'anastasis_api_recovery_redux.c', 'anastasis_api_backup_redux.c', 'validation_CH_AHV.c', @@ -46,6 +50,31 @@ libanastasisredux = library( install_dir: get_option('libdir'), ) +test_redux_codec = executable( + 'test_redux_codec', + ['test_redux_codec.c'], + dependencies: [ + gnunetutil_dep, + gnunetjson_dep, + gnunetcurl_dep, + talerutil_dep, + talerjson_dep, + json_dep, + libanastasisutil_dep, + libanastasis_dep, + declare_dependency(link_with: libanastasisredux), + ], + include_directories: [incdir, configuration_inc], + build_by_default: false, + install: false, +) +test( + 'test_redux_codec', + test_redux_codec, + env: {'ANASTASIS_CLI_SRCDIR': meson.project_source_root() / 'src' / 'cli'}, + suite: ['reducer'], +) + libanastasisredux_dep = declare_dependency(link_with: libanastasisredux) pkg.generate( libanastasisredux,