nts 1.1.0
nts: ^1.1.0 copied to clipboard
Authenticated NTP via Network Time Security (RFC 8915) for Dart and Flutter using the Native Assets pipeline.
Changelog #
1.1.0 #
Protocol-compliance and reliability hardening across the Rust core. The
public Dart surface (ntsQuery, ntsWarmCookies, NtsServerSpec,
NtsTimeSample, NtsError) is unchanged; consumer-visible behaviour
improves on the timeout, cookie-cache, and error-classification paths.
Rust crate nts_rust is bumped from 0.1.0 to 0.2.0 to mark the
internal protocol-validation tightening; the bindings (lib/src/ffi/)
and Native Assets bridge are unaffected.
NTS-KE handshake (rust/src/nts/ke.rs) #
- Replace the OS-default TCP connect with a deadline-aware connection
loop that honours the caller's
timeoutMs. Earlier releases passed the budget only to the read/write side of the socket and letTcpStream::connectblock on the platform default (typically 75 s on macOS / 21 s on Linux), which madentsQuery(..., timeoutMs: 5000)hang for the full kernel default when the KE endpoint blackholed SYNs. The new loop iterates the resolved address list, computes the per-attempt deadline from the remaining budget, and surfaces aKeError::Io(ErrorKind::TimedOut)on the first exhausted attempt rather than the last. Mapped throughFrom<KeError> for NtsErrortoNtsError.timeoutso the Dart-side switch arm is reached. - Regression test
connect_with_timeout_respects_budget_for_unroutable_ipexercises the deadline against192.0.2.1(RFC 5737 TEST-NET-1) and asserts the call returns within 1.5× the configured budget.
Cookie management (rust/src/api/nts.rs) #
- Introduce a monotonically-increasing
generation: u64onSessionand propagate it intoQueryContext::session_generationso each in-flight NTPv4 query carries the identity of the handshake that produced its cookies.Session::deposit_cookiesnow gates the cookie-jar update on a matching generation: cookies extracted from a response signed under generation N are silently dropped if the session has been re-handshaked to generation N+1 between dispatch and receipt. This closes a cross-session poisoning window where a late response from a stale session could install cookies bound to retired keys, causing the nextntsQueryto dispatch unauthenticatable cookies and fail the AEAD seal. - The generation counter is also incremented on every successful
Session::rehandshake, so the stale-cookie filter applies symmetrically to both concurrent-query races and explicitntsWarmCookiesinvocations during an in-flight query.
NTPv4 header validation (rust/src/nts/ntp.rs) #
- Add
STRATUM_UNSYNCHRONIZED_FLOOR = 16and reject any post-AEAD reply withstratum >= 16asNtpError::Unsynchronized. RFC 5905 reserves stratum 16 as the "unsynchronized" sentinel and 17–255 as reserved; previous versions only filtered LI=3, so a server in the alarm condition could surface a wall-clock offset to the discipline loop if it left LI=0. - Reorder the validation so the Stratum-0 short-circuit (Kiss-o'-Death)
runs before the LI=3 / stratum-ceiling check. Real-world KoD
packets routinely arrive with LI=3 because the server has no
synchronised time to advertise; the previous ordering swallowed the
4-octet kiss code (
RATE,DENY,RSTR,NTSN, …) into a genericUnsynchronizederror and stripped the diagnostic the caller needs to choose a back-off strategy. - Validation remains positioned after AEAD
open()and the origin-timestamp check.stratumand the leap indicator are part of the NTP AAD, so by this point the server has signed the value; off-path attackers cannot forge KoD or stratum-16 to disrupt the client. The post-AEAD ordering is pinned by the*_after_seal_*_tamper_as_aead_failuretest family. - New regression tests:
parse_response_prefers_kod_over_unsynchronized_when_both_setpins the new precedence (Stratum 0 + LI=3 ⇒KissOfDeath).parse_response_rejects_invalid_high_stratumpins the new stratum-ceiling check (stratum 16 + LI=0 ⇒Unsynchronized).
- Broaden the
Displayarm and rustdoc onNtpError::Unsynchronizedto"server reports unsynchronized clock (LI=3 or stratum >= 16)"so the diagnostic accurately reflects both triggers; the message passes throughNtsError::NtpProtocol(..)to the Dart side unchanged.
Housekeeping #
rust/src/nts/records.rs: replacebody.len() % 2 != 0with!body.len().is_multiple_of(2)indecode_u16_arrayto satisfy theclippy::manual_is_multiple_oflint (warn-by-default in clippy 1.92, surfaced oncecargo clippy --all-targets -- -D warningswas added to the release gate). Behaviour is unchanged.
Verification #
cargo test --lib: 95 passed, 0 failed, 3 ignored (live-network).cargo clippy --tests --all-targets -- -D warnings: clean across the workspace.
1.0.7 #
Documentation and published-tarball hygiene. No changes to the published Dart surface, the Rust crate, or the Native Assets bridge.
-
example/lib/src/state/nts_controller.dart: prepend a 46-line dartdoc block torunQuerythat documents the NTS-KE cold-start cost (TCP + TLS 1.3 + KE handshake + first NTPv4 exchange ≈ 4 RTTs end to end, no session-ticket resumption), the steady-state path (cached session keys, in-band cookie pool replenishment, ~1 RTT), and the attribution boundary (the latency is RFC 8915 protocol overhead, notRustLib.init(), the Native Assets pipeline, or per-call FFI cost). Includes a production note pointing atexample/main.dart'sntsWarmCookies()warm-then-query pattern as the canonical way to amortize the cold-start cost; the GUI deliberately does not follow it so that the protocol observation tool surfaces the unmasked latency. -
Repository-wide documentation refactor (7 files:
pubspec.yaml,analysis_options.yaml,DEVELOPMENT.md,README.md,example/.pubignore,example/README.md,tool/check_bindings.dart) to replace meta-commentary about pub.dev scorecards,panarubrics, and tag-drop heuristics with objective technical justifications. The platform allow-list now reads as RFC 8915's raw TCP/UDP requirement plus rustls+ring's lack of a wasm32 target; the FRB pin is justified by the silent-memory-corruption risk of a wire-format mismatch; the analyzer-exclude removal is justified by lockstep with the consumer's analyzer view; the// ignore_for_file:directives inlib/src/ffi/**are justified bypublic_member_api_docsbeing enabled and the FFI surface not being excluded. The IANA AEAD-registry reference inexample/GUI_GUIDE.mdis preserved as a legitimate protocol citation. -
.pubignore(new, root): introduce a root.pubignorethat mirrors the root.gitignorepatterns (per dart.dev/go/pubignore, a directory's.pubignorereplaces its.gitignorefor publish purposes) and additionally excludes consumer-irrelevant files:AGENTS.md,CLAUDE.md(AI-agent guidance),ARCHITECTURE.md,DEVELOPMENT.md(self-identified contributor-only documentation),analysis_options.yaml(consumer analyzers read the consumer's own config),flutter_rust_bridge.yaml(FRB codegen config; bindings ship pre-generated),tool/(CI drift check for FRB regeneration), andtest/(internal FFI smoke test, not a public-API verifier). -
example/.pubignore: addanalysis_options.yamlandtest/to the example's exclusion list for the same reasons as the root. The canonical consumer entry point remainsexample/main.dart. -
Net effect verified via
dart pub publish --dry-run: the published tarball drops from 840 KB (1.0.6) to 824 KB, twelve maintainer-only files are stripped, and the warning/hint output is unchanged. No source files inlib/,rust/, orhook/are touched, so the binding drift gate and Native Assets build hook are unaffected.
1.0.6 #
Binding regen consequent on the 1.0.5 analyzer-exclude removal. No changes to the published Dart surface, the Rust crate, or the Native Assets bridge.
lib/src/ffi/frb_generated.dart: regenerate against the currentanalysis_options.yaml. Removing theanalyzer.exclude: [lib/src/ffi/**]block in 1.0.5 (nts-2cq) had a side effect that the bindings CI job did not surface until the next commit that re-triggered the job:flutter_rust_bridge_codegenruns an analyzer-aware fix-up over the Dart it emits before exiting, that pass was a no-op while the FFI files were excluded, and with the exclude gone the pass appliesprefer_final_localsandprefer_const_constructorsto the synthesized dispatcher boilerplate. The committed file (last regenerated in 1.0.2,0349077) was therefore stale relative to the codegen's deterministic output. The regen is purely cosmetic —varlocals insidedco_decode_nts_error/sse_decode_*becomefinal, and the two nullaryNtsErrorvariants gainconstprefixes — and produces no wire-format or public-API change. The file-level// ignore_for_file:directives managed bytool/check_bindings.dartstill suppress both rules so future codegen output that emits a non-final local or non-const constructor remains acceptable to pana without re-failing the drift gate.
1.0.5 #
Example clarity and pub.dev metadata fidelity. No changes to the published Dart surface, the Rust crate, or the Native Assets bridge.
-
example/main.dart: switch the minimal sample from a singlentsQuery()call to a warm-then-query flow that callsntsWarmCookies()first and thenntsQuery(). The original one-call form lumped the NTS-KE handshake into the same latency budget as the NTPv4 exchange and never made the cookie pool visible; the new form mirrors the production access pattern, surfaces thecookies_remainingcounter onNtsTimeSample, and gives readers a self-contained reference for both stages of the protocol.example/example.mdis regenerated as a byte-for-byte fenced mirror so the pub.dev Example tab tracks the runnable sample. The exhaustiveNtsErrorswitch and theRustLib.init()bootstrap order are unchanged. -
example/example.md: drop the developer-facing meta-commentary about the rendering quirk that motivated the file's existence (panapriority list, theexample/main.dartshadowing dance from 1.0.3 / 1.0.4). The fenced sample is the consumer-visible artefact; the rendering history is recorded in this changelog and in thents-9tdcommit message, not in the file pub.dev publishes. -
analysis_options.yaml: remove theanalyzer.exclude: [lib/src/ffi/**]block so localdart analyze/flutter analyzeruns see the same surface pana sees on pub.dev. The FRB-generated files inlib/src/ffi/carry file-level// ignore_for_file:directives (managed bytool/check_bindings.dartand landed in 1.0.2) for the rules they cannot satisfy, which pana respects butanalyzer.excludedoes not — keeping both meant local CI was strictly more permissive than the pub.dev scorecard. With the exclude removed, lint drift between the two environments is impossible. -
pubspec.yaml: add a top-levelplatforms:allow-list withandroid,ios,macos,linux,windows. Earlier releases shipped without this block, which let pana award thewebandwasmplatform tags on the strength of the Dart surface compiling cleanly underdart2js/dart2wasm— but actual runtime use of any nts API on Web cannot work, because RFC 8915 needs raw TCP for NTS-KE on:4460and raw UDP for NTPv4 on:123(neither of which browsers expose to web pages), and therustls+ring+rustls-platform-verifierstack does not targetwasm32-unknown-unknown. Declaring the supported platforms explicitly drops both incorrect tags from the next pana rescore so the pub.dev scorecard reflects the package's true platform surface.
1.0.4 #
pub.dev Example tab fix (take two). No runtime changes.
-
Add
example/example.mdcontaining the minimal NTS-KE sample as a fenced ```dart block plus a pointer to the Flutter GUI showcase atexample/lib/main.dart. The 1.0.3 rename of the minimal sample toexample/main.dartdid not unblock the Example tab: empirical check on the published version-pinned URL still renderedexample/lib/main.dart. The bracket notationexample[/lib]/main.dartin dart.dev's package-layout doc is shorthand for two separate slots in pana's selection list, with thelib/form ranked higher than the bare form. The actual list lives inpana/lib/src/maintenance.dart:example/README.mdexample/example.md← new in 1.0.4, secures the slotexample/lib/main.dart(GUI showcase, no longer rendered)example/bin/main.dartexample/main.dart(1.0.3 rename target, also no longer rendered)
Slot 2 beats slot 3, so the new
example/example.mdfinally wins overexample/lib/main.dart. The minimal sample atexample/main.dartstays in the archive as the runnable Flutter target; the.mdis just a syntactic mirror so pub.dev picks it. -
No changes to the published Dart surface, the Rust crate, or the Native Assets bridge. The two new lines in
pubspec.yamlandCHANGELOG.mdare the only metadata edits.
1.0.3 #
pub.dev Example tab fix. No runtime changes.
- Rename
example/example.darttoexample/main.dartso pub.dev's Example tab renders the intended minimal single-call sample. pub.dev picks the rendered file from a hardcoded priority list documented at https://dart.dev/tools/pub/package-layout#examples; the previous layout placed the minimal sample at priority 5 (example[/lib]/example.dart) where it was shadowed by the Flutter GUI showcase at priority 2 (example/lib/main.dart). The bareexample/main.dartslot also sits at priority 2 but wins over thelib/variant, so the rename promotes the minimal sample without removing the GUI showcase from the published tarball. - Update
example/README.mdto spell the GUI entry point explicitly asflutter run -t lib/main.dart(or-t example/lib/main.dartfrom the repo root) so contributors don't accidentally launch the new top-levelexample/main.dartas the Flutter target. - Update root
README.mdandARCHITECTURE.mdto reference the new path. The 1.0.1 changelog entry that introducedexample/example.dartis left unchanged for historical accuracy.
1.0.2 #
Static-analysis score recovery. No runtime changes.
- Suppress pana-only lints across the FRB-generated bindings via the
// ignore_for_file:directive of each file, applied as a post-codegen patch step intool/check_bindings.dart. pana's static-analysis run uses a stricter ruleset thanflutter_lintsand surfaced 117+ INFO lints against the synthesized freezed wrappers (NtsError), auto-generated default constructors (NtsServerSpec,NtsTimeSample), and dispatcher boilerplate that FRB cannot back with Rust docstrings, costing 10 pub points. Patched files and rules:lib/src/ffi/api/nts.dart:public_member_api_docs.lib/src/ffi/frb_generated.dart:public_member_api_docs,prefer_final_locals,prefer_const_constructors.lib/src/ffi/frb_generated.io.dart:public_member_api_docs.lib/src/ffi/frb_generated.web.dart:public_member_api_docs. Localpana 0.23.12now reports 160 / 160 against the working tree.
1.0.1 #
Documentation and pub.dev metadata polish. No runtime changes.
- Restructure README around a What → Why → How flow and offload the
Rust toolchain, build hooks, and crate breakdown into new
ARCHITECTURE.mdandDEVELOPMENT.mdreference documents. - Add a self-contained
example/example.dartfor pub.dev's Example tab. - Resolve two
dartdocunresolved-reference warnings inlib/src/ffi/api/nts.dartby replacing Rust intra-doc link syntax with literal values in the upstream Rust docstrings and regenerating the bindings. - Trim the package description to fit pana's 180-char ceiling, add
five pub.dev topics (
ntp,time,networking,security,cryptography), and registerscreenshots/gui_showcase.pngas the package listing screenshot. - Expand the inline comment on the
flutter_rust_bridge: 2.12.0pin to document the wire-format rationale and the accepted pana warning.
1.0.0 #
Initial stable release.
Protocol #
- Network Time Security (RFC 8915) client implementing the full NTS-KE
handshake (TLS 1.3, ALPN
ntske/1, port 4460) followed by AEAD-protected NTPv4 (RFC 5905) over UDP/123. - AEAD algorithms: AES-SIV-CMAC-256 (IANA ID 15, default) and AES-128-GCM-SIV (IANA ID 16), negotiated during NTS-KE.
- Cookie management: in-memory cookie jar with automatic refresh via
ntsWarmCookies()when the pool is exhausted.
API #
ntsQuery({required NtsServerSpec spec, required int timeoutMs})returnsFuture<NtsTimeSample>with server transmit time, round-trip duration, stratum, negotiated AEAD ID, and fresh cookie count.ntsWarmCookies({required NtsServerSpec spec, required int timeoutMs})forces a fresh handshake and reports the number of cookies received.NtsErrorsealed class with eight typed variants (invalidSpec,network,keProtocol,ntpProtocol,authentication,timeout,noCookies,internal) for exhaustive pattern matching.
Implementation #
- Cryptographic core implemented in Rust (
rustlsfor TLS 1.3,aes-siv/aes-gcmfor AEAD,ringfor primitives). - Bridged to Dart via
flutter_rust_bridge2.12.0 (pinned exactly to match the Rust crate's wire format). - Bundled through the stable Native Assets API (
hook/build.dart+native_toolchain_rust); no manualcargoinvocation required from consumers.
Platform support #
Android, iOS, macOS, Linux, Windows. Web is not supported (no UDP socket primitive in the browser).
Build #
- Default release builds use the
log-stripCargo feature, elidinginfo!/debug!/trace!format strings at compile time;warn!anderror!survive for diagnostics. - The
verbose_logsuser-define inpubspec.yamlopts into a debug build with full logging (includingrustlsprotocol traces) for development.
Tooling #
tool/check_bindings.dartregenerates FRB bindings and fails CI if the committed Dart bindings orrust/src/frb_generated.rsdrift from the generator output.- CI matrix exercises both the declared SDK floor (Flutter 3.38.10 / Dart 3.10.9) and the pinned development version (Flutter 3.41.7 / Dart 3.11.5).
Requirements #
- Dart
^3.10.0, Flutter>=3.38.0. The lower bound matches thehookspackage (>=1.0.3) requirement. - Native Assets API (stable since Flutter 3.24).