From 43c696c2bcfef163f83f0859b74041805da09275 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= <tuxmain@zettascript.org>
Date: Fri, 27 Dec 2024 15:07:54 +0100
Subject: [PATCH] Revocation with legacy V1 document
 (nodes/rust/duniter-v2s!295)

* identity: legacy revocation
---
 Cargo.lock                                   |   4 +
 Cargo.toml                                   |   9 +-
 pallets/identity/Cargo.toml                  |   4 +
 pallets/identity/src/lib.rs                  | 111 +++++++++++++++++++
 pallets/identity/src/tests.rs                | 101 ++++++++++++++++-
 pallets/identity/src/weights.rs              |  12 ++
 resources/metadata.scale                     | Bin 149648 -> 149970 bytes
 runtime/g1/src/weights/pallet_identity.rs    |  10 ++
 runtime/gdev/src/weights/pallet_identity.rs  |  10 ++
 runtime/gtest/src/weights/pallet_identity.rs |  10 ++
 10 files changed, 267 insertions(+), 4 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 63c9bd6c3..9ed0d4fae 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 8446eb51e..5c450f3c3 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 7e163e2ab..f86d0f991 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 981a029ba..266d4aa99 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 9bc2205eb..ac853fc92 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 61d15aba7..f72819c37 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
GIT binary patch
delta 323
zcmbO*k@M1I&JC+?GAe9db5oSHKB6eKEI&InJ~JgXuOzdiGCn6YJu$hGh0zBjk(^kP
znV%P*lAm0f3zRoulnF~swkfr+W8f6|5Cqn#keH_c)2vWhoSB!dkf;DLL_xzP)G0ty
z0b!B?%p?X!MqP!3qEv5~9dMZhD}|DbRE4zCoE+Vp)V%bP3~c&!6pAy`^Abx+i&7Ob
z^OAE)Q&LklPrt`IjWJ;R(h^2%7QYhDyt2fc%oHE6e?V@9c*ZTiC^xZ$L6~I&L<XDb
b3Ta?P3Yo<~!@;IZpV-Byw4JGvvF|(pv1oZj

delta 40
ycmV+@0N4N0lL?TK39zf#0UWce*%bw|wcq2F0Zg}<bpbmBm!fb1BDX|(0glgtnGx&&

diff --git a/runtime/g1/src/weights/pallet_identity.rs b/runtime/g1/src/weights/pallet_identity.rs
index 69fdb129d..2082d2e1f 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 9a72d7a2e..b42e2bfe2 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 087183762..ece6cef2d 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]`.
-- 
GitLab