@@ -8,6 +8,8 @@ package ntp
88import (
99 "bytes"
1010 "context"
11+ "crypto/x509"
12+ "errors"
1113 "fmt"
1214 "math/bits"
1315 "net"
@@ -58,6 +60,13 @@ type Syncer struct {
5860 // NTS (Network Time Security) support
5961 UseNTS bool
6062 NTSNewSession NTSNewSessionFunc
63+
64+ // NTSBootstrapAttempts is the number of initial NTS session establishment
65+ // attempts during which a TLS certificate validity-period failure is tolerated
66+ // by retrying with a verifier that ignores certificate time constraints.
67+ NTSBootstrapAttempts int
68+
69+ ntsBootstrapUsed int
6170}
6271
6372// Measurement is a struct containing correction data based on a time request.
@@ -92,6 +101,8 @@ func NewSyncer(logger *zap.Logger, timeServers []string, useNTS bool) *Syncer {
92101
93102 UseNTS : useNTS ,
94103 NTSNewSession : DefaultNTSNewSession ,
104+
105+ NTSBootstrapAttempts : NTSBootstrapAttempts ,
95106 }
96107
97108 return syncer
@@ -521,7 +532,7 @@ func (syncer *Syncer) getNTSSession(server string) (NTSSession, error) {
521532 return nil , fmt .Errorf ("NTS session factory not configured" )
522533 }
523534
524- session , err := syncer .NTSNewSession (server )
535+ session , err := syncer .newNTSSession (server )
525536 if err != nil {
526537 return nil , err
527538 }
@@ -536,6 +547,51 @@ func (syncer *Syncer) getNTSSession(server string) (NTSSession, error) {
536547 return session , nil
537548}
538549
550+ // newNTSSession establishes an NTS session, applying the boot-time bootstrap
551+ // workaround for the TLS certificate time check.
552+ //
553+ // It first attempts strict validation. If that fails with a certificate
554+ // validity-period error and the bootstrap budget is not yet exhausted (and time
555+ // has not been synced yet), it retries with the certificate time check disabled
556+ // while still validating the chain and hostname. After NTSBootstrapAttempts such
557+ // retries, or once time is synced, validation is always strict.
558+ func (syncer * Syncer ) newNTSSession (server string ) (NTSSession , error ) {
559+ session , err := syncer .NTSNewSession (server , false )
560+ if err == nil {
561+ return session , nil
562+ }
563+
564+ if syncer .timeSyncNotified || syncer .ntsBootstrapUsed >= syncer .NTSBootstrapAttempts || ! isCertTimeError (err ) {
565+ return nil , err
566+ }
567+
568+ syncer .ntsBootstrapUsed ++
569+
570+ syncer .logger .Warn (
571+ "NTS certificate time validation failed, retrying while ignoring certificate time constraints (system clock may not be set yet)" ,
572+ zap .String ("server" , server ),
573+ zap .Int ("bootstrap_attempts_used" , syncer .ntsBootstrapUsed ),
574+ zap .Int ("bootstrap_attempts_limit" , syncer .NTSBootstrapAttempts ),
575+ zap .Error (err ),
576+ )
577+
578+ return syncer .NTSNewSession (server , true )
579+ }
580+
581+ // isCertTimeError reports whether err is a TLS certificate validation failure
582+ // caused by the certificate validity period (expired or not yet valid).
583+ //
584+ // Other certificate failures (unknown authority, hostname mismatch, etc.) are
585+ // deliberately excluded: those are genuine trust failures which must not be
586+ // bypassed even during bootstrap.
587+ func isCertTimeError (err error ) bool {
588+ if certErr , ok := errors.AsType [x509.CertificateInvalidError ](err ); ok {
589+ return certErr .Reason == x509 .Expired
590+ }
591+
592+ return false
593+ }
594+
539595// log2i returns 0 for v == 0 and v == 1.
540596func log2i (v uint64 ) int {
541597 if v == 0 {
0 commit comments