caddy/modules/caddypki/adminpki.go
Francis Lavoie bbad6931e3
pki: Implement API endpoints for certs and caddy trust (#4443)
* admin: Implement /pki/certificates/<id> API

* pki: Lower "skip_install_trust" log level to INFO

See https://github.com/caddyserver/caddy/issues/4058#issuecomment-976132935

It's not necessary to warn about this, because this was an option explicitly configured by the user. Still useful to log, but we don't need to be so loud about it.

* cmd: Export functions needed for PKI app, return API response to caller

* pki: Rewrite `caddy trust` command to use new admin endpoint instead

* pki: Rewrite `caddy untrust` command to support using admin endpoint

* Refactor cmd and pki packages for determining admin API endpoint
2022-03-02 11:08:36 -07:00

194 lines
5.1 KiB
Go

// Copyright 2020 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddypki
import (
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(adminPKI{})
}
// adminPKI is a module that serves a PKI endpoint to retrieve
// information about the CAs being managed by Caddy.
type adminPKI struct {
ctx caddy.Context
log *zap.Logger
pkiApp *PKI
}
// CaddyModule returns the Caddy module information.
func (adminPKI) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "admin.api.pki",
New: func() caddy.Module { return new(adminPKI) },
}
}
// Provision sets up the adminPKI module.
func (a *adminPKI) Provision(ctx caddy.Context) error {
a.ctx = ctx
a.log = ctx.Logger(a)
// First check if the PKI app was configured, because
// a.ctx.App() has the side effect of instantiating
// and provisioning an app even if it wasn't configured.
pkiAppConfigured := a.ctx.AppIsConfigured("pki")
if !pkiAppConfigured {
return nil
}
// Load the PKI app, so we can query it for information.
appModule, err := a.ctx.App("pki")
if err != nil {
return err
}
a.pkiApp = appModule.(*PKI)
return nil
}
// Routes returns the admin routes for the PKI app.
func (a *adminPKI) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{
{
Pattern: adminPKICertificatesEndpoint,
Handler: caddy.AdminHandlerFunc(a.handleCertificates),
},
}
}
// handleCertificates returns certificate information about a particular
// CA, by its ID. If the CA ID is the default, then the CA will be
// provisioned if it has not already been. Other CA IDs will return an
// error if they have not been previously provisioned.
func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodGet {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
// Prep for a JSON response
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
idPath := r.URL.Path
// Grab the CA ID from the request path, it should be the 4th segment
parts := strings.Split(idPath, "/")
if len(parts) < 4 || parts[3] == "" {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("request path is missing the CA ID"),
}
}
if parts[0] != "" || parts[1] != "pki" || parts[2] != "certificates" {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed object path"),
}
}
id := parts[3]
// Find the CA by ID, if PKI is configured
var ca *CA
ok := false
if a.pkiApp != nil {
ca, ok = a.pkiApp.CAs[id]
}
// If we didn't find the CA, and PKI is not configured
// then we'll either error out if the CA ID is not the
// default. If the CA ID is the default, then we'll
// provision it, because the user probably aims to
// change their config to enable PKI immediately after
// if they actually requested the local CA ID.
if !ok {
if id != DefaultCAID {
return caddy.APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("no certificate authority configured with id: %s", id),
}
}
// Provision the default CA, which generates and stores a root
// certificate in storage, if one doesn't already exist.
ca = new(CA)
err := ca.Provision(a.ctx, id, a.log)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
}
}
}
// Convert the root certificate to PEM
rootPem := string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ca.RootCertificate().Raw,
}))
// Convert the intermediate certificate to PEM
interPem := string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ca.IntermediateCertificate().Raw,
}))
// Build the response
response := CAInfo{
ID: ca.ID,
Name: ca.Name,
Root: rootPem,
Intermediate: interPem,
}
// Encode and write the JSON response
err := enc.Encode(response)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: err,
}
}
return nil
}
// CAInfo is the response from the certificates API endpoint
type CAInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Root string `json:"root"`
Intermediate string `json:"intermediate"`
}
const adminPKICertificatesEndpoint = "/pki/certificates/"
// Interface guards
var (
_ caddy.AdminRouter = (*adminPKI)(nil)
_ caddy.Provisioner = (*adminPKI)(nil)
)