use mdk_storage_traits::MdkStorageProvider;
use mdk_storage_traits::mls_codec::MlsCodec;
use nostr::secp256k1::rand::{RngCore, rngs::OsRng};
use nostr::{Event, PublicKey, RelayUrl, Tag, TagKind};
use openmls::ciphersuite::hash_ref::HashReference;
use openmls::key_packages::KeyPackage;
use openmls::prelude::*;
use openmls_basic_credential::SignatureKeyPair;
use tls_codec::{Deserialize as TlsDeserialize, Serialize as TlsSerialize};
use crate::MDK;
use crate::constant::{
DEFAULT_CIPHERSUITE, MLS_KEY_PACKAGE_KIND, MLS_KEY_PACKAGE_KIND_LEGACY, TAG_EXTENSIONS,
};
use crate::error::Error;
use crate::util::{ContentEncoding, NostrTagFormat, decode_content, encode_content};
#[derive(Debug, Clone)]
pub struct KeyPackageEventData {
pub content: String,
pub tags_30443: Vec<Tag>,
pub tags_443: Vec<Tag>,
pub hash_ref: Vec<u8>,
pub d_tag: String,
}
impl<Storage> MDK<Storage>
where
Storage: MdkStorageProvider,
{
pub fn create_key_package_for_event<I>(
&self,
public_key: &PublicKey,
relays: I,
) -> Result<KeyPackageEventData, Error>
where
I: IntoIterator<Item = RelayUrl>,
{
self.create_key_package_for_event_internal(public_key, relays, false)
}
pub fn create_key_package_for_event_with_options<I>(
&self,
public_key: &PublicKey,
relays: I,
protected: bool,
) -> Result<KeyPackageEventData, Error>
where
I: IntoIterator<Item = RelayUrl>,
{
self.create_key_package_for_event_internal(public_key, relays, protected)
}
fn create_key_package_for_event_internal<I>(
&self,
public_key: &PublicKey,
relays: I,
protected: bool,
) -> Result<KeyPackageEventData, Error>
where
I: IntoIterator<Item = RelayUrl>,
{
let (credential, signature_keypair) = self.generate_credential_with_key(public_key)?;
let capabilities: Capabilities = self.capabilities();
let key_package_bundle = KeyPackage::builder()
.leaf_node_capabilities(capabilities)
.mark_as_last_resort()
.build(
self.ciphersuite,
&self.provider,
&signature_keypair,
credential,
)?;
let hash_ref = key_package_bundle
.key_package()
.hash_ref(self.provider.crypto())?;
let hash_ref_bytes = MlsCodec::serialize(&hash_ref)
.map_err(|e| Error::Provider(format!("Failed to serialize hash_ref: {}", e)))?;
let key_package_serialized = key_package_bundle.key_package().tls_serialize_detached()?;
let encoding = ContentEncoding::Base64;
let encoded_content = encode_content(&key_package_serialized, encoding);
tracing::debug!(
target: "mdk_core::key_packages",
"Encoded key package using {} format (protected: {})",
encoding.as_tag_value(),
protected
);
let key_package_ref_hex = hex::encode(hash_ref.as_slice());
let mut d_bytes = [0u8; 32];
OsRng.fill_bytes(&mut d_bytes);
let d_value = hex::encode(d_bytes);
let mut tags_30443 = vec![
Tag::identifier(&d_value),
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::MlsCiphersuite, [self.ciphersuite_value()]),
Tag::custom(TagKind::MlsExtensions, self.extensions_value()),
Tag::custom(
TagKind::Custom("mls_proposals".into()),
self.proposals_value(),
),
Tag::relays(relays),
Tag::custom(TagKind::i(), [key_package_ref_hex]),
];
if protected {
tags_30443.push(Tag::protected());
}
tags_30443.push(Tag::client(format!("MDK/{}", env!("CARGO_PKG_VERSION"))));
tags_30443.push(Tag::custom(
TagKind::Custom("encoding".into()),
[encoding.as_tag_value()],
));
let tags_443: Vec<Tag> = tags_30443
.iter()
.filter(|t| t.kind() != TagKind::d())
.cloned()
.collect();
Ok(KeyPackageEventData {
content: encoded_content,
tags_30443,
tags_443,
hash_ref: hash_ref_bytes,
d_tag: d_value,
})
}
fn parse_serialized_key_package(
&self,
key_package_str: &str,
encoding: ContentEncoding,
) -> Result<KeyPackage, Error> {
let (key_package_bytes, format) =
decode_content(key_package_str, encoding, "key package").map_err(Error::KeyPackage)?;
tracing::debug!(
target: "mdk_core::key_packages",
"Decoded key package using {}", format
);
let key_package_in = KeyPackageIn::tls_deserialize(&mut key_package_bytes.as_slice())?;
let key_package =
key_package_in.validate(self.provider.crypto(), ProtocolVersion::Mls10)?;
Ok(key_package)
}
pub fn parse_key_package(&self, event: &Event) -> Result<KeyPackage, Error> {
if event.kind != MLS_KEY_PACKAGE_KIND && event.kind != MLS_KEY_PACKAGE_KIND_LEGACY {
return Err(Error::UnexpectedEvent {
expected: MLS_KEY_PACKAGE_KIND,
received: event.kind,
});
}
if event.kind == MLS_KEY_PACKAGE_KIND {
let d_tag = event.tags.iter().find(|t| t.kind() == TagKind::d());
match d_tag {
None => {
return Err(Error::KeyPackage(
"Missing required d tag for kind:30443 KeyPackage event".to_string(),
));
}
Some(tag) => {
let d_value = tag.as_slice().get(1).map(|s| s.as_str()).unwrap_or("");
if d_value.is_empty() {
return Err(Error::KeyPackage(
"d tag value must not be empty".to_string(),
));
}
if d_value.len() != 64 {
return Err(Error::KeyPackage(
"d tag must be exactly 64 hex characters (32 bytes)".to_string(),
));
}
hex::decode(d_value).map_err(|e| {
Error::KeyPackage(format!(
"d tag must contain valid hex-encoded data: {}",
e
))
})?;
}
}
}
self.validate_key_package_tags(event, None)?;
let encoding = ContentEncoding::from_tags(event.tags.iter())
.ok_or_else(|| Error::KeyPackage("Missing required encoding tag".to_string()))?;
let key_package = self.parse_serialized_key_package(&event.content, encoding)?;
let credential = BasicCredential::try_from(key_package.leaf_node().credential().clone())?;
let credential_identity = self.parse_credential_identity(credential.identity())?;
if credential_identity != event.pubkey {
return Err(Error::KeyPackageIdentityMismatch {
credential_identity: credential_identity.to_hex(),
event_signer: event.pubkey.to_hex(),
});
}
self.validate_key_package_tags(event, Some(&key_package))?;
Ok(key_package)
}
fn validate_key_package_tags(
&self,
event: &Event,
key_package: Option<&KeyPackage>,
) -> Result<(), Error> {
let require = |pred: fn(&Self, &Tag) -> bool, name: &str| {
event
.tags
.iter()
.find(|t| pred(self, t))
.ok_or_else(|| Error::KeyPackage(format!("Missing required tag: {}", name)))
};
let pv = require(Self::is_protocol_version_tag, "mls_protocol_version")?;
let cs = require(Self::is_ciphersuite_tag, "mls_ciphersuite")?;
let ext = require(Self::is_extensions_tag, "mls_extensions")?;
let prop_tag = event.tags.iter().find(|t| Self::is_proposals_tag(self, t));
if event.kind == MLS_KEY_PACKAGE_KIND && prop_tag.is_none() {
return Err(Error::KeyPackage(
"Missing required tag: mls_proposals".to_string(),
));
}
if let Some(prop) = prop_tag {
let slice = prop.as_slice();
if slice.len() != 2 || slice[1].as_str() != "0x000a" {
return Err(Error::KeyPackage(
"Invalid mls_proposals tag value, expected 0x000a".to_string(),
));
}
}
let relays = require(Self::is_relays_tag, "relays")?;
let i_tag = event
.tags
.iter()
.find(|t| Self::is_key_package_ref_tag(self, t));
if event.kind == MLS_KEY_PACKAGE_KIND && i_tag.is_none() {
return Err(Error::KeyPackage("Missing required tag: i".to_string()));
}
self.validate_protocol_version_tag(pv)?;
self.validate_ciphersuite_tag(cs)?;
self.validate_extensions_tag(ext)?;
self.validate_relays_tag(relays)?;
if let Some(i_t) = i_tag {
self.validate_key_package_ref_tag(i_t)?;
}
if let Some(kp) = key_package {
let computed_ref = kp.hash_ref(self.provider.crypto())?;
if let Some(i_t) = i_tag {
let i_tag_value = i_t
.as_slice()
.get(1)
.ok_or_else(|| Error::KeyPackage("Missing required i tag value".to_string()))?;
let i_tag_bytes = hex::decode(i_tag_value.as_str())
.map_err(|_| Error::KeyPackage("Invalid i tag hex".to_string()))?;
if i_tag_bytes != computed_ref.as_slice() {
return Err(Error::KeyPackage(
"KeyPackageRef in i tag does not match computed value from content"
.to_string(),
));
}
}
}
Ok(())
}
fn is_protocol_version_tag(&self, tag: &Tag) -> bool {
matches!(tag.kind(), TagKind::MlsProtocolVersion)
}
fn is_ciphersuite_tag(&self, tag: &Tag) -> bool {
matches!(tag.kind(), TagKind::MlsCiphersuite)
}
fn is_extensions_tag(&self, tag: &Tag) -> bool {
matches!(tag.kind(), TagKind::MlsExtensions)
}
fn is_relays_tag(&self, tag: &Tag) -> bool {
matches!(tag.kind(), TagKind::Relays)
}
fn is_key_package_ref_tag(&self, tag: &Tag) -> bool {
tag.kind() == TagKind::i()
}
fn tag_first_value<'a>(tag: &'a Tag, missing_msg: &str) -> Result<&'a str, Error> {
tag.content()
.ok_or_else(|| Error::KeyPackage(missing_msg.to_string()))
}
fn is_proposals_tag(&self, tag: &Tag) -> bool {
matches!(tag.kind(), TagKind::Custom(ref s) if s == "mls_proposals")
}
fn validate_protocol_version_tag(&self, tag: &Tag) -> Result<(), Error> {
let version_value = Self::tag_first_value(tag, "Protocol version tag must have a value")?;
if version_value != "1.0" {
return Err(Error::KeyPackage(format!(
"Unsupported protocol version: {}. Only version 1.0 is supported per MIP-00",
version_value
)));
}
Ok(())
}
fn validate_ciphersuite_tag(&self, tag: &Tag) -> Result<(), Error> {
let ciphersuite_value = Self::tag_first_value(tag, "Ciphersuite tag must have a value")?;
if ciphersuite_value.len() != 6 {
return Err(Error::KeyPackage(format!(
"Ciphersuite hex value must be 6 characters (0xXXXX), got: {}",
ciphersuite_value
)));
}
ciphersuite_value
.strip_prefix("0x")
.filter(|hex| hex.len() == 4 && hex.chars().all(|c| c.is_ascii_hexdigit()))
.ok_or_else(|| {
Error::KeyPackage(format!(
"Ciphersuite value must be 0x followed by 4 hex digits, got: {}",
ciphersuite_value
))
})?;
let expected_hex = DEFAULT_CIPHERSUITE.to_nostr_tag();
if ciphersuite_value.to_lowercase() != expected_hex.to_lowercase() {
return Err(Error::KeyPackage(format!(
"Unsupported ciphersuite: {}. Only {} (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) is supported",
ciphersuite_value, expected_hex
)));
}
Ok(())
}
fn validate_extensions_tag(&self, tag: &Tag) -> Result<(), Error> {
let slice = tag.as_slice();
let extension_values: Vec<&str> = slice.iter().skip(1).map(|s| s.as_str()).collect();
if extension_values.is_empty() {
return Err(Error::KeyPackage(
"Extensions tag must have at least one value".to_string(),
));
}
for (idx, ext_value) in extension_values.iter().enumerate() {
if ext_value.len() != 6 {
return Err(Error::KeyPackage(format!(
"Extension {} hex value must be 6 characters (0xXXXX), got: {}",
idx, ext_value
)));
}
ext_value
.strip_prefix("0x")
.filter(|hex| hex.len() == 4 && hex.chars().all(|c| c.is_ascii_hexdigit()))
.ok_or_else(|| {
Error::KeyPackage(format!(
"Extension {} value must be 0x followed by 4 hex digits, got: {}",
idx, ext_value
))
})?;
}
let normalized_extensions: std::collections::HashSet<String> =
extension_values.iter().map(|s| s.to_lowercase()).collect();
for required_ext in TAG_EXTENSIONS.iter() {
let required_hex = required_ext.to_nostr_tag();
if !normalized_extensions.contains(&required_hex) {
let ext_name = match u16::from(*required_ext) {
0x000a => "LastResort",
0xf2ee => "NostrGroupData",
_ => "Unknown",
};
return Err(Error::KeyPackage(format!(
"Missing required extension: {} ({})",
required_hex, ext_name
)));
}
}
Ok(())
}
fn validate_relays_tag(&self, tag: &Tag) -> Result<(), Error> {
let slice = tag.as_slice();
if slice.len() <= 1 {
return Err(Error::KeyPackage(
"Relays tag must have at least one relay URL".to_string(),
));
}
for (idx, relay_url_str) in slice.iter().skip(1).enumerate() {
RelayUrl::parse(relay_url_str).map_err(|e| {
Error::KeyPackage(format!(
"Invalid relay URL at index {}: {} ({})",
idx, relay_url_str, e
))
})?;
}
Ok(())
}
fn validate_key_package_ref_tag(&self, tag: &Tag) -> Result<(), Error> {
let slice = tag.as_slice();
if slice.len() != 2 {
return Err(Error::KeyPackage(
"i tag must contain exactly one value".to_string(),
));
}
let hex_value = Self::tag_first_value(tag, "Missing required i tag")?;
if hex_value.is_empty() {
return Err(Error::KeyPackage(
"i tag value must not be empty".to_string(),
));
}
hex::decode(hex_value).map_err(|e| {
Error::KeyPackage(format!("i tag must contain valid hex-encoded data: {}", e))
})?;
Ok(())
}
pub fn delete_key_package_from_storage(&self, key_package: &KeyPackage) -> Result<(), Error> {
let hash_ref = key_package.hash_ref(self.provider.crypto())?;
self.provider
.storage()
.delete_key_package(&hash_ref)
.map_err(|e| Error::Provider(e.to_string()))?;
Ok(())
}
pub fn delete_key_package_from_storage_by_hash_ref(
&self,
hash_ref_bytes: &[u8],
) -> Result<(), Error> {
let hash_ref: HashReference = MlsCodec::deserialize(hash_ref_bytes)
.map_err(|e| Error::Provider(format!("Failed to deserialize hash_ref: {}", e)))?;
self.provider
.storage()
.delete_key_package(&hash_ref)
.map_err(|e| Error::Provider(e.to_string()))?;
Ok(())
}
pub(crate) fn generate_credential_with_key(
&self,
public_key: &PublicKey,
) -> Result<(CredentialWithKey, SignatureKeyPair), Error> {
let public_key_bytes: Vec<u8> = public_key.to_bytes().to_vec();
let credential = BasicCredential::new(public_key_bytes);
let signature_keypair = SignatureKeyPair::new(self.ciphersuite.signature_algorithm())?;
signature_keypair
.store(self.provider.storage())
.map_err(|e| Error::Provider(e.to_string()))?;
Ok((
CredentialWithKey {
credential: credential.into(),
signature_key: signature_keypair.public().into(),
},
signature_keypair,
))
}
pub(crate) fn parse_credential_identity(
&self,
identity_bytes: &[u8],
) -> Result<PublicKey, Error> {
if identity_bytes.len() != 32 {
return Err(Error::KeyPackage(format!(
"Invalid credential identity length: {} (expected 32)",
identity_bytes.len()
)));
}
PublicKey::from_slice(identity_bytes)
.map_err(|e| Error::KeyPackage(format!("Invalid public key: {}", e)))
}
}
#[cfg(test)]
mod tests {
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use nostr::EventBuilder;
use nostr::Keys;
use nostr::Kind;
use super::*;
use crate::constant::DEFAULT_CIPHERSUITE;
use crate::test_util::create_nostr_group_config_data;
use crate::tests::create_test_mdk;
#[test]
fn test_key_package_creation_and_parsing() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: d_value,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays.clone())
.expect("Failed to create key package");
let parsing_mls = create_test_mdk();
let key_package = parsing_mls
.parse_serialized_key_package(&key_package_str, ContentEncoding::Base64)
.expect("Failed to parse key package");
assert_eq!(key_package.ciphersuite(), DEFAULT_CIPHERSUITE);
assert_eq!(tags.len(), 9);
assert_eq!(tags[0].kind(), TagKind::d());
assert_eq!(tags[1].kind(), TagKind::MlsProtocolVersion);
assert_eq!(tags[2].kind(), TagKind::MlsCiphersuite);
assert_eq!(tags[3].kind(), TagKind::MlsExtensions);
assert_eq!(tags[4].kind(), TagKind::Custom("mls_proposals".into()));
assert_eq!(tags[5].kind(), TagKind::Relays);
assert_eq!(tags[6].kind(), TagKind::i());
assert_eq!(tags[7].kind(), TagKind::Client);
assert_eq!(tags[8].kind(), TagKind::Custom("encoding".into()));
assert_eq!(d_value.len(), 64);
assert!(d_value.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(tags[0].content().unwrap(), d_value);
assert_eq!(
tags[5].content().unwrap(),
relays
.iter()
.map(|r| r.to_string())
.collect::<Vec<_>>()
.join(",")
);
assert!(
!tags.iter().any(|t| t.kind() == TagKind::Protected),
"Protected tag should not be present when protected=false"
);
let client_tag = tags[7].content().unwrap();
assert!(
client_tag.starts_with("MDK/"),
"Client tag should start with MDK/"
);
assert!(
client_tag.contains('.'),
"Client tag should contain version number"
);
}
#[test]
fn test_ciphersuite_tag_format() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: _,
tags_30443: tags,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays)
.expect("Failed to create key package");
let ciphersuite_tag = tags
.iter()
.find(|t| t.kind() == TagKind::MlsCiphersuite)
.expect("Ciphersuite tag not found");
let ciphersuite_value = ciphersuite_tag.content().unwrap();
assert!(
ciphersuite_value.starts_with("0x"),
"Ciphersuite value should start with '0x', got: {}",
ciphersuite_value
);
assert_eq!(
ciphersuite_value, "0x0001",
"Expected ciphersuite '0x0001' per MIP-00 spec, got: {}",
ciphersuite_value
);
}
#[test]
fn test_extensions_tag_format() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: _,
tags_30443: tags,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays)
.expect("Failed to create key package");
let extensions_tag = tags
.iter()
.find(|t| t.kind() == TagKind::MlsExtensions)
.expect("Extensions tag not found");
let tag_values: Vec<String> = extensions_tag
.as_slice()
.iter()
.map(|s| s.to_string())
.collect();
assert!(
tag_values.len() >= 3,
"Expected at least 3 values (tag name + 2 extensions), got: {}",
tag_values.len()
);
let extension_ids = &tag_values[1..];
for (i, ext_id) in extension_ids.iter().enumerate() {
assert!(
ext_id.starts_with("0x"),
"Extension ID {} should start with '0x', got: {}",
i,
ext_id
);
assert!(
ext_id.len() == 6, "Extension ID {} should be 6 chars (0xXXXX), got: {} with length {}",
i,
ext_id,
ext_id.len()
);
}
assert!(
extension_ids.contains(&"0x000a".to_string()),
"Should contain LastResort (0x000a)"
);
assert!(
extension_ids.contains(&"0xf2ee".to_string()),
"Should contain NostrGroupData (0xf2ee)"
);
assert_eq!(
extension_ids.len(),
2,
"Should have 2 extensions in tags (0x000a, 0xf2ee), found: {:?}",
extension_ids
);
}
#[test]
fn test_protocol_version_tag_format() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: _,
tags_30443: tags,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays)
.expect("Failed to create key package");
let version_tag = tags
.iter()
.find(|t| t.kind() == TagKind::MlsProtocolVersion)
.expect("Protocol version tag not found");
let version_value = version_tag.content().unwrap();
assert_eq!(
version_value, "1.0",
"Expected protocol version '1.0' per MIP-00 spec, got: {}",
version_value
);
}
#[test]
fn test_complete_tag_structure_mip00_compliance_without_protected() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![
RelayUrl::parse("wss://relay1.example.com").unwrap(),
RelayUrl::parse("wss://relay2.example.com").unwrap(),
];
let KeyPackageEventData {
content: _,
tags_30443: tags,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays.clone())
.expect("Failed to create key package");
assert_eq!(
tags.len(),
9,
"Should have exactly 9 tags without protected"
);
assert_eq!(
tags[0].kind(),
TagKind::d(),
"First tag should be d (identifier)"
);
assert_eq!(
tags[1].kind(),
TagKind::MlsProtocolVersion,
"Second tag should be mls_protocol_version"
);
assert_eq!(
tags[2].kind(),
TagKind::MlsCiphersuite,
"Third tag should be mls_ciphersuite"
);
assert_eq!(
tags[3].kind(),
TagKind::MlsExtensions,
"Fourth tag should be mls_extensions"
);
assert_eq!(
tags[4].kind(),
TagKind::Custom("mls_proposals".into()),
"Fifth tag should be mls_proposals"
);
assert_eq!(
tags[5].kind(),
TagKind::Relays,
"Sixth tag should be relays"
);
assert_eq!(
tags[6].kind(),
TagKind::i(),
"Seventh tag should be i (KeyPackageRef)"
);
assert_eq!(
tags[7].kind(),
TagKind::Client,
"Eighth tag should be client (no protected tag)"
);
assert_eq!(
tags[8].kind(),
TagKind::Custom("encoding".into()),
"Ninth tag should be encoding"
);
let relays_tag = &tags[5];
let relays_values: Vec<String> = relays_tag
.as_slice()
.iter()
.skip(1) .map(|s| s.to_string())
.collect();
assert_eq!(relays_values.len(), 2, "Should have exactly 2 relay URLs");
assert!(
relays_values.contains(&"wss://relay1.example.com".to_string()),
"Should contain relay1"
);
assert!(
relays_values.contains(&"wss://relay2.example.com".to_string()),
"Should contain relay2"
);
assert!(
!tags.iter().any(|t| t.kind() == TagKind::Protected),
"Protected tag should not be present when protected=false"
);
}
#[test]
fn test_complete_tag_structure_with_protected() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: _,
tags_30443: tags,
hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event_with_options(&test_pubkey, relays, true)
.expect("Failed to create key package");
assert!(
!hash_ref.is_empty(),
"hash_ref should be returned from create_key_package_for_event_with_options"
);
assert_eq!(
tags.len(),
10,
"Should have exactly 10 tags with protected=true"
);
assert_eq!(
tags[0].kind(),
TagKind::d(),
"First tag should be d (identifier)"
);
assert_eq!(
tags[6].kind(),
TagKind::i(),
"Seventh tag should be i (KeyPackageRef)"
);
assert_eq!(
tags[7].kind(),
TagKind::Protected,
"Eighth tag should be protected"
);
assert_eq!(
tags[8].kind(),
TagKind::Client,
"Ninth tag should be client"
);
assert_eq!(
tags[9].kind(),
TagKind::Custom("encoding".into()),
"Tenth tag should be encoding"
);
}
#[test]
fn test_key_package_deletion() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays.clone())
.expect("Failed to create key package");
let deletion_mls = create_test_mdk();
let key_package = deletion_mls
.parse_serialized_key_package(&key_package_str, ContentEncoding::Base64)
.expect("Failed to parse key package");
deletion_mls
.delete_key_package_from_storage(&key_package)
.expect("Failed to delete key package");
}
#[test]
fn test_key_package_deletion_by_hash_ref() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: _,
tags_30443: _,
hash_ref,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays)
.expect("Failed to create key package");
assert!(!hash_ref.is_empty(), "hash_ref bytes should not be empty");
mdk.delete_key_package_from_storage_by_hash_ref(&hash_ref)
.expect("Failed to delete key package by hash_ref");
mdk.delete_key_package_from_storage_by_hash_ref(&hash_ref)
.expect("Second deletion should succeed (idempotent)");
}
#[test]
fn test_invalid_key_package_parsing() {
let mdk = create_test_mdk();
let result = mdk.parse_serialized_key_package("invalid!@#$%", ContentEncoding::Base64);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should return KeyPackage error for invalid base64 encoding"
);
let result = mdk.parse_serialized_key_package("YWJjZGVm", ContentEncoding::Base64);
assert!(matches!(result, Err(Error::Tls(..))));
}
#[test]
fn test_credential_generation() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let result = mdk.generate_credential_with_key(&test_pubkey);
assert!(result.is_ok());
}
#[test]
fn test_parse_credential_identity() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let raw_bytes = test_pubkey.to_bytes();
assert_eq!(raw_bytes.len(), 32, "Raw public key should be 32 bytes");
let parsed = mdk
.parse_credential_identity(&raw_bytes)
.expect("Should parse 32-byte raw format");
assert_eq!(
parsed, test_pubkey,
"Parsed public key from raw bytes should match original"
);
let hex_string = test_pubkey.to_hex();
let utf8_bytes = hex_string.as_bytes();
assert_eq!(utf8_bytes.len(), 64);
let result = mdk.parse_credential_identity(utf8_bytes);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject 64-byte legacy format"
);
let invalid_33_bytes = vec![0u8; 33];
let result = mdk.parse_credential_identity(&invalid_33_bytes);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject 33-byte input"
);
let invalid_31_bytes = vec![0u8; 31];
let result = mdk.parse_credential_identity(&invalid_31_bytes);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject 31-byte input"
);
}
#[test]
fn test_new_credentials_use_32_byte_format() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let (credential_with_key, _) = mdk
.generate_credential_with_key(&test_pubkey)
.expect("Should generate credential");
let basic_credential = BasicCredential::try_from(credential_with_key.credential)
.expect("Should extract basic credential");
let identity_bytes = basic_credential.identity();
assert_eq!(
identity_bytes.len(),
32,
"New credentials should use 32-byte raw format, not 64-byte UTF-8 encoded hex"
);
let raw_bytes = test_pubkey.to_bytes();
assert_eq!(
identity_bytes, raw_bytes,
"Identity should be raw public key bytes"
);
let hex_string = test_pubkey.to_hex();
let utf8_bytes = hex_string.as_bytes();
assert_ne!(
identity_bytes, utf8_bytes,
"Identity should NOT be UTF-8 encoded hex string"
);
}
#[test]
fn test_validate_missing_required_tags() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", "0x000a"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event without protocol_version tag"
);
assert!(
result
.unwrap_err()
.to_string()
.contains("mls_protocol_version")
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", "0x000a"]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event without ciphersuite tag"
);
assert!(result.unwrap_err().to_string().contains("mls_ciphersuite"));
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event without extensions tag"
);
assert!(result.unwrap_err().to_string().contains("mls_extensions"));
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject event without relays tag");
assert!(result.unwrap_err().to_string().contains("relays"));
}
}
#[test]
fn test_validate_missing_mls_proposals_tag() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event without mls_proposals tag"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("mls_proposals"),
"Error should name the missing tag, got: {err}"
);
}
#[test]
fn test_validate_wrong_mls_proposals_value() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x0001"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should return KeyPackage error for wrong mls_proposals value"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("Invalid mls_proposals tag value"),
"Error should identify the invalid value, got: {err}"
);
}
#[test]
fn test_validate_relays_tag() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
Tag::relays(vec![]), Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject event with empty relays tag");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("at least one relay URL"),
"Error should mention needing at least one relay URL, got: {}",
error_msg
);
}
{
let invalid_tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
Tag::custom(TagKind::Relays, ["not-a-valid-url"]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(invalid_tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event with invalid relay URL format"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Invalid relay URL"),
"Error should mention invalid relay URL, got: {}",
error_msg
);
}
{
let invalid_tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
Tag::custom(TagKind::Relays, ["wss://valid.relay.com", "invalid-url"]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(invalid_tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event with invalid relay URL in list"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Invalid relay URL"),
"Error should mention invalid relay URL, got: {}",
error_msg
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_ok(),
"Should accept event with single valid relay URL, got error: {:?}",
result
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
Tag::relays(vec![
RelayUrl::parse("wss://relay1.example.com").unwrap(),
RelayUrl::parse("wss://relay2.example.com").unwrap(),
RelayUrl::parse("wss://relay3.example.com").unwrap(),
]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_ok(),
"Should accept event with multiple valid relay URLs, got error: {:?}",
result
);
}
}
#[test]
fn test_validate_invalid_protocol_version() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["2.0"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", "0x000a"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject protocol version 2.0");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Unsupported protocol version"),
"Error should mention unsupported protocol version, got: {}",
error_msg
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["0.9"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", "0x000a"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject protocol version 0.9");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Unsupported protocol version"),
"Error should mention unsupported protocol version, got: {}",
error_msg
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, Vec::<&str>::new()), Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", "0x000a"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject protocol version tag without value"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("must have a value"),
"Error should mention missing value, got: {}",
error_msg
);
}
}
#[test]
fn test_validate_invalid_ciphersuite_hex_format() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x01"]), Tag::custom(TagKind::MlsExtensions, ["0x0003"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject ciphersuite with invalid hex length"
);
assert!(result.unwrap_err().to_string().contains("6 characters"));
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0xGGGG"]), Tag::custom(TagKind::MlsExtensions, ["0x0003"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject ciphersuite with invalid hex characters"
);
assert!(result.unwrap_err().to_string().contains("4 hex digits"));
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, [""]), Tag::custom(TagKind::MlsExtensions, ["0x0003"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject empty ciphersuite value");
assert!(result.unwrap_err().to_string().contains("6 characters"));
}
}
#[test]
fn test_validate_invalid_extensions_hex_format() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x03", "0x000a"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject extension with invalid hex length"
);
assert!(result.unwrap_err().to_string().contains("6 characters"));
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", "0xZZZZ"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject extension with invalid hex characters"
);
assert!(result.unwrap_err().to_string().contains("4 hex digits"));
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x0003", ""]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject empty extension value");
assert!(result.unwrap_err().to_string().contains("6 characters"));
}
}
#[test]
fn test_validate_invalid_ciphersuite_values() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0002"]), Tag::custom(
TagKind::MlsExtensions,
["0x0003", "0x000a", "0x0002", "0xf2ee"],
),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject unsupported ciphersuite 0x0002"
);
assert!(
result
.unwrap_err()
.to_string()
.contains("Unsupported ciphersuite")
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(
TagKind::MlsCiphersuite,
["MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448"],
), Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject non-hex ciphersuite format");
assert!(result.unwrap_err().to_string().contains("6 characters"));
}
}
#[test]
fn test_validate_missing_required_extensions() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0xf2ee"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject event missing LastResort");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("0x000a"),
"Error should contain hex code 0x000a"
);
assert!(
error_msg.contains("LastResort"),
"Error should contain extension name"
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event missing NostrGroupData"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("0xf2ee"),
"Error should contain hex code 0xf2ee"
);
assert!(
error_msg.contains("NostrGroupData"),
"Error should contain extension name"
);
}
}
#[test]
fn test_validate_uppercase_hex_values() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000A", "0xF2EE"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex.clone())
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_ok(),
"Should accept uppercase hex digits in extensions, got error: {:?}",
result
);
}
{
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xF2Ee"]), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_ok(),
"Should accept mixed case hex digits in extensions, got error: {:?}",
result
);
}
}
#[test]
fn test_validate_ciphersuite_tag_without_value() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, Vec::<&str>::new()), Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject ciphersuite tag without value"
);
assert!(
result
.unwrap_err()
.to_string()
.contains("must have a value")
);
}
#[test]
fn test_validate_extensions_tag_without_values() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, Vec::<&str>::new()), Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [hex::encode([0xaa; 32])]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject extensions tag without values"
);
assert!(
result
.unwrap_err()
.to_string()
.contains("at least one value")
);
}
#[test]
fn test_parse_key_package_with_valid_tags() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_str)
.tags(tags)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_ok(),
"Should parse key package with valid MIP-00 tags, got error: {:?}",
result
);
}
#[test]
fn test_parse_key_package_fails_with_missing_tags() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let incomplete_tags = vec![
Tag::identifier("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(incomplete_tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_err(),
"Should fail to parse key package with missing required tags"
);
assert!(result.unwrap_err().to_string().contains("Missing required"));
}
#[test]
fn test_last_resort_keypackage_lifecycle() {
let bob_keys = Keys::generate();
let bob_mdk = create_test_mdk();
let bob_pubkey = bob_keys.public_key();
let alice_keys = Keys::generate();
let alice_mdk = create_test_mdk();
let relays = vec![RelayUrl::parse("wss://test.relay").unwrap()];
let KeyPackageEventData {
content: bob_key_package_hex,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = bob_mdk
.create_key_package_for_event(&bob_pubkey, relays.clone())
.expect("Failed to create key package");
let extensions_tag = tags
.iter()
.find(|t| t.kind() == TagKind::MlsExtensions)
.expect("Extensions tag not found");
let extension_ids: Vec<String> = extensions_tag
.as_slice()
.iter()
.skip(1) .map(|s| s.to_string())
.collect();
assert!(
extension_ids.contains(&"0x000a".to_string()),
"KeyPackage should include last_resort extension (0x000a)"
);
let bob_key_package_event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, bob_key_package_hex)
.tags(tags)
.sign_with_keys(&bob_keys)
.expect("Failed to sign event");
let group_config = create_nostr_group_config_data(vec![alice_keys.public_key()]);
let group_result = alice_mdk
.create_group(
&alice_keys.public_key(),
vec![bob_key_package_event.clone()],
group_config,
)
.expect("Failed to create group");
alice_mdk
.merge_pending_commit(&group_result.group.mls_group_id)
.expect("Failed to merge pending commit");
let welcome = &group_result.welcome_rumors[0];
bob_mdk
.process_welcome(&nostr::EventId::all_zeros(), welcome)
.expect("Failed to process welcome");
let pending_welcomes = bob_mdk
.get_pending_welcomes(None)
.expect("Failed to get pending welcomes");
assert!(
!pending_welcomes.is_empty(),
"Bob should have pending welcomes after processing"
);
bob_mdk
.accept_welcome(&pending_welcomes[0])
.expect("Failed to accept welcome");
let bob_groups = bob_mdk.get_groups().expect("Failed to get Bob's groups");
assert_eq!(bob_groups.len(), 1, "Bob should have joined 1 group");
let group = &bob_groups[0];
let rumor = crate::test_util::create_test_rumor(&bob_keys, "Test message");
let message_result = bob_mdk.create_message(&group.mls_group_id, rumor, None);
assert!(
message_result.is_ok(),
"Bob should be able to send messages (signing key retained)"
);
let rotation_result = bob_mdk.self_update(&group.mls_group_id);
assert!(rotation_result.is_ok(), "Bob should be able to rotate keys");
let rotation_result_data = rotation_result.expect("Rotation should succeed");
assert_eq!(
rotation_result_data.evolution_event.kind,
Kind::MlsGroupMessage,
"Rotation should create a group message event"
);
}
#[test]
fn test_key_package_base64_encoding() {
let config = crate::MdkConfig::default();
let mdk = crate::tests::create_test_mdk_with_config(config);
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays)
.expect("Failed to create key package");
assert!(
BASE64.decode(&key_package_str).is_ok(),
"Content should be valid base64, got: {}",
key_package_str
);
let encoding_tag = tags
.iter()
.find(|t| t.as_slice().first() == Some(&"encoding".to_string()));
assert!(encoding_tag.is_some(), "Should have encoding tag");
assert_eq!(
encoding_tag.unwrap().as_slice().get(1).map(|s| s.as_str()),
Some("base64"),
"Encoding tag should be 'base64'"
);
let parsed = mdk
.parse_serialized_key_package(&key_package_str, ContentEncoding::Base64)
.expect("Failed to parse base64 key package");
assert_eq!(parsed.ciphersuite(), DEFAULT_CIPHERSUITE);
}
#[test]
fn test_key_package_parsing_base64() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: base64_key_package,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, relays)
.expect("Failed to create base64 key package");
assert!(
BASE64.decode(&base64_key_package).is_ok(),
"Created key package should be base64"
);
assert!(
mdk.parse_serialized_key_package(&base64_key_package, ContentEncoding::Base64)
.is_ok(),
"Should parse base64 key package"
);
}
#[test]
fn test_parse_key_package_rejects_identity_mismatch() {
let mdk = create_test_mdk();
let victim_keys = nostr::Keys::generate();
let attacker_keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_hex,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&victim_keys.public_key(), relays)
.expect("Failed to create key package");
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&attacker_keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_err(),
"Should reject key package with identity mismatch"
);
let error = result.unwrap_err();
match error {
Error::KeyPackageIdentityMismatch {
credential_identity,
event_signer,
} => {
assert_eq!(
credential_identity,
victim_keys.public_key().to_hex(),
"credential_identity should be victim's public key"
);
assert_eq!(
event_signer,
attacker_keys.public_key().to_hex(),
"event_signer should be attacker's public key"
);
}
_ => panic!(
"Expected KeyPackageIdentityMismatch error, got: {:?}",
error
),
}
}
#[test]
fn test_parse_key_package_accepts_matching_identity() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_hex,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_ok(),
"Should accept key package with matching identity, got error: {:?}",
result
);
let key_package = result.unwrap();
let credential =
BasicCredential::try_from(key_package.leaf_node().credential().clone()).unwrap();
let parsed_pubkey = mdk
.parse_credential_identity(credential.identity())
.unwrap();
assert_eq!(
parsed_pubkey,
keys.public_key(),
"Parsed key package should have the correct identity"
);
}
#[test]
fn test_i_tag_contains_valid_key_package_ref() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let i_tag = tags
.iter()
.find(|t| t.kind() == TagKind::i())
.expect("i tag not found");
let i_tag_value = i_tag.content().expect("i tag should have content");
let i_tag_bytes = hex::decode(i_tag_value).expect("i tag value should be valid hex");
assert!(
!i_tag_bytes.is_empty(),
"i tag should contain non-empty hex data"
);
let key_package = mdk
.parse_serialized_key_package(&key_package_str, ContentEncoding::Base64)
.expect("Failed to parse key package");
let computed_ref = key_package
.hash_ref(mdk.provider.crypto())
.expect("Failed to compute hash_ref");
let computed_ref_hex = hex::encode(computed_ref.as_slice());
assert_eq!(
i_tag_value, computed_ref_hex,
"i tag value should match the computed KeyPackageRef"
);
}
#[test]
fn test_parse_key_package_rejects_mismatched_i_tag() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: mut tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let i_tag_idx = tags
.iter()
.position(|t| t.kind() == TagKind::i())
.expect("i tag should exist");
tags[i_tag_idx] = Tag::custom(TagKind::i(), [hex::encode([0xff; 32])]);
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_str)
.tags(tags)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_err(),
"Should reject key package with mismatched i tag"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("does not match"),
"Error should mention mismatch, got: {}",
error_msg
);
}
#[test]
fn test_validate_missing_i_tag() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject event without i tag");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Missing required tag: i"),
"Error should mention missing i tag, got: {}",
error_msg
);
}
#[test]
fn test_validate_invalid_i_tag_hex() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), ["not-valid-hex!@#"]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event with invalid hex in i tag"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("valid hex"),
"Error should mention invalid hex, got: {}",
error_msg
);
}
#[test]
fn test_validate_empty_i_tag() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), Vec::<&str>::new()),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(result.is_err(), "Should reject event with empty i tag");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("exactly one value"),
"Error should mention exactly one value, got: {}",
error_msg
);
}
#[test]
fn test_validate_multi_value_i_tag() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(
TagKind::i(),
[hex::encode([0xaa; 32]), hex::encode([0xbb; 32])],
),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event with multi-value i tag"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("exactly one value"),
"Error should mention exactly one value, got: {}",
error_msg
);
}
#[test]
fn test_parse_key_package_accepts_legacy_kind_443() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND_LEGACY, key_package_str)
.tags(tags)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_ok(),
"Should accept legacy kind:443 KeyPackage event through May 31, 2026, got: {:?}",
result
);
}
#[test]
fn test_parse_key_package_rejects_kind_30443_missing_d_tag() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let tags_without_d: Vec<Tag> = tags
.into_iter()
.filter(|t| t.kind() != TagKind::d())
.collect();
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_str)
.tags(tags_without_d)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject kind:30443 event with missing d tag, got: {:?}",
result
);
}
#[test]
fn test_parse_key_package_rejects_kind_30443_empty_d_tag() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let tags_with_empty_d: Vec<Tag> = tags
.into_iter()
.map(|t| {
if t.kind() == TagKind::d() {
Tag::identifier("")
} else {
t
}
})
.collect();
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_str)
.tags(tags_with_empty_d)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject kind:30443 event with empty d tag value, got: {:?}",
result
);
}
#[test]
fn test_parse_key_package_rejects_kind_30443_invalid_hex_d_tag() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let tags_with_invalid_hex_d: Vec<Tag> = tags
.into_iter()
.map(|t| {
if t.kind() == TagKind::d() {
Tag::identifier(
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
)
} else {
t
}
})
.collect();
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_str)
.tags(tags_with_invalid_hex_d)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject kind:30443 event with non-hex d tag value, got: {:?}",
result
);
}
#[test]
fn test_parse_key_package_rejects_kind_30443_wrong_length_d_tag() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_30443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let tags_with_short_d: Vec<Tag> = tags
.into_iter()
.map(|t| {
if t.kind() == TagKind::d() {
Tag::identifier("abcd1234")
} else {
t
}
})
.collect();
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_str)
.tags(tags_with_short_d)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
matches!(result, Err(Error::KeyPackage(_))),
"Should reject kind:30443 event with wrong-length d tag value, got: {:?}",
result
);
}
#[test]
fn test_validate_empty_string_i_tag() {
let mdk = create_test_mdk();
let test_pubkey =
PublicKey::from_hex("884704bd421671e01c13f854d2ce23ce2a5bfe9562f4f297ad2bc921ba30c3a6")
.unwrap();
let KeyPackageEventData {
content: key_package_hex,
tags_30443: _,
hash_ref: _,
d_tag: _,
..
} = mdk
.create_key_package_for_event(&test_pubkey, vec![])
.expect("Failed to create key package");
let tags = vec![
Tag::custom(TagKind::MlsProtocolVersion, ["1.0"]),
Tag::custom(TagKind::Custom("mls_proposals".into()), ["0x000a"]),
Tag::custom(TagKind::MlsCiphersuite, ["0x0001"]),
Tag::custom(TagKind::MlsExtensions, ["0x000a", "0xf2ee"]),
Tag::relays(vec![RelayUrl::parse("wss://relay.example.com").unwrap()]),
Tag::custom(TagKind::i(), [""]),
];
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND, key_package_hex)
.tags(tags)
.sign_with_keys(&nostr::Keys::generate())
.unwrap();
let result = mdk.validate_key_package_tags(&event, None);
assert!(
result.is_err(),
"Should reject event with empty string i tag value"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("must not be empty"),
"Error should mention empty value, got: {}",
error_msg
);
}
#[test]
fn test_parse_key_package_accepts_bare_legacy_kind_443() {
let mdk = create_test_mdk();
let keys = nostr::Keys::generate();
let relays = vec![RelayUrl::parse("wss://relay.example.com").unwrap()];
let KeyPackageEventData {
content: key_package_str,
tags_443: tags,
hash_ref: _hash_ref,
d_tag: _d_value,
..
} = mdk
.create_key_package_for_event(&keys.public_key(), relays)
.expect("Failed to create key package");
let bare_tags: Vec<Tag> = tags
.into_iter()
.filter(|t| t.kind() != TagKind::i())
.collect();
let event = EventBuilder::new(MLS_KEY_PACKAGE_KIND_LEGACY, key_package_str)
.tags(bare_tags)
.sign_with_keys(&keys)
.unwrap();
let result = mdk.parse_key_package(&event);
assert!(
result.is_ok(),
"Should accept bare legacy kind:443 KeyPackage event without d and i tags, got: {:?}",
result
);
}
}