57 lines
2 KiB
Dart
57 lines
2 KiB
Dart
|
|
/// 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<int> 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;
|
||
|
|
}
|