From 9e5762585e125f1d06421db66e4595bc31ea5f09 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:10:03 +0200 Subject: [PATCH 1/3] Record the means a certificate is validated with, and narrow one clause of 0103 (#243) 0029 requires the platform's own trust store and the platform's own path building and refuses a client-supplied evaluation by name. 0103 refuses a dependency that writes to a log. Every candidate that meets the first carries a logging facade and is refused by the second, and the one shape the second admits is refused by the first, so the two landed records refused each other's answer and #29's second condition sat behind an absence no issue held. 0243 names the means - rustls driving rustls-platform-verifier - admitted under 0103's clause for a dependency a landed record already requires, and narrows that record's fourth refused behaviour to writing to a log rather than to linking a facade whose sink is absent, on the condition that the core installs no logger and states that it installs none. The facade's own default sink is a no-op, read out of its source rather than assumed, so the property 0103 exists to guarantee is untouched. What it prevents is the collision being met as a red gate on a branch, where the cheapest way out is to weaken whichever record is nearer to hand, and a means being chosen at a call site by whoever reaches it first. The record carries the graph counted per triple, every licence expression in it derived rather than eyeballed, where the platform's path building stops, why 0029's six reason classes are not derivable from what the backends report, and what the crypto provider costs the target leg. Three residuals are named rather than softened: nothing refuses a logger installed tomorrow (#266), 0001 has no shape for a record that narrows one clause of another (#267), and one licence expression in the Android graph carries a term 0103's set names in neither half (#268). 0103 and 0029 each receive a pointer and nothing else, which is the one edit 0001 permits to a landed record. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- ...e-validation-and-the-self-signed-server.md | 4 + ...admits-a-dependency-and-what-is-refused.md | 6 + ...e-means-a-certificate-is-validated-with.md | 547 ++++++++++++++++++ docs/decisions/README.md | 1 + 4 files changed, 558 insertions(+) create mode 100644 docs/decisions/0243-the-means-a-certificate-is-validated-with.md diff --git a/docs/decisions/0029-certificate-validation-and-the-self-signed-server.md b/docs/decisions/0029-certificate-validation-and-the-self-signed-server.md index 4902321..0e395d6 100644 --- a/docs/decisions/0029-certificate-validation-and-the-self-signed-server.md +++ b/docs/decisions/0029-certificate-validation-and-the-self-signed-server.md @@ -22,6 +22,10 @@ The core validates that the machine which answered is the one the address named, using the platform's own trust store and the platform's own path building. What that means in detail is the platform's, and the core does not reimplement it. +0243 names the means this requirement is met through, per platform, and records +that the logging facade that means carries is admitted under 0103's fourth +behaviour as narrowed there. Nothing in this record moves. + A refusal is `certificate-rejected` from 0004, which already fixes its payload as the address, a reason class, and the presented certificate's fingerprint, and already fixes its retry property as no until a person decides. This record fixes diff --git a/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md b/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md index 9216fae..192b896 100644 --- a/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md +++ b/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md @@ -117,6 +117,12 @@ A dependency that writes to a log. 0100 and 0071 decide what leaves the core and in what shape, and a node writing to a global logger is a second exit for exactly the values 0071 classifies field by field. +0243 goes further on this fourth behaviour and narrows it: what is refused is +writing to a log, and linking a logging facade whose sink is absent is not that, +on the condition that the core installs no logger and states that it installs +none. The reasoning, the readings it rests on and what it costs are in that +record. Nothing else in this one moves. + A fifth ground, which 0061 supplied by refusing a real candidate rather than being argued from a principle. A dependency that carries its own field-bearing surface makes a rule in another record reach a second place: a tracing library's spans diff --git a/docs/decisions/0243-the-means-a-certificate-is-validated-with.md b/docs/decisions/0243-the-means-a-certificate-is-validated-with.md new file mode 100644 index 0000000..3cee695 --- /dev/null +++ b/docs/decisions/0243-the-means-a-certificate-is-validated-with.md @@ -0,0 +1,547 @@ +# 0243. The means a certificate is validated with, and what it costs + +Date: 2026-08-31 + +Status: accepted. Supersedes nothing. Superseded by nothing. + +Issue: #243 + +## The decision + +The core validates a certificate through `rustls` driving +`rustls-platform-verifier`, which dispatches to each platform's own verifier, +admitted under [0103](0103-what-admits-a-dependency-and-what-is-refused.md)'s +clause for a dependency a landed record already requires - the record being +[0029](0029-certificate-validation-and-the-self-signed-server.md), which requires +the platform's own trust store and the platform's own path building and refuses a +client-supplied evaluation by name - with that record's fourth refused behaviour +narrowed here to writing to a log rather than to linking a logging facade, on the +standing condition that the core installs no logger and states that it installs +none. + +## What was actually in the way, and it was not the choice of package + +Two landed records refused each other's answer, and that is what #243 turned out +to be about. Every candidate that satisfies +[0029](0029-certificate-validation-and-the-self-signed-server.md) carries a +logging facade, which +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) refuses outright as +its fourth behaviour. In the verifier crate it is ten call sites and a +non-optional entry in the manifest, so it is not a feature that can be switched +off: + + grep -rn 'log::' src/verification/ | grep -c '!' + 10 + grep -n -A2 'dependencies.log' Cargo.toml + 74:[dependencies.log] + 75-version = "0.4" + 76- + +The one shape +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) admits - `rustls` +with `rustls-native-certs` for the roots and `rustls-webpki` for the path +building - treats every root equally regardless of its status, which is the +platform's store without the platform's decisions, and +[0029](0029-certificate-validation-and-the-self-signed-server.md) refuses that. + +[0011](0011-the-language-the-toolchain-and-the-binding-layer.md) wrote this exact +case down as its own reversal condition before there was a graph to measure it +against, and +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) says the same from +its own side: where the rule refuses what a landed record requires, which of the +two moves is a decision above both rather than an exception written into either. +That decision was taken on #243 on 2026-08-30, and this record executes it. + +The reason it went the way it did is what each record is protecting. +[0029](0029-certificate-validation-and-the-self-signed-server.md) protects the +platform's own trust decisions and its revocation state, and no rewording +preserves that if the core stops asking the platform. +[0103](0103-what-admits-a-dependency-and-what-is-refused.md)'s fourth behaviour +protects against a second exit for the values +[0071](0071-what-may-leave-through-a-diagnostic-event.md) classifies field by +field, and the facade's own default sink writes nothing. Read out of the facade's +own source at `log v0.4.34`, the version the graph below resolves: + + grep -n '' src/lib.rs | sed -n '456p;1318,1327p' + 456:static mut LOGGER: &dyn Log = &NopLogger; + 1318:struct NopLogger; + 1319: + 1320:impl Log for NopLogger { + 1321: fn enabled(&self, _: &Metadata) -> bool { + 1322: false + 1323: } + 1324: + 1325: fn log(&self, _: &Record) {} + 1326: fn flush(&self) {} + 1327:} + +So a facade with no logger installed is not an exit, and the property +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) exists to guarantee +is untouched by linking one. + +## The narrowed rule, stated in full + +A dependency that WRITES to a log is refused, exactly as before. A dependency +that links a logging facade whose sink is absent is not refused, provided the +core installs no logger and states that it installs none. + +The condition is not decoration. A facade is harmless because nothing is +registered behind it, and the moment the core registers something the fourth +behaviour is back in force with nothing to have caught the change. The core +installs no logger today, and the two readings that say so are this: + + git grep -n 'set_logger\|set_boxed_logger' origin/main -- src/ ; echo "exit=$?" + exit=1 + git grep -n '^log =\|^tracing' origin/main -- Cargo.toml ; echo "exit=$?" + exit=1 + +Nothing refuses a logger installed tomorrow. `no-text-output` in +`.github/invariants/rules` refuses `println!` and its neighbours under `src/` and +reaches no logger installation, so the condition above is carried by this record +and by review. #266 is where a rule that refuses it is asked for. + +## What this record does to 0103, and what it does not + +The narrowing is written here rather than into +[0103](0103-what-admits-a-dependency-and-what-is-refused.md)'s own text, and the +reason is that record's neighbour. +[0001](0001-decision-records.md) permits three edits to a landed record and a +change to what it decided is not among them; a narrowing is a change to what it +decided. What +[0001](0001-decision-records.md) does permit is a pointer to a later record that +goes further on a case the earlier one already names, where the pointer changes +no sentence's meaning and takes no reason away, and that is what +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) and +[0029](0029-certificate-validation-and-the-self-signed-server.md) each receive. + +THE RESIDUAL IS REAL AND IT IS STATED RATHER THAN SOFTENED. +[0001](0001-decision-records.md) offers whole-record supersession and no partial +one, and superseding +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) entirely would +discard a licence set, a worth test and four other grounds that are unchanged. So +a reader who opens +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) and does not follow +its pointer reads its fourth behaviour one clause wider than the rule in force. +Nothing refuses that reader. #267 is where the shape of a partial supersession is +asked for, and this record does not invent one. + +## How the readings below were taken + +Every `cargo` reading in this record was taken in a scratch crate outside this +tree, on a Windows machine, with the toolchain `rust-toolchain.toml` pins. The +manifest of that crate declares two dependencies and nothing else: + + rustls = "0.23" + rustls-platform-verifier = "0.7" + +resolving to `rustls v0.23.43`, `rustls-platform-verifier v0.7.0` and the crypto +provider `rustls` names first in its own `default` list. The gate runs on +`ubuntu-latest`, so the cross-compile results in particular are this machine's and +not the runner's. That bound is stated once and applies to every number here. + +The tree these are measured against: + + git rev-parse origin/main + ded6d1fcbd9ad65b5a04fbf58c0a7d0e34dd2c10 + +## What the graph is, counted rather than supposed + +Per triple, the shipping graph beside the crate itself: + + cargo tree -e normal --target TRIPLE --prefix none | sed 's/ (\*)$//' \ + | sort -u | grep -v '^verifier-probe' | grep -v '^$' | wc -l + aarch64-linux-android 29 + armv7-linux-androideabi 29 + aarch64-apple-ios 17 + aarch64-apple-tvos 17 + aarch64-apple-darwin 17 + x86_64-pc-windows-msvc 13 + x86_64-unknown-linux-gnu 13 + +For scale, what this tree carries today is nine packages beside its own: + + cargo metadata --format-version 1 --locked | jq -r '.packages[].name' | wc -l + 10 + +The union across all seven triples is thirty-nine distinct packages, and that is +the number to hold rather than any single triple's, because every client links one +of the seven and the eleven of them link all of them between them. + +## The licence, read rather than assumed + +Every licence expression carried by the thirty-nine, derived rather than eyeballed: + + cargo metadata --format-version 1 --locked \ + | jq -r '.packages[] | "\(.name) v\(.version)\t\(.license)"' | sort > licences + for t in TRIPLE...; do + cargo tree -e normal --target "$t" --prefix none | sed 's/ (\*)$//;s/ (proc-macro)$//' + done | grep -v '^verifier-probe' | grep -v '^$' | sort -u > shipping + awk -F'\t' 'NR==FNR{l[$1]=$2;next}{print l[$0]}' licences shipping | sort | uniq -c | sort -rn + 26 MIT OR Apache-2.0 + 2 MIT + 2 ISC + 2 Apache-2.0 OR MIT + 2 Apache-2.0 OR ISC OR MIT + 1 Unlicense OR MIT + 1 ISC AND (Apache-2.0 OR ISC) AND Apache-2.0 AND MIT AND BSD-3-Clause AND (Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0) + 1 ISC AND (Apache-2.0 OR ISC) + 1 BSD-3-Clause + 1 (MIT OR Apache-2.0) AND Unicode-3.0 + +Nine of the ten expressions are satisfied by +[0103](0103-what-admits-a-dependency-and-what-is-refused.md)'s admitted set, +including the two conjunctive ones, because every member of each conjunction is +in the set and each dual offer inside them includes an admitted member. `MIT-0` +appears only inside a dual offer beside `Apache-2.0` and `ISC`, so it is admitted +on those. + +THE TENTH IS NOT, AND IT IS THE ONE COLLISION THIS RECORD LEAVES OPEN. +`unicode-ident` is offered as `(MIT OR Apache-2.0) AND Unicode-3.0`. The term is +conjunctive rather than a dual offer, so +[0103](0103-what-admits-a-dependency-and-what-is-refused.md)'s sentence about a +dual offer does not reach it, and `Unicode-3.0` is named in neither the admitted +half of that set nor the refused half. It arrives through a proc-macro rather +than through anything linked into a client, and it is present on exactly two of +the seven triples: + + cargo tree -e normal --target TRIPLE --prefix none | grep -c '^unicode-ident' + aarch64-linux-android 3 + armv7-linux-androideabi 3 + aarch64-apple-ios 0 + aarch64-apple-tvos 0 + aarch64-apple-darwin 0 + x86_64-pc-windows-msvc 0 + x86_64-unknown-linux-gnu 0 + +This record does not admit that term and does not refuse it. Extending an +enumerated licence set is a change to +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) of the kind this +record's own section above says it may not make, and it is a different question +from the one #243 asked. #268 is where it is asked, and until it is answered the +Android half of this means is a dependency this board has not licensed. + +## Where the platform's own path building stops + +This is the first thing #243 asks and the answer is not uniform. The crate's own +README states its dispatch per platform, and the row that matters is the second +from last: + + sed -n '20,27p' README.md + | OS | Certificate Store | Verification Method | Revocation Support | + |----------------|-----------------------------------------------|--------------------------------------|--------------------| + | Windows | Windows platform certificate store | Windows API certificate verification | Yes | + | macOS (10.14+) | macOS platform roots and keychain certificate | macOS `Security.framework` | Yes | + | iOS | iOS platform roots and keychain certificates | iOS `Security.framework` | Yes | + | Android | Android System Trust Store | Android Trust Manager | Sometimes[^1] | + | Linux | System CA bundle, or user-provided certs[^3] | webpki | No[^2] | + | WASM | webpki roots | webpki | No[^2] | + +The last row is outside +[0113](0113-the-target-triples-the-gate-compiles-for.md)'s set and is not a +platform this decision covers. + +On six of the seven triples the path building is the platform's. On the Linux +desktop client it is `rustls-webpki` over whatever root bundle the system happens +to hold, with no revocation at all, which the graph shows directly - that is the +one triple whose shipping set carries `rustls-native-certs` and `openssl-probe` +and no platform framework. + +So [0029](0029-certificate-validation-and-the-self-signed-server.md)'s sentence +that the core does not reimplement the platform's path building is met on six +triples and has nothing to meet on the seventh, because that platform has no path +building of its own to use. Every candidate does its own there, so this is a +departure the means does not cause and cannot avoid, and it is recorded rather +than left for somebody to discover in a refusal. + +## Whether 0029's six reason classes are derivable + +They are not, and this record does not pretend the question is settled by a +reading of source. What the crate maps, at the version measured, is narrower than +six classes on every backend, and the crate says so itself above each of the two +mapping tables: + + grep -rn 'Only map' src/ + src/verification/apple.rs:220: // Only map the errors we need for tests. + src/verification/windows.rs:677: // Only map the errors we have tests for. + +What each of the three backends reaches, and the Android one is the third of them: + + grep -n '=> InvalidCertificate' src/verification/windows.rs + 684: CRYPT_E_REVOKED => InvalidCertificate(CertificateError::Revoked), + 685: CERT_E_EXPIRED => InvalidCertificate(CertificateError::Expired), + 686: CERT_E_UNTRUSTEDROOT => InvalidCertificate(CertificateError::UnknownIssuer), + 687: CERT_E_WRONG_USAGE => InvalidCertificate(CertificateError::InvalidPurpose), + + grep -n 'errors::errSec' src/verification/apple.rs + 222: errors::errSecHostNameMismatch => Ok(TlsError::InvalidCertificate( + 225: errors::errSecCreateChainFailed => Ok(TlsError::InvalidCertificate( + 228: errors::errSecInvalidExtendedKeyUsage => Ok(TlsError::InvalidCertificate( + 231: errors::errSecCertificateRevoked => { + + grep -n 'VerifierStatus::' src/verification/android.rs | sed -n '1,5p' + 216: VerifierStatus::Expired => Err(InvalidCertificate(CertificateError::Expired)), + 219: Err(InvalidCertificate(CertificateError::UnknownIssuer)) + 223: Err(InvalidCertificate(CertificateError::Revoked)) + 226: Err(InvalidCertificate(CertificateError::BadEncoding)) + 228: VerifierStatus::InvalidExtension => Err(InvalidCertificate( + +Three consequences for +[0029](0029-certificate-validation-and-the-self-signed-server.md): + +`self-signed` and `issuer-unknown` arrive as one value on all three backends: the +one that reaches an unknown issuer is `UnknownIssuer` and there is no second value +beside it for a certificate that signed itself. + +`not-yet-valid` is not separated from `expired` on Windows, on Apple or on +Android, which is the pair +[0029](0029-certificate-validation-and-the-self-signed-server.md) names +separately on purpose for the television that came up believing it is 1970. + +`revoked` is a class the platforms report and +[0029](0029-certificate-validation-and-the-self-signed-server.md) does not carry. +It would land in `chain-unusable`, and a client then cannot tell a revoked +certificate from an unusable chain, which is the one difference an operator can +act on. + +THAT IS A READING OF SOURCE AND NOT A REFUSAL ON A WIRE, so it does not discharge +[0029](0029-certificate-validation-and-the-self-signed-server.md)'s own reversal +condition, which requires two platforms producing different classes for one +certificate measured on a real refusal. What this record gives that condition is +the thing it did not have: something to take the measurement with. The +measurement itself belongs to #29 and #21. + +## What it costs the gate, which is the largest cost and is not the graph + +`rustls` needs a crypto provider, and both providers it offers are C. Its own +`default` list names the first, and the only other is the second. Read out of +`rustls v0.23.43`'s own manifest: + + grep -n '' Cargo.toml | sed -n '70,76p;88,91p' + 70:default = [ + 71: "aws_lc_rs", + 72: "logging", + 73: "prefer-post-quantum", + 74: "std", + 75: "tls12", + 76:] + 88:ring = [ + 89: "dep:ring", + 90: "webpki/ring", + 91:] + +Both carry a native build: + + cargo metadata --format-version 1 --locked | jq -r '.packages[] + | select(.name=="aws-lc-sys" or .name=="ring") + | "\(.name) \(.version) links=\(.links) build=\(.dependencies|map(select(.kind=="build"))|map(.name)|join(","))"' + aws-lc-sys 0.44.0 links=aws_lc_0_44_0 build=bindgen,cc,cmake,dunce,fs_extra,pkg-config + ring 0.17.14 links=ring_core_0_17_14_ build=cc + +Today the target set compiles the library for all seven triples on one runner and +needs no C toolchain at all, because the graph is pure Rust. Taking either +provider ends that. On this machine, with all seven target standard libraries +installed and the pinned toolchain, six of the seven triples fail inside +`aws-lc-sys`'s build script rather than anywhere in Rust: + + for t in TRIPLE...; do cargo check --locked --target "$t"; done + +The first Android triple, as one exact line of what the six look like: + + warning: aws-lc-sys@0.44.0: Compiler family detection failed due to error: ToolNotFound: failed to find tool "aarch64-linux-android-clang": program not found + +What the other five reported, summarised rather than pasted, because each of the +Apple lines carries a page of quoted argument vector. Four of them are the same +`ToolNotFound` with a different tool name - `arm-linux-androideabi-clang` on the +second Android triple, `cc` on `aarch64-apple-darwin` and `x86_64-linux-gnu-gcc` +on `x86_64-unknown-linux-gnu`. The two remaining are the Apple device triples, +where `clang` is found and fails, reporting `ToolExecError` on the compilation of +`c11.c` with `--target=arm64-apple-ios` and `--target=arm64-apple-tvos` +respectively and a Windows SDK where a platform sysroot should be. + +The seventh is the host and it builds: + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 41s + +That is this machine and not the runner, and what it establishes is the shape +rather than the runner's verdict: the C cross-toolchain per triple is a real +prerequisite that the target leg does not have today, which on a Linux runner +means an Android NDK and an Apple SDK. The 4m 41s is one machine's cold build of +the provider and is the order of the cost rather than a number to hold the runner +to. + +The pure-Rust provider that would avoid all of it states its own maturity in its +version string: + + cargo search rustls-rustcrypto --limit 1 + rustls-rustcrypto = "0.0.2-alpha" # Pure Rust cryptography provider for the Rustls TLS library… + +so it is not a candidate today and is named here as the thing that would retire +this cost. + +## What the test harness gains + +Proving a refusal needs a fake that speaks TLS and can be made to present a +certificate the core will refuse. The fake today is deliberately not that, and +says so in its own header: + + git show origin/main:tests/fake_server/mod.rs | sed -n '29p' + //! here. This is `std::net` and `std::thread` and nothing else: an HTTP server + +Making certificates inside the suite is a test-tree dependency, which +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) relaxes the worth +test for and relaxes nothing else for. The expensive half is that the fake then +needs a server-side TLS implementation too, and that is the same question as the +shipping side rather than a separate one: the provider it would use is the +provider decided above. So this record does not add a second answer for the +suite. #21 and #29 own what the harness becomes; what they gain from here is that +the answer is already chosen for them. + +## What this record does not do + +It does not add a dependency. `Cargo.toml` is unchanged by it, and the line +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) requires beside a +manifest entry - the clause that admitted it and what would retire it - is +written when the entry arrives, which is #27's and #29's change rather than this +one's. + +It does not decide the transport, which is #27, and it does not touch the pin +register, which landed under #29 and needs none of this. + +It settles nothing about the Android licence term, which is #268, and it builds +no mechanism for the no-logger condition, which is #266. + +## Why this is written down before the code + +The core reaches no network at all today: + + git grep -n 'std::net' origin/main -- src/ ; echo "exit=$?" + exit=1 + +which is the only moment this can be decided rather than discovered. A means +chosen at a call site is chosen by whoever meets the call site first, and the +thing they will choose is the package that compiles, which on this question is +the one whose error type carries no reason class at all. + +The specific failure is narrower and it is the one this board already came within +one decision of. #29's second condition sat behind an absence nobody held, #29 +sits in front of the transport in #27, and #27 sits in front of every call any +other issue on this board makes. One decision was holding a milestone, and the +thing holding the decision was not a preference between packages but two landed +records refusing each other. Written afterwards, that collision is discovered as +a red gate on somebody's branch, and the cheapest way out of a red gate is to +weaken whichever record is nearer to hand. + +## Alternatives, and what each cost + +`native-tls`, which is SChannel on Windows, Security.framework on Apple and +OpenSSL everywhere else. The smallest graph of the four, measured the same way as +the one above: + + cargo tree -e normal --target TRIPLE --prefix none | sed 's/ (\*)$//' \ + | sort -u | grep -v '^nativetls-probe' | grep -v '^$' | wc -l + aarch64-linux-android 15 + armv7-linux-androideabi 15 + aarch64-apple-ios 7 + aarch64-apple-tvos 7 + aarch64-apple-darwin 14 + x86_64-pc-windows-msvc 4 + x86_64-unknown-linux-gnu 15 + +It fails [0029](0029-certificate-validation-and-the-self-signed-server.md) on that +record's own terms before +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) is reached. That +record requires the presented chain, the fingerprint, the reason class and the +subject, issuer and validity window to be handed to the client as data, and what +this one publishes is a newtype over the platform's own error with none of them +on it: + + grep -n 'pub struct Error' native-tls-0.2.18/src/lib.rs + 119:pub struct Error(imp::Error); + +It is refused by +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) twice over as well. +It carries the same logging facade on its OpenSSL targets, which are five of the +seven, and on Apple it pulls `tempfile`, which is that record's third behaviour, a +dependency that reads or writes the filesystem without being told where. Both are +in the trees above. Its Android target reaches OpenSSL rather than the Android +Trust Manager, so it does not meet +[0029](0029-certificate-validation-and-the-self-signed-server.md)'s platform +requirement there either. + +`rustls` with `rustls-native-certs` and `rustls-webpki`, and the logging feature +off. The shape that trips none of the five behaviours - the roots crate declares +one dependency and it is not a logger: + + grep -n -A3 '^\[dependencies' rustls-native-certs-0.8.4/Cargo.toml + 52:[dependencies.pki-types] + 53-version = "1.10" + 54-features = ["std"] + 55-package = "rustls-pki-types" + +so it costs no narrowing of any record. What it costs is the platform's +decisions, which the crate's own comparison table states in the row for it: + + sed -n '56,57p' README.md | awk -F'|' '{print $2 "|" $4}' + `rustls-platform-verifier` (non-Linux/BSD) | System store, with full (dis)trust decisions from every source available. + `rustls-native-certs` + `webpki` | System store, with no (dis)trust decisions. All roots are treated equally regardless of their status. + +That is the platform's store without the platform's judgement about it, and +[0029](0029-certificate-validation-and-the-self-signed-server.md) refuses exactly +it. It is what the Linux triple gets anyway, and the difference is that there it +is forced and here it would be chosen for six platforms that have something +better. + +The platform facilities reached directly from this tree, over `schannel`, +`security-framework`, `jni-sys` and a roots crate. The narrowest graph, none of +those reaches a logger, and it needs no record narrowed. It costs the largest +amount of security-critical code written here, which is the class +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) answers by name - +a protocol, a parser of somebody else's format, or a cryptographic primitive - +and it is the shape +[0029](0029-certificate-validation-and-the-self-signed-server.md)'s own +alternatives section warns about, the one that feels more careful than the +ordinary answer. + +Writing the validation here. +[0103](0103-what-admits-a-dependency-and-what-is-refused.md) answers this by +class in one sentence, and the sentence is about exactly this case: a wrong +implementation of a protocol, a parser or a cryptographic primitive is a defect +nobody sees until it is exploited. + +Dropping a platform family from the target set until it can be served. It costs +the least code of any option and it is the only one that makes the Android +licence term and the Android NDK both go away. It costs the platform, and +[0113](0113-the-target-triples-the-gate-compiles-for.md) is where a triple leaves +that set rather than here. + +## What would reverse this + +A pure-Rust crypto provider for `rustls` reaches a stable release. The C +cross-toolchain per triple then buys nothing, the target leg goes back to one +runner with no NDK and no SDK, and this record is superseded by one naming that +provider. The version string above is the thing to re-read. + +The verifier crate's platform dispatch stops reaching a platform's own verifier +on any triple in +[0113](0113-the-target-triples-the-gate-compiles-for.md)'s set, so that what is +carried is roots plus `webpki` there. The whole reason this means was chosen over +the smaller graph is gone for that platform, and the choice is retaken against +the alternatives above rather than inherited. + +The facade stops being harmless: a logger is installed anywhere in the core, or +the crate begins writing through a sink it registers itself. +[0103](0103-what-admits-a-dependency-and-what-is-refused.md)'s fourth behaviour +is then in force unnarrowed, and this record is superseded by one that says what +replaces the means rather than by an exception written into either record. + +The Android licence term is refused under #268. The two Android triples then +carry a package this board may not carry, and what moves is the means on those +triples, the licence set, or the triples themselves - and this record is +superseded by the one that says which. + +Two platforms are measured producing different reason classes for one +certificate, on a real refusal. That is +[0029](0029-certificate-validation-and-the-self-signed-server.md)'s reversal +condition rather than this record's, and it reverses the class set rather than +the means - but the measurement is only possible because of this record, so it is +named here as the thing to go and take. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index af4ebdb..cb4ed8d 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -60,3 +60,4 @@ allocated, and why a record is superseded rather than edited are in - [0114. Signing out, forgetting a server, and holding several sessions](0114-signing-out-and-forgetting-a-server.md) - [0115. Creating the core, stopping it, and a host that suspends it](0115-creating-and-stopping-the-core.md) - [0116. Learning that something cached has changed](0116-learning-that-something-cached-has-changed.md) +- [0243. The means a certificate is validated with, and what it costs](0243-the-means-a-certificate-is-validated-with.md) From c793c64e9e21286dc3c5648d53126a5e175bf2cb Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:10:39 +0200 Subject: [PATCH 2/3] Record what the first release contains and what it does not (#91) This repository is a library, so a release of it has nothing an operator can run and the question of what a release even means here had to be answered before anything is tagged. 0091 answers it: the library compiled for 0113's triples together with the probe #92 builds, published as a 0.x tag on this repository alone with its checksums and #87's attestations, and described by #95 as something an operator points at their own server rather than as a client. Every item in the contents is a closed issue rather than a description of work, and every condition in the bar is a closed issue, a green check or a run whose failure mode was demonstrated. Nothing in the list is a judgement somebody makes on the day, which is the failure this record is against: "is it ready" asked at the tag is answered by whoever is most tired of asking, and a preview shipped as a release is not withdrawn afterwards. It also fixes which speed numbers appear. A number appears only where #67 has published it with the command that produced it, and every number 0008 names that #67 has not published is listed in #95 as not measured, in those words - because a list assembled on the day is a list whose absences are invisible, and the absence is the part an operator needs. The four answers it rests on are entries 2, 3, 4 and 6 of #1, taken on 2026-08-24, read from that issue rather than recalled. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- .../0091-what-the-first-release-contains.md | 210 ++++++++++++++++++ docs/decisions/README.md | 1 + 2 files changed, 211 insertions(+) create mode 100644 docs/decisions/0091-what-the-first-release-contains.md diff --git a/docs/decisions/0091-what-the-first-release-contains.md b/docs/decisions/0091-what-the-first-release-contains.md new file mode 100644 index 0000000..3988693 --- /dev/null +++ b/docs/decisions/0091-what-the-first-release-contains.md @@ -0,0 +1,210 @@ +# 0091. What the first release contains, and what it does not + +Date: 2026-08-31 + +Status: accepted. Supersedes nothing. Superseded by nothing. + +Issue: #91 + +## The decision + +The first release of this repository is the library compiled for the triples +[0113](0113-the-target-triples-the-gate-compiles-for.md) names together with the +probe #92 builds, published as a `0.x` tag on this repository alone with its +checksums and #87's attestations beside it, described by #95 as something an +operator points at their own server rather than as a client; it contains no user +interface, no playback and no client for any platform; and it is called a release +rather than a preview only when every condition listed below is a closed issue or +a green check rather than a judgement somebody makes on the day. + +## What a release of this repository is + +A library has nothing to run, which is #92's opening sentence and the reason this +record has to answer what a release even means here before anything is tagged. An +operator cannot form an opinion about something they cannot execute, so the +release is the smallest thing they can: the library, plus one program that drives +it against their own server and prints what happened. + +That fixes the register the documentation is written in. The first paragraph of +#95 says what this is and what it is not, and the sentence it may not write is any +that lets a reader install this expecting a client. + +Four answers this rests on were taken on 2026-08-24, on the issue that collects +the decisions this plan may not make: + + gh issue view 1 --repo Flowfin/core --json comments \ + --jq '.comments[] | select(.body | startswith("Answering the open entries")) | .body' + +Entry 2 is Rust with a foreign function interface per platform, so there is a +library per target triple to release at all. Entry 3 is the two server lines +10.11 and 12.0. Entry 4 is that the interface stays at `0.x` and breaks freely +until a client actually consumes it. Entry 6 is releases on this repository only, +with the attestations attached and no language registry before a consumer exists. + +## What the first release contains + +Each line is a closed issue rather than a description of work, because a scope +whose items are descriptions is a scope somebody grades on the day. + +The library, compiled for every triple in +[0113](0113-the-target-triples-the-gate-compiles-for.md)'s set, built only by the +release workflow from a tag. #94, and #14 for the pinned toolchain it is built +with. + + git show origin/main:.github/targets/targets | grep -cE '^[a-z0-9_]+-' + 7 + +The probe, run against a real server by the operator who installed it, printing a +timing for every step and exercising each unhappy path on demand. #92, and behind +it #39 for the library surface it lists, #49 for the artwork it fetches and #57 +for the position it reports. The unhappy paths it offers are the ones the core has +routes for: #30, #31 and #32 for the sign-in routes, #34 and #35 for a token that +dies, #44 for a slow server and #45 for a server that is gone. A route no issue +above has closed is a route the probe does not offer and #95 lists as absent. + +The checksums, and the bill of materials and the provenance attestation beside +them. #87, attached by #94, which is what entry 6 decided the release carries in +place of a registry. + +A changelog section for the version, refused by a check rather than by a reviewer +when it is missing. #93, and #78 behind it for the scheme that section is written +against. + +The documentation an operator reads before installing. #95, with the data page +from #74 linked and summarised, and the licence named: + + gh api repos/Flowfin/core --jq '.license.spdx_id' + AGPL-3.0 + +## What it does not contain, said before it is installed rather than after + +No user interface. No playback of anything. No client for any platform. Those +three are [0003](0003-what-the-core-does-not-do.md)'s boundary rather than a gap +this release happens to leave, and #95 writes them in its first paragraph rather +than in a limitations section at the foot. + +No package in any language registry. Entry 6 answered that a registry can come +later, additively, when a client needs it, so the first release publishes bytes +and a checksum and nothing an author adds one line to a manifest for. + +No frozen interface. Entry 4 keeps this at `0.x`, so the version scheme #93 adopts +is a note to whoever reads this repository rather than a promise to strangers, and +#95 says so rather than letting a version number imply the other thing. + +No server line beyond the two in entry 3. What the release is built and tested +against is #88's set, and a line outside it is untested rather than unsupported by +implication. + +No verification on a platform family #97 did not reach. That issue's own condition +requires the unverified families to be named in its run output, so the release +names them too rather than letting a green run read as covering all seven triples. + +## The bar, as conditions rather than as a feeling + +It is a release when all of the following hold, and a preview until then. + +Every issue named in the two sections above is closed. + +The tag ran the full gate before anything was published, and the publishing job +restored no cache. #96. + +The verification on a machine that never built the artifact passed for each +claimed platform family, and a deliberately corrupted artifact failed it. #97. + +The gate's check names are required on `main`, so that a red check refuses the +merge that produced the tag rather than being weighed by a person. #26. + +Nothing above asks anybody to decide whether the thing is ready. Each one is a +closed issue, a green check or a run whose failure mode was demonstrated, which is +the whole of what this record is for. + +## The speed numbers this release publishes, and the ones it does not + +[0008](0008-what-the-core-can-measure-of-the-speed-budget.md) fixes which numbers +the core can measure alone and +[0064](0064-the-numbers-the-core-does-not-report.md) fixes the two it cannot. This +record adds one condition on top of them and no new numbers. + +A speed number appears in the first release only where #67 has published it with +the command that produced it, measured by #65's harness. Every number in +[0008](0008-what-the-core-can-measure-of-the-speed-budget.md) that #67 has not +published by then is listed in #95 as not measured, in those words. + +The two published targets #62 and #63 are the core's share and never the whole +number, which is +[0064](0064-the-numbers-the-core-does-not-report.md)'s sentence rather than this +record's, and #95 repeats it beside any figure it quotes. + +This is deliberately not "the numbers we have by then". A list assembled on the +day is a list whose absences are invisible, and the absence is the part an +operator needs. + +## Why this is written down before the code + +Nothing is published yet: + + gh api repos/Flowfin/core/releases --jq 'length' + 0 + +which is the only moment this can be written without the answer being read off +whatever happened to be finished. A scope decided at the tag is a scope shaped by +what compiled that week, and the parts that did not compile leave no trace in it +at all. + +The specific failure is narrower and it is the one #95 exists against. A +repository with a tag on it is read as a product by everybody who did not follow +the tracker, and the first person to install this will be looking for a client. +The distance between what they expect and what this is cannot be closed by a +release note written after they have installed it, because by then the sentence +they needed was the first one. + +The second failure is the bar. "Is it ready" asked on the day is answered by +whoever is most tired of asking, and a preview shipped as a release is not +withdrawn afterwards - the version number is already in somebody's manifest and +the expectation is already set. Conditions written now are conditions nobody has +an interest in yet, which is the only kind worth writing. + +## Alternatives, and what each cost + +No release until a client exists. The most honest position, and it is what entry 6 +priced for the publishing route. It costs the feedback the probe was built to +collect: the core would reach its first real server inside somebody's client, at +the moment when a second thing is already going wrong, and the fake server would +have been the only audience for every measurement until then. + +A release of the library alone, with no probe. Smaller, and it is what a library +repository normally publishes. It costs the operator entirely, since a library is +not something they can run, and it costs this repository the only route by which a +real server's behaviour reaches it before a client is written. + +A release when a milestone completes rather than when a list of issues closes. +Simpler to state and it reads well on a plan. It costs precision in the direction +that matters: a milestone is a bucket somebody can move an issue out of, and the +scope would then be whatever remained in it, decided by nobody. + +Calling the first one a preview and deferring this record. Cheap, and it removes +every argument above by removing the word. It costs the same argument later with a +version number already published, and the second time it is had against whoever +has already installed the thing. + +## What would reverse this + +A client begins consuming the interface before the first tag. Entry 4's condition +for freezing has then happened ahead of this record, the release is a promise to a +consumer rather than a note to a reader, and this record is superseded by one that +says what the release promises that client. + +Entry 6 is reopened and a language registry is chosen. What is published is then a +package rather than a file with a checksum beside it, the withdrawal properties are +different, and the contents section above is superseded rather than amended. + +#92's probe cannot be run by an operator on a platform family the release claims, +because it needs something their machine does not have. The release then contains +a library for a triple and nothing runnable on it, which is the case this record's +first section rules out, and it is superseded by one that says what a release +means for that family. + +Two of the conditions in the bar turn out to be uncheckable in practice, so that +somebody judges them on the day anyway. One is a condition written badly. Two is +this record's method failing, and it is replaced by one built from what the checks +can actually answer. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index cb4ed8d..e6ad389 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -48,6 +48,7 @@ allocated, and why a record is superseded rather than edited are in - [0069. Every host the core may contact](0069-every-host-the-core-may-contact.md) - [0071. What may leave through a diagnostic event](0071-what-may-leave-through-a-diagnostic-event.md) - [0072. Federation is a deliberate per-server act](0072-federation-is-a-deliberate-per-server-act.md) +- [0091. What the first release contains, and what it does not](0091-what-the-first-release-contains.md) - [0100. The diagnostics interface, and its relation to measurement spans](0100-the-diagnostics-interface.md) - [0101. What the core trusts, and what it is built to survive](0101-what-the-core-trusts.md) - [0102. The clocks every deadline is measured against](0102-the-clocks-every-deadline-is-measured-against.md) From 1471d5d4abd258155a0a2d158a710fa9ca26c7d1 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:10:57 +0200 Subject: [PATCH 3/3] Say in the parity table what the rust analysis leg does not prove (#81) The codeql row records that the leg refuses a finding the register does not excuse. It did not say which half of that is proven here and which is not, so a green tick on `Analyze (rust)` reads as evidence that the analysis finds defects in this tree, and it is not. What is proven from here is the reading: the five documents under .github/codeql/fixtures are one change apart from each other and demonstrate that a report carrying a finding is refused, that a result naming no rule is still read as a finding, and that a file with no run or no loaded rule is refused too. What is not proven is that the loaded pack finds a defect of this kind in this language. Two deliberate defects matching the loaded queries were written and reverted, once before this crate declared a dependency and once after, and both runs reported nothing. The pack is fetched at run time rather than tracked here, so no reading of this tree settles why. The negative disclosure is written where the row is read rather than only on the issue, because the row is what somebody consults when they want to know what this gate covers. The leg already prints the same bound on every run; this puts it where a reader who never opens a job log will meet it. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- docs/gate-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gate-parity.md b/docs/gate-parity.md index d509234..80a0c01 100644 --- a/docs/gate-parity.md +++ b/docs/gate-parity.md @@ -160,7 +160,7 @@ work is. | Workflow file | Check-run name on the default branch | What it protects against | Verdict | What lands it here, and why | | --- | --- | --- | --- | --- | | `build.yml` | `Package (JPRM) / Build package`, `Package (JPRM) / Generate SBOM` | A release artifact built somewhere other than the release path, and one shipped without a bill of materials or a provenance attestation | waiting | Entry 2 of #1. One answer produces a library per target triple and another produces a specification and a suite, and only the first has an artifact to build. Where there is one, #94 builds it and #87 attaches the bill of materials and the attestation. Its two check runs reach the default branch through `dotnet.yml`, which calls this file. | -| `codeql.yml` | `Analyze (actions)`, `Analyze (csharp)`, `Analyze (javascript-typescript)` | A defect a compiler and a review both pass, found by semantic analysis rather than by a pattern | adapted | #81. `codeql.yml` runs here as one analysis rather than three, because this repository has one language, and its check-run name is `Analyze (rust)`. The deviation and its reason: fewer analyses, because there are fewer languages. The analysis over the workflow files that the third name there covers is not adopted, because `zizmor.yml` already reads those same files here and putting a second analyser over them is an argument nobody has made; that sentence is in `.github/workflows/zizmor.yml` too. The verdict on a finding is this repository's rather than the action's: the action uploads and does not fail a build, so `.github/codeql/codeql.sh` reads the file the analysis wrote and refuses a finding the register in `.github/codeql/excluded-rules` does not excuse by name, and refuses a file carrying no run or no loaded rule, because a query set that never loaded reports zero findings and reads exactly like a clean tree. #81 also reaches a third body of code neither of those covers, the shell this gate is written in, and that half has landed under its own row at the foot of this table rather than here. | +| `codeql.yml` | `Analyze (actions)`, `Analyze (csharp)`, `Analyze (javascript-typescript)` | A defect a compiler and a review both pass, found by semantic analysis rather than by a pattern | adapted | #81. `codeql.yml` runs here as one analysis rather than three, because this repository has one language, and its check-run name is `Analyze (rust)`. The deviation and its reason: fewer analyses, because there are fewer languages. The analysis over the workflow files that the third name there covers is not adopted, because `zizmor.yml` already reads those same files here and putting a second analyser over them is an argument nobody has made; that sentence is in `.github/workflows/zizmor.yml` too. The verdict on a finding is this repository's rather than the action's: the action uploads and does not fail a build, so `.github/codeql/codeql.sh` reads the file the analysis wrote and refuses a finding the register in `.github/codeql/excluded-rules` does not excuse by name, and refuses a file carrying no run or no loaded rule, because a query set that never loaded reports zero findings and reads exactly like a clean tree. #81 also reaches a third body of code neither of those covers, the shell this gate is written in, and that half has landed under its own row at the foot of this table rather than here. What the must-bite requirement is discharged against here is the reading rather than the tree: the five documents in `.github/codeql/fixtures/` are one change apart from each other and prove that a report carrying a finding is refused, that a result naming no rule is still read as a finding, and that a file with no run or no loaded rule is refused too. WHAT IT DOES NOT PROVE IS THAT THE LOADED PACK FINDS A DEFECT OF THIS KIND IN THIS LANGUAGE, and that is stated here rather than left to be read off a green tick. Two deliberate defects matching the loaded queries were written on branches and reverted, once before this crate declared a dependency and once after, and both runs reported nothing; #81 carries both readings. The pack is fetched at run time rather than tracked here, so no reading of this tree settles why, and a green run of this leg is evidence about the reader and not about the queries. | | `dco.yml` | none, it runs only on a pull request | A contribution nobody certified they had the right to make | satisfied | `dco.yml` runs here. The text its refusal message points a contributor at landed through #106, and the second file that message names landed through #23. | | `dependency-review.yml` | none, it runs only on a pull request | A pull request that adds a dependency with a known advisory against it | satisfied | `dependency-review.yml` runs here. It reads the dependency graph of a change, which is a graph now: #19 landed the manifest and the committed lockfile, so a change moving a node in it is what this check reads. | | `dotnet.yml` | `build`, `ABI floor build`, and the two `Package (JPRM)` runs through `build.yml` | A change that does not compile, does not pass its tests, drops coverage on the surface that decides security outcomes, restores a dependency graph that drifted from the lockfile, or uses something the oldest supported line does not have | adapted | One workflow there is several things here, because this board separates them by check name: `build` is #15, the test run is #16, the locked restore is #19, the coverage bar is #84, and the packaging job is `build.yml`'s row. `ABI floor build` is adapted rather than adopted, since the core talks to a server over its interface instead of linking against it, so the floor is a server interface version and the leg runs the suite against the oldest line's fixtures, which is #88. |