diff --git a/Cargo.lock b/Cargo.lock index 63c9bd6c3d4b821958c12253c90bfe74cde1d97a..9ed0d4faeda5549e5857dc515a32bc1103e4c8b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8302,10 +8302,14 @@ dependencies = [ name = "pallet-identity" version = "1.0.0" dependencies = [ + "base64 0.22.1", + "bs58 0.5.1", "duniter-primitives", + "ed25519-dalek", "frame-benchmarking", "frame-support", "frame-system", + "log", "parity-scale-codec", "scale-info", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8446eb51eba4a7ece733b8396e30395efe78c2fb..5c450f3c364740c06bb71198b556adc8e4158901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,9 +48,11 @@ version = '1.0.0' [workspace.dependencies] # crates.io dependencies anyhow = { version = "1.0.81", default-features = false } +base64 = { version = "0.22.1", default-features = false } countmap = { version = "0.2.0", default-features = false } ctrlc = { version = "3.4.4", default-features = false } cucumber = { version = "0.20.2", default-features = false } +ed25519-dalek = { version = "2.1.1", default-features = false } env_logger = { version = "0.11.3", default-features = false } notify = { version = "6.1.1", default-features = false } portpicker = { version = "0.1.1", default-features = false } @@ -64,13 +66,15 @@ bs58 = { version = "0.5.1", default-features = false } placeholder = { version = "1.1.4", default-features = false } clap = { version = "4.5.3" } clap_complete = { version = "4.5.1" } -reqwest = { version = "0.12.0", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12.0", default-features = false, features = [ + "rustls-tls", +] } glob = { version = "0.3.1", default-features = false } convert_case = { version = "0.6.0", default-features = false } subweight-core = { version = "3.3.1", default-features = false } version_check = { version = "0.9.4", default-features = false } codec = { package = "parity-scale-codec", version = "3.6.9", default-features = false } -enum-as-inner = { version = "=0.5.1", default-features = false } #https://github.com/bluejekyll/trust-dns/issues/1946 +enum-as-inner = { version = "=0.5.1", default-features = false } #https://github.com/bluejekyll/trust-dns/issues/1946 futures = { version = "0.3.30", default-features = false } tera = { version = "1", default-features = false } hex = { version = "0.4.3", default-features = false } @@ -274,4 +278,3 @@ zeroize = { opt-level = 3 } lto = "thin" # Substrate runtime requires unwinding. panic = "unwind" - diff --git a/pallets/identity/Cargo.toml b/pallets/identity/Cargo.toml index 7e163e2ab298f42ee421f0429d1eee12e79feab6..f86d0f991a07c616da8cd97aaa0975a96297bb8b 100644 --- a/pallets/identity/Cargo.toml +++ b/pallets/identity/Cargo.toml @@ -42,11 +42,15 @@ default-features = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] +base64 = { workspace = true } +bs58 = { workspace = true } codec = { workspace = true, features = ["derive"] } duniter-primitives = { workspace = true } +ed25519-dalek = { workspace = true } frame-benchmarking = { workspace = true, optional = true } frame-support = { workspace = true } frame-system = { workspace = true } +log = { workspace = true } scale-info = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] } sp-core = { workspace = true } diff --git a/pallets/identity/src/lib.rs b/pallets/identity/src/lib.rs index 981a029bab39cad33777bbc92f8210712189a37e..266d4aa99ef5c5139f35c25e1c3ba6a461272b40 100644 --- a/pallets/identity/src/lib.rs +++ b/pallets/identity/src/lib.rs @@ -86,6 +86,7 @@ pub const LINK_IDTY_PAYLOAD_PREFIX: [u8; 4] = [b'l', b'i', b'n', b'k']; #[frame_support::pallet] pub mod pallet { use super::*; + use base64::Engine; use frame_support::{pallet_prelude::*, traits::StorageVersion}; use frame_system::pallet_prelude::*; @@ -570,6 +571,114 @@ pub mod pallet { Ok(().into()) } + /// Revoke an identity using a legacy (DUBP) revocation document + /// + /// - `revocation document`: the full-length revocation document, signature included + /// + /// Any signed origin can execute this call. + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::revoke_identity_legacy())] + pub fn revoke_identity_legacy( + origin: OriginFor<T>, + revocation_document: Vec<u8>, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + + // Strip possible Unicode magic number that is not part of the protocol + let revocation_document = revocation_document + .strip_prefix(b"\xef\xbb\xbf") + .unwrap_or(&revocation_document); + let mut lines = revocation_document.split(|b| *b == b'\n'); + ensure!( + lines.next() == Some(b"Version: 10"), + Error::<T>::InvalidLegacyRevocationFormat + ); + ensure!( + lines.next() == Some(b"Type: Revocation"), + Error::<T>::InvalidLegacyRevocationFormat + ); + ensure!( + lines.next() == Some(b"Currency: g1"), + Error::<T>::InvalidLegacyRevocationFormat + ); + let line_issuer = lines + .next() + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + let line_username = lines + .next() + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + let _line_blockstamp = lines + .next() + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + let _line_idty_signature = lines + .next() + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + let line_signature = lines + .next() + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + ensure!( + lines.next() == Some(b""), + Error::<T>::InvalidLegacyRevocationFormat + ); + ensure!( + lines.next().is_none(), + Error::<T>::InvalidLegacyRevocationFormat + ); + let document = revocation_document + .get(0..revocation_document.len().saturating_sub(89)) + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + let mut signature = [0; 64]; + base64::prelude::BASE64_STANDARD + .decode_slice(line_signature, &mut signature) + .map_err(|_| Error::<T>::InvalidLegacyRevocationFormat)?; + let issuer = bs58::decode( + line_issuer + .get(8..) + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?, + ) + .into_array_const::<32>() + .map_err(|_| Error::<T>::InvalidLegacyRevocationFormat)?; + ed25519_dalek::VerifyingKey::from_bytes(&issuer) + .map_err(|_| Error::<T>::InvalidLegacyRevocationFormat)? + .verify_strict(document, &ed25519_dalek::Signature::from_bytes(&signature)) + .map_err(|_| Error::<T>::InvalidSignature)?; + let username = line_username + .get(14..) + .ok_or(Error::<T>::InvalidLegacyRevocationFormat)?; + let idty_index = <IdentitiesNames<T>>::get(IdtyName(username.into())) + .ok_or(Error::<T>::IdtyNotFound)?; + + let idty_value = Identities::<T>::get(idty_index).ok_or(Error::<T>::IdtyNotFound)?; + + match idty_value.status { + IdtyStatus::Unconfirmed => Err(Error::<T>::CanNotRevokeUnconfirmed), + IdtyStatus::Unvalidated => Err(Error::<T>::CanNotRevokeUnvalidated), + IdtyStatus::Member => Ok(()), + IdtyStatus::NotMember => Ok(()), + IdtyStatus::Revoked => Err(Error::<T>::AlreadyRevoked), + }?; + + let revocation_key = T::AccountId::decode(&mut &issuer[..]).unwrap(); + + ensure!( + if let Some((ref old_owner_key, last_change)) = idty_value.old_owner_key { + // old owner key can also revoke the identity until the period expired + revocation_key == idty_value.owner_key + || (&revocation_key == old_owner_key + && frame_system::Pallet::<T>::block_number() + < last_change + T::ChangeOwnerKeyPeriod::get()) + } else { + revocation_key == idty_value.owner_key + }, + Error::<T>::InvalidRevocationKey + ); + + // finally if all checks pass, remove identity + Self::do_revoke_identity(idty_index, RevocationReason::User); + + Ok(().into()) + } + /// Remove identity names from storage. /// /// This function allows a privileged root origin to remove multiple identity names from storage @@ -701,6 +810,8 @@ pub mod pallet { InsufficientBalance, /// Owner key currently used as validator. OwnerKeyUsedAsValidator, + /// Legacy revocation document format is invalid + InvalidLegacyRevocationFormat, } // INTERNAL FUNCTIONS // diff --git a/pallets/identity/src/tests.rs b/pallets/identity/src/tests.rs index 9bc2205ebad0eba7b654884651743deca7a1d199..ac853fc925c2b027cb22f3a87f9d204df886609d 100644 --- a/pallets/identity/src/tests.rs +++ b/pallets/identity/src/tests.rs @@ -14,11 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. +use core::str::FromStr; + use crate::{mock::*, *}; use codec::Encode; use frame_support::{assert_noop, assert_ok, dispatch::DispatchResultWithPostInfo}; use sp_core::{sr25519::Pair as KeyPair, Pair}; -use sp_runtime::{MultiSignature, MultiSigner}; +use sp_runtime::{AccountId32, MultiSignature, MultiSigner}; type IdtyVal = IdtyValue<u64, AccountId, ()>; @@ -87,6 +89,23 @@ fn inactive_bob() -> GenesisIdty<Test> { } } +// From legacy credentials Charlie:Charlie +fn legacy_charlie() -> GenesisIdty<Test> { + GenesisIdty { + index: 102, + name: IdtyName::from("Charlie"), + value: IdtyVal { + data: (), + next_creatable_identity_on: 0, + old_owner_key: None, + owner_key: AccountId32::from_str("5H2nLXGku46iztpqdRwsCAiP6vHZbShhKmSV4yyufQgEUFvV") + .unwrap(), + next_scheduled: 0, + status: crate::IdtyStatus::Member, + }, + } +} + #[test] fn test_no_identity() { new_test_ext(IdentityConfig { @@ -610,6 +629,86 @@ fn test_idty_revocation() { ); }); } + +// # Generate dummy revocation documents in Python. +// # The seed derivation is not the same as sr25519.from_seed so this doesn't work yet. +// ```python +// import duniterpy, substrateinterface +// s = duniterpy.key.SigningKey.from_credentials("Charlie", "Charlie") +// block = duniterpy.documents.BlockID(42, "A"*64) +// idty = duniterpy.documents.Identity(s.pubkey, "Charlie", block, s) +// r = duniterpy.documents.Revocation(idty, s) +// print("SS58 address:", substrateinterface.base.ss58_encode(s.vk)) +// print(r.signed_raw()) +// ``` +#[test] +fn test_idty_revocation_legacy() { + new_test_ext(IdentityConfig { + identities: vec![alice(), legacy_charlie()], + }) + .execute_with(|| { + // We need to initialize at least one block before any call + run_to_block(1); + + let valid_revocation_document = r"Version: 10 +Type: Revocation +Currency: g1 +Issuer: Fnf2xaxYdQpB4kU45DMLQ9Ey4bd6DtoebKJajRkLBUXm +IdtyUniqueID: Charlie +IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +IdtySignature: 7KUagcMiQw05rwbkBsRrnNqPRHu/Y5ukCLoAEpb/1tXAQsSNf2gRi1h5PWIGs9y/vHnFXvF5epKsOjA6X75vDg== +CfiG4xhcWS+/DgxY0xFIyOA9TVr4Im3XEXcCApNgXC+Ns9jy2yrNoC3NF8MCD63cZ8QTRfrr4Iv6n3leYCCcDQ== +"; + +let revocation_document_bad_username = r"Version: 10 +Type: Revocation +Currency: g1 +Issuer: Fnf2xaxYdQpB4kU45DMLQ9Ey4bd6DtoebKJajRkLBUXm +IdtyUniqueID: Alice +IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +IdtySignature: dqO8nnYWZDDDzadMXpOVwehJXQ9wocE9QTKsVBa88rPONLhz12QA6Ytib2+VtPU+gnewO2mRVOvzdYKXemQPDg== +0q/Dy4jwLTjZGSOu4GWdkfW+SqXRAPHUwwvWQenqiuNuL2eEc0x2hM0MWhIOuSLy2ifNq6PfSH/dBrV5CgYIAw== +"; + +let revocation_document_bad_signer = r"Version: 10 +Type: Revocation +Currency: g1 +Issuer: 9cFLFh12MZSL8HHW9KvEDGTEEyKALGjEdHpNP8rqmmw3 +IdtyUniqueID: Charlie +IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +IdtySignature: 8P2vjDHZf4tHaGpZYTuTXJ9Xe+3qQ0FAM6fypvwl2mYqLs1ZfE07gp4mqRpNY90rC9+CIIi7eHvv2uAlFVpfCQ== +iWOssQ1y2svWeUD4byjJx6n+/Xgf0pgMe1FDhnR9oN76Ri9B8SQfP+hFD3GCth7sZRD162sR83g3UvYpFHJLBQ== +"; + + assert_eq!( + Identity::revoke_identity_legacy( + RuntimeOrigin::signed(account(1).id), + revocation_document_bad_username.into() + ), + Err(Error::<Test>::InvalidRevocationKey.into()) + ); + + assert_eq!( + Identity::revoke_identity_legacy( + RuntimeOrigin::signed(account(1).id), + revocation_document_bad_signer.into() + ), + Err(Error::<Test>::InvalidRevocationKey.into()) + ); + + // Anyone can submit a revocation payload + assert_ok!(Identity::revoke_identity_legacy( + RuntimeOrigin::signed(account(42).id), + valid_revocation_document.into() + )); + + System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRevoked { + idty_index: 102, + reason: RevocationReason::User, + })); + }); +} + #[test] fn test_inactive_genesis_members() { new_test_ext(IdentityConfig { diff --git a/pallets/identity/src/weights.rs b/pallets/identity/src/weights.rs index 61d15aba77a28c887301ec22b8c7863c24dedc58..f72819c3717da2372c724ff703a9bcc4eb23b4f6 100644 --- a/pallets/identity/src/weights.rs +++ b/pallets/identity/src/weights.rs @@ -23,6 +23,7 @@ pub trait WeightInfo { fn confirm_identity() -> Weight; fn change_owner_key() -> Weight; fn revoke_identity() -> Weight; + fn revoke_identity_legacy() -> Weight; fn prune_item_identities_names(i: u32) -> Weight; fn fix_sufficients() -> Weight; fn link_account() -> Weight; @@ -84,6 +85,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(6)) } + fn revoke_identity_legacy() -> Weight { + // Proof Size summary in bytes: + // Measured: `778` + // Estimated: `6718` + // Minimum execution time: 829_174_000 picoseconds. + Weight::from_parts(869_308_000, 0) + .saturating_add(Weight::from_parts(0, 6718)) + .saturating_add(RocksDbWeight::get().reads(6)) + .saturating_add(RocksDbWeight::get().writes(6)) + } + fn prune_item_identities_names(i: u32) -> Weight { // Proof Size summary in bytes: // Measured: `0` diff --git a/resources/metadata.scale b/resources/metadata.scale index db221c7f4528732b3c91dd29a07b5d5d748e2599..b94203243911efd41ac9ee2620ab2e3f180b98b9 100644 Binary files a/resources/metadata.scale and b/resources/metadata.scale differ diff --git a/runtime/g1/src/weights/pallet_identity.rs b/runtime/g1/src/weights/pallet_identity.rs index 69fdb129d8d0f9a3414b06fc03b6fe80e663bdd3..2082d2e1fb36f0071c70d7b18a135416f88a782a 100644 --- a/runtime/g1/src/weights/pallet_identity.rs +++ b/runtime/g1/src/weights/pallet_identity.rs @@ -135,6 +135,16 @@ impl<T: frame_system::Config> pallet_identity::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(5)) } + fn revoke_identity_legacy() -> Weight { + // Proof Size summary in bytes: + // Measured: `697` + // Estimated: `6637` + // Minimum execution time: 70_321_000 picoseconds. + Weight::from_parts(72_274_000, 0) + .saturating_add(Weight::from_parts(0, 6637)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(5)) + } /// Storage: `Identity::IdentitiesNames` (r:0 w:999) /// Proof: `Identity::IdentitiesNames` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `i` is `[2, 1000]`. diff --git a/runtime/gdev/src/weights/pallet_identity.rs b/runtime/gdev/src/weights/pallet_identity.rs index 9a72d7a2eccf70d26c2550b5b873ea5e14ebd91d..b42e2bfe2d2fbb7472e31fac3a841459fe64b073 100644 --- a/runtime/gdev/src/weights/pallet_identity.rs +++ b/runtime/gdev/src/weights/pallet_identity.rs @@ -137,6 +137,16 @@ impl<T: frame_system::Config> pallet_identity::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(5)) } + fn revoke_identity_legacy() -> Weight { + // Proof Size summary in bytes: + // Measured: `697` + // Estimated: `6637` + // Minimum execution time: 70_095_000 picoseconds. + Weight::from_parts(71_457_000, 0) + .saturating_add(Weight::from_parts(0, 6637)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(5)) + } /// Storage: `Identity::IdentitiesNames` (r:0 w:999) /// Proof: `Identity::IdentitiesNames` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `i` is `[2, 1000]`. diff --git a/runtime/gtest/src/weights/pallet_identity.rs b/runtime/gtest/src/weights/pallet_identity.rs index 0871837620e15a4717a488b3ba4e6922b3dc8bbd..ece6cef2de8ad008479ac1e986016814eec7b05e 100644 --- a/runtime/gtest/src/weights/pallet_identity.rs +++ b/runtime/gtest/src/weights/pallet_identity.rs @@ -135,6 +135,16 @@ impl<T: frame_system::Config> pallet_identity::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(5)) } + fn revoke_identity_legacy() -> Weight { + // Proof Size summary in bytes: + // Measured: `697` + // Estimated: `6637` + // Minimum execution time: 69_941_000 picoseconds. + Weight::from_parts(71_920_000, 0) + .saturating_add(Weight::from_parts(0, 6637)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(5)) + } /// Storage: `Identity::IdentitiesNames` (r:0 w:999) /// Proof: `Identity::IdentitiesNames` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `i` is `[2, 1000]`.