From 65fe44c953211a0a81ccaca8c9caff9e13ec3e1c Mon Sep 17 00:00:00 2001 From: gionag Date: Fri, 2 Oct 2026 07:06:41 +0200 Subject: [PATCH 1/4] authority(0.7.1g1P2-Step3a): establish certificate consumption as a positive fact M40-M43 in MutationAuthorityDigestCertificateConsumption, one predicate per function and chained with ?: so the refusal order is readable and the whole consumption is a single expression: only a certificate passing every predicate yields the valid consumption. M44 (single use) is new. M47 now takes the established fact as a parameter instead of rediscovering it, so a disappearance can never become its own evidence - the enforcement is the shape of the API, not a convention a refactor can quietly undo. validConsumptions defaults to the empty set, the fail-closed state, so existing behaviour is unchanged: the eight pre-existing ceremony tests pass untouched. M43 is a named rule with its own diagnostic rather than an implicit consequence of how a certificate was found, so provenance stays visible. Consumption stays bounded: toDigest must equal the verifier's fresh projection, fromDigest must match the cited admission, and admissionSetDigest must equal the exact base authorization set. Two genuine Detekt findings fixed by hand, not baselined: ReturnCount in the consumption chain and the MaxLineLength on the same line. --- ...ationAuthorityDigestCertificateCeremony.kt | 57 ++++++- ...onAuthorityDigestCertificateConsumption.kt | 152 +++++++++++++++++ ...thorityDigestCertificateConsumptionTest.kt | 155 ++++++++++++++++++ 3 files changed, 355 insertions(+), 9 deletions(-) create mode 100644 build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt create mode 100644 build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt index 09cd01aea..27d56ccde 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt @@ -20,20 +20,27 @@ package dev.tramai.build.quality */ object MutationAuthorityDigestCertificateCeremony { /** - * M45, M46 and M47 over the base/candidate certificate ledgers. + * M44, M45, M46 and M47 over the base/candidate certificate ledgers. * * @param base the certificate ledger as it exists in the PR's authority base * @param candidate the certificate ledger as the PR proposes it * @param baseSha the exact authority base SHA this transition is proposed against + * @param validConsumptions the source digests of certificates whose consumption in this + * transition was **independently established** by M40-M43. This is the fact M44 and M47 act on: + * they never re-derive it, and they never treat the absence of a certificate as evidence that + * it was consumed. An empty set is the fail-closed state, so a caller that establishes nothing + * gets the strictest behaviour. */ fun checks( base: MutationAuthorityDigestCertificates, candidate: MutationAuthorityDigestCertificates, baseSha: String, + validConsumptions: Set = emptySet(), ): List = mintChecks(base, candidate, baseSha) + retentionChecks(base, candidate) + - removalChecks(base, candidate) + singleUseChecks(base, candidate, validConsumptions) + + removalChecks(base, candidate, validConsumptions) /** * M45: a newly introduced certificate binds the exact authority base it is proposed against. @@ -94,25 +101,57 @@ object MutationAuthorityDigestCertificateCeremony { return diagnostics } + /** + * M44: single use. A certificate that was consumed in this transition must not also be retained. + * Leaving it in place would let the same migration authority be consumed again by a later + * transition, so consumption and retention are mutually exclusive. + * + * Judged only over consumptions that were **independently established** (M40-M43). A source + * digest that is not in the base is not this rule's business - M43 refuses that citation - so a + * candidate cannot use this rule to fail a transition by citing certificates that do not exist. + */ + private fun singleUseChecks( + base: MutationAuthorityDigestCertificates, + candidate: MutationAuthorityDigestCertificates, + validConsumptions: Set, + ): List { + val diagnostics = mutableListOf() + val baseByFromDigest = base.byFromDigest() + val candidateByFromDigest = candidate.byFromDigest() + for (fromDigest in validConsumptions.sorted()) { + if (fromDigest !in baseByFromDigest) continue + if (fromDigest in candidateByFromDigest) { + diagnostics += + certificateFailure( + fromDigest, + "M44: the digest-migration certificate for source digest " + + "${short(fromDigest)} was consumed by this transition but is retained in " + + "the candidate. A certificate authorises one migration once.", + ) + } + } + return diagnostics + } + /** * M47: removal custody. A certificate may only disappear by being consumed; it may not be * cancelled silently. * - * Until the certificate-aware consumption path exists there is no way to *prove* a valid - * consumption, so this rule takes its strictly fail-closed half: **any** base certificate absent - * from the candidate fails. That is deliberate, not an unfinished branch. A disappearance is - * never its own evidence - when M40-M44 land, this becomes "unless validly consumed by the same - * transition", judged against an independently established consumption, and never against the - * mere absence of the certificate. + * The consuming half is [validConsumptions] - the fact established by M40-M43, taken here as an + * input rather than rediscovered. That is structural, not conventional: this function has no + * access to the semantic predicates, so a disappearance can never become its own evidence. If + * nothing valid was established (empty set, including the fail-closed default), every base + * certificate absent from the candidate fails, exactly as before the consumption path existed. */ private fun removalChecks( base: MutationAuthorityDigestCertificates, candidate: MutationAuthorityDigestCertificates, + validConsumptions: Set, ): List { val diagnostics = mutableListOf() val candidateByFromDigest = candidate.byFromDigest() for ((fromDigest, _) in base.byFromDigest()) { - if (fromDigest !in candidateByFromDigest) { + if (fromDigest !in candidateByFromDigest && fromDigest !in validConsumptions) { diagnostics += certificateFailure( fromDigest, diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt new file mode 100644 index 000000000..e48b0d509 --- /dev/null +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt @@ -0,0 +1,152 @@ +package dev.tramai.build.quality + +/** + * Certificate consumption: M40-M43 (0.7.1g1P2). + * + * These rules establish a **positive** fact — "this certificate validly authorises this migration in + * this transition" — before anything acts on it. M44 and M47 consume that established fact; neither + * of them re-derives it. That separation is deliberate: if removal custody re-ran the semantic + * checks itself, the two implementations could drift, and a disappearance could quietly become its + * own evidence. Here the only way to reach the fact is to pass every predicate below. + * + * Consumption is bounded, not a licence. A certificate names one exact source digest, one exact + * target digest and one exact admission set, and it may only be consumed by a transition whose fresh + * measurement actually projects to that target: + * + * ``` + * M43 the certificate already existed in the base (the consuming candidate cannot invent the + * semantic upgrade it consumes - the M31 analogue) + * M40 toDigest == the verifier's fresh authority projection (never candidate-supplied: T7) + * M41 fromDigest == the cited admission's own populationDigest (the historical authority) + * M42 admissionSetDigest == the exact base authorization set (bounded: no different, later set) + * ``` + * + * Every predicate is fail-closed: a missing or mismatched value yields a diagnostic, never a pass by + * omission. Each predicate is its own function so the refusal order is readable, and so the whole + * consumption is one chain: the first predicate that refuses is the diagnostic, and only a + * certificate that satisfies all of them yields `null` (the valid consumption). + */ +object MutationAuthorityDigestCertificateConsumption { + /** + * M43 + M40 + M41 + M42 for one candidate consumption. Returns `null` when the consumption is + * valid, otherwise the diagnostic that refuses it. + * + * @param certificate the certificate being cited + * @param certificateFromBase whether that certificate is present in the base ledger. This is + * M43, checked explicitly rather than assumed from how the caller found the certificate, so + * provenance stays a named rule in diagnostics and tests. + * @param freshAuthorityProjectionHash the authority-v2 projection the verifier computed from + * its own fresh measurement (M40). Callers must pass measured data, never candidate data. + * @param citedPopulationDigest the `populationDigest` of the admission being consumed (M41). + * @param baseAdmissionIdentities the exact identities authorised in the base (M42). + */ + fun verify( + certificate: MutationAuthorityDigestCertificate, + certificateFromBase: Boolean, + freshAuthorityProjectionHash: String, + citedPopulationDigest: String, + baseAdmissionIdentities: List, + ): VerificationDiagnostic? = + provenance(certificate, certificateFromBase) + ?: targetDigest(certificate, freshAuthorityProjectionHash) + ?: algorithmPair(certificate) + ?: sourceDigest(certificate, citedPopulationDigest) + ?: admissionSet(certificate, baseAdmissionIdentities) + + private fun provenance( + certificate: MutationAuthorityDigestCertificate, + certificateFromBase: Boolean, + ): VerificationDiagnostic? = + if (certificateFromBase) { + null + } else { + failure( + certificate, + "M43: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} is cited for consumption by the same transition " + + "that introduces it. Migration authority must already exist in the base: a " + + "candidate cannot mint the semantic upgrade it consumes.", + ) + } + + private fun targetDigest( + certificate: MutationAuthorityDigestCertificate, + freshAuthorityProjectionHash: String, + ): VerificationDiagnostic? = + if (certificate.toDigest == freshAuthorityProjectionHash) { + null + } else { + failure( + certificate, + "M40: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} certifies target digest " + + "${short(certificate.toDigest)}, but this transition's fresh measurement projects " + + "to ${short(freshAuthorityProjectionHash)}. A certificate authorises one exact " + + "migration, not any migration.", + ) + } + + private fun algorithmPair(certificate: MutationAuthorityDigestCertificate): VerificationDiagnostic? = + if (certificate.fromAlgorithm == MutationAuthorityDigestCertificates.ALGORITHM_RAW_V1 && + certificate.toAlgorithm == MutationAuthorityDigestCertificates.ALGORITHM_AUTHORITY_V2 + ) { + null + } else { + failure( + certificate, + "M41: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} declares the migration " + + "'${certificate.fromAlgorithm}' -> '${certificate.toAlgorithm}', which is not the " + + "'${MutationAuthorityDigestCertificates.ALGORITHM_RAW_V1}' -> " + + "'${MutationAuthorityDigestCertificates.ALGORITHM_AUTHORITY_V2}' authority " + + "upgrade this consumption requires.", + ) + } + + private fun sourceDigest( + certificate: MutationAuthorityDigestCertificate, + citedPopulationDigest: String, + ): VerificationDiagnostic? = + if (certificate.fromDigest == citedPopulationDigest) { + null + } else { + failure( + certificate, + "M41: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} does not match the cited admission's historical " + + "population digest ${short(citedPopulationDigest)}. A certificate translates the " + + "authority the admission was actually minted under, not a different one.", + ) + } + + private fun admissionSet( + certificate: MutationAuthorityDigestCertificate, + baseAdmissionIdentities: List, + ): VerificationDiagnostic? { + val bound = MutationAuthorityDigestCertificates.admissionSetDigest(baseAdmissionIdentities) + if (certificate.admissionSetDigest == bound) return null + return failure( + certificate, + "M42: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} binds admission set " + + "${short(certificate.admissionSetDigest)}, which is not the exact set of " + + "${baseAdmissionIdentities.size} base authorizations (${short(bound)}). The " + + "certificate is bounded: it cannot cover a different or later admission set.", + ) + } + + private fun failure( + certificate: MutationAuthorityDigestCertificate, + message: String, + ): VerificationDiagnostic = + VerificationDiagnostic.failure( + DiagnosticCode.MUTATION_RATCHET_AUTHORITY_INVALID, + message, + findingId = certificate.fromDigest, + ) + + private fun short(digest: String): String = digest.take(SHORT_HASH_LENGTH) + + /** Prefix length used to identify a digest in diagnostics; the full value stays in findingId. */ + private const val SHORT_HASH_LENGTH = 8 +} diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt new file mode 100644 index 000000000..94257e55b --- /dev/null +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt @@ -0,0 +1,155 @@ +package dev.tramai.build.quality + +import dev.tramai.build.quality.MutationAuthorityDigestCertificates.Companion.ALGORITHM_AUTHORITY_V2 +import dev.tramai.build.quality.MutationAuthorityDigestCertificates.Companion.ALGORITHM_RAW_V1 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * T14, T15, T16 and T19 at the rule level: certificate consumption establishes a positive fact, and + * the lifecycle rules act on that fact rather than on a certificate's disappearance. + */ +class MutationAuthorityDigestCertificateConsumptionTest { + private val raw = "1".repeat(64) + private val authority = "2".repeat(64) + private val baseSha = "a".repeat(40) + private val identities = listOf("id-a", "id-b") + private val setDigest = MutationAuthorityDigestCertificates.admissionSetDigest(identities) + + private fun certificate( + fromDigest: String = raw, + toDigest: String = authority, + fromAlgorithm: String = ALGORITHM_RAW_V1, + toAlgorithm: String = ALGORITHM_AUTHORITY_V2, + admissionSetDigest: String = setDigest, + ) = MutationAuthorityDigestCertificate( + fromAlgorithm = fromAlgorithm, + fromDigest = fromDigest, + toAlgorithm = toAlgorithm, + toDigest = toDigest, + admissionSetDigest = admissionSetDigest, + fromBaseSha = baseSha, + reason = "test certificate", + ) + + private fun ledger(vararg certificates: MutationAuthorityDigestCertificate) = + MutationAuthorityDigestCertificates(schemaVersion = "1", certificates = certificates.toList()) + + private fun verify( + certificate: MutationAuthorityDigestCertificate = certificate(), + fromBase: Boolean = true, + freshAuthorityProjectionHash: String = authority, + citedPopulationDigest: String = raw, + base: List = identities, + ) = MutationAuthorityDigestCertificateConsumption.verify( + certificate = certificate, + certificateFromBase = fromBase, + freshAuthorityProjectionHash = freshAuthorityProjectionHash, + citedPopulationDigest = citedPopulationDigest, + baseAdmissionIdentities = base, + ) + + @Test + fun `a certificate satisfying every predicate establishes the consumption fact`() { + assertNull(verify()) + } + + @Test + fun `M43 a candidate-minted certificate cannot enter the consumption fact set`() { + val diagnostic = assertNotNull(verify(fromBase = false)) + + assertTrue(diagnostic.message.contains("M43"), diagnostic.message) + assertEquals(raw, diagnostic.findingId) + } + + @Test + fun `M40 a certificate whose target digest is not this measurement's projection is refused`() { + val diagnostic = assertNotNull(verify(freshAuthorityProjectionHash = "3".repeat(64))) + + assertTrue(diagnostic.message.contains("M40"), diagnostic.message) + } + + @Test + fun `M41 a certificate for a different source digest than the cited admission is refused`() { + val diagnostic = assertNotNull(verify(certificate = certificate(fromDigest = "4".repeat(64)))) + + assertTrue(diagnostic.message.contains("M41"), diagnostic.message) + } + + @Test + fun `M41 a certificate declaring a different migration pair is refused`() { + val diagnostic = + assertNotNull(verify(certificate = certificate(fromAlgorithm = ALGORITHM_AUTHORITY_V2))) + + assertTrue(diagnostic.message.contains("M41"), diagnostic.message) + } + + @Test + fun `M42 a certificate bounding a different admission set is refused`() { + val diagnostic = + assertNotNull(verify(certificate = certificate(admissionSetDigest = "5".repeat(64)))) + + assertTrue(diagnostic.message.contains("M42"), diagnostic.message) + } + + @Test + fun `M42 a certificate that also covers an unauthorized identity is refused`() { + val diagnostic = assertNotNull(verify(base = identities + "id-c")) + + assertTrue(diagnostic.message.contains("M42"), diagnostic.message) + } + + @Test + fun `T16 M44 a consumed certificate retained in the candidate fails`() { + val diagnostics = + MutationAuthorityDigestCertificateCeremony.checks( + base = ledger(certificate()), + candidate = ledger(certificate()), + baseSha = baseSha, + validConsumptions = setOf(raw), + ) + + assertTrue(diagnostics.single().message.contains("M44"), diagnostics.single().message) + } + + @Test + fun `T19 M47 a removed certificate with no established consumption still fails`() { + val diagnostics = + MutationAuthorityDigestCertificateCeremony.checks( + base = ledger(certificate()), + candidate = ledger(), + baseSha = baseSha, + ) + + assertTrue(diagnostics.single().message.contains("M47"), diagnostics.single().message) + } + + @Test + fun `T19 M47 removal is permitted only by a consumption established in the same transition`() { + val diagnostics = + MutationAuthorityDigestCertificateCeremony.checks( + base = ledger(certificate()), + candidate = ledger(), + baseSha = baseSha, + validConsumptions = setOf(raw), + ) + + assertTrue(diagnostics.isEmpty(), diagnostics.joinToString { it.message }) + } + + @Test + fun `T19 M47 removal is not permitted by a consumption established for a different digest`() { + val diagnostics = + MutationAuthorityDigestCertificateCeremony.checks( + base = ledger(certificate()), + candidate = ledger(), + baseSha = baseSha, + validConsumptions = setOf("6".repeat(64)), + ) + + assertTrue(diagnostics.single().message.contains("M47"), diagnostics.single().message) + } +} From 4e6d0ffe36e35dadf75c5f5d2b19c0123ffa4b6e Mon Sep 17 00:00:00 2001 From: gionag Date: Fri, 2 Oct 2026 07:21:53 +0200 Subject: [PATCH 2/4] authority(0.7.1g1P2-Step3a rev2): consumption becomes an attributed fact, not a caller claim Review blocker on the previous head: Set let a caller state "trust me, this digest was consumed", and certificateFromBase: Boolean let a caller assert M43 provenance. Both are now closed structurally rather than by convention. - CertificateConsumption is a sealed result. M44 and M47 take Set, never raw digests, so the attribution travels with the value: there is no way to express an unproven consumption in the lifecycle API. - M43 is derived inside verifyCertificateConsumption by looking the certificate up in the base ledger and comparing enforced payloads. The fromBase Boolean is gone, so provenance cannot drift from the truth, and the two failure modes (absent from base / base payload differing from the cited certificate) are distinct diagnostics. - The Valid constructor is internal, so the normal public API cannot fabricate a fact. Not a capability system: the point is that no ordinary call site can bypass the proof. - verifyCertificateConsumption returns the fact, so Step 3b's call site cannot become part of the authority proof: a refactor cannot add a digest under the wrong condition and have M44/M47 still trust it. Every Valid fact in the tests is obtained by running the real verifier over a real base ledger; none is hand-constructed. Rules untouched: M36/M37 literally unchanged, and validConsumptions still defaults to the empty fail-closed set, so the eight pre-existing ceremony tests pass unmodified. Verification: 33 tests / 0 failures (consumption 13, loader 12, ceremony 8); spotless clean; Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED, class build-logic. All formatting and Detekt findings fixed by hand - no suppression, no baseline entry. --- .../build/quality/CertificateConsumption.kt | 201 ++++++++++++++++++ ...ationAuthorityDigestCertificateCeremony.kt | 23 +- ...onAuthorityDigestCertificateConsumption.kt | 152 ------------- ...thorityDigestCertificateConsumptionTest.kt | 113 +++++++--- 4 files changed, 295 insertions(+), 194 deletions(-) create mode 100644 build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt delete mode 100644 build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt new file mode 100644 index 000000000..fb4b2f59f --- /dev/null +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt @@ -0,0 +1,201 @@ +package dev.tramai.build.quality + +/** + * Certificate consumption: M40-M43 (0.7.1g1P2). + * + * These rules produce a **positive, attributed fact** — [CertificateConsumption.Valid] — and the only + * way to obtain one is through [verifyCertificateConsumption], which derives every predicate from real + * authority inputs: the base certificate ledger, the cited admission, the exact base authorization + * set and the verifier's own fresh measurement. + * + * That matters because lifecycle rules must not be able to act on a claim: + * + * - M44 and M47 take `Set`, never raw digests, so a caller cannot + * state "trust me, this digest was consumed". The attribution is carried by the value. + * - M43 provenance is derived here by looking the certificate up in the base ledger. There is no + * `fromBase` flag for a caller to assert, so the Boolean cannot drift from the truth. + * - The Valid constructor is `internal`, so the normal public API cannot fabricate a fact by + * bypassing the proof. This is deliberately not a capability system: the goal is that no ordinary + * call site can accidentally manufacture consumption authority. + * + * Consumption is bounded, not a licence. A certificate names one exact source digest, one exact + * target digest and one exact admission set, and it may only be consumed by a transition whose fresh + * measurement actually projects to that target: + * + * ``` + * M43 the certificate is present in the base with the payload it was minted with (the consuming + * candidate cannot invent the semantic upgrade it consumes - the M31 analogue) + * M40 toDigest == the verifier's fresh authority projection (never candidate-supplied: T7) + * M41 fromDigest == the cited admission's own populationDigest (the historical authority) + * M42 admissionSetDigest == the exact base authorization set (bounded: no different, later set) + * ``` + * + * Every predicate is fail-closed: a missing or mismatched value yields [CertificateConsumption.Invalid], + * never a pass by omission. Each predicate is its own function so the refusal order is readable, and + * the whole consumption is one chain: the first predicate that refuses decides, and only a + * certificate satisfying all of them yields `Valid`. + */ +sealed interface CertificateConsumption { + /** The proven fact: this certificate validly authorises this migration in this transition. */ + data class Valid internal constructor( + val fromDigest: String, + val toDigest: String, + val admissionSetDigest: String, + ) : CertificateConsumption + + /** The refusal, carrying the named rule that rejected it. */ + data class Invalid( + val diagnostic: VerificationDiagnostic, + ) : CertificateConsumption +} + +/** + * M43 + M40 + M41 + M42 for one candidate consumption, derived from base authority. + * + * @param certificate the certificate being cited + * @param baseCertificates the certificate ledger as it exists in the authority base. M43 is derived + * from it, not asserted: a certificate that is absent, or whose enforced payload differs from the + * base copy, cannot be consumed. + * @param citedAdmissionPopulationDigest the `populationDigest` of the admission being consumed (M41). + * @param baseAdmissionIdentities the exact identities authorised in the base (M42). + * @param freshAuthorityProjectionHash the authority-v2 projection the verifier computed from its own + * fresh measurement (M40). Callers must pass measured data, never candidate data. + */ +fun verifyCertificateConsumption( + certificate: MutationAuthorityDigestCertificate, + baseCertificates: MutationAuthorityDigestCertificates, + citedAdmissionPopulationDigest: String, + baseAdmissionIdentities: List, + freshAuthorityProjectionHash: String, +): CertificateConsumption { + val inBase = baseCertificates.byFromDigest()[certificate.fromDigest] + val refusal = + provenance(certificate, inBase) + ?: targetDigest(certificate, freshAuthorityProjectionHash) + ?: algorithmPair(certificate) + ?: sourceDigest(certificate, citedAdmissionPopulationDigest) + ?: admissionSet(certificate, baseAdmissionIdentities) + return refusal?.let { CertificateConsumption.Invalid(it) } + ?: CertificateConsumption.Valid( + fromDigest = certificate.fromDigest, + toDigest = certificate.toDigest, + admissionSetDigest = certificate.admissionSetDigest, + ) +} + +/** + * M43, derived from the base ledger rather than asserted by the caller. Two distinct failures: the + * certificate is not in the base at all (the transition is minting the authority it consumes), or it + * is present but its enforced payload differs (the consumption cites something other than the + * certificate that was minted). + */ +private fun provenance( + certificate: MutationAuthorityDigestCertificate, + inBase: MutationAuthorityDigestCertificate?, +): VerificationDiagnostic? = + when { + inBase == null -> { + failure( + certificate, + "M43: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} is not present in the authority base, so this " + + "transition would consume migration authority it introduces itself. Migration " + + "authority must already exist in the base.", + ) + } + + inBase.enforcedPayload() != certificate.enforcedPayload() -> { + failure( + certificate, + "M43: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} does not match the certificate present in the " + + "authority base. A consumption must cite the base certificate as it was minted.", + ) + } + + else -> { + null + } + } + +private fun targetDigest( + certificate: MutationAuthorityDigestCertificate, + freshAuthorityProjectionHash: String, +): VerificationDiagnostic? = + if (certificate.toDigest == freshAuthorityProjectionHash) { + null + } else { + failure( + certificate, + "M40: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} certifies target digest " + + "${short(certificate.toDigest)}, but this transition's fresh measurement projects to " + + "${short(freshAuthorityProjectionHash)}. A certificate authorises one exact " + + "migration, not any migration.", + ) + } + +private fun algorithmPair(certificate: MutationAuthorityDigestCertificate): VerificationDiagnostic? = + if (certificate.fromAlgorithm == MutationAuthorityDigestCertificates.ALGORITHM_RAW_V1 && + certificate.toAlgorithm == MutationAuthorityDigestCertificates.ALGORITHM_AUTHORITY_V2 + ) { + null + } else { + failure( + certificate, + "M41: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} declares the migration " + + "'${certificate.fromAlgorithm}' -> '${certificate.toAlgorithm}', which is not the " + + "'${MutationAuthorityDigestCertificates.ALGORITHM_RAW_V1}' -> " + + "'${MutationAuthorityDigestCertificates.ALGORITHM_AUTHORITY_V2}' authority upgrade " + + "this consumption requires.", + ) + } + +private fun sourceDigest( + certificate: MutationAuthorityDigestCertificate, + citedAdmissionPopulationDigest: String, +): VerificationDiagnostic? = + if (certificate.fromDigest == citedAdmissionPopulationDigest) { + null + } else { + failure( + certificate, + "M41: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} does not match the cited admission's historical " + + "population digest ${short(citedAdmissionPopulationDigest)}. A certificate " + + "translates the authority the admission was actually minted under, not a different " + + "one.", + ) + } + +private fun admissionSet( + certificate: MutationAuthorityDigestCertificate, + baseAdmissionIdentities: List, +): VerificationDiagnostic? { + val bound = MutationAuthorityDigestCertificates.admissionSetDigest(baseAdmissionIdentities) + if (certificate.admissionSetDigest == bound) return null + return failure( + certificate, + "M42: the digest-migration certificate for source digest " + + "${short(certificate.fromDigest)} binds admission set " + + "${short(certificate.admissionSetDigest)}, which is not the exact set of " + + "${baseAdmissionIdentities.size} base authorizations (${short(bound)}). The certificate " + + "is bounded: it cannot cover a different or later admission set.", + ) +} + +private fun failure( + certificate: MutationAuthorityDigestCertificate, + message: String, +): VerificationDiagnostic = + VerificationDiagnostic.failure( + DiagnosticCode.MUTATION_RATCHET_AUTHORITY_INVALID, + message, + findingId = certificate.fromDigest, + ) + +private fun short(digest: String): String = digest.take(SHORT_HASH_LENGTH) + +/** Prefix length used to identify a digest in diagnostics; the full value stays in findingId. */ +private const val SHORT_HASH_LENGTH = 8 diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt index 27d56ccde..10c4612ae 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateCeremony.kt @@ -25,17 +25,18 @@ object MutationAuthorityDigestCertificateCeremony { * @param base the certificate ledger as it exists in the PR's authority base * @param candidate the certificate ledger as the PR proposes it * @param baseSha the exact authority base SHA this transition is proposed against - * @param validConsumptions the source digests of certificates whose consumption in this - * transition was **independently established** by M40-M43. This is the fact M44 and M47 act on: - * they never re-derive it, and they never treat the absence of a certificate as evidence that - * it was consumed. An empty set is the fail-closed state, so a caller that establishes nothing - * gets the strictest behaviour. + * @param validConsumptions the proven consumptions this transition established through + * [verifyCertificateConsumption]. Only attributed [CertificateConsumption.Valid] facts can be + * passed, so a caller cannot state "this digest was consumed" - the fact carries its own + * proof. M44 and M47 never re-derive it and never treat the absence of a certificate as + * evidence that it was consumed. An empty set is the fail-closed state, so a caller that + * establishes nothing gets the strictest behaviour. */ fun checks( base: MutationAuthorityDigestCertificates, candidate: MutationAuthorityDigestCertificates, baseSha: String, - validConsumptions: Set = emptySet(), + validConsumptions: Set = emptySet(), ): List = mintChecks(base, candidate, baseSha) + retentionChecks(base, candidate) + @@ -113,12 +114,13 @@ object MutationAuthorityDigestCertificateCeremony { private fun singleUseChecks( base: MutationAuthorityDigestCertificates, candidate: MutationAuthorityDigestCertificates, - validConsumptions: Set, + validConsumptions: Set, ): List { val diagnostics = mutableListOf() val baseByFromDigest = base.byFromDigest() val candidateByFromDigest = candidate.byFromDigest() - for (fromDigest in validConsumptions.sorted()) { + for (consumed in validConsumptions.sortedBy { it.fromDigest }) { + val fromDigest = consumed.fromDigest if (fromDigest !in baseByFromDigest) continue if (fromDigest in candidateByFromDigest) { diagnostics += @@ -146,12 +148,13 @@ object MutationAuthorityDigestCertificateCeremony { private fun removalChecks( base: MutationAuthorityDigestCertificates, candidate: MutationAuthorityDigestCertificates, - validConsumptions: Set, + validConsumptions: Set, ): List { val diagnostics = mutableListOf() val candidateByFromDigest = candidate.byFromDigest() for ((fromDigest, _) in base.byFromDigest()) { - if (fromDigest !in candidateByFromDigest && fromDigest !in validConsumptions) { + val consumed = validConsumptions.any { it.fromDigest == fromDigest } + if (fromDigest !in candidateByFromDigest && !consumed) { diagnostics += certificateFailure( fromDigest, diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt deleted file mode 100644 index e48b0d509..000000000 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumption.kt +++ /dev/null @@ -1,152 +0,0 @@ -package dev.tramai.build.quality - -/** - * Certificate consumption: M40-M43 (0.7.1g1P2). - * - * These rules establish a **positive** fact — "this certificate validly authorises this migration in - * this transition" — before anything acts on it. M44 and M47 consume that established fact; neither - * of them re-derives it. That separation is deliberate: if removal custody re-ran the semantic - * checks itself, the two implementations could drift, and a disappearance could quietly become its - * own evidence. Here the only way to reach the fact is to pass every predicate below. - * - * Consumption is bounded, not a licence. A certificate names one exact source digest, one exact - * target digest and one exact admission set, and it may only be consumed by a transition whose fresh - * measurement actually projects to that target: - * - * ``` - * M43 the certificate already existed in the base (the consuming candidate cannot invent the - * semantic upgrade it consumes - the M31 analogue) - * M40 toDigest == the verifier's fresh authority projection (never candidate-supplied: T7) - * M41 fromDigest == the cited admission's own populationDigest (the historical authority) - * M42 admissionSetDigest == the exact base authorization set (bounded: no different, later set) - * ``` - * - * Every predicate is fail-closed: a missing or mismatched value yields a diagnostic, never a pass by - * omission. Each predicate is its own function so the refusal order is readable, and so the whole - * consumption is one chain: the first predicate that refuses is the diagnostic, and only a - * certificate that satisfies all of them yields `null` (the valid consumption). - */ -object MutationAuthorityDigestCertificateConsumption { - /** - * M43 + M40 + M41 + M42 for one candidate consumption. Returns `null` when the consumption is - * valid, otherwise the diagnostic that refuses it. - * - * @param certificate the certificate being cited - * @param certificateFromBase whether that certificate is present in the base ledger. This is - * M43, checked explicitly rather than assumed from how the caller found the certificate, so - * provenance stays a named rule in diagnostics and tests. - * @param freshAuthorityProjectionHash the authority-v2 projection the verifier computed from - * its own fresh measurement (M40). Callers must pass measured data, never candidate data. - * @param citedPopulationDigest the `populationDigest` of the admission being consumed (M41). - * @param baseAdmissionIdentities the exact identities authorised in the base (M42). - */ - fun verify( - certificate: MutationAuthorityDigestCertificate, - certificateFromBase: Boolean, - freshAuthorityProjectionHash: String, - citedPopulationDigest: String, - baseAdmissionIdentities: List, - ): VerificationDiagnostic? = - provenance(certificate, certificateFromBase) - ?: targetDigest(certificate, freshAuthorityProjectionHash) - ?: algorithmPair(certificate) - ?: sourceDigest(certificate, citedPopulationDigest) - ?: admissionSet(certificate, baseAdmissionIdentities) - - private fun provenance( - certificate: MutationAuthorityDigestCertificate, - certificateFromBase: Boolean, - ): VerificationDiagnostic? = - if (certificateFromBase) { - null - } else { - failure( - certificate, - "M43: the digest-migration certificate for source digest " + - "${short(certificate.fromDigest)} is cited for consumption by the same transition " + - "that introduces it. Migration authority must already exist in the base: a " + - "candidate cannot mint the semantic upgrade it consumes.", - ) - } - - private fun targetDigest( - certificate: MutationAuthorityDigestCertificate, - freshAuthorityProjectionHash: String, - ): VerificationDiagnostic? = - if (certificate.toDigest == freshAuthorityProjectionHash) { - null - } else { - failure( - certificate, - "M40: the digest-migration certificate for source digest " + - "${short(certificate.fromDigest)} certifies target digest " + - "${short(certificate.toDigest)}, but this transition's fresh measurement projects " + - "to ${short(freshAuthorityProjectionHash)}. A certificate authorises one exact " + - "migration, not any migration.", - ) - } - - private fun algorithmPair(certificate: MutationAuthorityDigestCertificate): VerificationDiagnostic? = - if (certificate.fromAlgorithm == MutationAuthorityDigestCertificates.ALGORITHM_RAW_V1 && - certificate.toAlgorithm == MutationAuthorityDigestCertificates.ALGORITHM_AUTHORITY_V2 - ) { - null - } else { - failure( - certificate, - "M41: the digest-migration certificate for source digest " + - "${short(certificate.fromDigest)} declares the migration " + - "'${certificate.fromAlgorithm}' -> '${certificate.toAlgorithm}', which is not the " + - "'${MutationAuthorityDigestCertificates.ALGORITHM_RAW_V1}' -> " + - "'${MutationAuthorityDigestCertificates.ALGORITHM_AUTHORITY_V2}' authority " + - "upgrade this consumption requires.", - ) - } - - private fun sourceDigest( - certificate: MutationAuthorityDigestCertificate, - citedPopulationDigest: String, - ): VerificationDiagnostic? = - if (certificate.fromDigest == citedPopulationDigest) { - null - } else { - failure( - certificate, - "M41: the digest-migration certificate for source digest " + - "${short(certificate.fromDigest)} does not match the cited admission's historical " + - "population digest ${short(citedPopulationDigest)}. A certificate translates the " + - "authority the admission was actually minted under, not a different one.", - ) - } - - private fun admissionSet( - certificate: MutationAuthorityDigestCertificate, - baseAdmissionIdentities: List, - ): VerificationDiagnostic? { - val bound = MutationAuthorityDigestCertificates.admissionSetDigest(baseAdmissionIdentities) - if (certificate.admissionSetDigest == bound) return null - return failure( - certificate, - "M42: the digest-migration certificate for source digest " + - "${short(certificate.fromDigest)} binds admission set " + - "${short(certificate.admissionSetDigest)}, which is not the exact set of " + - "${baseAdmissionIdentities.size} base authorizations (${short(bound)}). The " + - "certificate is bounded: it cannot cover a different or later admission set.", - ) - } - - private fun failure( - certificate: MutationAuthorityDigestCertificate, - message: String, - ): VerificationDiagnostic = - VerificationDiagnostic.failure( - DiagnosticCode.MUTATION_RATCHET_AUTHORITY_INVALID, - message, - findingId = certificate.fromDigest, - ) - - private fun short(digest: String): String = digest.take(SHORT_HASH_LENGTH) - - /** Prefix length used to identify a digest in diagnostics; the full value stays in findingId. */ - private const val SHORT_HASH_LENGTH = 8 -} diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt index 94257e55b..8fe22cf50 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationAuthorityDigestCertificateConsumptionTest.kt @@ -1,19 +1,22 @@ package dev.tramai.build.quality +import dev.tramai.build.quality.CertificateConsumption.Valid import dev.tramai.build.quality.MutationAuthorityDigestCertificates.Companion.ALGORITHM_AUTHORITY_V2 import dev.tramai.build.quality.MutationAuthorityDigestCertificates.Companion.ALGORITHM_RAW_V1 import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull import kotlin.test.assertTrue /** - * T14, T15, T16 and T19 at the rule level: certificate consumption establishes a positive fact, and - * the lifecycle rules act on that fact rather than on a certificate's disappearance. + * T14, T15, T16 and T19 at the rule level: consumption is a fact produced from base authority, and + * the lifecycle rules act only on facts the verifier attributed. + * + * Every `Valid` fact below is obtained by running the real verifier over a real base ledger - none is + * hand-constructed - so these tests exercise the same route a Gradle call site will use. */ class MutationAuthorityDigestCertificateConsumptionTest { private val raw = "1".repeat(64) + private val otherRaw = "7".repeat(64) private val authority = "2".repeat(64) private val baseSha = "a".repeat(40) private val identities = listOf("id-a", "id-b") @@ -21,18 +24,17 @@ class MutationAuthorityDigestCertificateConsumptionTest { private fun certificate( fromDigest: String = raw, - toDigest: String = authority, fromAlgorithm: String = ALGORITHM_RAW_V1, - toAlgorithm: String = ALGORITHM_AUTHORITY_V2, admissionSetDigest: String = setDigest, + reason: String = "test certificate", ) = MutationAuthorityDigestCertificate( fromAlgorithm = fromAlgorithm, fromDigest = fromDigest, - toAlgorithm = toAlgorithm, - toDigest = toDigest, + toAlgorithm = ALGORITHM_AUTHORITY_V2, + toDigest = authority, admissionSetDigest = admissionSetDigest, fromBaseSha = baseSha, - reason = "test certificate", + reason = reason, ) private fun ledger(vararg certificates: MutationAuthorityDigestCertificate) = @@ -40,64 +42,110 @@ class MutationAuthorityDigestCertificateConsumptionTest { private fun verify( certificate: MutationAuthorityDigestCertificate = certificate(), - fromBase: Boolean = true, + base: MutationAuthorityDigestCertificates = ledger(certificate()), + citedAdmissionPopulationDigest: String = raw, + baseAdmissionIdentities: List = identities, freshAuthorityProjectionHash: String = authority, - citedPopulationDigest: String = raw, - base: List = identities, - ) = MutationAuthorityDigestCertificateConsumption.verify( + ) = verifyCertificateConsumption( certificate = certificate, - certificateFromBase = fromBase, + baseCertificates = base, + citedAdmissionPopulationDigest = citedAdmissionPopulationDigest, + baseAdmissionIdentities = baseAdmissionIdentities, freshAuthorityProjectionHash = freshAuthorityProjectionHash, - citedPopulationDigest = citedPopulationDigest, - baseAdmissionIdentities = base, ) + private fun validFact(certificate: MutationAuthorityDigestCertificate = certificate()): Valid { + val result = + verify( + certificate = certificate, + base = ledger(certificate), + citedAdmissionPopulationDigest = certificate.fromDigest, + ) + assertTrue(result is CertificateConsumption.Valid, "expected a valid consumption, got $result") + return result as CertificateConsumption.Valid + } + + private fun refusal( + certificate: MutationAuthorityDigestCertificate = certificate(), + base: MutationAuthorityDigestCertificates = ledger(certificate), + citedAdmissionPopulationDigest: String = raw, + baseAdmissionIdentities: List = identities, + freshAuthorityProjectionHash: String = authority, + ): VerificationDiagnostic { + val result = + verify( + certificate = certificate, + base = base, + citedAdmissionPopulationDigest = citedAdmissionPopulationDigest, + baseAdmissionIdentities = baseAdmissionIdentities, + freshAuthorityProjectionHash = freshAuthorityProjectionHash, + ) + assertTrue(result is CertificateConsumption.Invalid, "expected a refusal, got $result") + return (result as CertificateConsumption.Invalid).diagnostic + } + @Test - fun `a certificate satisfying every predicate establishes the consumption fact`() { - assertNull(verify()) + fun `a certificate satisfying every predicate yields the attributed fact`() { + val fact = validFact() + + assertEquals(raw, fact.fromDigest) + assertEquals(authority, fact.toDigest) + assertEquals(setDigest, fact.admissionSetDigest) } @Test - fun `M43 a candidate-minted certificate cannot enter the consumption fact set`() { - val diagnostic = assertNotNull(verify(fromBase = false)) + fun `M43 a certificate absent from the base cannot be consumed`() { + val diagnostic = refusal(base = MutationAuthorityDigestCertificates.NONE) assertTrue(diagnostic.message.contains("M43"), diagnostic.message) assertEquals(raw, diagnostic.findingId) } + @Test + fun `M43 a candidate-minted certificate cannot become a consumption fact`() { + val diagnostic = refusal(base = ledger(certificate(fromDigest = otherRaw))) + + assertTrue(diagnostic.message.contains("M43"), diagnostic.message) + } + + @Test + fun `M43 a certificate whose base payload was rewritten cannot be consumed`() { + val diagnostic = refusal(base = ledger(certificate(reason = "rewritten in the candidate"))) + + assertTrue(diagnostic.message.contains("M43"), diagnostic.message) + } + @Test fun `M40 a certificate whose target digest is not this measurement's projection is refused`() { - val diagnostic = assertNotNull(verify(freshAuthorityProjectionHash = "3".repeat(64))) + val diagnostic = refusal(freshAuthorityProjectionHash = "3".repeat(64)) assertTrue(diagnostic.message.contains("M40"), diagnostic.message) } @Test fun `M41 a certificate for a different source digest than the cited admission is refused`() { - val diagnostic = assertNotNull(verify(certificate = certificate(fromDigest = "4".repeat(64)))) + val diagnostic = refusal(citedAdmissionPopulationDigest = otherRaw) assertTrue(diagnostic.message.contains("M41"), diagnostic.message) } @Test fun `M41 a certificate declaring a different migration pair is refused`() { - val diagnostic = - assertNotNull(verify(certificate = certificate(fromAlgorithm = ALGORITHM_AUTHORITY_V2))) + val diagnostic = refusal(certificate = certificate(fromAlgorithm = ALGORITHM_AUTHORITY_V2)) assertTrue(diagnostic.message.contains("M41"), diagnostic.message) } @Test fun `M42 a certificate bounding a different admission set is refused`() { - val diagnostic = - assertNotNull(verify(certificate = certificate(admissionSetDigest = "5".repeat(64)))) + val diagnostic = refusal(certificate = certificate(admissionSetDigest = "5".repeat(64))) assertTrue(diagnostic.message.contains("M42"), diagnostic.message) } @Test fun `M42 a certificate that also covers an unauthorized identity is refused`() { - val diagnostic = assertNotNull(verify(base = identities + "id-c")) + val diagnostic = refusal(baseAdmissionIdentities = identities + "id-c") assertTrue(diagnostic.message.contains("M42"), diagnostic.message) } @@ -109,7 +157,7 @@ class MutationAuthorityDigestCertificateConsumptionTest { base = ledger(certificate()), candidate = ledger(certificate()), baseSha = baseSha, - validConsumptions = setOf(raw), + validConsumptions = setOf(validFact()), ) assertTrue(diagnostics.single().message.contains("M44"), diagnostics.single().message) @@ -128,26 +176,27 @@ class MutationAuthorityDigestCertificateConsumptionTest { } @Test - fun `T19 M47 removal is permitted only by a consumption established in the same transition`() { + fun `T19 M47 removal is permitted by the fact for that exact certificate`() { val diagnostics = MutationAuthorityDigestCertificateCeremony.checks( base = ledger(certificate()), candidate = ledger(), baseSha = baseSha, - validConsumptions = setOf(raw), + validConsumptions = setOf(validFact()), ) assertTrue(diagnostics.isEmpty(), diagnostics.joinToString { it.message }) } @Test - fun `T19 M47 removal is not permitted by a consumption established for a different digest`() { + fun `T19 M47 removal is not permitted by a fact proving a different certificate`() { + val other = certificate(fromDigest = otherRaw) val diagnostics = MutationAuthorityDigestCertificateCeremony.checks( base = ledger(certificate()), candidate = ledger(), baseSha = baseSha, - validConsumptions = setOf("6".repeat(64)), + validConsumptions = setOf(validFact(other)), ) assertTrue(diagnostics.single().message.contains("M47"), diagnostics.single().message) From 861d31a6e73b0cca290844eda3accf8513649407 Mon Sep 17 00:00:00 2001 From: gionag Date: Fri, 2 Oct 2026 07:31:24 +0200 Subject: [PATCH 3/4] authority(0.7.1g1P2-Step3a rev3): Valid becomes unconstructible outside the proof Follow-up blocker: `internal` on the constructor is module-wide, and the Gradle call sites live in this very module, so Step 3b could still legally write: val fake = CertificateConsumption.Valid(fromDigest = "...", ...) and hand it to the ceremony without ever running M40-M43. The fact was attributed, but still forgeable by ordinary same-module code. - Valid's constructor is now `private`, so nothing outside the class can construct one - including other files in build-logic. - The verification itself moved inside Valid's companion (`Valid.verify`), because that is the only scope from which a private constructor is reachable. So the proof and the fact live in the same place by construction: there is no path to a Valid that skips M43-M42. - The top-level verifyCertificateConsumption now delegates to it, keeping the call-site name stable while leaving exactly one implementation of the proof. - @ConsistentCopyVisibility keeps the generated copy() private too, so it is not an escape hatch under Kotlin 2.3's visibility behaviour. This property is verified by the compiler, not by a test: the forgery snippet above no longer compiles. No test needed editing, because the existing tests obtain every fact through the verifier and never construct one. Verification: 33 tests / 0 failures; spotless clean; Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED, 3 changed files, class build-logic. --- .../build/quality/CertificateConsumption.kt | 89 +++++++++++++------ 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt index fb4b2f59f..13dc7ce13 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt @@ -36,12 +36,60 @@ package dev.tramai.build.quality * certificate satisfying all of them yields `Valid`. */ sealed interface CertificateConsumption { - /** The proven fact: this certificate validly authorises this migration in this transition. */ - data class Valid internal constructor( + /** + * The proven fact: this certificate validly authorises this migration in this transition. + * + * The constructor is **private**, so no code outside this class - including other files in the + * same module, which is where the Gradle call sites live - can construct one. The only production + * path is [Valid.verify], which is the verification itself: it derives M43 provenance from the + * base ledger and runs M40-M42 before the fact exists. `@ConsistentCopyVisibility` keeps the + * generated `copy()` private too, so it is not an escape hatch either. + */ + @ConsistentCopyVisibility + data class Valid private constructor( val fromDigest: String, val toDigest: String, val admissionSetDigest: String, - ) : CertificateConsumption + ) : CertificateConsumption { + companion object { + /** + * M43 + M40 + M41 + M42, derived from base authority. The only way a [Valid] comes into + * existence. + * + * @param certificate the certificate being cited + * @param baseCertificates the certificate ledger as it exists in the authority base. M43 + * is derived from it, not asserted: a certificate that is absent, or whose enforced + * payload differs from the base copy, cannot be consumed. + * @param citedAdmissionPopulationDigest the `populationDigest` of the admission being + * consumed (M41). + * @param baseAdmissionIdentities the exact identities authorised in the base (M42). + * @param freshAuthorityProjectionHash the authority-v2 projection the verifier computed + * from its own fresh measurement (M40). Callers must pass measured data, never + * candidate data. + */ + internal fun verify( + certificate: MutationAuthorityDigestCertificate, + baseCertificates: MutationAuthorityDigestCertificates, + citedAdmissionPopulationDigest: String, + baseAdmissionIdentities: List, + freshAuthorityProjectionHash: String, + ): CertificateConsumption { + val inBase = baseCertificates.byFromDigest()[certificate.fromDigest] + val refusal = + provenance(certificate, inBase) + ?: targetDigest(certificate, freshAuthorityProjectionHash) + ?: algorithmPair(certificate) + ?: sourceDigest(certificate, citedAdmissionPopulationDigest) + ?: admissionSet(certificate, baseAdmissionIdentities) + return refusal?.let { CertificateConsumption.Invalid(it) } + ?: Valid( + fromDigest = certificate.fromDigest, + toDigest = certificate.toDigest, + admissionSetDigest = certificate.admissionSetDigest, + ) + } + } + } /** The refusal, carrying the named rule that rejected it. */ data class Invalid( @@ -50,16 +98,8 @@ sealed interface CertificateConsumption { } /** - * M43 + M40 + M41 + M42 for one candidate consumption, derived from base authority. - * - * @param certificate the certificate being cited - * @param baseCertificates the certificate ledger as it exists in the authority base. M43 is derived - * from it, not asserted: a certificate that is absent, or whose enforced payload differs from the - * base copy, cannot be consumed. - * @param citedAdmissionPopulationDigest the `populationDigest` of the admission being consumed (M41). - * @param baseAdmissionIdentities the exact identities authorised in the base (M42). - * @param freshAuthorityProjectionHash the authority-v2 projection the verifier computed from its own - * fresh measurement (M40). Callers must pass measured data, never candidate data. + * The consumption entry point for call sites: delegates to [CertificateConsumption.Valid.verify], so + * there is exactly one implementation of the proof and no second route to a fact. */ fun verifyCertificateConsumption( certificate: MutationAuthorityDigestCertificate, @@ -67,21 +107,14 @@ fun verifyCertificateConsumption( citedAdmissionPopulationDigest: String, baseAdmissionIdentities: List, freshAuthorityProjectionHash: String, -): CertificateConsumption { - val inBase = baseCertificates.byFromDigest()[certificate.fromDigest] - val refusal = - provenance(certificate, inBase) - ?: targetDigest(certificate, freshAuthorityProjectionHash) - ?: algorithmPair(certificate) - ?: sourceDigest(certificate, citedAdmissionPopulationDigest) - ?: admissionSet(certificate, baseAdmissionIdentities) - return refusal?.let { CertificateConsumption.Invalid(it) } - ?: CertificateConsumption.Valid( - fromDigest = certificate.fromDigest, - toDigest = certificate.toDigest, - admissionSetDigest = certificate.admissionSetDigest, - ) -} +): CertificateConsumption = + CertificateConsumption.Valid.verify( + certificate = certificate, + baseCertificates = baseCertificates, + citedAdmissionPopulationDigest = citedAdmissionPopulationDigest, + baseAdmissionIdentities = baseAdmissionIdentities, + freshAuthorityProjectionHash = freshAuthorityProjectionHash, + ) /** * M43, derived from the base ledger rather than asserted by the caller. Two distinct failures: the From ad34afeaaf3e25fbf4d8be468602ce0f26c41eff Mon Sep 17 00:00:00 2001 From: gionag Date: Fri, 2 Oct 2026 07:38:56 +0200 Subject: [PATCH 4/4] docs(0.7.1g1P2-Step3a): file KDoc states the private constructor, not internal Review nit on 861d31a6: the file-level KDoc still claimed the Valid constructor is `internal` while the class KDoc and the implementation correctly say `private`. A stale description of a security property is worth correcting rather than leaving, because it is the kind of line a later reader trusts instead of the code. Documentation only - no behaviour, no rule, no test changed. --- .../dev/tramai/build/quality/CertificateConsumption.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt index 13dc7ce13..d14a79121 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/CertificateConsumption.kt @@ -14,9 +14,11 @@ package dev.tramai.build.quality * state "trust me, this digest was consumed". The attribution is carried by the value. * - M43 provenance is derived here by looking the certificate up in the base ledger. There is no * `fromBase` flag for a caller to assert, so the Boolean cannot drift from the truth. - * - The Valid constructor is `internal`, so the normal public API cannot fabricate a fact by - * bypassing the proof. This is deliberately not a capability system: the goal is that no ordinary - * call site can accidentally manufacture consumption authority. + * - The Valid constructor is `private` (with `@ConsistentCopyVisibility` on the class), so no code + * outside the verification itself can construct one - not even other files in this module, which + * is where the Gradle call sites live. The proof and the fact share a scope by construction: there + * is no path to a Valid that skips M43-M42. This is deliberately not a capability system: the goal + * is that no ordinary call site can accidentally manufacture consumption authority. * * Consumption is bounded, not a licence. A certificate names one exact source digest, one exact * target digest and one exact admission set, and it may only be consumed by a transition whose fresh