diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/caddyhttp/autohttps.go | 99 | ||||
-rw-r--r-- | modules/caddypki/ca.go | 12 | ||||
-rw-r--r-- | modules/caddypki/certificates.go | 5 | ||||
-rw-r--r-- | modules/caddypki/crypto.go | 74 | ||||
-rw-r--r-- | modules/caddytls/automation.go | 44 | ||||
-rw-r--r-- | modules/caddytls/certmanagers.go | 208 | ||||
-rw-r--r-- | modules/caddytls/folderloader.go | 35 |
7 files changed, 339 insertions, 138 deletions
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 6c37d70..3e38d1b 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -31,13 +31,20 @@ import ( // HTTPS is enabled automatically and by default when // qualifying hostnames are available from the config. type AutoHTTPSConfig struct { - // If true, automatic HTTPS will be entirely disabled. + // If true, automatic HTTPS will be entirely disabled, + // including certificate management and redirects. Disabled bool `json:"disable,omitempty"` // If true, only automatic HTTP->HTTPS redirects will - // be disabled. + // be disabled, but other auto-HTTPS features will + // remain enabled. DisableRedir bool `json:"disable_redirects,omitempty"` + // If true, automatic certificate management will be + // disabled, but other auto-HTTPS features will + // remain enabled. + DisableCerts bool `json:"disable_certificates,omitempty"` + // Hosts/domain names listed here will not be included // in automatic HTTPS (they will not have certificates // loaded nor redirects applied). @@ -104,12 +111,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er srv.AutoHTTPS = new(AutoHTTPSConfig) } if srv.AutoHTTPS.Disabled { + app.logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName)) continue } // skip if all listeners use the HTTP port if !srv.listenersUseAnyPortOtherThan(app.httpPort()) { - app.logger.Info("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server", + app.logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server", zap.String("server_name", srvName), zap.Int("http_port", app.httpPort()), ) @@ -166,30 +174,35 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // for all the hostnames we found, filter them so we have // a deduplicated list of names for which to obtain certs - for d := range serverDomainSet { - if certmagic.SubjectQualifiesForCert(d) && - !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { - // if a certificate for this name is already loaded, - // don't obtain another one for it, unless we are - // supposed to ignore loaded certificates - if !srv.AutoHTTPS.IgnoreLoadedCerts && - len(app.tlsApp.AllMatchingCertificates(d)) > 0 { - app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", - zap.String("domain", d), - zap.String("server_name", srvName), - ) - continue - } + // (only if cert management not disabled for this server) + if srv.AutoHTTPS.DisableCerts { + app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) + } else { + for d := range serverDomainSet { + if certmagic.SubjectQualifiesForCert(d) && + !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { + // if a certificate for this name is already loaded, + // don't obtain another one for it, unless we are + // supposed to ignore loaded certificates + if !srv.AutoHTTPS.IgnoreLoadedCerts && + len(app.tlsApp.AllMatchingCertificates(d)) > 0 { + app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", + zap.String("domain", d), + zap.String("server_name", srvName), + ) + continue + } - // most clients don't accept wildcards like *.tld... we - // can handle that, but as a courtesy, warn the user - if strings.Contains(d, "*") && - strings.Count(strings.Trim(d, "."), ".") == 1 { - app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", - zap.String("domain", d)) - } + // most clients don't accept wildcards like *.tld... we + // can handle that, but as a courtesy, warn the user + if strings.Contains(d, "*") && + strings.Count(strings.Trim(d, "."), ".") == 1 { + app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", + zap.String("domain", d)) + } - uniqueDomainsForCerts[d] = struct{}{} + uniqueDomainsForCerts[d] = struct{}{} + } } } @@ -200,12 +213,11 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // nothing left to do if auto redirects are disabled if srv.AutoHTTPS.DisableRedir { + app.logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName)) continue } - app.logger.Info("enabling automatic HTTP->HTTPS redirects", - zap.String("server_name", srvName), - ) + app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName)) // create HTTP->HTTPS redirects for _, addr := range srv.Listen { @@ -459,6 +471,16 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri } } + // if no external managers were configured, enable + // implicit Tailscale support for convenience + if ap.Managers == nil { + ts, err := implicitTailscale(ctx) + if err != nil { + return err + } + ap.Managers = []certmagic.CertificateManager{ts} + } + // while we're here, is this the catch-all/base policy? if !foundBasePolicy && len(ap.Subjects) == 0 { basePolicy = ap @@ -467,8 +489,14 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri } if basePolicy == nil { - // no base policy found, we will make one! - basePolicy = new(caddytls.AutomationPolicy) + // no base policy found, we will make one! (with implicit Tailscale integration) + ts, err := implicitTailscale(ctx) + if err != nil { + return err + } + basePolicy = &caddytls.AutomationPolicy{ + Managers: []certmagic.CertificateManager{ts}, + } } // if the basePolicy has an existing ACMEIssuer (particularly to @@ -482,8 +510,8 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri } } if baseACMEIssuer == nil { - // note that this happens if basePolicy.Issuer is nil - // OR if it is not nil but is not an ACMEIssuer + // note that this happens if basePolicy.Issuers is empty + // OR if it is not empty but does not have not an ACMEIssuer baseACMEIssuer = new(caddytls.ACMEIssuer) } @@ -653,4 +681,11 @@ func (app *App) automaticHTTPSPhase2() error { return nil } +// implicitTailscale returns a new and provisioned Tailscale module configured to be optional. +func implicitTailscale(ctx caddy.Context) (caddytls.Tailscale, error) { + ts := caddytls.Tailscale{Optional: true} + err := ts.Provision(ctx) + return ts, err +} + type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index e3102fb..7fefee6 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -239,7 +239,7 @@ func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, e if err != nil { return nil, nil, fmt.Errorf("loading root key: %v", err) } - rootKey, err = pemDecodePrivateKey(rootKeyPEM) + rootKey, err = certmagic.PEMDecodePrivateKey(rootKeyPEM) if err != nil { return nil, nil, fmt.Errorf("decoding root key: %v", err) } @@ -263,7 +263,7 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err err if err != nil { return nil, nil, fmt.Errorf("saving root certificate: %v", err) } - rootKeyPEM, err := pemEncodePrivateKey(rootKey) + rootKeyPEM, err := certmagic.PEMEncodePrivateKey(rootKey) if err != nil { return nil, nil, fmt.Errorf("encoding root key: %v", err) } @@ -275,7 +275,7 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err err return rootCert, rootKey, nil } -func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { +func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.PrivateKey) (interCert *x509.Certificate, interKey crypto.PrivateKey, err error) { interCertPEM, err := ca.storage.Load(ca.storageKeyIntermediateCert()) if err != nil { if _, ok := err.(certmagic.ErrNotExist); !ok { @@ -301,7 +301,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface if err != nil { return nil, nil, fmt.Errorf("loading intermediate key: %v", err) } - interKey, err = pemDecodePrivateKey(interKeyPEM) + interKey, err = certmagic.PEMDecodePrivateKey(interKeyPEM) if err != nil { return nil, nil, fmt.Errorf("decoding intermediate key: %v", err) } @@ -310,7 +310,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface return interCert, interKey, nil } -func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { +func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.PrivateKey) (interCert *x509.Certificate, interKey crypto.PrivateKey, err error) { repl := ca.newReplacer() interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey) @@ -325,7 +325,7 @@ func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (i if err != nil { return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err) } - interKeyPEM, err := pemEncodePrivateKey(interKey) + interKeyPEM, err := certmagic.PEMEncodePrivateKey(interKey) if err != nil { return nil, nil, fmt.Errorf("encoding intermediate key: %v", err) } diff --git a/modules/caddypki/certificates.go b/modules/caddypki/certificates.go index a55c165..bd260da 100644 --- a/modules/caddypki/certificates.go +++ b/modules/caddypki/certificates.go @@ -15,6 +15,7 @@ package caddypki import ( + "crypto" "crypto/x509" "time" @@ -30,7 +31,7 @@ func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey inte return newCert(rootProfile) } -func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey interface{}) (cert *x509.Certificate, privateKey interface{}, err error) { +func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey crypto.PrivateKey) (cert *x509.Certificate, privateKey crypto.PrivateKey, err error) { interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey) if err != nil { return @@ -39,7 +40,7 @@ func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey return newCert(interProfile) } -func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey interface{}, err error) { +func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey crypto.PrivateKey, err error) { certBytes, err := profile.CreateCertificate() if err != nil { return diff --git a/modules/caddypki/crypto.go b/modules/caddypki/crypto.go index dbc6f38..386ce62 100644 --- a/modules/caddypki/crypto.go +++ b/modules/caddypki/crypto.go @@ -17,14 +17,12 @@ package caddypki import ( "bytes" "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "os" - "strings" + + "github.com/caddyserver/certmagic" ) func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { @@ -45,70 +43,6 @@ func pemEncodeCert(der []byte) ([]byte, error) { return pemEncode("CERTIFICATE", der) } -// pemEncodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes. -// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported. -func pemEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) { - var pemType string - var keyBytes []byte - switch key := key.(type) { - case *ecdsa.PrivateKey: - var err error - pemType = "EC" - keyBytes, err = x509.MarshalECPrivateKey(key) - if err != nil { - return nil, err - } - case *rsa.PrivateKey: - pemType = "RSA" - keyBytes = x509.MarshalPKCS1PrivateKey(key) - case *ed25519.PrivateKey: - var err error - pemType = "ED25519" - keyBytes, err = x509.MarshalPKCS8PrivateKey(key) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("unsupported key type: %T", key) - } - return pemEncode(pemType+" PRIVATE KEY", keyBytes) -} - -// pemDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. -// Borrowed from Go standard library, to handle various private key and PEM block types. -// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308 -// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238) -// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported. -func pemDecodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) { - keyBlockDER, _ := pem.Decode(keyPEMBytes) - if keyBlockDER == nil { - return nil, fmt.Errorf("no PEM data found") - } - - if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { - return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) - } - - if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil { - return key, nil - } - - if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil { - switch key := key.(type) { - case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: - return key, nil - default: - return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key) - } - } - - if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil { - return key, nil - } - - return nil, fmt.Errorf("unknown private key type") -} - func pemEncode(blockType string, b []byte) ([]byte, error) { var buf bytes.Buffer err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: b}) @@ -137,7 +71,7 @@ type KeyPair struct { } // Load loads the certificate and key. -func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) { +func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) { switch kp.Format { case "", "pem_file": certData, err := os.ReadFile(kp.Certificate) @@ -153,7 +87,7 @@ func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) { if err != nil { return nil, nil, err } - key, err := pemDecodePrivateKey(keyData) + key, err := certmagic.PEMDecodePrivateKey(keyData) if err != nil { return nil, nil, err } diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 2a701bf..95b1772 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -89,6 +89,14 @@ type AutomationPolicy struct { // zerossl. IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"` + // Modules that can get a custom certificate to use for any + // given TLS handshake at handshake-time. Custom certificates + // can be useful if another entity is managing certificates + // and Caddy need only get it and serve it. + // + // TODO: This is an EXPERIMENTAL feature. It is subject to change or removal. + ManagersRaw []json.RawMessage `json:"get_certificate,omitempty" caddy:"namespace=tls.get_certificate inline_key=via"` + // If true, certificates will be requested with MustStaple. Not all // CAs support this, and there are potentially serious consequences // of enabling this feature without proper threat modeling. @@ -113,7 +121,7 @@ type AutomationPolicy struct { // If true, certificates will be managed "on demand"; that is, during // TLS handshakes or when needed, as opposed to at startup or config - // load. + // load. This enables On-Demand TLS for this policy. OnDemand bool `json:"on_demand,omitempty"` // Disables OCSP stapling. Disabling OCSP stapling puts clients at @@ -129,10 +137,12 @@ type AutomationPolicy struct { // EXPERIMENTAL. Subject to change. OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"` - // Issuers stores the decoded issuer parameters. This is only - // used to populate an underlying certmagic.Config's Issuers - // field; it is not referenced thereafter. - Issuers []certmagic.Issuer `json:"-"` + // Issuers and Managers store the decoded issuer and manager modules; + // they are only used to populate an underlying certmagic.Config's + // fields during provisioning so that the modules can survive a + // re-provisioning. + Issuers []certmagic.Issuer `json:"-"` + Managers []certmagic.CertificateManager `json:"-"` magic *certmagic.Config storage certmagic.Storage @@ -153,6 +163,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { ap.storage = cmStorage } + // on-demand TLS var ond *certmagic.OnDemandConfig if ap.OnDemand { ond = &certmagic.OnDemandConfig{ @@ -176,6 +187,22 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { } } + // we don't store loaded modules directly in the certmagic config since + // policy provisioning may happen more than once (during auto-HTTPS) and + // loading a module clears its config bytes; thus, load the module and + // store them on the policy before putting it on the config + + // load and provision any cert manager modules + if ap.ManagersRaw != nil { + vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw") + if err != nil { + return fmt.Errorf("loading external certificate manager modules: %v", err) + } + for _, getCertVal := range vals.([]interface{}) { + ap.Managers = append(ap.Managers, getCertVal.(certmagic.CertificateManager)) + } + } + // load and provision any explicitly-configured issuer modules if ap.IssuersRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw") @@ -225,9 +252,10 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { DisableStapling: ap.DisableOCSPStapling, ResponderOverrides: ap.OCSPOverrides, }, - Storage: storage, - Issuers: issuers, - Logger: tlsApp.logger, + Storage: storage, + Issuers: issuers, + Managers: ap.Managers, + Logger: tlsApp.logger, } ap.magic = certmagic.New(tlsApp.certCache, template) diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go new file mode 100644 index 0000000..653e9f5 --- /dev/null +++ b/modules/caddytls/certmanagers.go @@ -0,0 +1,208 @@ +package caddytls + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/certmagic" + "github.com/tailscale/tscert" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(Tailscale{}) + caddy.RegisterModule(HTTPCertGetter{}) +} + +// Tailscale is a module that can get certificates from the local Tailscale process. +type Tailscale struct { + // If true, this module will operate in "best-effort" mode and + // ignore "soft" errors; i.e. try Tailscale, and if it doesn't connect + // or return a certificate, oh well. Failure to connect to Tailscale + // results in a no-op instead of an error. Intended for the use case + // where this module is added implicitly for convenience, even if + // Tailscale isn't necessarily running. + Optional bool `json:"optional,omitempty"` + + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (Tailscale) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.get_certificate.tailscale", + New: func() caddy.Module { return new(Tailscale) }, + } +} + +func (ts *Tailscale) Provision(ctx caddy.Context) error { + ts.logger = ctx.Logger(ts) + return nil +} + +func (ts Tailscale) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + canGetCert, err := ts.canHazCertificate(ctx, hello) + if err == nil && !canGetCert { + return nil, nil // pass-thru: Tailscale can't offer a cert for this name + } + if err != nil { + ts.logger.Warn("could not get status; will try to get certificate anyway", zap.Error(err)) + } + return tscert.GetCertificate(hello) +} + +// canHazCertificate returns true if Tailscale reports it can get a certificate for the given ClientHello. +func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (bool, error) { + if ts.Optional && !strings.HasSuffix(strings.ToLower(hello.ServerName), tailscaleDomainAliasEnding) { + return false, nil + } + status, err := tscert.GetStatus(ctx) + if err != nil { + if ts.Optional { + return false, nil // ignore error if we don't expect/require it to work anyway + } + return false, err + } + for _, domain := range status.CertDomains { + if certmagic.MatchWildcard(hello.ServerName, domain) { + return true, nil + } + } + return false, nil +} + +// UnmarshalCaddyfile deserializes Caddyfile tokens into ts. +// +// ... tailscale +// +func (Tailscale) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + } + return nil +} + +// tailscaleDomainAliasEnding is the ending for all Tailscale custom domains. +const tailscaleDomainAliasEnding = ".ts.net" + +// HTTPCertGetter can get a certificate via HTTP(S) request. +type HTTPCertGetter struct { + // The URL from which to download the certificate. Required. + // + // The URL will be augmented with query string parameters taken + // from the TLS handshake: + // + // - server_name: The SNI value + // - signature_schemes: Comma-separated list of hex IDs of signatures + // - cipher_suites: Comma-separated list of hex IDs of cipher suites + // + // To be valid, the response must be HTTP 200 with a PEM body + // consisting of blocks for the certificate chain and the private + // key. + URL string `json:"url,omitempty"` + + ctx context.Context +} + +// CaddyModule returns the Caddy module information. +func (hcg HTTPCertGetter) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.get_certificate.http", + New: func() caddy.Module { return new(HTTPCertGetter) }, + } +} + +func (hcg *HTTPCertGetter) Provision(ctx caddy.Context) error { + hcg.ctx = ctx + if hcg.URL == "" { + return fmt.Errorf("URL is required") + } + return nil +} + +func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + sigs := make([]string, len(hello.SignatureSchemes)) + for i, sig := range hello.SignatureSchemes { + sigs[i] = fmt.Sprintf("%x", uint16(sig)) // you won't believe what %x uses if the val is a Stringer + } + suites := make([]string, len(hello.CipherSuites)) + for i, cs := range hello.CipherSuites { + suites[i] = fmt.Sprintf("%x", cs) + } + + parsed, err := url.Parse(hcg.URL) + if err != nil { + return nil, err + } + qs := parsed.Query() + qs.Set("server_name", hello.ServerName) + qs.Set("signature_schemes", strings.Join(sigs, ",")) + qs.Set("cipher_suites", strings.Join(suites, ",")) + parsed.RawQuery = qs.Encode() + + req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("got HTTP %d", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + cert, err := tlsCertFromCertAndKeyPEMBundle(bodyBytes) + if err != nil { + return nil, err + } + + return &cert, nil +} + +// UnmarshalCaddyfile deserializes Caddyfile tokens into ts. +// +// ... http <url> +// +func (hcg *HTTPCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + hcg.URL = d.Val() + if d.NextArg() { + return d.ArgErr() + } + for nesting := d.Nesting(); d.NextBlock(nesting); { + return d.Err("block not allowed here") + } + } + return nil +} + +// Interface guards +var ( + _ certmagic.CertificateManager = (*Tailscale)(nil) + _ caddy.Provisioner = (*Tailscale)(nil) + _ caddyfile.Unmarshaler = (*Tailscale)(nil) + + _ certmagic.CertificateManager = (*HTTPCertGetter)(nil) + _ caddy.Provisioner = (*HTTPCertGetter)(nil) + _ caddyfile.Unmarshaler = (*HTTPCertGetter)(nil) +) diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index 0ff0629..33b31a5 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -61,10 +61,14 @@ func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { return nil } - cert, err := x509CertFromCertAndKeyPEMFile(fpath) + bundle, err := os.ReadFile(fpath) if err != nil { return err } + cert, err := tlsCertFromCertAndKeyPEMBundle(bundle) + if err != nil { + return fmt.Errorf("%s: %w", fpath, err) + } certs = append(certs, Certificate{Certificate: cert}) @@ -77,12 +81,7 @@ func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { return certs, nil } -func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { - bundle, err := os.ReadFile(fpath) - if err != nil { - return tls.Certificate{}, err - } - +func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) { certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer) var foundKey bool // use only the first key in the file @@ -96,8 +95,7 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { if derBlock.Type == "CERTIFICATE" { // Re-encode certificate as PEM, appending to certificate chain - err = pem.Encode(certBuilder, derBlock) - if err != nil { + if err := pem.Encode(certBuilder, derBlock); err != nil { return tls.Certificate{}, err } } else if derBlock.Type == "EC PARAMETERS" { @@ -105,18 +103,16 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { // parameters and key (parameter block should come first) if !foundKey { // Encode parameters - err = pem.Encode(keyBuilder, derBlock) - if err != nil { + if err := pem.Encode(keyBuilder, derBlock); err != nil { return tls.Certificate{}, err } // Key must immediately follow derBlock, bundle = pem.Decode(bundle) if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" { - return tls.Certificate{}, fmt.Errorf("%s: expected elliptic private key to immediately follow EC parameters", fpath) + return tls.Certificate{}, fmt.Errorf("expected elliptic private key to immediately follow EC parameters") } - err = pem.Encode(keyBuilder, derBlock) - if err != nil { + if err := pem.Encode(keyBuilder, derBlock); err != nil { return tls.Certificate{}, err } foundKey = true @@ -124,28 +120,27 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { } else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { // RSA key if !foundKey { - err = pem.Encode(keyBuilder, derBlock) - if err != nil { + if err := pem.Encode(keyBuilder, derBlock); err != nil { return tls.Certificate{}, err } foundKey = true } } else { - return tls.Certificate{}, fmt.Errorf("%s: unrecognized PEM block type: %s", fpath, derBlock.Type) + return tls.Certificate{}, fmt.Errorf("unrecognized PEM block type: %s", derBlock.Type) } } certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes() if len(certPEMBytes) == 0 { - return tls.Certificate{}, fmt.Errorf("%s: failed to parse PEM data", fpath) + return tls.Certificate{}, fmt.Errorf("failed to parse PEM data") } if len(keyPEMBytes) == 0 { - return tls.Certificate{}, fmt.Errorf("%s: no private key block found", fpath) + return tls.Certificate{}, fmt.Errorf("no private key block found") } cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes) if err != nil { - return tls.Certificate{}, fmt.Errorf("%s: making X509 key pair: %v", fpath, err) + return tls.Certificate{}, fmt.Errorf("making X509 key pair: %v", err) } return cert, nil |