Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package dev.tramai.build.quality

import java.security.MessageDigest

/**
* Base-side digest-migration certificate (0.7.1g1P2, M40-M47).
*
* This is the artifact that carries a *semantic* upgrade of the whole-population authority digest
* without rewriting historical authority. The P1 authorizations are bound to `raw-v1` raw-status
* digests and must stay byte-identical; the certificate stands beside them and states, in base
* authority, that one exact `fromDigest` is equivalent to one exact `toDigest` for one exact,
* bounded set of authorizations. M36's payload comparison never sees it, because it lives in its
* own ledger - no exception branch, no permitted-field list, is added to the admissions rules.
*
* ```
* mint against the exact base (M45)
* -> retain byte-identically (M46)
* -> consume only from base (M43 + M42 + M41 + M40)
* -> remove in the valid consuming transition (M44 + M47)
* ```
*
* ## Field semantics
*
* - [fromAlgorithm] / [fromDigest] - the digest semantics the covered authorizations were minted
* under, and that exact digest. M41 requires this to match the cited admission's
* `populationDigest`; a certificate can never be applied to a different authorization than the
* one whose digest it names.
* - [toAlgorithm] / [toDigest] - the digest semantics the covered authorizations are to be consumed
* under, and that exact digest. M40 requires [toDigest] to equal the verifier's own fresh
* authority projection, never anything the candidate supplies.
* - [admissionSetDigest] - SHA-256 over the exact set of covered identities, sorted and joined with
* `"\n"` with a trailing newline (the same recipe this repository uses for manifest digests). M42
* requires it to equal the exact set of base authorizations, which is what makes the certificate
* *bounded*: it cannot be widened to cover a different or later set.
* - [fromBaseSha] - the authority base the certificate is minted against. **Enforced at
* introduction only (M45); thereafter immutable provenance.** Same semantics as
* [MutationPopulationAdmission.fromBaseSha]: a certificate legitimately survives intermediate
* merges, so requiring it to track the immediate base would make delayed consumption impossible;
* its enforcement value is anti-replay at mint time.
* - [reason] - the recorded provenance of the supersession, part of the enforced payload.
* - [authorizedBy], [authorizedAt] - **audit only, no enforcement value.** Recording them is fine;
* letting identity or actor carry trust would make them a bypass.
*
* ## Rule ownership
*
* M45 (mint binding), M46 (retained immutability) and M47 (removal custody) are lifecycle rules and
* live in [MutationAuthorityDigestCertificateCeremony]. M40-M44 are *consumption* rules and arrive
* with the certificate-aware consumption path; until then a base certificate may not disappear at
* all, which is fail-closed and is the stricter half of M47.
*/
data class MutationAuthorityDigestCertificate(
val fromAlgorithm: String,
val fromDigest: String,
val toAlgorithm: String,
val toDigest: String,
val admissionSetDigest: String,
val fromBaseSha: String,
val reason: String,
val authorizedBy: String? = null,
val authorizedAt: String? = null,
) {
/**
* The enforceable payload: every bound field except the audit-only ones. Base/candidate
* byte-identity is judged over this (M46), so rewriting any bound field fails while touching
* audit metadata alone can never manufacture authority.
*/
fun enforcedPayload(): List<Any?> =
listOf(
fromAlgorithm,
fromDigest,
toAlgorithm,
toDigest,
admissionSetDigest,
fromBaseSha,
reason,
)
}

