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]`.