diff --git a/dag/extdeps/cloudflare/r2.dag b/dag/extdeps/cloudflare/r2.dag index 20789ee10ac..b3abd7c955e 100644 --- a/dag/extdeps/cloudflare/r2.dag +++ b/dag/extdeps/cloudflare/r2.dag @@ -66,6 +66,13 @@ data r2_get_bucket_authority: ExternalAuthority = ExternalAuthority { } } +data r2_custom_domains_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "developers.cloudflare.com/r2/buckets/public-buckets/" + } +} + data r2_tokens_doc_as_of: Timestamp = "2026-08-18" // THE READ ID IS THE ONE PERMISSION GROUP ID THIS MODULE PINS, AND THE WRITE ID IS DELIBERATELY NOT @@ -480,6 +487,17 @@ fn r2_published_location_citations() -> List { ] } +data r2_custom_domain_reading: NonEmptyStr = "Public access through r2.dev subdomains is rate-limited and should only be used for development purposes. Domain access through a custom domain allows you to use Cloudflare Cache to accelerate access to your R2 bucket. Last updated Jun 16, 2026. Read 2026-09-18." + +data r2_custom_domain_citation: FactCitation = FactCitation { + fact: DeclarationRef { + module_path: "extdeps.cloudflare.r2", + decl_name: "r2_custom_domain_reading", + field: WholeDeclaration + }, + authority: r2_custom_domains_authority, +} + // THE r2_s3_endpoint_url ROW IS RETIRED: gunbc.cloudflare.r2_origin_object reaches it through // r2_object_url on the executing put_origin_object and fetch_origin_object entries, which is the diff --git a/dag/gunbc/census_closure_frontier.dag b/dag/gunbc/census_closure_frontier.dag index a268f623c93..f6cf0f52ffc 100644 --- a/dag/gunbc/census_closure_frontier.dag +++ b/dag/gunbc/census_closure_frontier.dag @@ -48,6 +48,7 @@ import extdeps.tools.sha256sum { sha256_verify_native_transport_frontier_rows } import gunbc.cloudflare.r2_token_mint { cloudflare_r2_token_mint_frontier_rows } import extdeps.cloudflare.account_api_tokens { cloudflare_account_api_tokens_frontier_rows } import extdeps.cloudflare.r2 { cloudflare_r2_frontier_rows } +import gunbc.cloudflare.boot_origin { cloudflare_boot_origin_frontier_rows } import gunbc.network_boot_delivery { network_boot_delivery_frontier_rows } import product.supplier.cloudflare_r2 { cloudflare_r2_supplier_frontier_rows } import extdeps.backblaze.b2 { backblaze_b2_frontier_rows } @@ -111,6 +112,7 @@ fn census_closure_frontier_row_groups() -> List> { cloudflare_r2_token_mint_frontier_rows, cloudflare_account_api_tokens_frontier_rows, cloudflare_r2_frontier_rows, + cloudflare_boot_origin_frontier_rows, network_boot_delivery_frontier_rows, cloudflare_r2_supplier_frontier_rows, backblaze_b2_frontier_rows, diff --git a/dag/gunbc/cloudflare/boot_origin.dag b/dag/gunbc/cloudflare/boot_origin.dag new file mode 100644 index 00000000000..c9e1b12754c --- /dev/null +++ b/dag/gunbc/cloudflare/boot_origin.dag @@ -0,0 +1,383 @@ +module gunbc.cloudflare.boot_origin + +import std.types { Bool, List, NonEmptyStr, String } +import std.content_hash { ContentHash, Sha256Hash, Fnv1a64, Sha1Hash, Sha512Hash, Sha256Digest, content_hash_eq_cryptographic } +import std.human_intervention { + HumanIntervention, + HumanInterventionIdentity, + FleetOnce, + NoDischargeSiteModeled, +} +import extdeps.external_authority { FactCitation } +import extdeps.cloudflare.r2 { r2_custom_domain_citation } +import extdeps.dns.domain_name { HostName, DomainNameParseError, parse_host_name, domain_name_wire } +import std.decl_ref { DeclarationRef, decl_ref } +import std.roster_frontier { FrontierRow, frontier_row_decl } +import std.dissolution { unbound_dissolution } +import extdeps.uri { Uri, uri_https } +import gunbc.network_boot_delivery { + FleetBootServiceReceipt, + BootBundleMember, + BootBundleKernel, + BootBundleInitrd, + BootBundleRoot, + BootBundleAvailabilityReceipt, + BootBundleAvailabilityStanding, + BootBundleAvailable, + BootBundleRefused, + BootBundleUnobserved, + BootBundleObservedRefusal, + BootBundleReadBackServedOtherBytes, + BootBundleKeyOccupiedByOtherBytes, + BootBundleMemberInWrongSlot, +} +import gunbc.cloudflare.r2_origin { + FabricDurableOrigin, + FabricBootOrigin, + allocated_bucket_name, + bucket_purpose_admits_public_read, +} + +// THE BOOT ORIGIN: one R2 bucket, public only through a custom domain, holding immutable +// digest-keyed boot artifacts. Its separation from the durable origin is consumed, not restated: +// gunbc.cloudflare.r2_origin allocates the two purposes two buckets and admits public read to +// exactly one. + +// THE ARM64 BUNDLE IS KERNEL + INITRD + ROOT IMAGE (parent lane ruling). The member vocabulary is +// gunbc.network_boot_delivery BootBundleMember, consumed rather than restated. The chainloader is not +// a member: it lives at the site edge and only its digest is on the signed manifest. +type BootBundleObject { + member: BootBundleMember + digest: Sha256Digest +} + +// THE BUCKET IS PUBLIC, SO WHAT A PRODUCER HANDS THE PUBLISHER IS CLASSIFIED BEFORE A KEY IS MINTED. +// A host credential, a cloud token or an install secret named for publication refuses here, typed, +// and never reaches a PUT: anything under a digest key is world-readable through the custom domain. +// The residual is the image's CONTENT -- a rootfs that embeds a secret is a producer fact this fold +// cannot see, and that obligation stays with the image producer. +type BootPublicationCandidate + = CandidateBootArtifact { object: BootBundleObject } + | CandidateHostCredential + | CandidateCloudToken + | CandidateInstallSecret + +type BootPublicationAdmission + = BootPublicationAdmitted { object: BootBundleObject } + | BootPublicationRefusedSecretMaterial {} + +fn admit_boot_publication(candidate: BootPublicationCandidate) -> BootPublicationAdmission { + match candidate { + CandidateBootArtifact { object: o } => BootPublicationAdmitted { object: o } + CandidateHostCredential => BootPublicationRefusedSecretMaterial {} + CandidateCloudToken => BootPublicationRefusedSecretMaterial {} + CandidateInstallSecret => BootPublicationRefusedSecretMaterial {} + } +} + +// A KEY IS THE DIGEST AND NOTHING ELSE. There is no mutable name ("latest", an arch alias) because a +// key that can be re-pointed makes the object at it a different fact over time, and the signed +// manifest names bytes, not pointers. Only SHA-256 is admitted: any other family refuses rather than +// minting a key a verifier would check under the wrong algorithm. +data boot_origin_key_prefix: String = "sha256/" + +fn boot_object_key(digest: Sha256Digest) -> NonEmptyStr { + join([boot_origin_key_prefix, digest.hex as String], "") as NonEmptyStr +} + +type BootObjectKeyAdmission + = BootObjectKeyAdmitted { key: NonEmptyStr } + | BootObjectKeyRefusedHashFamily {} + +fn admit_boot_object_key(hash: ContentHash) -> BootObjectKeyAdmission { + match hash { + Sha256Hash(d) => BootObjectKeyAdmitted { key: boot_object_key(digest: d) } + Fnv1a64(_) => BootObjectKeyRefusedHashFamily {} + Sha1Hash(_) => BootObjectKeyRefusedHashFamily {} + Sha512Hash(_) => BootObjectKeyRefusedHashFamily {} + } +} + +// WHAT A CREATE-ONLY PUT MEANS. The transport sends If-None-Match: * (extdeps.tools.curl), so an +// occupied key answers 412 rather than being overwritten. A 412 is not yet a verdict: the occupant +// is read back and its digest compared. Identical bytes are the idempotent re-publish; different +// bytes are a refusal, never an overwrite and never a success. +type BootPutAnswer + = BootPutCreated {} + | BootPutKeyOccupied { occupant_digest: Sha256Digest } + | BootPutFailed { cause: NonEmptyStr } + +type BootPublishVerdict + = BootObjectPublished { key: NonEmptyStr } + | BootObjectAlreadyIdentical { key: NonEmptyStr } + | BootObjectOccupiedByDifferentBytes { key: NonEmptyStr, occupant: Sha256Digest } + | BootObjectPublishFailed { key: NonEmptyStr, cause: NonEmptyStr } + +fn classify_boot_publish(object: BootBundleObject, answer: BootPutAnswer) -> BootPublishVerdict { + let key = boot_object_key(digest: object.digest) + match answer { + BootPutCreated {} => BootObjectPublished { key: key } + BootPutKeyOccupied { occupant_digest: o } => + if content_hash_eq_cryptographic(left: o, right: object.digest) { + BootObjectAlreadyIdentical { key: key } + } else { + BootObjectOccupiedByDifferentBytes { key: key, occupant: o } + } + BootPutFailed { cause: c } => BootObjectPublishFailed { key: key, cause: c } + } +} + +// THE HOSTNAME IS UNALLOCATED, AND THAT IS AN ARM. Which Cloudflare zone and FQDN serve the boot +// origin is an operator decision this repository cannot derive; until it is recorded every readback +// refuses rather than addressing a guessed host. Same shape as gunbc.cloudflare.r2_origin +// CloudflareAccountUnallocated. +type BootOriginHostnameStanding + = BootOriginHostnameAllocated { fqdn: HostName } + | BootOriginHostnameUnallocated { intervention: HumanIntervention } + +data boot_origin_hostname_intervention_identity: String = "cloudflare-r2-boot-origin-hostname" + +fn boot_origin_hostname_standing() -> BootOriginHostnameStanding { + BootOriginHostnameUnallocated { + intervention: HumanIntervention { + identity: boot_origin_hostname_intervention_identity as HumanInterventionIdentity, + frequency: FleetOnce, + subject: "the boot origin's production custom-domain hostname" as NonEmptyStr, + instruction: "Choose the Cloudflare zone on the fleet account and the FQDN that will serve the gunbai-fabric-boot bucket, then record the FQDN (its zone is its parent domain) as the BootOriginHostnameAllocated arm of boot_origin_hostname_standing. An r2.dev hostname is refused: it is rate-limited and development-only." as NonEmptyStr, + completion_evidence: NoDischargeSiteModeled + } + } +} + +// THE MACHINE-FACING ENDPOINT IS THE RECEIPT'S, NOT A SECOND SPELLING. Readback addresses +// FleetBootServiceReceipt.origin_custom_domain, and an r2.dev host is refused as a production origin +// (extdeps.cloudflare.r2 r2_custom_domain_reading). +// THE HOST IS PARSED BY THE DNS AUTHORITY (extdeps.dns.domain_name parse_host_name), not re-validated +// here. A locator carrying a port, a path or a trailing slash is not a host name and refuses with that +// module's typed cause; labels arrive case-folded, so r2.dev is a comparison of the last two labels. +fn host_is_r2_dev(host: HostName) -> Bool { + let rev = reverse(host.name.labels) + match rev.first() { + Absent => false + Present { value: tld } => + match (rev |> skip(n: 1)).first() { + Absent => false + Present { value: second } => (tld as String) == "dev" && (second as String) == "r2" + } + } +} + +type BootReadbackLocator + = BootReadbackAt { url: Uri } + | BootReadbackRefusedR2Dev { locator: NonEmptyStr, ground: FactCitation } + | BootReadbackRefusedNotHttps {} + | BootReadbackRefusedNotAHostName { locator: NonEmptyStr, cause: DomainNameParseError } + +fn boot_readback_locator(receipt: FleetBootServiceReceipt, key: NonEmptyStr) -> BootReadbackLocator { + let domain = receipt.origin_custom_domain + if domain.scheme != extdeps.uri.Https { + BootReadbackRefusedNotHttps {} + } else { + match parse_host_name(raw: domain.locator as String) { + Err { value: cause } => BootReadbackRefusedNotAHostName { locator: domain.locator, cause: cause } + Ok { value: host } => + if host_is_r2_dev(host: host) { + BootReadbackRefusedR2Dev { locator: domain.locator, ground: r2_custom_domain_citation } + } else { + BootReadbackAt { + url: uri_https(locator: join([domain_name_wire(name: host.name), "/", key as String], "") as NonEmptyStr) + } + } + } + } +} + +// A READBACK CONFIRMS ONLY THE DIGEST IT WAS PUBLISHED UNDER. Served bytes hashing to anything else +// (a stale edge, a wrong object) are a mismatch, and an unreadable object is not a confirmation. +type BootReadbackObservation + = BootReadbackServed { served_digest: Sha256Digest, served_from: Uri } + | BootReadbackUnserved { cause: NonEmptyStr } + +type BootReadbackVerdict + = BootObjectReadBackConfirmed { key: NonEmptyStr, served_from: Uri } + | BootObjectReadBackMismatch { key: NonEmptyStr, served: Sha256Digest } + | BootObjectReadBackUnavailable { key: NonEmptyStr, cause: NonEmptyStr } + +fn classify_boot_readback(object: BootBundleObject, observation: BootReadbackObservation) -> BootReadbackVerdict { + let key = boot_object_key(digest: object.digest) + match observation { + BootReadbackServed { served_digest: s, served_from: origin } => + if content_hash_eq_cryptographic(left: s, right: object.digest) { + BootObjectReadBackConfirmed { key: key, served_from: origin } + } else { + BootObjectReadBackMismatch { key: key, served: s } + } + BootReadbackUnserved { cause: c } => BootObjectReadBackUnavailable { key: key, cause: c } + } +} + +// THE SEPARATION, AS ONE FOLD. Distinct buckets, and public read admitted to the boot purpose only. +fn boot_origin_is_separate_from_durable_origin() -> Bool { + let names_distinct = match allocated_bucket_name(purpose: FabricBootOrigin {}) { + Absent => false + Present { value: boot } => + match allocated_bucket_name(purpose: FabricDurableOrigin {}) { + Absent => true + Present { value: durable } => (boot as String) != (durable as String) + } + } + names_distinct + && bucket_purpose_admits_public_read(purpose: FabricBootOrigin {}) + && !bucket_purpose_admits_public_read(purpose: FabricDurableOrigin {}) +} + +// THE AVAILABILITY RECEIPT IS PRODUCED HERE AND DECLARED IN 0A (gunbc.network_boot_delivery +// BootBundleAvailabilityReceipt). A member counts only when its create-only PUT published it or found +// the same bytes already there, AND its readback through the custom domain served that digest, AND it +// sits in the slot it claims. served_from is the origin the readbacks were observed at. A member +// observed wrong is refused, and a member never observed leaves the bundle Unobserved; see below. +type BootBundleMemberEvidence { + object: BootBundleObject + publish: BootPublishVerdict + readback: BootReadbackVerdict +} + +// AN OBSERVATION THAT REFUSES IS NOT AN ABSENT ONE (review 68011). A member whose key holds other +// bytes, whose readback served another digest, or which sits in another member's slot was observed and +// found wrong, and becomes gunbc.network_boot_delivery BootBundleRefused naming the member, the cause +// and the observation that saw it. Only a failed PUT or an unserved readback is absence (Unobserved). +type BootMemberVerdict + = BootMemberHolds { served_from: Uri } + | BootMemberAbsent + | BootMemberRefused { member: BootBundleMember, cause: BootBundleObservedRefusal, observation: DeclarationRef } + +fn classify_boot_member( + evidence: BootBundleMemberEvidence, + slot: BootBundleMember, + publication_observation: DeclarationRef, + readback_observation: DeclarationRef, +) -> BootMemberVerdict { + if evidence.object.member != slot { + BootMemberRefused { member: slot, cause: BootBundleMemberInWrongSlot, observation: publication_observation } + } else { + match evidence.publish { + BootObjectOccupiedByDifferentBytes { key: _, occupant: _ } => + BootMemberRefused { member: slot, cause: BootBundleKeyOccupiedByOtherBytes, observation: publication_observation } + BootObjectPublishFailed { key: _, cause: _ } => BootMemberAbsent + BootObjectPublished { key: _ } => classify_boot_member_readback(verdict: evidence.readback, slot: slot, readback_observation: readback_observation) + BootObjectAlreadyIdentical { key: _ } => classify_boot_member_readback(verdict: evidence.readback, slot: slot, readback_observation: readback_observation) + } + } +} + +fn classify_boot_member_readback(verdict: BootReadbackVerdict, slot: BootBundleMember, readback_observation: DeclarationRef) -> BootMemberVerdict { + match verdict { + BootObjectReadBackConfirmed { key: _, served_from: origin } => BootMemberHolds { served_from: origin } + BootObjectReadBackMismatch { key: _, served: _ } => + BootMemberRefused { member: slot, cause: BootBundleReadBackServedOtherBytes, observation: readback_observation } + BootObjectReadBackUnavailable { key: _, cause: _ } => BootMemberAbsent + } +} + +fn boot_member_holds(verdict: BootMemberVerdict) -> Bool { + match verdict { + BootMemberHolds { served_from: _ } => true + BootMemberAbsent => false + BootMemberRefused { member: _, cause: _, observation: _ } => false + } +} + +// THE ROUTE IS THE ONE THE READBACKS OBSERVED, never the fleet receipt's expectation (review 68098). +// When every holding member was served from the fleet's custom domain that is the answer; otherwise +// the first member origin that differs from it is reported, so gunbc.network_boot_delivery's +// RouteBundleServedFrom comparison is over an observed fact and can go red. +fn observed_bundle_origin(verdicts: List, expected: Uri) -> Uri { + fold(verdicts, init: expected, f: (acc, v) => + if acc != expected { + acc + } else { + match v { + BootMemberHolds { served_from: origin } => origin + BootMemberAbsent => acc + BootMemberRefused { member: _, cause: _, observation: _ } => acc + } + }) +} + +// A refusal on any member wins, in kernel, initrd, root order so the first located cause is reported; +// then any absence leaves the bundle Unobserved; only three holding members mint the receipt. +fn first_boot_member_refusal(verdicts: List) -> BootBundleAvailabilityStanding? { + fold(verdicts, init: none, f: (acc, v) => + match acc { + Present { value: _ } => acc + Absent => + match v { + BootMemberRefused { member: m, cause: c, observation: o } => + Present { value: BootBundleRefused { member: m, cause: c, observation: o } } + BootMemberHolds { served_from: _ } => none + BootMemberAbsent => none + } + }) +} + +fn boot_bundle_availability( + fleet: FleetBootServiceReceipt, + kernel: BootBundleMemberEvidence, + initrd: BootBundleMemberEvidence, + root: BootBundleMemberEvidence, + publication_observation: DeclarationRef, + readback_observation: DeclarationRef, +) -> BootBundleAvailabilityStanding { + let verdicts = [ + classify_boot_member(evidence: kernel, slot: BootBundleKernel, publication_observation: publication_observation, readback_observation: readback_observation), + classify_boot_member(evidence: initrd, slot: BootBundleInitrd, publication_observation: publication_observation, readback_observation: readback_observation), + classify_boot_member(evidence: root, slot: BootBundleRoot, publication_observation: publication_observation, readback_observation: readback_observation), + ] + match first_boot_member_refusal(verdicts: verdicts) { + Present { value: refused } => refused + Absent => + if all(verdicts, v => boot_member_holds(verdict: v)) { + BootBundleAvailable { + receipt: BootBundleAvailabilityReceipt { + kernel_digest: Sha256Hash(kernel.object.digest), + initrd_digest: Sha256Hash(initrd.object.digest), + root_digest: Sha256Hash(root.object.digest), + served_from: observed_bundle_origin(verdicts: verdicts, expected: fleet.origin_custom_domain), + publication_observation: publication_observation, + readback_observation: readback_observation, + } + } + } else { + BootBundleUnobserved + } + } +} + +// NO EXECUTOR REACHES THESE FOLDS YET, and that is a declared frontier rather than silence (DESIGN +// section 3c). The PUT/GET route is gunbc.cloudflare.r2_origin_object put_origin_object and +// fetch_origin_object, bound to the durable bucket today; the boot route is the same executor +// parameterized by BucketPurpose. It cannot run until the boot write credential is minted and a named +// intake-Linux producer yields ARM64 kernel/initrd/root bytes. +data cloudflare_boot_origin_frontier_rows: List = [ + frontier_row_decl( + ref: decl_ref(module_path: "gunbc.cloudflare.boot_origin", decl_name: "classify_boot_publish"), + reason: "publish folds (admit_boot_publication, admit_boot_object_key, classify_boot_publish) have no executing PUT route for the boot bucket", + dissolution: unbound_dissolution( + description: "dissolves when put_origin_object is parameterized by BucketPurpose and a boot publish entry calls admit_boot_publication, admit_boot_object_key and classify_boot_publish on real ARM64 bundle bytes from a named producer; a witness over supplied answers does not fire this" + ) + ), + frontier_row_decl( + ref: decl_ref(module_path: "gunbc.cloudflare.boot_origin", decl_name: "boot_readback_locator"), + reason: "readback folds (boot_readback_locator, classify_boot_readback, boot_bundle_availability) have no executing GET over the custom domain; the hostname standing is Unallocated", + dissolution: unbound_dissolution( + description: "dissolves when boot_origin_hostname_standing is Allocated and a boot readback entry fetches every published key through boot_readback_locator and classifies it with classify_boot_readback, then mints boot_bundle_availability from those verdicts" + ) + ), + frontier_row_decl( + ref: decl_ref(module_path: "gunbc.cloudflare.r2_origin", decl_name: "fabric_boot_origin_standing"), + reason: "both boot-origin credentials stand OriginCredentialMintEntryUnbuilt: gunbc.cloudflare.r2_token_mint_run mints only for the durable origin, so no entry mints the boot read or write token", + dissolution: unbound_dissolution( + description: "dissolves when gunbc.cloudflare.r2_token_mint_run is parameterized by BucketPurpose and a boot mint entry has executed, stored each token under its fabric_boot_origin secret id, and fabric_boot_origin_standing names that entry as OriginCredentialUnminted or pins the stored version as OriginCredentialPinned" + ) + ), +] diff --git a/dag/gunbc/cloudflare/r2_origin.dag b/dag/gunbc/cloudflare/r2_origin.dag index 3400ec2cb39..edaa837c38b 100644 --- a/dag/gunbc/cloudflare/r2_origin.dag +++ b/dag/gunbc/cloudflare/r2_origin.dag @@ -82,8 +82,9 @@ fn fleet_cloudflare_account_standing() -> FleetCloudflareAccountStanding { // manifest read back from a disposable cache establishes nothing, so the origin has to be a named // place with a stated admission. Widening it later is a change to this row, which is exactly the // visibility a bucket that quietly became a general dumping ground would not have. -type DurableOriginAdmission +type OriginAdmission = FabricCommitManifestsAndContentAddressedOutputs {} + | DigestKeyedBootArtifacts {} // A CREDENTIAL FOR THE ORIGIN IS TWO FACTS AND A STANDING. The S3 access key id IS the Cloudflare // token id (extdeps.cloudflare.r2 R2S3AccessPair, cited) and is not a secret: it is what the @@ -99,6 +100,10 @@ type DurableOriginAdmission // and would have grown a differently shaped write field beside it; two shapes for one concept is the // fork DESIGN section 3 forbids, so both directions inhabit OriginCredentialStanding and the field // names carry the direction. +// +// OriginCredentialMintEntryUnbuilt: no mint entry exists for this credential yet; secret_id is where +// its token will be stored once one does. Distinct from OriginCredentialUnminted so mint_entry keeps +// one meaning: an entry to run. type OriginCredentialStanding = OriginCredentialPinned { access_key_id: NonEmptyStr @@ -107,11 +112,14 @@ type OriginCredentialStanding | OriginCredentialUnminted { mint_entry: DeclarationRef } + | OriginCredentialMintEntryUnbuilt { + secret_id: NonEmptyStr + } -type DurableOriginBucket { +type R2OriginBucket { bucket_name: NonEmptyStr jurisdiction: R2Jurisdiction - admits: DurableOriginAdmission + purpose: BucketPurpose read_credential: OriginCredentialStanding write_credential: OriginCredentialStanding } @@ -137,8 +145,12 @@ type DurableOriginBucket { // constructor that admits a repeated key on either axis. The authored list below remains a fallible // ingress that refuses loudly; that residual, and its own next-rung trigger, are stated once in // std.allocation_roster rather than restated here. +// THE BOOT ORIGIN IS A SECOND PURPOSE, NOT A WIDENING OF THE FIRST. The durable origin is private and byte-owning for commit readback; the +// boot origin is read by machines over a public custom domain. Admitting boot bytes into the durable +// bucket would make it public, and the durable origin's read token would become a boot reader's. type BucketPurpose = FabricDurableOrigin {} + | FabricBootOrigin {} type R2BucketAllocation { purpose: BucketPurpose @@ -148,6 +160,35 @@ type R2BucketAllocation { fn bucket_purpose_key(purpose: BucketPurpose) -> String { match purpose { FabricDurableOrigin {} => "fabric-durable-origin" + FabricBootOrigin {} => "fabric-boot-origin" + } +} + +// WHAT A BUCKET ADMITS IS DERIVED FROM ITS PURPOSE, never carried beside it, so a durable bucket +// with boot admission (or the reverse) has no constructor. +fn bucket_purpose_admission(purpose: BucketPurpose) -> OriginAdmission { + match purpose { + FabricDurableOrigin {} => FabricCommitManifestsAndContentAddressedOutputs {} + FabricBootOrigin {} => DigestKeyedBootArtifacts {} + } +} + +// EVERY ADMITTED POPULATION IS WRITE-ONCE, so the PUT is create-only for each: a content-addressed +// object or a manifest slot is never legitimately overwritten, and a digest-keyed boot artifact is +// its digest. A future population that may be replaced must say so here, not at the call site. +fn origin_admission_is_create_only(admission: OriginAdmission) -> Bool { + match admission { + FabricCommitManifestsAndContentAddressedOutputs {} => true + DigestKeyedBootArtifacts {} => true + } +} + +// PUBLIC READ IS A PROPERTY OF THE PURPOSE, decided by an exhaustive match so a new purpose must +// state it. Only the boot origin is ever attached to a custom domain. +fn bucket_purpose_admits_public_read(purpose: BucketPurpose) -> Bool { + match purpose { + FabricDurableOrigin {} => false + FabricBootOrigin {} => true } } @@ -168,6 +209,10 @@ data gunbc_fleet_r2_bucket_allocations: List = [ purpose: FabricDurableOrigin {}, bucket_name: "gunbai-fabric-origin", }, + R2BucketAllocation { + purpose: FabricBootOrigin {}, + bucket_name: "gunbai-fabric-boot", + }, ] fn allocated_bucket_name(purpose: BucketPurpose) -> NonEmptyStr? { @@ -200,22 +245,22 @@ data fabric_durable_origin_write_secret_id: NonEmptyStr = "cloudflare-r2-origin- // AN UNALLOCATED PURPOSE HAS NO BUCKET, AND THAT IS AN ARM RATHER THAN A DEFAULT. Reconstructing a // name at this seam is the derivation the roster exists to forbid, and returning an empty one would // address the account's root instead of a bucket. -type DurableOriginStanding - = DurableOriginAllocated { bucket: DurableOriginBucket } - | DurableOriginUnallocated { purpose: BucketPurpose } +type R2OriginStanding + = R2OriginAllocated { bucket: R2OriginBucket } + | R2OriginUnallocated { purpose: BucketPurpose } // DEFAULT JURISDICTION, STATED RATHER THAN DEFAULTED. Eu, Us, Fedramp, and FedrampHigh are // data-residency commitments this fleet has not made and cannot honour, and picking one would assert // a compliance boundary nothing enforces. -fn fabric_durable_origin_standing() -> DurableOriginStanding { +fn fabric_durable_origin_standing() -> R2OriginStanding { match allocated_bucket_name(purpose: FabricDurableOrigin {}) { - Absent => DurableOriginUnallocated { purpose: FabricDurableOrigin {} } + Absent => R2OriginUnallocated { purpose: FabricDurableOrigin {} } Present { value: name } => - DurableOriginAllocated { - bucket: DurableOriginBucket { + R2OriginAllocated { + bucket: R2OriginBucket { bucket_name: name, jurisdiction: R2JurisdictionDefault, - admits: FabricCommitManifestsAndContentAddressedOutputs {}, + purpose: FabricDurableOrigin {}, read_credential: OriginCredentialPinned { access_key_id: fabric_durable_origin_read_access_key_id, token_value: secret_ref_pin_version( @@ -303,3 +348,30 @@ fn fleet_cloudflare_bootstrap_custody() -> BootstrapCustodyStanding { } } } + +// THE BOOT ORIGIN'S TWO CUSTODY LOCI, disjoint from the durable origin's and from each other. The +// read token is held by whatever verifies publication; the write token only by the publisher. Neither +// is minted yet: gunbc.cloudflare.r2_token_mint_run mints per R2OriginTokenProfile against the durable +// bucket only, so until it is parameterized by BucketPurpose both arms are Unminted and every boot +// PUT refuses at the standing, before any network call. Accessor rows in +// gunbc.auth.fleet_secret_accessor_roster land with the minted versions, never before: a row whose +// secret does not exist would stop that roster's fold for every other lane. +data fabric_boot_origin_read_secret_id: NonEmptyStr = "cloudflare-r2-boot-origin-read-token" + +data fabric_boot_origin_write_secret_id: NonEmptyStr = "cloudflare-r2-boot-origin-write-token" + +fn fabric_boot_origin_standing() -> R2OriginStanding { + match allocated_bucket_name(purpose: FabricBootOrigin {}) { + Absent => R2OriginUnallocated { purpose: FabricBootOrigin {} } + Present { value: name } => + R2OriginAllocated { + bucket: R2OriginBucket { + bucket_name: name, + jurisdiction: R2JurisdictionDefault, + purpose: FabricBootOrigin {}, + read_credential: OriginCredentialMintEntryUnbuilt { secret_id: fabric_boot_origin_read_secret_id }, + write_credential: OriginCredentialMintEntryUnbuilt { secret_id: fabric_boot_origin_write_secret_id }, + } + } + } +} diff --git a/dag/gunbc/cloudflare/r2_origin_object.dag b/dag/gunbc/cloudflare/r2_origin_object.dag index 15412038c1f..8f342ba7bc6 100644 --- a/dag/gunbc/cloudflare/r2_origin_object.dag +++ b/dag/gunbc/cloudflare/r2_origin_object.dag @@ -31,14 +31,17 @@ import gunbc.cloudflare.r2_origin { CloudflareAccountAllocated, CloudflareAccountUnallocated, fleet_cloudflare_account_standing, - DurableOriginBucket, - DurableOriginAllocated, - DurableOriginUnallocated, + R2OriginBucket, + R2OriginAllocated, + R2OriginUnallocated, fabric_durable_origin_standing, bucket_purpose_key, OriginCredentialStanding, OriginCredentialPinned, OriginCredentialUnminted, + OriginCredentialMintEntryUnbuilt, + bucket_purpose_admission, + origin_admission_is_create_only, } import gunbc.cloudflare.r2_s3_netrc { R2S3NetrcPlaced, @@ -86,7 +89,7 @@ import gunbc.fabric_m0_origin_readback { // // THE PUT IS CREATE-IF-ABSENT, DERIVED FROM THE ADMISSION RATHER THAN CHOSEN. The origin admits // commit manifests and content-addressed outputs (gunbc.cloudflare.r2_origin -// DurableOriginAdmission): a content-addressed object at its own digest is byte-identical to any +// OriginAdmission): a content-addressed object at its own digest is byte-identical to any // earlier write, and a manifest slot is written once per attempt, so no admitted object is ever // legitimately OVERWRITTEN. If-None-Match: * makes that a store-side refusal (412) instead of a // convention. A 412 is reported as its own arm -- the object is already there, which for both @@ -187,10 +190,11 @@ type OriginTarget locator: R2ObjectLocator access_key_id: NonEmptyStr token_value: SecretRef + create_only: Bool } | OriginTargetRefused { cause: NonEmptyStr } -fn origin_credential_for(direction: OriginDirection, bucket: DurableOriginBucket) -> OriginCredentialStanding { +fn origin_credential_for(direction: OriginDirection, bucket: R2OriginBucket) -> OriginCredentialStanding { match direction { OriginRead => bucket.read_credential OriginWrite => bucket.write_credential @@ -233,9 +237,9 @@ fn resolve_origin_target(direction: OriginDirection, key: NonEmptyStr) -> Origin OriginTargetRefused { cause: concat("NO CLOUDFLARE ACCOUNT ID: ", intervention.instruction as String) as NonEmptyStr } CloudflareAccountAllocated { company: _, account_id: account_id } => match fabric_durable_origin_standing() { - DurableOriginUnallocated { purpose: purpose } => + R2OriginUnallocated { purpose: purpose } => OriginTargetRefused { cause: concat("NO BUCKET ALLOCATED FOR ", bucket_purpose_key(purpose: purpose)) as NonEmptyStr } - DurableOriginAllocated { bucket: bucket } => + R2OriginAllocated { bucket: bucket } => match origin_credential_for(direction: direction, bucket: bucket) { OriginCredentialUnminted { mint_entry: entry } => OriginTargetRefused { @@ -245,6 +249,14 @@ fn resolve_origin_target(direction: OriginDirection, key: NonEmptyStr) -> Origin " and pin the stored version in gunbc.cloudflare.r2_origin before this operation can sign a request", ], "") as NonEmptyStr, } + OriginCredentialMintEntryUnbuilt { secret_id: secret_id } => + OriginTargetRefused { + cause: join([ + "THE ORIGIN'S ", origin_direction_word(direction: direction), + " CREDENTIAL HAS NO MINT ENTRY: nothing mints ", secret_id as String, + " yet, so this operation cannot sign a request", + ], "") as NonEmptyStr, + } OriginCredentialPinned { access_key_id: access_key_id, token_value: token_value } => OriginTargetReady { locator: R2ObjectLocator { @@ -254,6 +266,7 @@ fn resolve_origin_target(direction: OriginDirection, key: NonEmptyStr) -> Origin }, access_key_id: access_key_id, token_value: token_value, + create_only: origin_admission_is_create_only(admission: bucket_purpose_admission(purpose: bucket.purpose)), } } } @@ -296,7 +309,7 @@ fn put_origin_object( OriginTransportPresent => match resolve_origin_target(direction: OriginWrite, key: key) { OriginTargetRefused { cause: cause } => put_without_credential_exposure(verdict: OriginPutRefused { key: key, cause: cause }) - OriginTargetReady { locator: locator, access_key_id: access_key_id, token_value: token_value } => + OriginTargetReady { locator: locator, access_key_id: access_key_id, token_value: token_value, create_only: create_only } => match place_r2_s3_netrc( host: r2_s3_endpoint_host(endpoint: locator.endpoint) as NonEmptyStr, access_key_id: access_key_id, @@ -313,7 +326,7 @@ fn put_origin_object( local_path: local_path as String, netrc_path: netrc_binding_path(binding: binding), sigv4_region: r2_s3_sigv4_region, - create_only: true, + create_only: create_only, ), transport: LocalExec, ) @@ -375,7 +388,7 @@ fn fetch_origin_object( OriginTransportPresent => match resolve_origin_target(direction: OriginRead, key: key) { OriginTargetRefused { cause: cause } => OriginFetchUnavailable { key: key, cause: cause } - OriginTargetReady { locator: locator, access_key_id: access_key_id, token_value: token_value } => { + OriginTargetReady { locator: locator, access_key_id: access_key_id, token_value: token_value, create_only: _ } => { let staging = shell.Mktemp.Dir() if staging.success == false { OriginFetchUnavailable { key: key, cause: "no private directory could be minted to stage the origin's bytes, so nothing was fetched" } diff --git a/dag/gunbc/cloudflare/r2_token_mint_run.dag b/dag/gunbc/cloudflare/r2_token_mint_run.dag index a1c5f7583cd..895d0a9e73f 100644 --- a/dag/gunbc/cloudflare/r2_token_mint_run.dag +++ b/dag/gunbc/cloudflare/r2_token_mint_run.dag @@ -39,10 +39,10 @@ import gunbc.cloudflare.r2_token_mint { } import gunbc.cloudflare.r2_origin { CloudflareAccountId, - DurableOriginBucket, - DurableOriginStanding, - DurableOriginAllocated, - DurableOriginUnallocated, + R2OriginBucket, + R2OriginStanding, + R2OriginAllocated, + R2OriginUnallocated, BucketPurpose, bucket_purpose_key, fabric_durable_origin_standing, @@ -59,6 +59,7 @@ import gunbc.cloudflare.r2_origin { OriginCredentialStanding, OriginCredentialPinned, OriginCredentialUnminted, + OriginCredentialMintEntryUnbuilt, } import gunbc.auth.access_token_source { resolve_access_token, @@ -172,7 +173,7 @@ fn r2_origin_token_name_suffix(profile: R2OriginTokenProfile) -> String { } } -fn r2_origin_token_name(profile: R2OriginTokenProfile, bucket: DurableOriginBucket) -> String { +fn r2_origin_token_name(profile: R2OriginTokenProfile, bucket: R2OriginBucket) -> String { join([bucket.bucket_name as String, r2_origin_token_name_suffix(profile: profile)], "") } @@ -183,7 +184,7 @@ fn r2_origin_token_secret_id(profile: R2OriginTokenProfile) -> NonEmptyStr { } } -fn r2_origin_token_credential(profile: R2OriginTokenProfile, bucket: DurableOriginBucket) -> OriginCredentialStanding { +fn r2_origin_token_credential(profile: R2OriginTokenProfile, bucket: R2OriginBucket) -> OriginCredentialStanding { match profile { R2OriginObjectRead => bucket.read_credential R2OriginObjectWrite => bucket.write_credential @@ -350,7 +351,7 @@ fn classify_r2_mint( profile: R2OriginTokenProfile, account: FleetCloudflareAccountStanding, custody: BootstrapCustodyStanding, - origin: DurableOriginStanding, + origin: R2OriginStanding, container: R2MintContainerAdmission, ) -> R2MintPreconditions { match container_admission_refusal(container: container) { @@ -362,9 +363,9 @@ fn classify_r2_mint( R2MintBootstrapCustodyUnrecorded { intervention: intervention } BootstrapCustodyPinned { locus: locus } => match origin { - DurableOriginUnallocated { purpose: purpose } => + R2OriginUnallocated { purpose: purpose } => R2MintOriginUnallocated { purpose: purpose } - DurableOriginAllocated { bucket: bucket } => + R2OriginAllocated { bucket: bucket } => match account { CloudflareAccountUnallocated { company: _, intervention: intervention } => R2MintAccountUnallocated { intervention: intervention } @@ -545,16 +546,17 @@ fn list_account_tokens( fn origin_credential_pin_is_exact(credential: OriginCredentialStanding) -> Bool { match credential { OriginCredentialUnminted { mint_entry: _ } => false + OriginCredentialMintEntryUnbuilt { secret_id: _ } => false OriginCredentialPinned { access_key_id: _, token_value: locus } => (locus.version as String) != "latest" && (locus.version as String) != "pending" } } -fn origin_read_pin_is_exact(origin: DurableOriginStanding) -> Bool { +fn origin_read_pin_is_exact(origin: R2OriginStanding) -> Bool { match origin { - DurableOriginUnallocated { purpose: _ } => false - DurableOriginAllocated { bucket: bucket } => + R2OriginUnallocated { purpose: _ } => false + R2OriginAllocated { bucket: bucket } => origin_credential_pin_is_exact(credential: bucket.read_credential) } } @@ -915,10 +917,10 @@ fn run_object_write() -> ProcessExit uses net: Network { mint_origin_token_and_store(profile: R2OriginObjectWrite) } -fn origin_credential_already_pinned(profile: R2OriginTokenProfile, origin: DurableOriginStanding) -> Bool { +fn origin_credential_already_pinned(profile: R2OriginTokenProfile, origin: R2OriginStanding) -> Bool { match origin { - DurableOriginUnallocated { purpose: _ } => false - DurableOriginAllocated { bucket: bucket } => + R2OriginUnallocated { purpose: _ } => false + R2OriginAllocated { bucket: bucket } => origin_credential_pin_is_exact(credential: r2_origin_token_credential(profile: profile, bucket: bucket)) } } diff --git a/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag b/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag index a7b907c6b67..6573c020a86 100644 --- a/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag +++ b/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag @@ -23,6 +23,8 @@ data surface_shorthand_preempts_resolved_identity: RecurringFailureMode = Recurr "NEXT RUNG FOR THIS SPECIMEN, a capability and not an artifact: an ambiguous bare name that reaches the shared slot is REFUSED, landed together with the renames that make the corpus pass it and never with an allow-list, so a reference with no declaration-aware answer has no evaluation at all. That makes the ambiguity unwritable rather than unlikely to be observed, and it is the step the existing trigger below names for evaluator construction in general.", + "THIRD SPECIMEN (2026-09-19, gunbc#11603, review 68098/68166). A bare `Https` or `Http`, imported by name from extdeps.uri, evaluates to NoSuchVariable in any closure that also reaches gunbc.cloudflare.r2_origin, while the same literal evaluates in a minimal module; v2.extdeps.coordination CoordinationEffectKind also declares an `Http` variant, and the resolved import is discarded for a bare-spelling lookup that the colliding family defeats. The module-qualified `extdeps.uri.Https` evaluates correctly in the same closure, which is the discriminator and the route #11603 now uses (the interim uri_scheme_is_https predicate was deleted). Still open: the bare imported form. Reproduce with `gunbc run --entry dag/test/claim/cloudflare_boot_origin_witness_test.dag --function readback_refuses_plain_http` after replacing the qualified reference in boot_origin.dag with the bare imported one.", + "Next-rung trigger: host literals and intentional primitive representations are typed parser/resolver constructions, and evaluator value construction is total over every resolved binding with no raw-spelling arm able to override it.)", ], diff --git a/dag/test/claim/cloudflare_boot_origin_witness_test.dag b/dag/test/claim/cloudflare_boot_origin_witness_test.dag new file mode 100644 index 00000000000..44c146997df --- /dev/null +++ b/dag/test/claim/cloudflare_boot_origin_witness_test.dag @@ -0,0 +1,392 @@ +module test.claim.cloudflare_boot_origin_witness + +import std.types { Bool, NonEmptyStr, String, list_length } +import std.decl_ref { decl_ref } +import std.content_hash { Sha256Digest, Sha256DigestHex, Sha256Hash, Sha1Hash, Sha1Digest, Sha1DigestHex } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.crypto.mac { MacKeyId } +import extdeps.uri { Uri, uri_https, uri_from_wire } +import gunbc.network_boot_delivery { + FleetBootServiceReceipt, + BootBundleKernel, + BootBundleInitrd, + BootBundleRoot, + BootBundleMember, + BootBundleAvailable, + BootBundleRefused, + BootBundleUnobserved, + BootBundleAvailabilityStanding, + BootBundleObservedRefusal, + BootBundleReadBackServedOtherBytes, + BootBundleKeyOccupiedByOtherBytes, + BootBundleMemberInWrongSlot, +} +import gunbc.cloudflare.r2_origin { + fabric_boot_origin_standing, + fabric_durable_origin_standing, + R2OriginAllocated, + R2OriginUnallocated, + OriginCredentialPinned, + OriginCredentialUnminted, + OriginCredentialMintEntryUnbuilt, + DigestKeyedBootArtifacts, + bucket_purpose_admission, + FabricCommitManifestsAndContentAddressedOutputs, + fabric_boot_origin_read_secret_id, + fabric_boot_origin_write_secret_id, + fabric_durable_origin_read_secret_id, + fabric_durable_origin_write_secret_id, +} +import gunbc.cloudflare.boot_origin { + BootBundleObject, + BootPublicationCandidate, + BootPutAnswer, + BootPutCreated, + BootPutKeyOccupied, + BootPutFailed, + BootObjectPublished, + BootObjectAlreadyIdentical, + BootObjectOccupiedByDifferentBytes, + BootObjectPublishFailed, + classify_boot_publish, + admit_boot_object_key, + BootObjectKeyAdmitted, + BootObjectKeyRefusedHashFamily, + boot_object_key, + boot_readback_locator, + BootReadbackAt, + BootReadbackRefusedR2Dev, + BootReadbackRefusedNotHttps, + BootReadbackRefusedNotAHostName, + classify_boot_readback, + BootReadbackServed, + BootReadbackUnserved, + BootObjectReadBackConfirmed, + BootObjectReadBackMismatch, + BootObjectReadBackUnavailable, + boot_origin_hostname_standing, + BootOriginHostnameAllocated, + BootOriginHostnameUnallocated, + boot_origin_is_separate_from_durable_origin, + admit_boot_publication, + boot_bundle_availability, + BootBundleMemberEvidence, + CandidateBootArtifact, + CandidateHostCredential, + CandidateCloudToken, + CandidateInstallSecret, + BootPublicationAdmitted, + BootPublicationRefusedSecretMaterial, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data digest_a_hex: String = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +data digest_b_hex: String = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +fn digest(hex: String) -> Sha256Digest { + Sha256Digest { hex: hex as Sha256DigestHex } +} + +fn kernel_a() -> BootBundleObject { + BootBundleObject { member: BootBundleKernel, digest: digest(hex: digest_a_hex) } +} + +fn receipt_at(domain: Uri) -> FleetBootServiceReceipt { + FleetBootServiceReceipt { + https_boot_endpoint: uri_https(locator: "boot.example.test"), + origin_custom_domain: domain, + signing_key_identity: "fixture-key" as MacKeyId, + observation: decl_ref(module_path: "test.claim.cloudflare_boot_origin_witness", decl_name: "receipt_at"), + observed_at: 0, + } +} + +test fn key_is_the_sha256_digest_and_nothing_mutable() -> Bool { + (boot_object_key(digest: digest(hex: digest_a_hex)) as String) == join(["sha256/", digest_a_hex], "") + && match admit_boot_object_key(hash: Sha256Hash(digest(hex: digest_a_hex))) { + BootObjectKeyAdmitted { key: k } => (k as String) == join(["sha256/", digest_a_hex], "") + BootObjectKeyRefusedHashFamily {} => false + } +} + +test fn non_sha256_digest_refuses_a_key() -> Bool { + match admit_boot_object_key(hash: Sha1Hash(Sha1Digest { hex: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as Sha1DigestHex })) { + BootObjectKeyAdmitted { key: _ } => false + BootObjectKeyRefusedHashFamily {} => true + } +} + +test fn create_only_put_publishes() -> Bool { + match classify_boot_publish(object: kernel_a(), answer: BootPutCreated {}) { + BootObjectPublished { key: _ } => true + BootObjectAlreadyIdentical { key: _ } => false + BootObjectOccupiedByDifferentBytes { key: _, occupant: _ } => false + BootObjectPublishFailed { key: _, cause: _ } => false + } +} + +test fn occupied_key_with_same_bytes_is_idempotent() -> Bool { + match classify_boot_publish(object: kernel_a(), answer: BootPutKeyOccupied { occupant_digest: digest(hex: digest_a_hex) }) { + BootObjectPublished { key: _ } => false + BootObjectAlreadyIdentical { key: _ } => true + BootObjectOccupiedByDifferentBytes { key: _, occupant: _ } => false + BootObjectPublishFailed { key: _, cause: _ } => false + } +} + +test fn occupied_key_with_different_bytes_refuses() -> Bool { + match classify_boot_publish(object: kernel_a(), answer: BootPutKeyOccupied { occupant_digest: digest(hex: digest_b_hex) }) { + BootObjectPublished { key: _ } => false + BootObjectAlreadyIdentical { key: _ } => false + BootObjectOccupiedByDifferentBytes { key: _, occupant: o } => (o.hex as String) == digest_b_hex + BootObjectPublishFailed { key: _, cause: _ } => false + } +} + +test fn failed_put_is_not_success() -> Bool { + match classify_boot_publish(object: kernel_a(), answer: BootPutFailed { cause: "503" }) { + BootObjectPublished { key: _ } => false + BootObjectAlreadyIdentical { key: _ } => false + BootObjectOccupiedByDifferentBytes { key: _, occupant: _ } => false + BootObjectPublishFailed { key: _, cause: _ } => true + } +} + +test fn readback_addresses_the_receipt_custom_domain() -> Bool { + match boot_readback_locator(receipt: receipt_at(domain: uri_https(locator: "boot-origin.example.test")), key: boot_object_key(digest: digest(hex: digest_a_hex))) { + BootReadbackAt { url: u } => (u.locator as String) == join(["boot-origin.example.test/sha256/", digest_a_hex], "") + BootReadbackRefusedR2Dev { locator: _, ground: _ } => false + BootReadbackRefusedNotHttps {} => false + BootReadbackRefusedNotAHostName { locator: _, cause: _ } => false + } +} + +test fn readback_refuses_r2_dev() -> Bool { + match boot_readback_locator(receipt: receipt_at(domain: uri_https(locator: "pub-0123.r2.dev")), key: boot_object_key(digest: digest(hex: digest_a_hex))) { + BootReadbackAt { url: _ } => false + BootReadbackRefusedR2Dev { locator: _, ground: g } => (g.fact.decl_name as String) == "r2_custom_domain_reading" + BootReadbackRefusedNotHttps {} => false + BootReadbackRefusedNotAHostName { locator: _, cause: _ } => false + } +} + +test fn readback_confirms_only_the_published_digest() -> Bool { + let ok = match classify_boot_readback(object: kernel_a(), observation: BootReadbackServed { served_digest: digest(hex: digest_a_hex), served_from: uri_https(locator: "boot-origin.example.test") }) { + BootObjectReadBackConfirmed { key: _, served_from: _ } => true + BootObjectReadBackMismatch { key: _, served: _ } => false + BootObjectReadBackUnavailable { key: _, cause: _ } => false + } + let stale = match classify_boot_readback(object: kernel_a(), observation: BootReadbackServed { served_digest: digest(hex: digest_b_hex), served_from: uri_https(locator: "boot-origin.example.test") }) { + BootObjectReadBackConfirmed { key: _, served_from: _ } => false + BootObjectReadBackMismatch { key: _, served: _ } => true + BootObjectReadBackUnavailable { key: _, cause: _ } => false + } + let gone = match classify_boot_readback(object: kernel_a(), observation: BootReadbackUnserved { cause: "404" }) { + BootObjectReadBackConfirmed { key: _, served_from: _ } => false + BootObjectReadBackMismatch { key: _, served: _ } => false + BootObjectReadBackUnavailable { key: _, cause: _ } => true + } + ok && stale && gone +} + +test fn hostname_is_unallocated_until_the_operator_records_it() -> Bool { + match boot_origin_hostname_standing() { + BootOriginHostnameAllocated { fqdn: _ } => false + BootOriginHostnameUnallocated { intervention: _ } => true + } +} + +test fn boot_origin_is_a_separate_bucket_from_the_durable_origin() -> Bool { + boot_origin_is_separate_from_durable_origin() +} + +test fn boot_origin_credentials_are_disjoint_and_have_no_mint_entry() -> Bool { + let disjoint = (fabric_boot_origin_read_secret_id as String) != (fabric_boot_origin_write_secret_id as String) + && !((fabric_boot_origin_read_secret_id as String) == (fabric_durable_origin_read_secret_id as String)) + && !((fabric_boot_origin_write_secret_id as String) == (fabric_durable_origin_write_secret_id as String)) + let standing = match fabric_boot_origin_standing() { + R2OriginUnallocated { purpose: _ } => false + R2OriginAllocated { bucket: b } => + (b.bucket_name as String) == "gunbai-fabric-boot" + && match bucket_purpose_admission(purpose: b.purpose) { + FabricCommitManifestsAndContentAddressedOutputs {} => false + DigestKeyedBootArtifacts {} => true + } + && match b.read_credential { + OriginCredentialPinned { access_key_id: _, token_value: _ } => false + OriginCredentialUnminted { mint_entry: _ } => false + OriginCredentialMintEntryUnbuilt { secret_id: s } => (s as String) == (fabric_boot_origin_read_secret_id as String) + } + && match b.write_credential { + OriginCredentialPinned { access_key_id: _, token_value: _ } => false + OriginCredentialUnminted { mint_entry: _ } => false + OriginCredentialMintEntryUnbuilt { secret_id: s } => (s as String) == (fabric_boot_origin_write_secret_id as String) + } + } + disjoint && standing +} + +// The plain-HTTP receipt is parsed by extdeps.uri uri_from_wire, the production parser, rather than +// built from a bare scheme literal. +test fn readback_refuses_plain_http() -> Bool { + match uri_from_wire(url: "http://boot-origin.example.test") { + Absent => false + Present { value: plain } => + match boot_readback_locator(receipt: receipt_at(domain: plain), key: boot_object_key(digest: digest(hex: digest_a_hex))) { + BootReadbackAt { url: _ } => false + BootReadbackRefusedR2Dev { locator: _, ground: _ } => false + BootReadbackRefusedNotHttps {} => true + BootReadbackRefusedNotAHostName { locator: _, cause: _ } => false + } + } +} + +fn refuses_secret(candidate: BootPublicationCandidate) -> Bool { + match admit_boot_publication(candidate: candidate) { + BootPublicationAdmitted { object: _ } => false + BootPublicationRefusedSecretMaterial {} => true + } +} + +test fn public_bucket_refuses_secret_material() -> Bool { + refuses_secret(candidate: CandidateHostCredential) + && refuses_secret(candidate: CandidateCloudToken) + && refuses_secret(candidate: CandidateInstallSecret) + && !refuses_secret(candidate: CandidateBootArtifact { object: kernel_a() }) +} + +fn refuses_non_host(locator: NonEmptyStr) -> Bool { + match boot_readback_locator(receipt: receipt_at(domain: uri_https(locator: locator)), key: boot_object_key(digest: digest(hex: digest_a_hex))) { + BootReadbackAt { url: _ } => false + BootReadbackRefusedR2Dev { locator: _, ground: _ } => false + BootReadbackRefusedNotHttps {} => false + BootReadbackRefusedNotAHostName { locator: _, cause: _ } => true + } +} + +// review 67763: a port or a trailing slash is not a host name and must not reach BootReadbackAt. +test fn readback_refuses_locators_that_are_not_host_names() -> Bool { + refuses_non_host(locator: "pub-0123.r2.dev:443") + && refuses_non_host(locator: "pub-0123.r2.dev:443/") + && refuses_non_host(locator: "boot-origin.example.test/") + && !refuses_non_host(locator: "boot-origin.example.test") +} + +// review 67763: host names are case-insensitive, so an uppercase r2.dev is still r2.dev. +test fn readback_refuses_r2_dev_in_any_case() -> Bool { + match boot_readback_locator(receipt: receipt_at(domain: uri_https(locator: "pub-0123.R2.Dev")), key: boot_object_key(digest: digest(hex: digest_a_hex))) { + BootReadbackAt { url: _ } => false + BootReadbackRefusedR2Dev { locator: _, ground: _ } => true + BootReadbackRefusedNotHttps {} => false + BootReadbackRefusedNotAHostName { locator: _, cause: _ } => false + } +} + +fn member_evidence(slot: BootBundleMember, hex: String, publish_answer: BootPutAnswer, served_hex: String) -> BootBundleMemberEvidence { + member_evidence_from(slot: slot, hex: hex, publish_answer: publish_answer, served_hex: served_hex, origin: "boot-origin.example.test") +} + +fn member_evidence_from(slot: BootBundleMember, hex: String, publish_answer: BootPutAnswer, served_hex: String, origin: NonEmptyStr) -> BootBundleMemberEvidence { + let object = BootBundleObject { member: slot, digest: digest(hex: hex) } + BootBundleMemberEvidence { + object: object, + publish: classify_boot_publish(object: object, answer: publish_answer), + readback: classify_boot_readback(object: object, observation: BootReadbackServed { served_digest: digest(hex: served_hex), served_from: uri_https(locator: origin) }), + } +} + +data digest_c_hex: String = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + +fn fleet_at_origin() -> FleetBootServiceReceipt { + receipt_at(domain: uri_https(locator: "boot-origin.example.test")) +} + +fn standing(kernel: BootBundleMemberEvidence, initrd: BootBundleMemberEvidence, root: BootBundleMemberEvidence) -> BootBundleAvailabilityStanding { + boot_bundle_availability( + fleet: fleet_at_origin(), + kernel: kernel, + initrd: initrd, + root: root, + publication_observation: decl_ref(module_path: "test.claim.cloudflare_boot_origin_witness", decl_name: "member_evidence"), + readback_observation: decl_ref(module_path: "test.claim.cloudflare_boot_origin_witness", decl_name: "member_evidence_from"), + ) +} + +fn is_unobserved(st: BootBundleAvailabilityStanding) -> Bool { + match st { + BootBundleAvailable { receipt: _ } => false + BootBundleRefused { member: _, cause: _, observation: _ } => false + BootBundleUnobserved => true + } +} + +// A refusal on the expected member, with the expected cause, carried by the expected observation. +fn is_refused(st: BootBundleAvailabilityStanding, member: BootBundleMember, cause: BootBundleObservedRefusal, observation_decl: String) -> Bool { + match st { + BootBundleAvailable { receipt: _ } => false + BootBundleUnobserved => false + BootBundleRefused { member: m, cause: c, observation: o } => + m == member && c == cause && (o.decl_name as String) == observation_decl + } +} + +fn kernel_ok() -> BootBundleMemberEvidence { + member_evidence(slot: BootBundleKernel, hex: digest_a_hex, publish_answer: BootPutCreated {}, served_hex: digest_a_hex) +} + +fn initrd_ok() -> BootBundleMemberEvidence { + member_evidence(slot: BootBundleInitrd, hex: digest_b_hex, publish_answer: BootPutCreated {}, served_hex: digest_b_hex) +} + +// All three members published (or found identical) and read back at their digest: available, with the +// receipt's served_from being the fleet receipt's custom domain. +test fn bundle_available_when_every_member_is_published_and_read_back() -> Bool { + let i = member_evidence(slot: BootBundleInitrd, hex: digest_b_hex, publish_answer: BootPutKeyOccupied { occupant_digest: digest(hex: digest_b_hex) }, served_hex: digest_b_hex) + let r = member_evidence(slot: BootBundleRoot, hex: digest_c_hex, publish_answer: BootPutCreated {}, served_hex: digest_c_hex) + match standing(kernel: kernel_ok(), initrd: i, root: r) { + BootBundleAvailable { receipt: rc } => (rc.served_from.locator as String) == "boot-origin.example.test" + BootBundleRefused { member: _, cause: _, observation: _ } => false + BootBundleUnobserved => false + } +} + +// Absence is a failed PUT or an unserved readback, and only absence is Unobserved. +test fn bundle_unobserved_only_when_a_member_is_absent() -> Bool { + let failed_root = member_evidence(slot: BootBundleRoot, hex: digest_c_hex, publish_answer: BootPutFailed { cause: "503" }, served_hex: digest_c_hex) + let root_object = BootBundleObject { member: BootBundleRoot, digest: digest(hex: digest_c_hex) } + let unserved_root = BootBundleMemberEvidence { + object: root_object, + publish: classify_boot_publish(object: root_object, answer: BootPutCreated {}), + readback: classify_boot_readback(object: root_object, observation: BootReadbackUnserved { cause: "404" }), + } + is_unobserved(st: standing(kernel: kernel_ok(), initrd: initrd_ok(), root: failed_root)) + && is_unobserved(st: standing(kernel: kernel_ok(), initrd: initrd_ok(), root: unserved_root)) +} + +// review 68011, one failing input per cause: each observed mismatch refuses naming the member. +test fn bundle_refuses_a_readback_that_served_other_bytes() -> Bool { + let stale_root = member_evidence(slot: BootBundleRoot, hex: digest_c_hex, publish_answer: BootPutCreated {}, served_hex: digest_a_hex) + is_refused(st: standing(kernel: kernel_ok(), initrd: initrd_ok(), root: stale_root), member: BootBundleRoot, cause: BootBundleReadBackServedOtherBytes, observation_decl: "member_evidence_from") +} + +test fn bundle_refuses_a_key_occupied_by_other_bytes() -> Bool { + let occupied_root = member_evidence(slot: BootBundleRoot, hex: digest_c_hex, publish_answer: BootPutKeyOccupied { occupant_digest: digest(hex: digest_a_hex) }, served_hex: digest_c_hex) + is_refused(st: standing(kernel: kernel_ok(), initrd: initrd_ok(), root: occupied_root), member: BootBundleRoot, cause: BootBundleKeyOccupiedByOtherBytes, observation_decl: "member_evidence") +} + +test fn bundle_refuses_a_member_in_the_wrong_slot() -> Bool { + let swapped = member_evidence(slot: BootBundleInitrd, hex: digest_c_hex, publish_answer: BootPutCreated {}, served_hex: digest_c_hex) + is_refused(st: standing(kernel: kernel_ok(), initrd: initrd_ok(), root: swapped), member: BootBundleRoot, cause: BootBundleMemberInWrongSlot, observation_decl: "member_evidence") +} + +// review 68098: served_from is the observed readback origin, so a bundle read back from anywhere but +// the fleet's custom domain carries that origin and the join's route check can go red. +test fn bundle_served_from_is_the_observed_readback_origin() -> Bool { + let elsewhere_root = member_evidence_from(slot: BootBundleRoot, hex: digest_c_hex, publish_answer: BootPutCreated {}, served_hex: digest_c_hex, origin: "stale-edge.example.test") + match standing(kernel: kernel_ok(), initrd: initrd_ok(), root: elsewhere_root) { + BootBundleAvailable { receipt: rc } => (rc.served_from.locator as String) == "stale-edge.example.test" + BootBundleRefused { member: _, cause: _, observation: _ } => false + BootBundleUnobserved => false + } +} diff --git a/dag/test/claim/cloudflare_r2_origin_mint_run_witness_test.dag b/dag/test/claim/cloudflare_r2_origin_mint_run_witness_test.dag index 17a646073aa..0c8e594b276 100644 --- a/dag/test/claim/cloudflare_r2_origin_mint_run_witness_test.dag +++ b/dag/test/claim/cloudflare_r2_origin_mint_run_witness_test.dag @@ -35,10 +35,10 @@ import gunbc.cloudflare.r2_origin { cloudflare_bootstrap_custody_intervention, cloudflare_bootstrap_custody_version, fabric_durable_origin_standing, - DurableOriginAllocated, - DurableOriginUnallocated, - DurableOriginStanding, - DurableOriginBucket, + R2OriginAllocated, + R2OriginUnallocated, + R2OriginStanding, + R2OriginBucket, FabricDurableOrigin, BucketPurpose, R2BucketAllocation, @@ -52,9 +52,12 @@ import gunbc.cloudflare.r2_origin { fabric_durable_origin_write_secret_id, cloudflare_bootstrap_custody_secret_id, FabricCommitManifestsAndContentAddressedOutputs, + DigestKeyedBootArtifacts, + bucket_purpose_admission, OriginCredentialStanding, OriginCredentialPinned, OriginCredentialUnminted, + OriginCredentialMintEntryUnbuilt, } import gunbc.cloudflare.r2_permission_group_observe { cloudflare_r2_bucket_item_write_observed_permission_group_id, @@ -334,10 +337,11 @@ test fn r2_bucket_colliding_roster_serves_no_value() -> Bool { // spelled in one place; this is the join that proves it stayed that way. test fn r2_origin_bucket_read_credential_is_the_minted_container() -> Bool { match fabric_durable_origin_standing() { - DurableOriginUnallocated { purpose: _ } => false - DurableOriginAllocated { bucket: bucket } => + R2OriginUnallocated { purpose: _ } => false + R2OriginAllocated { bucket: bucket } => match bucket.read_credential { OriginCredentialUnminted { mint_entry: _ } => false + OriginCredentialMintEntryUnbuilt { secret_id: _ } => false OriginCredentialPinned { access_key_id: access_key_id, token_value: locus } => (locus.secret as String) == (fabric_durable_origin_read_secret_id as String) && (locus.project as String) == (fleet_secrets_gcp_project as String) @@ -346,8 +350,9 @@ test fn r2_origin_bucket_read_credential_is_the_minted_container() -> Bool { && (access_key_id as String) == (fabric_durable_origin_read_access_key_id as String) } && (bucket.bucket_name as String) == (declared_bucket_name()) - && match bucket.admits { + && match bucket_purpose_admission(purpose: bucket.purpose) { FabricCommitManifestsAndContentAddressedOutputs {} => true + DigestKeyedBootArtifacts {} => false } } } @@ -358,10 +363,11 @@ test fn r2_origin_bucket_read_credential_is_the_minted_container() -> Bool { // the read test above) rather than leaving a stale expectation green. test fn r2_origin_bucket_write_credential_is_unminted_and_names_its_mint_entry() -> Bool { match fabric_durable_origin_standing() { - DurableOriginUnallocated { purpose: _ } => false - DurableOriginAllocated { bucket: bucket } => + R2OriginUnallocated { purpose: _ } => false + R2OriginAllocated { bucket: bucket } => match bucket.write_credential { OriginCredentialPinned { access_key_id: _, token_value: _ } => false + OriginCredentialMintEntryUnbuilt { secret_id: _ } => false OriginCredentialUnminted { mint_entry: entry } => (entry.module_path as String) == "gunbc.cloudflare.r2_token_mint_run" && (entry.decl_name as String) == "run_object_write" @@ -411,10 +417,11 @@ test fn r2_origin_write_mint_is_ready_and_carries_the_observed_write_group() -> // fleet_secrets_project_number stays put. test fn r2_origin_read_pin_names_this_project_version() -> Bool { match fabric_durable_origin_standing() { - DurableOriginUnallocated { purpose: _ } => false - DurableOriginAllocated { bucket: bucket } => + R2OriginUnallocated { purpose: _ } => false + R2OriginAllocated { bucket: bucket } => match bucket.read_credential { OriginCredentialUnminted { mint_entry: _ } => false + OriginCredentialMintEntryUnbuilt { secret_id: _ } => false OriginCredentialPinned { access_key_id: _, token_value: locus } => gcp_resource_names_this_version( returned: secret_ref_version_resource(ref: locus), @@ -425,12 +432,12 @@ test fn r2_origin_read_pin_names_this_project_version() -> Bool { } } -fn origin_with_latest_read_pin() -> DurableOriginStanding { - DurableOriginAllocated { - bucket: DurableOriginBucket { +fn origin_with_latest_read_pin() -> R2OriginStanding { + R2OriginAllocated { + bucket: R2OriginBucket { bucket_name: "gunbai-fabric-origin", jurisdiction: R2JurisdictionDefault, - admits: FabricCommitManifestsAndContentAddressedOutputs {}, + purpose: FabricDurableOrigin {}, read_credential: OriginCredentialPinned { access_key_id: fabric_durable_origin_read_access_key_id, token_value: secret_ref_pin_version( diff --git a/dag/test/claim/cloudflare_r2_origin_object_witness_test.dag b/dag/test/claim/cloudflare_r2_origin_object_witness_test.dag index 7aad2cdde32..d6207287085 100644 --- a/dag/test/claim/cloudflare_r2_origin_object_witness_test.dag +++ b/dag/test/claim/cloudflare_r2_origin_object_witness_test.dag @@ -344,7 +344,8 @@ fn declared_bucket_name() -> String { test fn origin_read_target_resolves_to_the_declared_bucket_and_read_credential() -> Bool { match resolve_origin_target(direction: OriginRead, key: "fabric-m0/v1/commit/attempt-1") { OriginTargetRefused { cause: _ } => false - OriginTargetReady { locator: locator, access_key_id: access_key_id, token_value: locus } => + OriginTargetReady { locator: locator, access_key_id: access_key_id, token_value: locus, create_only: create_only } => + create_only && (locator.bucket_name as String) == declared_bucket_name() && locator.endpoint.account_id == (fleet_cloudflare_account_id as String) && (locator.key as String) == "fabric-m0/v1/commit/attempt-1" @@ -361,7 +362,7 @@ test fn origin_read_target_resolves_to_the_declared_bucket_and_read_credential() // retired into the write twin of the read test above, deliberately. test fn origin_write_target_refuses_until_the_write_credential_is_pinned() -> Bool { match resolve_origin_target(direction: OriginWrite, key: "k") { - OriginTargetReady { locator: _, access_key_id: _, token_value: _ } => false + OriginTargetReady { locator: _, access_key_id: _, token_value: _, create_only: _ } => false OriginTargetRefused { cause: cause } => string_contains(s: cause as String, pattern: "run_object_write") && string_contains(s: cause as String, pattern: "gunbc.cloudflare.r2_token_mint_run")