From 3973933c8af02765e26bf5afb6d099d164aced0a Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Fri, 10 Apr 2026 12:47:20 -0500 Subject: [PATCH] fix: enforce presented TLS chain validation in connect --- CHANGELOG.md | 2 + README.md | 1 + cmd/certkit/connect.go | 41 ++++ cmd/certkit/connect_test.go | 60 ++++++ connect.go | 235 +++++++++++++++++++--- connect_test.go | 208 ++++++++++++++++++-- go.mod | 12 +- go.sum | 41 ++-- probe_legacy.go | 5 + web/package-lock.json | 376 ++++++++++++++++++------------------ 10 files changed, 734 insertions(+), 247 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28db6c5c..829c3932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `verify --roots ` to include PEM/DER/PKCS#7/PKCS#12/JKS certificates as an additional file-backed trust source ([`0ee41ad`]) - Add `CheckTrustAnchorsResult` so library callers can inspect `trust_anchors` plus trust-source load warnings from `CheckTrustAnchors` ([#171]) - Display top-level X.509 certificate extensions with human-readable OID names in `inspect`, `verify --verbose`, and `connect --verbose`, including critical and unhandled-critical markers ([`512d96c`]) +- Add `connect --tls-version` to pin the negotiated protocol version when comparing server TLS behavior across TLS 1.0-1.3 ([#190]) ### Changed @@ -36,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Make `connect` fail when the peer omits part of the trust path and validation only succeeds after local chain completion, instead of accepting the incomplete server-presented chain ([#190]) - Use bundle folder name as Kubernetes secret `metadata.name` instead of the CN-derived prefix, so the secret name matches the export directory ([#178]) - Validate `bundleName` in bundle config YAML against DNS-1123 rules at load time; invalid names now produce a fatal error with the file path and line number ([#178]) - Deduplicate extension block formatting between `connect --verbose` and the shared internal formatter to prevent drift in extension flag rendering ([`30d67f8`]) diff --git a/README.md b/README.md index fa746e11..beebd3c1 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,7 @@ JSON output includes `trust_anchors` and `trust_warnings` for the leaf and displ | `--format` | `text` | Output format: text, json | | `--no-ocsp` | `false` | Disable automatic OCSP revocation check | | `--servername` | | Override SNI hostname (defaults to host) | +| `--tls-version` | | Pin TLS version: 1.0, 1.1, 1.2, or 1.3 (default: auto) | Port defaults to 443 if not specified. OCSP revocation status is checked automatically (best-effort); use `--no-ocsp` to disable. Use `--verbose` for extended details (serial, key info, signature algorithm, key usage, EKU, extensions) plus a PEM-formatted copy of the server-sent certificate chain with `# Subject`, `# Issuer`, and validity headers. diff --git a/cmd/certkit/connect.go b/cmd/certkit/connect.go index a7dbee2b..2fb2d056 100644 --- a/cmd/certkit/connect.go +++ b/cmd/certkit/connect.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" @@ -25,6 +26,7 @@ import ( var ( connectServerName string connectFormat string + connectTLSVersion string connectCRL bool connectNoOCSP bool connectCiphers bool @@ -34,6 +36,7 @@ var ( errConnectEmptyHost = errors.New("empty host") errConnectEmptyPort = errors.New("empty port") errConnectInvalidPort = errors.New("invalid port") + errConnectUnsupportedTLS = errors.New("unsupported TLS version") ) var connectCmd = &cobra.Command{ @@ -50,9 +53,13 @@ the server supports with security ratings. Network fetches for AIA/OCSP/CRL block private/internal endpoints by default. Use --allow-private-network to opt in for internal PKI environments. +Use --tls-version 1.2 or --tls-version 1.3 to compare version-specific +certificate presentation and validation behavior. + Exits with code 2 if chain verification fails or the certificate is revoked.`, Example: ` certkit connect example.com certkit connect example.com:8443 + certkit connect example.com --tls-version 1.2 certkit connect example.com --crl certkit connect example.com --ciphers certkit connect example.com --servername alt.example.com @@ -64,6 +71,7 @@ Exits with code 2 if chain verification fails or the certificate is revoked.`, func init() { connectCmd.Flags().StringVar(&connectServerName, "servername", "", "Override SNI hostname (defaults to host)") connectCmd.Flags().StringVar(&connectFormat, "format", "text", "Output format: text, json") + connectCmd.Flags().StringVar(&connectTLSVersion, "tls-version", "", "Pin TLS version: 1.0, 1.1, 1.2, or 1.3 (default: auto)") connectCmd.Flags().BoolVar(&connectCRL, "crl", false, "Check CRL distribution points for revocation") connectCmd.Flags().BoolVar(&connectNoOCSP, "no-ocsp", false, "Disable automatic OCSP revocation check") connectCmd.Flags().BoolVar(&connectCiphers, "ciphers", false, "Enumerate all supported cipher suites with security ratings") @@ -72,6 +80,7 @@ func init() { connectCmd.Flags().BoolVar(&connectAllowPrivateNetwork, "allow-private-network", false, "Allow AIA/OCSP/CRL fetches to private/internal endpoints") registerCompletion(connectCmd, completionInput{"format", fixedCompletion("text", "json")}) + registerCompletion(connectCmd, completionInput{"tls-version", fixedCompletion("1.0", "1.1", "1.2", "1.3")}) } // connectResultJSON is a JSON-serializable version of ConnectResult. @@ -127,6 +136,10 @@ func runConnect(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("parsing address %q: %w", args[0], err) } + version, err := parseConnectTLSVersion(connectTLSVersion) + if err != nil { + return fmt.Errorf("parsing --tls-version: %w", err) + } policy, err := selectedPolicy(connectFIPS1402, connectFIPS1403) if err != nil { return err @@ -141,6 +154,7 @@ func runConnect(cmd *cobra.Command, args []string) error { result, err := certkit.ConnectTLS(ctx, certkit.ConnectTLSInput{ Host: host, Port: port, + Version: version, ServerName: connectServerName, DisableOCSP: connectNoOCSP, CheckCRL: connectCRL, @@ -317,6 +331,23 @@ func runConnect(cmd *cobra.Command, args []string) error { return nil } +func parseConnectTLSVersion(raw string) (uint16, error) { + switch strings.TrimSpace(strings.ToLower(raw)) { + case "", "auto": + return 0, nil + case "1.0", "tls1.0", "tls1": + return tls.VersionTLS10, nil + case "1.1", "tls1.1": + return tls.VersionTLS11, nil + case "1.2", "tls1.2": + return tls.VersionTLS12, nil + case "1.3", "tls1.3": + return tls.VersionTLS13, nil + default: + return 0, fmt.Errorf("%w: %q (use auto, 1.0, 1.1, 1.2, or 1.3)", errConnectUnsupportedTLS, raw) + } +} + // formatConnectVerbose formats a ConnectResult with extended certificate details. func formatConnectVerbose(r *certkit.ConnectResult, now time.Time) string { var out strings.Builder @@ -417,6 +448,16 @@ func formatConnectChainPEM(chain []*x509.Certificate) string { } func collectConnectTrustStatus(result *certkit.ConnectResult) ([][]string, [][]string) { + if result.TrustPathStatus == certkit.ConnectTrustPathStatusPresentedInvalid && len(result.VerifiedChains) == 0 { + anchors := make([][]string, len(result.PeerChain)) + warnings := make([][]string, len(result.PeerChain)) + for i := range result.PeerChain { + anchors[i] = []string{} + warnings[i] = []string{} + } + return anchors, warnings + } + intermediates := connectTrustIntermediates(result) anchors := make([][]string, 0, len(result.PeerChain)) diff --git a/cmd/certkit/connect_test.go b/cmd/certkit/connect_test.go index 3bc574a6..da7b2fd4 100644 --- a/cmd/certkit/connect_test.go +++ b/cmd/certkit/connect_test.go @@ -3,6 +3,7 @@ package main import ( "crypto/ecdsa" "crypto/rand" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -134,6 +135,45 @@ func TestRunConnect_InvalidPortInput(t *testing.T) { } } +func TestParseConnectTLSVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want uint16 + wantErrContains string + }{ + {name: "auto default", input: "", want: 0}, + {name: "auto explicit", input: "auto", want: 0}, + {name: "tls12", input: "1.2", want: tls.VersionTLS12}, + {name: "tls13 alias", input: "tls1.3", want: tls.VersionTLS13}, + {name: "invalid", input: "1.4", wantErrContains: `unsupported TLS version: "1.4"`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := parseConnectTLSVersion(tt.input) + if tt.wantErrContains != "" { + if err == nil { + t.Fatalf("parseConnectTLSVersion(%q) expected error", tt.input) + } + if !strings.Contains(err.Error(), tt.wantErrContains) { + t.Fatalf("parseConnectTLSVersion(%q) error = %q, want substring %q", tt.input, err.Error(), tt.wantErrContains) + } + return + } + if err != nil { + t.Fatalf("parseConnectTLSVersion(%q) unexpected error: %v", tt.input, err) + } + if got != tt.want { + t.Fatalf("parseConnectTLSVersion(%q) = 0x%04x, want 0x%04x", tt.input, got, tt.want) + } + }) + } +} + func TestConnectTextStatusSectionConsistency(t *testing.T) { // WHY: shared status lines must stay identical between standard and verbose // connect output to prevent contract drift. @@ -145,6 +185,7 @@ func TestConnectTextStatusSectionConsistency(t *testing.T) { Protocol: "TLS 1.3", CipherSuite: "TLS_AES_128_GCM_SHA256", ServerName: "test.example.com", + VerifyError: "x509: certificate signed by unknown authority", ALPN: "h2", AIAFetched: true, CT: &certkit.CTResult{Status: "ok", Total: 3, Valid: 3}, @@ -191,6 +232,25 @@ func TestConnectTextStatusSectionConsistency(t *testing.T) { } } +func TestCollectConnectTrustStatus_PresentedTrustPathInvalid(t *testing.T) { + t.Parallel() + + result := &certkit.ConnectResult{ + PeerChain: []*x509.Certificate{{}}, + TrustPathStatus: certkit.ConnectTrustPathStatusPresentedInvalid, + VerifiedChains: nil, + ChainTrustAnchors: nil, + } + + anchors, warnings := collectConnectTrustStatus(result) + if len(anchors) != 1 || len(anchors[0]) != 0 { + t.Fatalf("anchors = %#v, want one empty entry", anchors) + } + if len(warnings) != 1 || len(warnings[0]) != 0 { + t.Fatalf("warnings = %#v, want one empty entry", warnings) + } +} + func TestFormatConnectVerbose_IncludesChainPEMWithMetadata(t *testing.T) { t.Parallel() diff --git a/connect.go b/connect.go index 76f0783d..2894387d 100644 --- a/connect.go +++ b/connect.go @@ -15,6 +15,7 @@ import ( "fmt" "io" "log/slog" + "maps" "net" "slices" "strings" @@ -30,6 +31,8 @@ const maxTextLineBytes = 16 * 1024 var ( errConnectHostRequired = errors.New("connecting to TLS server: host is required") + errConnectUnsupportedVersion = errors.New("connecting to TLS server: unsupported TLS version") + errConnectPresentedChain = errors.New("server did not present a valid trust path") errCipherScanHostRequired = errors.New("scanning cipher suites: host is required") errConnectNonTLSService = errors.New("remote service does not appear to speak TLS") errStartTLSUnsupportedProtocol = errors.New("unsupported STARTTLS protocol") @@ -143,9 +146,127 @@ func DiagnoseConnectChain(input DiagnoseConnectChainInput) []ChainDiagnostic { break } + if !presentedChainHasIssuerPath(input.PeerChain) && len(input.PeerChain) > 0 { + leaf := input.PeerChain[0] + diags = append(diags, ChainDiagnostic{ + Check: "missing-intermediate", + Status: "warn", + Detail: fmt.Sprintf("presented chain does not contain an issuer path for leaf %q to a higher CA certificate", FormatDNFromRaw(leaf.RawSubject, leaf.Subject)), + }) + } + return diags } +func diagnoseAIARepairedChain(peerChain, aiaCerts []*x509.Certificate) []ChainDiagnostic { + if len(peerChain) == 0 || len(aiaCerts) == 0 { + return nil + } + if isPeerChainMissingIssuer(peerChain) { + return []ChainDiagnostic{{ + Check: "missing-intermediate", + Status: "warn", + Detail: "server does not send intermediate certificates; chain was completed via AIA", + }} + } + return []ChainDiagnostic{{ + Check: "aia-repaired-chain", + Status: "warn", + Detail: "server did not present a valid trust path; local validation succeeded after AIA fetch", + }} +} + +func isPeerChainMissingIssuer(peerChain []*x509.Certificate) bool { + for i, cert := range peerChain { + if cert == nil || bytes.Equal(cert.RawIssuer, cert.RawSubject) { + continue + } + issuerPresent := false + for j := i + 1; j < len(peerChain); j++ { + candidate := peerChain[j] + if candidate != nil && bytes.Equal(cert.RawIssuer, candidate.RawSubject) { + issuerPresent = true + break + } + } + if !issuerPresent { + return true + } + } + return false +} + +func presentedChainHasIssuerPath(peerChain []*x509.Certificate) bool { + if len(peerChain) == 0 { + return false + } + + var dfs func(index int, visited map[int]bool) bool + dfs = func(index int, visited map[int]bool) bool { + cert := peerChain[index] + if cert == nil { + return false + } + candidateFound := false + for j, issuer := range peerChain { + if j == index || issuer == nil || !bytes.Equal(cert.RawIssuer, issuer.RawSubject) { + continue + } + if cert.CheckSignatureFrom(issuer) != nil { + continue + } + candidateFound = true + if visited[j] { + continue + } + nextVisited := make(map[int]bool, len(visited)+1) + maps.Copy(nextVisited, visited) + nextVisited[j] = true + if dfs(j, nextVisited) { + return true + } + } + return !candidateFound && (len(peerChain) == 1 || index != 0) + } + return dfs(0, map[int]bool{0: true}) +} + +func presentedChainBuildsVerifiedPath(peerChain []*x509.Certificate, verifiedChains [][]*x509.Certificate) bool { + if len(peerChain) == 0 || len(verifiedChains) == 0 { + return false + } + + peerCerts := make(map[string]bool, len(peerChain)) + for _, cert := range peerChain { + if cert != nil { + peerCerts[string(cert.Raw)] = true + } + } + + for _, chain := range verifiedChains { + if len(chain) == 0 { + continue + } + requiredCount := len(chain) - 1 + if len(chain) == 1 { + requiredCount = 1 + } + + complete := true + for i := range requiredCount { + cert := chain[i] + if cert == nil || !peerCerts[string(cert.Raw)] { + complete = false + break + } + } + if complete { + return true + } + } + return false +} + // SortDiagnostics sorts diagnostics: errors before warnings, then alphabetically // by check name within each group for stable output order. func SortDiagnostics(diags []ChainDiagnostic) { @@ -179,6 +300,19 @@ func DiagnoseVerifyError(verifyErr error) []ChainDiagnostic { return nil } +func validateConnectVersion(version uint16) error { + switch version { + case 0, tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12, tls.VersionTLS13: + return nil + default: + return fmt.Errorf("%w: 0x%04x", errConnectUnsupportedVersion, version) + } +} + +func allowLegacyFallback(version uint16) bool { + return version == 0 || version <= tls.VersionTLS12 +} + // DiagnoseNegotiatedCipher returns diagnostics for the cipher suite and protocol // version that were actually negotiated during the TLS handshake. This catches // issues like CBC mode or deprecated TLS versions even without a full --ciphers scan. @@ -245,6 +379,8 @@ type ConnectTLSInput struct { Host string // Port is the TCP port (default: "443"). Port string + // Version pins the TLS protocol version. Zero means auto-negotiate. + Version uint16 // ConnectTimeout is used when ctx has no deadline (default: 10s). ConnectTimeout time.Duration // ServerName overrides the SNI hostname (defaults to Host). @@ -322,6 +458,9 @@ type ConnectResult struct { TLSSCTs [][]byte `json:"-"` // VerifiedChains contains the verified certificate chains. VerifiedChains [][]*x509.Certificate `json:"-"` + // TrustPathStatus records whether the server-presented certificates + // themselves formed a valid trust path during connection verification. + TrustPathStatus ConnectTrustPathStatus `json:"-"` // VerifyError is non-empty if chain verification failed. VerifyError string `json:"verify_error,omitempty"` // Diagnostics contains chain configuration warnings (root-in-chain, duplicate-cert, missing-intermediate). @@ -349,12 +488,26 @@ type ConnectResult struct { LegacyProbe bool `json:"legacy_probe,omitempty"` } +// ConnectTrustPathStatus describes whether the certificates presented by the +// peer formed a complete trust path to a trusted anchor. +type ConnectTrustPathStatus string + +// ConnectTLS trust-path status values. +const ( + ConnectTrustPathStatusUnknown ConnectTrustPathStatus = "" + ConnectTrustPathStatusPresentedValid ConnectTrustPathStatus = "presented-valid" + ConnectTrustPathStatusPresentedInvalid ConnectTrustPathStatus = "presented-invalid" +) + // ConnectTLS connects to a TLS server and returns connection details including // the negotiated protocol, cipher suite, and peer certificate chain. func ConnectTLS(ctx context.Context, input ConnectTLSInput) (*ConnectResult, error) { if input.Host == "" { return nil, errConnectHostRequired } + if err := validateConnectVersion(input.Version); err != nil { + return nil, err + } port := input.Port if port == "" { port = "443" @@ -386,7 +539,11 @@ func ConnectTLS(ctx context.Context, input ConnectTLSInput) (*ConnectResult, err var clientAuth *ClientAuthInfo - tlsConf := newConnectTLSConfig(serverName, &clientAuth) + tlsConf := newConnectTLSConfig(connectTLSConfigInput{ + serverName: serverName, + clientAuth: &clientAuth, + version: input.Version, + }) tlsConn := tls.Client(sniffConn, tlsConf) defer func() { _ = tlsConn.Close() }() @@ -399,7 +556,7 @@ func ConnectTLS(ctx context.Context, input ConnectTLSInput) (*ConnectResult, err handshakeErr := tlsConn.HandshakeContext(connectCtx) var tlsAlert tls.AlertError - if handshakeErr != nil && clientAuth == nil && errors.As(handshakeErr, &tlsAlert) { + if handshakeErr != nil && clientAuth == nil && errors.As(handshakeErr, &tlsAlert) && allowLegacyFallback(input.Version) { // Close the failed TLS connection before opening a new one. // The deferred tlsConn.Close() will be a no-op after this. _ = tlsConn.Close() @@ -414,6 +571,7 @@ func ConnectTLS(ctx context.Context, input ConnectTLSInput) (*ConnectResult, err legacyResult, legacyErr := legacyFallbackConnect(fallbackCtx, legacyFallbackInput{ addr: addr, serverName: serverName, + version: input.Version, }) if legacyErr != nil { return nil, fmt.Errorf("tls handshake with %s: %w; legacy fallback: %w", addr, handshakeErr, legacyErr) @@ -577,20 +735,24 @@ func (result *ConnectResult) populate(ctx context.Context, input ConnectTLSInput result.Diagnostics = append(result.Diagnostics, DiagnoseConnectChain(DiagnoseConnectChainInput{ PeerChain: result.PeerChain, })...) + if !presentedChainHasIssuerPath(result.PeerChain) { + result.TrustPathStatus = ConnectTrustPathStatusPresentedInvalid + } } // Verify the chain ourselves to capture the error message. if len(result.PeerChain) > 0 { leaf := result.PeerChain[0] - opts := x509.VerifyOptions{ + serverIntermediates := x509.NewCertPool() + for _, cert := range result.PeerChain[1:] { + serverIntermediates.AddCert(cert) + } + serverVerifyOpts := x509.VerifyOptions{ DNSName: serverName, - Intermediates: x509.NewCertPool(), + Intermediates: serverIntermediates, Roots: input.RootCAs, } - for _, cert := range result.PeerChain[1:] { - opts.Intermediates.AddCert(cert) - } - chains, verifyErr := leaf.Verify(opts) + chains, verifyErr := leaf.Verify(serverVerifyOpts) if verifyErr != nil && !input.DisableAIA && len(leaf.IssuingCertificateURL) > 0 { // Attempt AIA walking to fetch missing intermediates. aiaTimeout := input.AIATimeout @@ -607,17 +769,23 @@ func (result *ConnectResult) populate(ctx context.Context, input ConnectTLSInput slog.Debug("AIA fetch warning", "warning", w) } if len(aiaCerts) > 0 { + aiaIntermediates := x509.NewCertPool() + for _, cert := range result.PeerChain[1:] { + aiaIntermediates.AddCert(cert) + } for _, c := range aiaCerts { - opts.Intermediates.AddCert(c) + aiaIntermediates.AddCert(c) } - chains, verifyErr = leaf.Verify(opts) - if verifyErr == nil { + aiaChains, aiaVerifyErr := leaf.Verify(x509.VerifyOptions{ + DNSName: serverName, + Intermediates: aiaIntermediates, + Roots: input.RootCAs, + }) + if aiaVerifyErr == nil { result.AIAFetched = true - result.Diagnostics = append(result.Diagnostics, ChainDiagnostic{ - Check: "missing-intermediate", - Status: "warn", - Detail: "server does not send intermediate certificates; chain was completed via AIA", - }) + result.VerifiedChains = aiaChains + result.TrustPathStatus = ConnectTrustPathStatusPresentedInvalid + result.Diagnostics = append(result.Diagnostics, diagnoseAIARepairedChain(result.PeerChain, aiaCerts)...) } } } @@ -625,7 +793,13 @@ func (result *ConnectResult) populate(ctx context.Context, input ConnectTLSInput result.VerifyError = verifyErr.Error() result.Diagnostics = append(result.Diagnostics, DiagnoseVerifyError(verifyErr)...) } else { - result.VerifiedChains = chains + if !presentedChainBuildsVerifiedPath(result.PeerChain, chains) { + result.VerifyError = errConnectPresentedChain.Error() + result.TrustPathStatus = ConnectTrustPathStatusPresentedInvalid + } else { + result.VerifiedChains = chains + result.TrustPathStatus = ConnectTrustPathStatusPresentedValid + } } } @@ -723,9 +897,15 @@ func (result *ConnectResult) populate(ctx context.Context, input ConnectTLSInput } } -func newConnectTLSConfig(serverName string, clientAuth **ClientAuthInfo) *tls.Config { - return &tls.Config{ - ServerName: serverName, +type connectTLSConfigInput struct { + serverName string + clientAuth **ClientAuthInfo + version uint16 +} + +func newConnectTLSConfig(input connectTLSConfigInput) *tls.Config { + cfg := &tls.Config{ + ServerName: input.serverName, InsecureSkipVerify: true, //nolint:gosec // We do our own verification below. GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { info := &ClientAuthInfo{Requested: true} @@ -745,10 +925,15 @@ func newConnectTLSConfig(serverName string, clientAuth **ClientAuthInfo) *tls.Co for _, scheme := range cri.SignatureSchemes { info.SignatureSchemes = append(info.SignatureSchemes, signatureSchemeString(scheme)) } - *clientAuth = info + *input.clientAuth = info return &tls.Certificate{}, nil }, } + if input.version != 0 { + cfg.MinVersion = input.version + cfg.MaxVersion = input.version + } + return cfg } type connectResultFromTLSStateInput struct { @@ -1024,7 +1209,11 @@ func connectViaStartTLS(handshakeCtx, verifyCtx context.Context, input connectVi bufferedConn := &bufferedPrefixConn{Conn: conn, reader: reader} var clientAuth *ClientAuthInfo - tlsConn := tls.Client(bufferedConn, newConnectTLSConfig(input.serverName, &clientAuth)) + tlsConn := tls.Client(bufferedConn, newConnectTLSConfig(connectTLSConfigInput{ + serverName: input.serverName, + clientAuth: &clientAuth, + version: input.connectInput.Version, + })) closeConn = tlsConn handshakeErr := tlsConn.HandshakeContext(handshakeCtx) state := tlsConn.ConnectionState() @@ -2860,6 +3049,8 @@ func FormatConnectStatusLines(r *ConnectResult) string { } switch { + case r.VerifyError != "" && r.AIAFetched: + fmt.Fprintf(&out, "Verify: failed (%s; locally completed via AIA)\n", r.VerifyError) case r.VerifyError != "": fmt.Fprintf(&out, "Verify: failed (%s)\n", r.VerifyError) case r.AIAFetched: diff --git a/connect_test.go b/connect_test.go index a1c20d7b..60b88e2c 100644 --- a/connect_test.go +++ b/connect_test.go @@ -348,6 +348,75 @@ func TestConnectTLS(t *testing.T) { } } +func TestConnectTLS_TLSVersionPin(t *testing.T) { + t.Parallel() + + root := generateTestCA(t, "Version Pin Root") + leaf := generateTestLeafCert(t, root) + + rootPool := x509.NewCertPool() + rootPool.AddCert(root.Cert) + + t.Run("pins tls 1.2 when server supports both", func(t *testing.T) { + t.Parallel() + + port := startTLSServerWithConfig(t, &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{{ + Certificate: [][]byte{leaf.DER, root.CertDER}, + PrivateKey: leaf.Key, + }}, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + result, err := ConnectTLS(ctx, ConnectTLSInput{ + Host: "127.0.0.1", + Port: port, + Version: tls.VersionTLS12, + RootCAs: rootPool, + }) + if err != nil { + t.Fatalf("ConnectTLS failed: %v", err) + } + if result.Protocol != "TLS 1.2" { + t.Fatalf("Protocol = %q, want %q", result.Protocol, "TLS 1.2") + } + if result.VerifyError != "" { + t.Fatalf("VerifyError = %q, want empty", result.VerifyError) + } + }) + + t.Run("fails when pinned version is unsupported", func(t *testing.T) { + t.Parallel() + + //nolint:gosec // This test intentionally constrains the server to TLS 1.2 to verify version pin failures. + port := startTLSServerWithConfig(t, &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{{ + Certificate: [][]byte{leaf.DER, root.CertDER}, + PrivateKey: leaf.Key, + }}, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := ConnectTLS(ctx, ConnectTLSInput{ + Host: "127.0.0.1", + Port: port, + Version: tls.VersionTLS13, + RootCAs: rootPool, + }) + if err == nil { + t.Fatal("expected handshake failure when TLS 1.3 is unavailable") + } + }) +} + func TestDetectStartTLSServerName(t *testing.T) { t.Parallel() @@ -2354,12 +2423,14 @@ func TestFormatConnectResult(t *testing.T) { notWantStrings: []string{"[WARN] hostname-mismatch:"}, }, { - name: "AIA-aware verify line", - aiaFetched: true, + name: "AIA-aware verify line", + verifyError: "x509: certificate signed by unknown authority", + aiaFetched: true, diagnostics: []ChainDiagnostic{ {Check: "missing-intermediate", Status: "warn", Detail: "server does not send intermediate certificates; chain was completed via AIA"}, }, - wantStrings: []string{"Verify: ok (intermediates fetched via AIA)", "[WARN] missing-intermediate:"}, + wantStrings: []string{"Verify: failed (x509: certificate signed by unknown authority; locally completed via AIA)", "[WARN] missing-intermediate:"}, + notWantStrings: []string{"Verify: ok"}, }, { name: "no diagnostics section when empty", @@ -2546,8 +2617,9 @@ func TestFormatConnectResult(t *testing.T) { } func TestDiagnoseConnectChain(t *testing.T) { - // WHY: DiagnoseConnectChain should flag root-in-chain, duplicate certs, and - // misordered chains without false positives for clean chains. + // WHY: DiagnoseConnectChain should flag root-in-chain, duplicate certs, + // missing intermediates, and misordered chains without false positives for + // clean chains. t.Parallel() root, intermediates, leaf := buildChain(t, 3) @@ -2591,8 +2663,8 @@ func TestDiagnoseConnectChain(t *testing.T) { { name: "missing intermediate does not trigger misordered-chain", peerChain: []*x509.Certificate{leaf, root}, - wantChecks: []string{"root-in-chain"}, - wantDetailContains: [][]string{{"Chain Root CA", "position 1"}}, + wantChecks: []string{"root-in-chain", "missing-intermediate"}, + wantDetailContains: [][]string{{"Chain Root CA", "position 1"}, {"chain-leaf.example.com", "issuer path"}}, }, { name: "duplicate-cert detected", @@ -2600,6 +2672,12 @@ func TestDiagnoseConnectChain(t *testing.T) { wantChecks: []string{"duplicate-cert"}, wantDetailContains: [][]string{{"CN=Intermediate CA 1", "positions 1 and 2"}}, }, + { + name: "duplicate leaf also flags missing intermediate", + peerChain: []*x509.Certificate{leaf, leaf}, + wantChecks: []string{"duplicate-cert", "missing-intermediate"}, + wantDetailContains: [][]string{{"chain-leaf.example.com", "positions 0 and 1"}, {"chain-leaf.example.com", "issuer path"}}, + }, { name: "leaf-only chain", peerChain: []*x509.Certificate{leaf}, @@ -2608,11 +2686,12 @@ func TestDiagnoseConnectChain(t *testing.T) { { name: "root-in-chain and duplicate-cert", peerChain: []*x509.Certificate{leaf, root, root}, - wantChecks: []string{"root-in-chain", "duplicate-cert", "root-in-chain"}, + wantChecks: []string{"root-in-chain", "duplicate-cert", "root-in-chain", "missing-intermediate"}, wantDetailContains: [][]string{ {"CN=Chain Root CA", "position 1"}, {"CN=Chain Root CA", "positions 1 and 2"}, {"CN=Chain Root CA", "position 2"}, + {"chain-leaf.example.com", "issuer path"}, }, }, } @@ -2668,6 +2747,46 @@ func TestDiagnoseConnectChain(t *testing.T) { } } +func TestPresentedChainBuildsVerifiedPath(t *testing.T) { + t.Parallel() + + root, intermediates, leaf := buildChain(t, 3) + verifiedChain := [][]*x509.Certificate{{leaf, intermediates[0], root}} + + tests := []struct { + name string + peerChain []*x509.Certificate + want bool + }{ + { + name: "accepts presented intermediate with root omitted", + peerChain: []*x509.Certificate{leaf, intermediates[0]}, + want: true, + }, + { + name: "rejects missing intermediate", + peerChain: []*x509.Certificate{leaf}, + want: false, + }, + { + name: "rejects wrong presented issuer", + peerChain: []*x509.Certificate{leaf, root}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := presentedChainBuildsVerifiedPath(tt.peerChain, verifiedChain) + if got != tt.want { + t.Fatalf("presentedChainBuildsVerifiedPath() = %v, want %v", got, tt.want) + } + }) + } +} + func TestSortDiagnostics(t *testing.T) { // WHY: SortDiagnostics must order errors before warnings and sort checks lexicographically. t.Parallel() @@ -3129,12 +3248,15 @@ func TestConnectTLS_AIAFetch(t *testing.T) { t.Errorf("leaf CN = %q, want %q", result.PeerChain[0].Subject.CommonName, "localhost") } - if result.VerifyError != "" { - t.Errorf("expected verify to succeed, got error %q", result.VerifyError) + if result.VerifyError == "" { + t.Error("expected presented-chain verification failure when server omits intermediate") } if !result.AIAFetched { t.Error("expected AIAFetched=true") } + if len(result.VerifiedChains) == 0 { + t.Fatal("expected AIA-completed VerifiedChains") + } missingIntermediate := false for _, diag := range result.Diagnostics { if diag.Check == "missing-intermediate" { @@ -3275,6 +3397,55 @@ func TestConnectTLS_RootInChainDiagnostic(t *testing.T) { } } +func TestConnectTLS_DuplicateLeafMissingIntermediateFails(t *testing.T) { + // WHY: Platform trust helpers must not mask a peer chain that duplicates the + // leaf and omits the required intermediate. + t.Parallel() + + root := generateTestCA(t, "Dup Leaf Missing Intermediate Root") + intermediate := generateIntermediateCA(t, root, "Dup Leaf Missing Intermediate CA") + leaf := generateTestLeafCert(t, intermediate) + + port := startTLSServer(t, [][]byte{leaf.DER, leaf.DER}, leaf.Key) + + rootPool := x509.NewCertPool() + rootPool.AddCert(root.Cert) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + result, err := ConnectTLS(ctx, ConnectTLSInput{ + Host: "127.0.0.1", + Port: port, + RootCAs: rootPool, + }) + if err != nil { + t.Fatalf("ConnectTLS failed: %v", err) + } + if result.VerifyError == "" { + t.Fatal("expected verification failure for duplicate leaf with missing intermediate") + } + if len(result.VerifiedChains) != 0 { + t.Fatalf("expected no VerifiedChains from incomplete presented path, got %d", len(result.VerifiedChains)) + } + + var missingIntermediate, duplicateLeaf bool + for _, diag := range result.Diagnostics { + switch diag.Check { + case "missing-intermediate": + missingIntermediate = true + case "duplicate-cert": + duplicateLeaf = true + } + } + if !missingIntermediate { + t.Fatal("expected missing-intermediate diagnostic") + } + if !duplicateLeaf { + t.Fatal("expected duplicate-cert diagnostic") + } +} + func TestConnectTLS_AIAFetch_FallbackURL(t *testing.T) { // WHY: ConnectTLS should continue AIA walking when earlier URLs fail. t.Parallel() @@ -3330,8 +3501,11 @@ func TestConnectTLS_AIAFetch_FallbackURL(t *testing.T) { if !result.AIAFetched { t.Fatal("expected AIAFetched=true") } - if result.VerifyError != "" { - t.Fatalf("expected AIA chain verification success, got %q", result.VerifyError) + if result.VerifyError == "" { + t.Fatal("expected presented-chain verification failure when server omits intermediate") + } + if len(result.VerifiedChains) == 0 { + t.Fatal("expected AIA-completed VerifiedChains") } } @@ -4406,13 +4580,17 @@ func TestConnectTLS_CRL_AIAFetchedIssuer(t *testing.T) { t.Fatalf("ConnectTLS failed: %v", err) } - // Chain should verify via AIA-fetched intermediate. - if result.VerifyError != "" { - t.Fatalf("expected chain to verify via AIA, got error: %s", result.VerifyError) + // Presented-chain verification should still fail, but AIA should provide a + // usable issuer for CRL validation. + if result.VerifyError == "" { + t.Fatal("expected presented-chain verification failure when server omits intermediate") } if !result.AIAFetched { t.Error("expected AIAFetched=true") } + if len(result.VerifiedChains) == 0 { + t.Fatal("expected AIA-completed VerifiedChains") + } // CRL check should use the AIA-fetched intermediate as issuer. if result.CRL == nil { diff --git a/go.mod b/go.mod index 27734583..c5536afe 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,16 @@ require ( github.com/breml/rootcerts v0.3.4 github.com/google/certificate-transparency-go v1.3.3 github.com/jmoiron/sqlx v1.4.0 - github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-isatty v0.0.21 github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/smallstep/pkcs7 v0.2.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - golang.org/x/crypto v0.49.0 - golang.org/x/sys v0.42.0 + golang.org/x/crypto v0.50.0 + golang.org/x/sys v0.43.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.47.0 - software.sslmate.com/src/go-pkcs12 v0.7.0 + modernc.org/sqlite v1.48.2 + software.sslmate.com/src/go-pkcs12 v0.7.1 ) require ( @@ -31,7 +31,7 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect k8s.io/klog/v2 v2.140.0 // indirect - modernc.org/libc v1.70.0 // indirect + modernc.org/libc v1.72.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 9d62577c..e9fb942a 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -67,8 +67,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -91,22 +91,21 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -116,8 +115,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -145,10 +144,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= -modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= +modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= +modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -157,8 +156,8 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= -modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -167,11 +166,11 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= -modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= +modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= -software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.7.1 h1:bxkUPRsvTPNRBZa4M/aSX4PyMOEbq3V8I6hbkG4F4Q8= +software.sslmate.com/src/go-pkcs12 v0.7.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/probe_legacy.go b/probe_legacy.go index d7de3acd..5a4de407 100644 --- a/probe_legacy.go +++ b/probe_legacy.go @@ -35,6 +35,7 @@ var ( errLegacyCertEntrySizeTruncated = errors.New("certificate entry truncated") errLegacyNoServerHello = errors.New("no server hello received") errLegacyNoServerCertificates = errors.New("no certificates received from server") + errLegacyVersionMismatch = errors.New("server negotiated unexpected TLS version during legacy fallback") ) // legacyCipherDef describes a cipher suite not implemented by Go's crypto/tls. @@ -338,6 +339,7 @@ func parseCertificateMessage(data []byte) ([]*x509.Certificate, error) { type legacyFallbackInput struct { addr string serverName string + version uint16 } // legacyFallbackResult contains the result of a legacy TLS fallback connection. @@ -399,6 +401,9 @@ func legacyFallbackConnect(ctx context.Context, input legacyFallbackInput) (*leg if shResult == nil { return nil, errLegacyNoServerHello } + if input.version != 0 && shResult.version != input.version { + return nil, fmt.Errorf("%w: expected %s, got %s", errLegacyVersionMismatch, tlsVersionString(input.version), tlsVersionString(shResult.version)) + } if len(certs) == 0 { return nil, errLegacyNoServerCertificates } diff --git a/web/package-lock.json b/web/package-lock.json index 5e604e92..52180e32 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,34 +14,32 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.9.tgz", + "integrity": "sha512-zd9c/Wdso6v1U7v6w3i/hbAr4K7NaSHImdpvmLt+Y9ea5BhilnIGNkfhOJ7FEIuPipAnE9tZeDOll05WDT0kgg==", "dev": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", - "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.9.tgz", + "integrity": "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg==", "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7" + "is-potential-custom-element-name": "^1.0.1" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -68,9 +66,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz", - "integrity": "sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==", + "version": "4.20260410.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260410.1.tgz", + "integrity": "sha512-dPZT4aXxwhGHFhWA9iZhWVfFoO8g9exiLzeaS8y43Dw0Sard6Gb3o5LJjReav3ejHbQLHUfGEiZsRPGW8qmgMg==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -170,9 +168,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", - "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", + "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", "dev": true, "funding": [ { @@ -215,21 +213,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", "optional": true, @@ -238,9 +236,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -274,26 +272,28 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -301,9 +301,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", - "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -318,9 +318,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz", - "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -335,9 +335,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz", - "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz", - "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -369,9 +369,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz", - "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -386,9 +386,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -406,9 +406,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz", - "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -426,9 +426,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -446,9 +446,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -466,9 +466,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -486,9 +486,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz", - "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -506,9 +506,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz", - "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -523,9 +523,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz", - "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -533,16 +533,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz", - "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -557,9 +559,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz", - "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -574,9 +576,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", - "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, @@ -624,31 +626,31 @@ "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", - "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", - "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.1", + "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -669,26 +671,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", - "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", - "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.1", + "@vitest/utils": "4.1.4", "pathe": "^2.0.3" }, "funding": { @@ -696,14 +698,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", - "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -712,9 +714,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", - "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", "dev": true, "license": "MIT", "funding": { @@ -722,15 +724,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", - "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", + "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -912,14 +914,14 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", - "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@asamuzakjp/dom-selector": "^7.0.3", + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", @@ -1226,9 +1228,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", + "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -1323,9 +1325,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "dev": true, "funding": [ { @@ -1372,14 +1374,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz", - "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.11" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -1388,21 +1390,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.11", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", - "@rolldown/binding-darwin-x64": "1.0.0-rc.11", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/saxes": { @@ -1464,9 +1466,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, "license": "MIT", "engines": { @@ -1474,14 +1476,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -1501,22 +1503,22 @@ } }, "node_modules/tldts": { - "version": "7.0.27", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", - "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.27" + "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.27", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", - "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", "dev": true, "license": "MIT" }, @@ -1555,9 +1557,9 @@ "optional": true }, "node_modules/undici": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", - "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", + "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", "dev": true, "license": "MIT", "engines": { @@ -1565,16 +1567,16 @@ } }, "node_modules/vite": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz", - "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.11", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -1592,7 +1594,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -1643,19 +1645,19 @@ } }, "node_modules/vitest": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", - "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.1", - "@vitest/mocker": "4.1.1", - "@vitest/pretty-format": "4.1.1", - "@vitest/runner": "4.1.1", - "@vitest/snapshot": "4.1.1", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -1666,7 +1668,7 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", + "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, @@ -1683,10 +1685,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.1", - "@vitest/browser-preview": "4.1.1", - "@vitest/browser-webdriverio": "4.1.1", - "@vitest/ui": "4.1.1", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -1710,6 +1714,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true },