f5eac39ea ("feature/acme, ipn/ipnlocal: start moving ACME/cert state
into an extension") started to move the cert code into feature/acme
but was meant as a baby step.
This goes further, moving almost everything, leaving only some hooks
in ipnlocal.
When we later move "serve" support out to feature/serve, this will
look a bit different in that the hooks currently in ipnlocal will move
to feature/serve (cert support already depends on serve).
As part of this, cert-related tests move to feaure/acme too, which
means some test infra from ipnlocal now moves to shared ipnlocaltest.
(it's not big at the moment, but I imagine it growing)
Updates #12614
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9ea89aa9754f12d54b81751b6bd830f2664241ff
76 lines
2.0 KiB
Go
76 lines
2.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package acme
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// handleC2NTLSCertStatus returns info about the last TLS certificate
|
|
// issued for the provided domain. It is the implementation of
|
|
// [ipnlocal.HookHandleC2NTLSCertStatus]; control calls it to clean up
|
|
// DNS TXT records when they're no longer needed by LetsEncrypt.
|
|
//
|
|
// It does not kick off a cert fetch or async refresh. It only reports
|
|
// anything that's already sitting on disk, and only reports metadata
|
|
// about the public cert (stuff that'd be the in CT logs anyway).
|
|
func (e *extension) handleC2NTLSCertStatus(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
|
|
cs, err := e.getCertStore(b)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
domain := r.FormValue("domain")
|
|
if domain == "" {
|
|
http.Error(w, "no 'domain'", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ret := &tailcfg.C2NTLSCertInfo{}
|
|
pair, err := getCertPEMCached(cs, domain, b.Clock().Now())
|
|
ret.Valid = err == nil
|
|
if err != nil {
|
|
ret.Error = err.Error()
|
|
if errors.Is(err, errCertExpired) {
|
|
ret.Expired = true
|
|
} else if errors.Is(err, ipn.ErrStateNotExist) {
|
|
ret.Missing = true
|
|
ret.Error = "no certificate"
|
|
}
|
|
} else {
|
|
block, _ := pem.Decode(pair.CertPEM)
|
|
if block == nil {
|
|
ret.Error = "invalid PEM"
|
|
ret.Valid = false
|
|
} else {
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
ret.Error = fmt.Sprintf("invalid certificate: %v", err)
|
|
ret.Valid = false
|
|
} else {
|
|
ret.NotBefore = cert.NotBefore.UTC().Format(time.RFC3339)
|
|
ret.NotAfter = cert.NotAfter.UTC().Format(time.RFC3339)
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, ret)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|