Skip to content

Commit 5952e36

Browse files
authored
feat(crates/mdk-core): make NIP-70 protected tag optional in key package creation for increased relay compatibility (marmot-protocol#173)
1 parent bf4b822 commit 5952e36

7 files changed

Lines changed: 203 additions & 53 deletions

File tree

crates/mdk-core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
### Breaking changes
2727

28+
- **`create_key_package_for_event` Return Type Change**: The return type changed from `(String, [Tag; 7])` to `(String, Vec<Tag>)`. Most code patterns (iteration, indexing) continue to work unchanged. This change was necessary because the protected tag is now optional. ([#173](https://github.com/marmot-protocol/mdk/pull/173), related: [#168](https://github.com/marmot-protocol/mdk/issues/168))
29+
- **`create_key_package_for_event` No Longer Adds Protected Tag**: The `create_key_package_for_event()` function no longer adds the NIP-70 protected tag (`["-"]`) by default. This is a behavioral change - existing code that relied on the protected tag being present will now produce key packages without it. Key packages can now be republished by third parties to any relay. This improves relay compatibility since many popular relays (Damus, Primal, nos.lol) reject protected events outright. For users who need the protected tag, use the new `create_key_package_for_event_with_options()` function with `protected: true`. ([#173](https://github.com/marmot-protocol/mdk/pull/173), related: [#168](https://github.com/marmot-protocol/mdk/issues/168))
2830
- **OpenMLS 0.8.0 Upgrade**: Upgraded from a git-pinned openmls 0.7.1 to the crates.io openmls 0.8.0 release. This resolves security advisory [GHSA-8x3w-qj7j-gqhf](https://github.com/openmls/openmls/security/advisories/GHSA-8x3w-qj7j-gqhf) (improper tag validation) and moves GREASE support from a git pin to an official release. Companion crates updated: `openmls_traits` 0.5, `openmls_basic_credential` 0.5, `openmls_rust_crypto` 0.5. ([#174](https://github.com/marmot-protocol/mdk/pull/174))
2931

3032
### Changed
@@ -39,6 +41,7 @@
3941

4042
### Added
4143

44+
- **`create_key_package_for_event_with_options`**: New function that allows specifying whether to include the NIP-70 protected tag. Use this if you need to publish to relays that accept protected events. ([#173](https://github.com/marmot-protocol/mdk/pull/173), related: [#168](https://github.com/marmot-protocol/mdk/issues/168))
4245
- **MIP-04 Epoch Fallback for Media Decryption**: `decrypt_from_download` now resolves the correct decryption key via an O(1) epoch hint lookup instead of only using the current epoch's exporter secret. Added `NoExporterSecretForEpoch` variant to `EncryptedMediaError` for programmatic error matching. ([#167](https://github.com/marmot-protocol/mdk/pull/167))
4346
- **`PreviouslyFailed` Result Variant**: Added `MessageProcessingResult::PreviouslyFailed` variant to handle cases where a previously failed message arrives again but the MLS group ID cannot be extracted. This prevents crashes in client applications by returning a result instead of throwing an error. ([#165](https://github.com/marmot-protocol/mdk/pull/165), fixes [#154](https://github.com/marmot-protocol/mdk/issues/154), [#159](https://github.com/marmot-protocol/mdk/issues/159))
4447
- **Message Retry Support**: Implemented better handling for retryable message states. When a message fails processing, it now preserves the `message_event_id` and other context. Added logic to allow reprocessing of messages marked as `Retryable`, with automatic state recovery to `Processed` upon success. ([#161](https://github.com/marmot-protocol/mdk/pull/161))

crates/mdk-core/examples/group_inspection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async fn main() -> Result<(), Error> {
6868

6969
let member1_event =
7070
nostr::event::builder::EventBuilder::new(nostr::Kind::MlsKeyPackage, member1_kp_encoded)
71-
.tags(member1_tags.to_vec())
71+
.tags(member1_tags)
7272
.build(member1_keys.public_key())
7373
.sign(&member1_keys)
7474
.await?;
@@ -78,7 +78,7 @@ async fn main() -> Result<(), Error> {
7878

7979
let member2_event =
8080
nostr::event::builder::EventBuilder::new(nostr::Kind::MlsKeyPackage, member2_kp_encoded)
81-
.tags(member2_tags.to_vec())
81+
.tags(member2_tags)
8282
.build(member2_keys.public_key())
8383
.sign(&member2_keys)
8484
.await?;

crates/mdk-core/examples/key_package_inspection.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ async fn main() -> Result<(), Error> {
4141
// ====================================
4242
println!("=== Creating Key Package ===\n");
4343

44-
let (key_package_encoded, tags) =
45-
mdk.create_key_package_for_event(&keys.public_key(), [relay_url.clone()])?;
44+
// Create key package with protected=true to demonstrate NIP-70 tag
45+
let (key_package_encoded, tags) = mdk.create_key_package_for_event_with_options(
46+
&keys.public_key(),
47+
[relay_url.clone()],
48+
true,
49+
)?;
4650

4751
println!("Key Package Created Successfully!");
4852
println!(" Encoded length: {} bytes", key_package_encoded.len());
@@ -73,7 +77,7 @@ async fn main() -> Result<(), Error> {
7377
println!("=== Creating Nostr Event ===\n");
7478

7579
let key_package_event = EventBuilder::new(Kind::MlsKeyPackage, key_package_encoded.clone())
76-
.tags(tags.to_vec())
80+
.tags(tags)
7781
.build(keys.public_key())
7882
.sign(&keys)
7983
.await?;

crates/mdk-core/src/key_packages.rs

Lines changed: 150 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ where
2424
///
2525
/// The key package contains the user's credential and capabilities required for MLS operations.
2626
///
27+
/// **Note**: This function does NOT add the NIP-70 protected tag, ensuring maximum relay
28+
/// compatibility. Many popular relays (Damus, Primal, nos.lol) reject protected events.
29+
/// If you need the protected tag, use `create_key_package_for_event_with_options` instead.
30+
///
2731
/// # Returns
2832
///
2933
/// A tuple containing:
3034
/// * A base64-encoded string containing the serialized key package
31-
/// * A fixed array of 7 tags for the Nostr event, in order:
35+
/// * A vector of tags for the Nostr event:
3236
/// - `mls_protocol_version` - MLS protocol version (e.g., "1.0")
3337
/// - `mls_ciphersuite` - Ciphersuite identifier (e.g., "0x0001")
3438
/// - `mls_extensions` - Required MLS extensions
3539
/// - `relays` - Relay URLs for distribution
36-
/// - `protected` - Marks the event as protected
3740
/// - `client` - Client identifier and version
3841
/// - `encoding` - The encoding format tag ("base64")
3942
///
@@ -47,7 +50,67 @@ where
4750
&self,
4851
public_key: &PublicKey,
4952
relays: I,
50-
) -> Result<(String, [Tag; 7]), Error>
53+
) -> Result<(String, Vec<Tag>), Error>
54+
where
55+
I: IntoIterator<Item = RelayUrl>,
56+
{
57+
self.create_key_package_for_event_internal(public_key, relays, false)
58+
}
59+
60+
/// Creates a key package for a Nostr event with additional options.
61+
///
62+
/// This is the same as `create_key_package_for_event` but allows specifying
63+
/// whether to include the NIP-70 protected tag.
64+
///
65+
/// # Arguments
66+
///
67+
/// * `public_key` - The Nostr public key for the credential
68+
/// * `relays` - Relay URLs where the key package will be published
69+
/// * `protected` - Whether to add the NIP-70 protected tag (`["-"]`). When `true`, relays
70+
/// that implement NIP-70 will reject republishing by third parties. However, many popular
71+
/// relays (Damus, Primal, nos.lol) reject protected events entirely. Only set to `true`
72+
/// if publishing to relays known to accept NIP-70 protected events.
73+
///
74+
/// # Returns
75+
///
76+
/// A tuple containing:
77+
/// * A base64-encoded string containing the serialized key package
78+
/// * A vector of tags for the Nostr event
79+
///
80+
/// # Errors
81+
///
82+
/// This function will return an error if:
83+
/// * It fails to generate the credential and signature keypair
84+
/// * It fails to build the key package
85+
/// * It fails to serialize the key package
86+
pub fn create_key_package_for_event_with_options<I>(
87+
&self,
88+
public_key: &PublicKey,
89+
relays: I,
90+
protected: bool,
91+
) -> Result<(String, Vec<Tag>), Error>
92+
where
93+
I: IntoIterator<Item = RelayUrl>,
94+
{
95+
self.create_key_package_for_event_internal(public_key, relays, protected)
96+
}
97+
98+
/// Internal implementation for creating key packages.
99+
///
100+
/// This is the shared implementation used by both `create_key_package_for_event` and
101+
/// `create_key_package_for_event_with_options`. It generates an MLS key package with
102+
/// the user's credential and builds the Nostr event tags.
103+
///
104+
/// The `protected` parameter controls whether the NIP-70 protected tag (`["-"]`) is
105+
/// included in the output tags. When `true`, the tag is inserted between the `relays`
106+
/// and `client` tags, resulting in 7 total tags. When `false`, the protected tag is
107+
/// omitted, resulting in 6 total tags.
108+
fn create_key_package_for_event_internal<I>(
109+
&self,
110+
public_key: &PublicKey,
111+
relays: I,
112+
protected: bool,
113+
) -> Result<(String, Vec<Tag>), Error>
51114
where
52115
I: IntoIterator<Item = RelayUrl>,
53116
{
@@ -75,23 +138,28 @@ where
75138

76139
tracing::debug!(
77140
target: "mdk_core::key_packages",
78-
"Encoded key package using {} format",
79-
encoding.as_tag_value()
141+
"Encoded key package using {} format (protected: {})",
142+
encoding.as_tag_value(),
143+
protected
80144
);
81145

82-
let tags = [
146+
let mut tags = vec![
83147
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
84148
Tag::custom(TagKind::MlsCiphersuite, [self.ciphersuite_value()]),
85149
Tag::custom(TagKind::MlsExtensions, self.extensions_value()),
86150
Tag::relays(relays),
87-
Tag::protected(),
88-
Tag::client(format!("MDK/{}", env!("CARGO_PKG_VERSION"))),
89-
Tag::custom(
90-
TagKind::Custom("encoding".into()),
91-
[encoding.as_tag_value()],
92-
),
93151
];
94152

153+
if protected {
154+
tags.push(Tag::protected());
155+
}
156+
157+
tags.push(Tag::client(format!("MDK/{}", env!("CARGO_PKG_VERSION"))));
158+
tags.push(Tag::custom(
159+
TagKind::Custom("encoding".into()),
160+
[encoding.as_tag_value()],
161+
));
162+
95163
Ok((encoded_content, tags))
96164
}
97165

@@ -530,7 +598,7 @@ mod tests {
530598
.unwrap();
531599
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
532600

533-
// Create key package
601+
// Create key package without protected tag (default for maximum relay compatibility)
534602
let (key_package_str, tags) = mdk
535603
.create_key_package_for_event(&test_pubkey, relays.clone())
536604
.expect("Failed to create key package");
@@ -546,14 +614,14 @@ mod tests {
546614
// Verify the key package has the expected properties
547615
assert_eq!(key_package.ciphersuite(), DEFAULT_CIPHERSUITE);
548616

549-
assert_eq!(tags.len(), 7);
617+
// Without protected tag: 6 tags (3 MLS + relays + client + encoding)
618+
assert_eq!(tags.len(), 6);
550619
assert_eq!(tags[0].kind(), TagKind::MlsProtocolVersion);
551620
assert_eq!(tags[1].kind(), TagKind::MlsCiphersuite);
552621
assert_eq!(tags[2].kind(), TagKind::MlsExtensions);
553622
assert_eq!(tags[3].kind(), TagKind::Relays);
554-
assert_eq!(tags[4].kind(), TagKind::Protected);
555-
assert_eq!(tags[5].kind(), TagKind::Client);
556-
assert_eq!(tags[6].kind(), TagKind::Custom("encoding".into()));
623+
assert_eq!(tags[4].kind(), TagKind::Client);
624+
assert_eq!(tags[5].kind(), TagKind::Custom("encoding".into()));
557625

558626
assert_eq!(
559627
tags[3].content().unwrap(),
@@ -564,11 +632,14 @@ mod tests {
564632
.join(",")
565633
);
566634

567-
// Verify protected tag is present
568-
assert_eq!(tags[4].kind(), TagKind::Protected);
635+
// Verify protected tag is NOT present when protected=false
636+
assert!(
637+
!tags.iter().any(|t| t.kind() == TagKind::Protected),
638+
"Protected tag should not be present when protected=false"
639+
);
569640

570641
// Verify client tag contains version
571-
let client_tag = tags[5].content().unwrap();
642+
let client_tag = tags[4].content().unwrap();
572643
assert!(
573644
client_tag.starts_with("MDK/"),
574645
"Client tag should start with MDK/"
@@ -725,10 +796,10 @@ mod tests {
725796
);
726797
}
727798

728-
/// Test complete tag structure matches Marmot spec (MIP-00)
799+
/// Test complete tag structure matches Marmot spec (MIP-00) without protected tag
729800
/// This is an integration test ensuring all tags work together correctly
730801
#[test]
731-
fn test_complete_tag_structure_mip00_compliance() {
802+
fn test_complete_tag_structure_mip00_compliance_without_protected() {
732803
let mdk = create_test_mdk();
733804
let test_pubkey =
734805
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
@@ -742,8 +813,13 @@ mod tests {
742813
.create_key_package_for_event(&test_pubkey, relays.clone())
743814
.expect("Failed to create key package");
744815

745-
// Verify we have exactly 7 tags (3 MLS required + relays + protected + client + encoding)
746-
assert_eq!(tags.len(), 7, "Should have exactly 7 tags");
816+
// Verify we have exactly 6 tags (3 MLS required + relays + client + encoding)
817+
// No protected tag when protected=false
818+
assert_eq!(
819+
tags.len(),
820+
6,
821+
"Should have exactly 6 tags without protected"
822+
);
747823

748824
// Verify tag order matches spec example
749825
assert_eq!(
@@ -768,18 +844,13 @@ mod tests {
768844
);
769845
assert_eq!(
770846
tags[4].kind(),
771-
TagKind::Protected,
772-
"Fifth tag should be protected"
773-
);
774-
assert_eq!(
775-
tags[5].kind(),
776847
TagKind::Client,
777-
"Sixth tag should be client"
848+
"Fifth tag should be client (no protected tag)"
778849
);
779850
assert_eq!(
780-
tags[6].kind(),
851+
tags[5].kind(),
781852
TagKind::Custom("encoding".into()),
782-
"Seventh tag should be encoding"
853+
"Sixth tag should be encoding"
783854
);
784855

785856
// Verify relays tag format
@@ -801,8 +872,49 @@ mod tests {
801872
"Should contain relay2"
802873
);
803874

804-
// Verify protected tag is present
805-
assert_eq!(tags[4].kind(), TagKind::Protected);
875+
// Verify protected tag is NOT present
876+
assert!(
877+
!tags.iter().any(|t| t.kind() == TagKind::Protected),
878+
"Protected tag should not be present when protected=false"
879+
);
880+
}
881+
882+
/// Test complete tag structure with protected tag (NIP-70)
883+
#[test]
884+
fn test_complete_tag_structure_with_protected() {
885+
let mdk = create_test_mdk();
886+
let test_pubkey =
887+
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
888+
.unwrap();
889+
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
890+
891+
let (_, tags) = mdk
892+
.create_key_package_for_event_with_options(&test_pubkey, relays, true)
893+
.expect("Failed to create key package");
894+
895+
// Verify we have exactly 7 tags (3 MLS required + relays + protected + client + encoding)
896+
assert_eq!(
897+
tags.len(),
898+
7,
899+
"Should have exactly 7 tags with protected=true"
900+
);
901+
902+
// Verify protected tag is present at the correct position
903+
assert_eq!(
904+
tags[4].kind(),
905+
TagKind::Protected,
906+
"Fifth tag should be protected"
907+
);
908+
assert_eq!(
909+
tags[5].kind(),
910+
TagKind::Client,
911+
"Sixth tag should be client"
912+
);
913+
assert_eq!(
914+
tags[6].kind(),
915+
TagKind::Custom("encoding".into()),
916+
"Seventh tag should be encoding"
917+
);
806918
}
807919

808920
#[test]
@@ -1722,7 +1834,7 @@ mod tests {
17221834

17231835
// Create an event signed by the same keys used in the credential
17241836
let event = EventBuilder::new(Kind::MlsKeyPackage, key_package_str)
1725-
.tags(tags.to_vec())
1837+
.tags(tags)
17261838
.sign_with_keys(&keys)
17271839
.unwrap();
17281840

@@ -1821,7 +1933,7 @@ mod tests {
18211933

18221934
// Create the KeyPackage event
18231935
let bob_key_package_event = EventBuilder::new(Kind::MlsKeyPackage, bob_key_package_hex)
1824-
.tags(tags.to_vec())
1936+
.tags(tags)
18251937
.sign_with_keys(&bob_keys)
18261938
.expect("Failed to sign event");
18271939

@@ -1974,7 +2086,7 @@ mod tests {
19742086

19752087
// Attacker signs the event with their own keys (NOT the victim's keys)
19762088
let event = EventBuilder::new(Kind::MlsKeyPackage, key_package_hex)
1977-
.tags(tags.to_vec())
2089+
.tags(tags)
19782090
.sign_with_keys(&attacker_keys)
19792091
.unwrap();
19802092

@@ -2027,7 +2139,7 @@ mod tests {
20272139

20282140
// Sign the event with the same keys (legitimate scenario)
20292141
let event = EventBuilder::new(Kind::MlsKeyPackage, key_package_hex)
2030-
.tags(tags.to_vec())
2142+
.tags(tags)
20312143
.sign_with_keys(&keys)
20322144
.unwrap();
20332145

crates/mdk-core/src/test_util.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ where
4040
.expect("Failed to create key package");
4141

4242
EventBuilder::new(Kind::MlsKeyPackage, key_package_hex)
43-
.tags(tags.to_vec())
43+
.tags(tags)
4444
.sign_with_keys(member_keys)
4545
.expect("Failed to sign event")
4646
}
@@ -63,7 +63,7 @@ where
6363
.expect("Failed to create key package");
6464

6565
EventBuilder::new(Kind::MlsKeyPackage, key_package_hex)
66-
.tags(tags.to_vec())
66+
.tags(tags)
6767
.sign_with_keys(signing_keys)
6868
.expect("Failed to sign event")
6969
}

0 commit comments

Comments
 (0)