v0.5.2-pre.006-fix-010
This commit is contained in:
13
ks-wallet-demo-scenarios/CHANGELOG.md
Normal file
13
ks-wallet-demo-scenarios/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- file: ks-wallet-demo-scenarios/CHANGELOG.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# CHANGELOG — ks-wallet-demo-scenarios
|
||||
|
||||
## `0.5.2-pre.006-delta-fix-010`
|
||||
|
||||
- crée la crate réutilisable `ks-wallet-demo-scenarios` sans dépendance vers Tauri ou le desktop ;
|
||||
- ajoute `rotate_wallet_password()` pour valider qu’un changement de mot de passe conserve exactement l’identité publique ;
|
||||
- ajoute `expect_wallet_password_rejected()` pour exiger le rejet de l’ancien mot de passe avec le code `wallet_native_authentication_failed` ;
|
||||
- ajoute `unlock_and_sign_wallet()` pour vérifier une ouverture authentifiée et une vraie capacité de signature sans exposer le secret ;
|
||||
- ajoute un test externe ordonné A → B → rejet de A → ouverture/signature avec B → restauration B → A → ouverture/signature finale avec A ;
|
||||
- prépare la crate à recevoir ultérieurement les scénarios d’import/export, migration et formats tiers sans étendre `kb-app-demo-desktop`.
|
||||
20
ks-wallet-demo-scenarios/Cargo.toml
Normal file
20
ks-wallet-demo-scenarios/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
# file: ks-wallet-demo-scenarios/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "ks-wallet-demo-scenarios"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ks-core = { path = "../ks-core" }
|
||||
ks-wallet = { path = "../ks-wallet" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
43
ks-wallet-demo-scenarios/README.md
Normal file
43
ks-wallet-demo-scenarios/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- file: ks-wallet-demo-scenarios/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# ks-wallet-demo-scenarios
|
||||
|
||||
`ks-wallet-demo-scenarios` regroupe les scénarios d’intégration réutilisables qui valident `ks-wallet` comme le ferait un consommateur externe.
|
||||
|
||||
## Responsabilité
|
||||
|
||||
La crate n’implémente ni stockage de secret, ni chiffrement, ni codec `.kswallet`. Elle dépend uniquement de l’API publique de `ks-wallet` et orchestre des séquences vérifiables autour des invariants wallet.
|
||||
|
||||
La première surface couvre le cycle de mot de passe natif :
|
||||
|
||||
1. changement du mot de passe A vers B avec conservation de l’identité publique ;
|
||||
2. vérification explicite que A est rejeté ;
|
||||
3. ouverture et signature avec B ;
|
||||
4. restauration du mot de passe B vers A ;
|
||||
5. contrôle final d’ouverture et de signature avec A restauré.
|
||||
|
||||
Les étapes publiques restent séparées afin d’être réutilisables dans d’autres scénarios. Le test d’intégration les exécute dans un seul test ordonné : il ne dépend jamais de l’ordre d’exécution de plusieurs `#[test]`.
|
||||
|
||||
## Frontières de sécurité
|
||||
|
||||
- les fonctions reçoivent des `ks_wallet::WalletPassword` déjà construits ;
|
||||
- aucun mot de passe n’est sérialisé, journalisé ou inclus dans les summaries ;
|
||||
- les summaries contiennent uniquement l’identité publique, un code de rejet, une signature publique et la longueur du challenge lorsque ces informations sont nécessaires ;
|
||||
- la création du wallet de fixture appartient au test externe et utilise un répertoire temporaire privé ;
|
||||
- les fonctions de scénario ne lisent jamais directement les bytes du `.kswallet`.
|
||||
|
||||
## Relations
|
||||
|
||||
- `ks-wallet` possède le format, le secret, l’authentification et la capacité `Signer` ;
|
||||
- `ks-wallet-demo-scenarios` possède uniquement l’orchestration de validation ;
|
||||
- `kb-app-demo-desktop` n’est pas requis pour valider le cycle de mot de passe ;
|
||||
- les futurs scénarios d’import/export ou de migration pourront être ajoutés ici sans alourdir l’API Tauri.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [USAGE.md](USAGE.md)
|
||||
- [TODO.md](TODO.md)
|
||||
- [CHANGELOG.md](CHANGELOG.md)
|
||||
- [`ks-wallet`](../ks-wallet/README.md)
|
||||
- [ROADMAP général](../ROADMAP.md)
|
||||
19
ks-wallet-demo-scenarios/TODO.md
Normal file
19
ks-wallet-demo-scenarios/TODO.md
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- file: ks-wallet-demo-scenarios/TODO.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# TODO — ks-wallet-demo-scenarios
|
||||
|
||||
## `0.5.2`
|
||||
|
||||
- [x] créer une crate de scénarios indépendante de Tauri et consommatrice exclusive de l’API publique `ks-wallet` ;
|
||||
- [x] ajouter une étape réutilisable de rotation de mot de passe avec conservation stricte de l’alias et de la pubkey ;
|
||||
- [x] ajouter une étape qui exige le rejet de l’ancien mot de passe par le code d’authentification natif attendu ;
|
||||
- [x] ajouter une étape d’ouverture authentifiée et de signature d’un challenge non vide ;
|
||||
- [x] composer un test externe ordonné A → B → rejet de A → ouverture/signature avec B → B → A → ouverture/signature avec A restauré.
|
||||
|
||||
## Version ultérieure non déterminée
|
||||
|
||||
- [ ] ajouter des scénarios de round-trip pour chaque nouveau format d’import/export supporté par `ks-wallet` ;
|
||||
- [ ] ajouter des scénarios de migration legacy vers `.kswallet` lorsque plusieurs formats sources doivent être comparés ;
|
||||
- [ ] ajouter des scénarios d’ouverture explicite hors store si une validation opérateur supplémentaire devient utile ;
|
||||
- [ ] n’ajouter une CLI que si une validation manuelle hors tests apporte une valeur distincte des consommateurs existants.
|
||||
88
ks-wallet-demo-scenarios/USAGE.md
Normal file
88
ks-wallet-demo-scenarios/USAGE.md
Normal file
@@ -0,0 +1,88 @@
|
||||
<!-- file: ks-wallet-demo-scenarios/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de ks-wallet-demo-scenarios
|
||||
|
||||
## Exécuter la validation intégrée
|
||||
|
||||
La validation autonome de la crate utilise un wallet synthétique et n’altère aucun wallet réel du workspace :
|
||||
|
||||
```bash
|
||||
cargo test -p ks-wallet-demo-scenarios
|
||||
```
|
||||
|
||||
Le test suit un ordre déterministe dans une seule fonction : A → B, rejet de A, ouverture/signature avec B, B → A, puis ouverture/signature finale avec A.
|
||||
|
||||
## Réutiliser une rotation
|
||||
|
||||
```rust
|
||||
let summary = match ks_wallet_demo_scenarios::rotate_wallet_password(
|
||||
&manager,
|
||||
&alias,
|
||||
current_password,
|
||||
replacement_password,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
|
||||
println!("public key={}", summary.identity.public_key);
|
||||
```
|
||||
|
||||
La fonction inspecte l’identité avant rotation, appelle `WalletManager::change_password()` puis refuse le résultat si l’alias ou la pubkey a changé.
|
||||
|
||||
## Vérifier qu’un ancien mot de passe est rejeté
|
||||
|
||||
```rust
|
||||
let rejection = match ks_wallet_demo_scenarios::expect_wallet_password_rejected(
|
||||
&manager,
|
||||
&alias,
|
||||
old_password,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rejection.rejection_code,
|
||||
"wallet_native_authentication_failed",
|
||||
);
|
||||
```
|
||||
|
||||
Un échec provenant d’une autre cause n’est pas converti en succès du scénario. Une ouverture réussie avec le mot de passe supposé rejeté fait échouer explicitement la validation.
|
||||
|
||||
## Vérifier ouverture et signature
|
||||
|
||||
```rust
|
||||
let signing = match ks_wallet_demo_scenarios::unlock_and_sign_wallet(
|
||||
&manager,
|
||||
&alias,
|
||||
active_password,
|
||||
b"wallet scenario challenge",
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
|
||||
assert!(!signing.signature.is_empty());
|
||||
```
|
||||
|
||||
Le challenge doit être non vide. La fonction compare l’identité inspectée à l’identité authentifiée avant de signer, puis verrouille la capacité ouverte avant le retour.
|
||||
|
||||
## Limites actuelles
|
||||
|
||||
La crate ne fournit pas encore de CLI ni de gestion de secrets par variables d’environnement. Les consommateurs construisent les `WalletPassword` explicitement et les transmettent aux étapes de scénario.
|
||||
|
||||
Les scénarios import/export, migration legacy et formats tiers seront ajoutés lorsqu’ils apporteront une validation supplémentaire au-delà des tests unitaires de `ks-wallet`.
|
||||
23
ks-wallet-demo-scenarios/src/lib.rs
Normal file
23
ks-wallet-demo-scenarios/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// file: ks-wallet-demo-scenarios/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! Reusable integration scenarios for the public `ks-wallet` boundary.
|
||||
|
||||
mod password_lifecycle;
|
||||
|
||||
/// Safe summary returned after a password is rejected as expected.
|
||||
pub use self::password_lifecycle::WalletPasswordRejectionSummary;
|
||||
/// Safe summary returned after a password rotation preserving wallet identity.
|
||||
pub use self::password_lifecycle::WalletPasswordRotationSummary;
|
||||
/// Safe summary returned after authenticated message signing.
|
||||
pub use self::password_lifecycle::WalletSigningScenarioSummary;
|
||||
/// Requires one supplied password to fail native wallet authentication.
|
||||
pub use self::password_lifecycle::expect_wallet_password_rejected;
|
||||
/// Changes one native wallet password and verifies that its public identity is preserved.
|
||||
pub use self::password_lifecycle::rotate_wallet_password;
|
||||
/// Authenticates one native wallet and signs an explicit non-empty challenge.
|
||||
pub use self::password_lifecycle::unlock_and_sign_wallet;
|
||||
161
ks-wallet-demo-scenarios/src/password_lifecycle.rs
Normal file
161
ks-wallet-demo-scenarios/src/password_lifecycle.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
// file: ks-wallet-demo-scenarios/src/password_lifecycle.rs
|
||||
// version: 1
|
||||
|
||||
//! Password lifecycle scenarios driven only through the public wallet API.
|
||||
|
||||
/// Safe summary returned after a password rotation preserving wallet identity.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WalletPasswordRotationSummary {
|
||||
/// Persistent wallet identity retained across the password rotation.
|
||||
pub identity: ks_wallet::WalletIdentity,
|
||||
}
|
||||
|
||||
/// Safe summary returned after a password is rejected as expected.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WalletPasswordRejectionSummary {
|
||||
/// Persistent wallet identity whose old password was rejected.
|
||||
pub identity: ks_wallet::WalletIdentity,
|
||||
/// Stable `ks-wallet` error code that proved the authentication rejection.
|
||||
pub rejection_code: std::string::String,
|
||||
}
|
||||
|
||||
/// Safe summary returned after authenticated message signing.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WalletSigningScenarioSummary {
|
||||
/// Persistent wallet identity used by the signing capability.
|
||||
pub identity: ks_wallet::WalletIdentity,
|
||||
/// Base58 signature produced for the supplied challenge.
|
||||
pub signature: std::string::String,
|
||||
/// Number of challenge bytes signed by the wallet.
|
||||
pub message_length: usize,
|
||||
}
|
||||
|
||||
/// Changes one native wallet password and verifies that its public identity is preserved.
|
||||
pub async fn rotate_wallet_password(
|
||||
manager: &ks_wallet::WalletManager,
|
||||
alias: &ks_wallet::WalletAlias,
|
||||
current_password: ks_wallet::WalletPassword,
|
||||
replacement_password: ks_wallet::WalletPassword,
|
||||
) -> ks_core::Result<crate::WalletPasswordRotationSummary> {
|
||||
let before = match manager.lookup(alias).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(handle)) => handle,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_native_not_found",
|
||||
"password lifecycle scenario requires an existing native wallet",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let changed = match manager.change_password(alias, current_password, replacement_password).await
|
||||
{
|
||||
std::result::Result::Ok(handle) => handle,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if before.alias() != changed.alias() || before.public_key() != changed.public_key() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_password_rotation_identity_changed",
|
||||
"password rotation changed the native wallet public identity",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(crate::WalletPasswordRotationSummary {
|
||||
identity: changed.identity(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Requires one supplied password to fail native wallet authentication.
|
||||
pub async fn expect_wallet_password_rejected(
|
||||
manager: &ks_wallet::WalletManager,
|
||||
alias: &ks_wallet::WalletAlias,
|
||||
rejected_password: ks_wallet::WalletPassword,
|
||||
) -> ks_core::Result<crate::WalletPasswordRejectionSummary> {
|
||||
let handle = match manager.lookup(alias).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(handle)) => handle,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_native_not_found",
|
||||
"password rejection scenario requires an existing native wallet",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rejection = match manager.unlock(alias, rejected_password).await {
|
||||
std::result::Result::Ok(wallet) => {
|
||||
let accepted_identity = wallet.identity();
|
||||
wallet.lock();
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_rejected_password_accepted",
|
||||
format!(
|
||||
"password expected to be rejected authenticated wallet alias {} with public key {}",
|
||||
accepted_identity.alias.as_str(),
|
||||
accepted_identity.public_key
|
||||
),
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
if rejection.code() != "wallet_native_authentication_failed" {
|
||||
return std::result::Result::Err(rejection);
|
||||
}
|
||||
return std::result::Result::Ok(crate::WalletPasswordRejectionSummary {
|
||||
identity: handle.identity(),
|
||||
rejection_code: rejection.code().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Authenticates one native wallet and signs an explicit non-empty challenge.
|
||||
pub async fn unlock_and_sign_wallet(
|
||||
manager: &ks_wallet::WalletManager,
|
||||
alias: &ks_wallet::WalletAlias,
|
||||
password: ks_wallet::WalletPassword,
|
||||
challenge: &[u8],
|
||||
) -> ks_core::Result<crate::WalletSigningScenarioSummary> {
|
||||
if challenge.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_signing_challenge_empty",
|
||||
"wallet signing scenario requires a non-empty challenge",
|
||||
));
|
||||
}
|
||||
let expected = match manager.lookup(alias).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(handle)) => handle,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_native_not_found",
|
||||
"wallet signing scenario requires an existing native wallet",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet = match manager.unlock(alias, password).await {
|
||||
std::result::Result::Ok(wallet) => wallet,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let identity = wallet.identity();
|
||||
if &identity.alias != expected.alias() || identity.public_key.as_str() != expected.public_key()
|
||||
{
|
||||
wallet.lock();
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_unlocked_identity_mismatch",
|
||||
"authenticated wallet identity differs from the inspected native wallet",
|
||||
));
|
||||
}
|
||||
let signature = match wallet.sign_message(challenge) {
|
||||
std::result::Result::Ok(signature) => signature,
|
||||
std::result::Result::Err(error) => {
|
||||
wallet.lock();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
wallet.lock();
|
||||
if signature.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_demo_signature_empty",
|
||||
"authenticated wallet produced an empty signature",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(crate::WalletSigningScenarioSummary {
|
||||
identity,
|
||||
signature,
|
||||
message_length: challenge.len(),
|
||||
});
|
||||
}
|
||||
90
ks-wallet-demo-scenarios/tests/password_lifecycle.rs
Normal file
90
ks-wallet-demo-scenarios/tests/password_lifecycle.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// file: ks-wallet-demo-scenarios/tests/password_lifecycle.rs
|
||||
// version: 1
|
||||
|
||||
//! External ordered password lifecycle scenario.
|
||||
|
||||
const PASSWORD_A: &str = "ks-wallet-demo-password-A";
|
||||
const PASSWORD_B: &str = "ks-wallet-demo-password-B";
|
||||
const CHALLENGE_B: &[u8] = b"ks-wallet-demo-scenarios password-B signing proof";
|
||||
const CHALLENGE_A_RESTORED: &[u8] = b"ks-wallet-demo-scenarios restored password-A signing proof";
|
||||
|
||||
fn make_directory_private(path: &std::path::Path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).unwrap_or_else(
|
||||
|error| panic!("wallet scenario directory permissions must be set: {error}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn password(value: &str) -> ks_wallet::WalletPassword {
|
||||
return ks_wallet::WalletPassword::new(value.to_owned())
|
||||
.unwrap_or_else(|error| panic!("scenario password must be accepted: {error}"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_rotation_runs_a_to_b_rejects_a_then_restores_a() {
|
||||
let directory = tempfile::tempdir()
|
||||
.unwrap_or_else(|error| panic!("temporary scenario directory must exist: {error}"));
|
||||
make_directory_private(directory.path());
|
||||
let manager = ks_wallet::WalletManager::new(directory.path())
|
||||
.unwrap_or_else(|error| panic!("wallet manager must be created: {error}"));
|
||||
let alias = ks_wallet::WalletAlias::parse("password-lifecycle")
|
||||
.unwrap_or_else(|error| panic!("scenario alias must be valid: {error}"));
|
||||
let created = manager
|
||||
.create(alias.clone(), password(PASSWORD_A))
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("scenario wallet creation must succeed: {error}"));
|
||||
let initial_public_key = created.public_key();
|
||||
created.lock();
|
||||
let rotated_to_b = ks_wallet_demo_scenarios::rotate_wallet_password(
|
||||
&manager,
|
||||
&alias,
|
||||
password(PASSWORD_A),
|
||||
password(PASSWORD_B),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("password A to B rotation must succeed: {error}"));
|
||||
assert_eq!(rotated_to_b.identity.public_key, initial_public_key);
|
||||
let rejected_a = ks_wallet_demo_scenarios::expect_wallet_password_rejected(
|
||||
&manager,
|
||||
&alias,
|
||||
password(PASSWORD_A),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("old password A must be rejected: {error}"));
|
||||
assert_eq!(rejected_a.identity.public_key, initial_public_key);
|
||||
assert_eq!(rejected_a.rejection_code, "wallet_native_authentication_failed");
|
||||
let signed_with_b = ks_wallet_demo_scenarios::unlock_and_sign_wallet(
|
||||
&manager,
|
||||
&alias,
|
||||
password(PASSWORD_B),
|
||||
CHALLENGE_B,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("password B must unlock and sign: {error}"));
|
||||
assert_eq!(signed_with_b.identity.public_key, initial_public_key);
|
||||
assert_eq!(signed_with_b.message_length, CHALLENGE_B.len());
|
||||
assert!(!signed_with_b.signature.is_empty());
|
||||
let restored_to_a = ks_wallet_demo_scenarios::rotate_wallet_password(
|
||||
&manager,
|
||||
&alias,
|
||||
password(PASSWORD_B),
|
||||
password(PASSWORD_A),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("password B to A restoration must succeed: {error}"));
|
||||
assert_eq!(restored_to_a.identity.public_key, initial_public_key);
|
||||
let signed_with_restored_a = ks_wallet_demo_scenarios::unlock_and_sign_wallet(
|
||||
&manager,
|
||||
&alias,
|
||||
password(PASSWORD_A),
|
||||
CHALLENGE_A_RESTORED,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("restored password A must unlock and sign: {error}"));
|
||||
assert_eq!(signed_with_restored_a.identity.public_key, initial_public_key);
|
||||
assert_eq!(signed_with_restored_a.message_length, CHALLENGE_A_RESTORED.len());
|
||||
assert!(!signed_with_restored_a.signature.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user