data class MutationAuthorityDigestCertificates(
val schemaVersion: String,
val certificates: List<MutationAuthorityDigestCertificate>,
) {
/**
* Keyed by [MutationAuthorityDigestCertificate.fromDigest]: the source of a migration is
* unique. Two certificates claiming the same source digest would contradict each other, and the
* loader rejects that rather than leaving the winner to map iteration order.
*/
fun byFromDigest(): Map<String, MutationAuthorityDigestCertificate> = certificates.associateBy { it.fromDigest }

companion object {
/** The digest semantics a certificate may migrate from and to. */
const val ALGORITHM_RAW_V1 = "raw-v1"
const val ALGORITHM_AUTHORITY_V2 = "authority-v2"

/** Byte mask for the hex rendering of a digest. */
private const val HEX_BYTE_MASK = 0xFF

/** No certificates: strictly the most restrictive state, so no migration is possible. */
val NONE = MutationAuthorityDigestCertificates(schemaVersion = "1", certificates = emptyList())

/**
* The bounded admission-set digest (M42): SHA-256 over the covered identities, sorted and
* joined with `"\n"` with a trailing newline.
*
* The recipe is part of the contract, not an implementation detail: whoever mints a
* certificate must be able to reproduce this value exactly from the authorization set, and a
* test pins it against the real ledger.
*/
fun admissionSetDigest(identities: Collection<String>): String {
val payload = identities.sorted().joinToString("\n", postfix = if (identities.isEmpty()) "" else "\n")
return MessageDigest
.getInstance("SHA-256")
.digest(payload.toByteArray(Charsets.UTF_8))
.joinToString("") { "%02x".format(it.toInt() and HEX_BYTE_MASK) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package dev.tramai.build.quality

/**
* The digest-migration certificate's authority lifecycle (0.7.1g1P2, M45-M47).
*
* The certificate is itself base authority, so its whole lifecycle is fail-closed, not only its
* consumption. These three rules are the lifecycle half; M40-M44 are consumption rules and arrive
* with the certificate-aware consumption path.
*
* ```
* mint against the exact base (M45) <- this file
* -> retain byte-identically (M46) <- this file
* -> consume only from base (M43 + M42 + M41 + M40)
* -> remove in the valid consuming transition (M44 + M47) <- M47's consuming half is here
* ```
*
* Written standalone over the two ledgers plus the authority [baseSha], because a lifecycle rule is
* a transition property: it cannot be expressed by parsing one file, and it needs no coupling to the
* ratchet's own types.
*/
object MutationAuthorityDigestCertificateCeremony {
/**
* 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
*/
fun checks(
base: MutationAuthorityDigestCertificates,
candidate: MutationAuthorityDigestCertificates,
baseSha: String,
): List<VerificationDiagnostic> =
mintChecks(base, candidate, baseSha) +
retentionChecks(base, candidate) +
removalChecks(base, candidate)

/**
* M45: a newly introduced certificate binds the exact authority base it is proposed against.
* Anti-replay at mint time: a certificate payload cannot be replayed onto a different authority
* base. Deliberately not re-checked later - a certificate legitimately survives intermediate
* merges, so requiring it to track the immediate base would make delayed consumption impossible.
*/
private fun mintChecks(
base: MutationAuthorityDigestCertificates,
candidate: MutationAuthorityDigestCertificates,
baseSha: String,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val baseByFromDigest = base.byFromDigest()
for ((fromDigest, certificate) in candidate.byFromDigest()) {
if (fromDigest in baseByFromDigest) continue
if (certificate.fromBaseSha != baseSha) {
diagnostics +=
certificateFailure(
fromDigest,
"M45: the digest-migration certificate for source digest ${short(fromDigest)} " +
"records fromBaseSha '${certificate.fromBaseSha}', not the authority base " +
"'$baseSha' this transition is proposed against. fromBaseSha is the base of " +
"the minting transition.",
)
}
}
return diagnostics
}

/**
* M46: a certificate present in the base is immutable from the moment it is introduced. A
* retained copy differing in any field of its enforced payload is a rewrite, whether or not the
* migration it authorizes is being consumed in this transition. Correcting a certificate means
* minting a new one in a later transition, never editing a retained one.
*
* Judged over [MutationAuthorityDigestCertificate.enforcedPayload], so touching audit metadata
* alone never fails - and never manufactures authority either.
*/
private fun retentionChecks(
base: MutationAuthorityDigestCertificates,
candidate: MutationAuthorityDigestCertificates,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val candidateByFromDigest = candidate.byFromDigest()
for ((fromDigest, baseCertificate) in base.byFromDigest()) {
val candidateCertificate = candidateByFromDigest[fromDigest] ?: continue
if (baseCertificate.enforcedPayload() != candidateCertificate.enforcedPayload()) {
diagnostics +=
certificateFailure(
fromDigest,
"M46: the retained digest-migration certificate for source digest " +
"${short(fromDigest)} does not match the base certificate. A certificate, " +
"once introduced, is immutable.",
)
}
}
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.
*/
private fun removalChecks(
base: MutationAuthorityDigestCertificates,
candidate: MutationAuthorityDigestCertificates,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val candidateByFromDigest = candidate.byFromDigest()
for ((fromDigest, _) in base.byFromDigest()) {
if (fromDigest !in candidateByFromDigest) {
diagnostics +=
certificateFailure(
fromDigest,
"M47: the digest-migration certificate for source digest ${short(fromDigest)} " +
"was removed without being consumed. A certificate may not be cancelled " +
"silently.",
)
}
}
return diagnostics
}

private fun certificateFailure(
fromDigest: String,
message: String,
): VerificationDiagnostic =
VerificationDiagnostic.failure(
DiagnosticCode.MUTATION_RATCHET_AUTHORITY_INVALID,
message,
findingId = 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
}
Loading
Loading