From 19bc32d6c4f8874368426b45e043bb35ed89615e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 15:38:55 +0000 Subject: [PATCH] fix(mls): check LeafNode lifetimes against the policy's clock The vendored ts-mls KeyPackages are valid 2026-09-17 to 2026-10-03, and MlsGroup checked lifetimes against wall time, so CordnLifecycleInteropTest and KotlinArtifactProducerTest started failing on 2026-10-04 - and the pre-push hook with them, for every push. MlsGroupPolicy gains now(), defaulting to TimeUtils.now(); the two lifetime checks (UpdatePath leaves and Add proposals) read it. The fixture tests run under CordnGroupPolicy with the clock pinned inside the fixtures' window, since ts-mls signed those lifetimes and they cannot be extended here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../vitorpamplona/quartz/mls/group/MlsGroup.kt | 4 ++-- .../quartz/mls/group/MlsGroupPolicy.kt | 9 +++++++++ .../cordn/interop/CordnLifecycleInteropTest.kt | 2 +- .../cordn/interop/KotlinArtifactProducerTest.kt | 2 +- .../quartz/cordn/interop/TsMlsFixtures.kt | 15 +++++++++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroup.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroup.kt index 083cf18c08..710089098f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroup.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroup.kt @@ -2001,7 +2001,7 @@ class MlsGroup private constructor( // the signer authorized. val lifetime = updatePath.leafNode.lifetime if (lifetime != null) { - val now = TimeUtils.now() + val now = policy.now() require(now >= lifetime.notBefore && now <= lifetime.notAfter) { "LeafNode lifetime expired or not yet valid in UpdatePath" } @@ -2809,7 +2809,7 @@ class MlsGroup private constructor( // Validate lifetime val lifetime = leafNode.lifetime if (lifetime != null) { - val now = TimeUtils.now() + val now = policy.now() require(now >= lifetime.notBefore && now <= lifetime.notAfter) { "KeyPackage lifetime expired or not yet valid" } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroupPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroupPolicy.kt index 33ea399729..092c66143d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroupPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroupPolicy.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.mls.group import com.vitorpamplona.quartz.mls.messages.CommitResult import com.vitorpamplona.quartz.mls.tree.Capabilities import com.vitorpamplona.quartz.mls.tree.Extension +import com.vitorpamplona.quartz.utils.TimeUtils /** * The application's rules about who in a group may do what. @@ -128,6 +129,14 @@ interface MlsGroupPolicy { */ val knownExtensionTypes: Set get() = emptySet() + /** + * The clock, in unix seconds, that LeafNode lifetimes (RFC 9420 ยง7.2) are + * checked against. Wall time in production; a test pins it to a moment + * inside a recorded fixture's validity window, so fixtures signed by + * another implementation don't expire out from under the suite. + */ + fun now(): Long = TimeUtils.now() + companion object { /** RFC 9420 exactly as written: any member may commit anything valid. */ val Permissive: MlsGroupPolicy = object : MlsGroupPolicy {} diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/CordnLifecycleInteropTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/CordnLifecycleInteropTest.kt index 5915d75df1..20f07d40f5 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/CordnLifecycleInteropTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/CordnLifecycleInteropTest.kt @@ -260,7 +260,7 @@ class CordnLifecycleInteropTest { val group = MlsGroup.create( identity = CordnCredential.of(alice).identity, - policy = CordnGroupPolicy, + policy = TsMlsFixtures.cordnPolicy, initialExtensions = listOf(CordnGroupMetadata(name = "from Kotlin").toExtension()), ) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/KotlinArtifactProducerTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/KotlinArtifactProducerTest.kt index 73c726fe72..b50583a525 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/KotlinArtifactProducerTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/KotlinArtifactProducerTest.kt @@ -83,7 +83,7 @@ class KotlinArtifactProducerTest { val group = MlsGroup.create( identity = CordnCredential.of(alice).identity, - policy = CordnGroupPolicy, + policy = TsMlsFixtures.cordnPolicy, initialExtensions = listOf(metadata.toExtension()), ) write("k-alice.pk", alice) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/TsMlsFixtures.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/TsMlsFixtures.kt index 5628e53cd9..f915bd36bd 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/TsMlsFixtures.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/TsMlsFixtures.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.cordn.interop +import com.vitorpamplona.quartz.cordn.groups.CordnGroupPolicy import com.vitorpamplona.quartz.mls.codec.TlsReader import com.vitorpamplona.quartz.mls.codec.TlsWriter import com.vitorpamplona.quartz.mls.crypto.Ed25519 +import com.vitorpamplona.quartz.mls.group.MlsGroupPolicy import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle import com.vitorpamplona.quartz.mls.messages.MlsKeyPackage import kotlin.io.encoding.Base64 @@ -43,6 +45,19 @@ object TsMlsFixtures { fun b64(name: String): ByteArray = Base64.decode(text(name)) fun hex(name: String): ByteArray = text(name).chunked(2).map { it.toInt(16).toByte() }.toByteArray() + + /** + * 2026-09-21T09:46:40Z, inside the fixtures' KeyPackage lifetime + * (2026-09-17T03:34:16Z to 2026-10-03T08:34:16Z). ts-mls signed those + * lifetimes, so they cannot be extended here; the clock moves instead. + */ + const val NOW: Long = 1_790_000_000L + + /** [CordnGroupPolicy] with its clock pinned to [NOW]. */ + val cordnPolicy: MlsGroupPolicy = + object : MlsGroupPolicy by CordnGroupPolicy { + override fun now(): Long = NOW + } } /**