/// Trust-on-first-use pinning for the Gemini protocol. Gemini hosts use /// self-signed certificates with no authority behind them, so the client /// itself decides what to trust: the first visit pins the server /// certificate's fingerprint, and later visits compare against that pin. /// /// A fingerprint change alone is ambiguous: it is the routine path when an /// operator renews an expired certificate and the alarm path when someone /// else is on the line. The decision below is expiry-aware, so the common /// renewal never interrupts the reader while an unexplained change, one that /// happens while the pinned certificate is still valid, does. library; import 'package:crypto/crypto.dart'; /// A pinned server certificate: its SHA-256 fingerprint over the DER /// encoding, and the certificate's own end of validity, which tells a later /// change from a renewal. class TofuPin { const TofuPin({required this.fingerprint, required this.expires}); /// Sixty-four lowercase hex characters. final String fingerprint; final DateTime expires; @override bool operator ==(Object other) => other is TofuPin && other.fingerprint == fingerprint; @override int get hashCode => fingerprint.hashCode; } /// The pin of a DER-encoded certificate. TofuPin pinOf(List derCertificate, {required DateTime expires}) => TofuPin(fingerprint: sha256.convert(derCertificate).toString(), expires: expires); /// What the client should do with a presented certificate. enum TofuDecision { /// Nothing is pinned yet; accept and pin what was presented. firstUse, /// The presented certificate matches the pin. match, /// The pin has expired and the presented certificate replaces it. rePin, /// The pin is still valid and the presented certificate is not it. refuse, } TofuDecision decideTofu(TofuPin? pin, String fingerprint, DateTime now) { if (pin == null) return TofuDecision.firstUse; if (pin.fingerprint == fingerprint) return TofuDecision.match; return pin.expires.isAfter(now) ? TofuDecision.refuse : TofuDecision.rePin; }