@ -16,7 +16,6 @@ package caddypki
import (
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"strings"
@ -26,27 +25,27 @@ import (
)
func init ( ) {
caddy . RegisterModule ( adminPK I { } )
caddy . RegisterModule ( adminA PI { } )
}
// adminPK I is a module that serves a PKI endpoint to retrieve
// adminA PI is a module that serves PKI endpoints to retrieve
// information about the CAs being managed by Caddy.
type adminPK I struct {
type adminA PI struct {
ctx caddy . Context
log * zap . Logger
pkiApp * PKI
}
// CaddyModule returns the Caddy module information.
func ( adminPK I ) CaddyModule ( ) caddy . ModuleInfo {
func ( adminA PI ) CaddyModule ( ) caddy . ModuleInfo {
return caddy . ModuleInfo {
ID : "admin.api.pki" ,
New : func ( ) caddy . Module { return new ( adminPK I ) } ,
New : func ( ) caddy . Module { return new ( adminA PI ) } ,
}
}
// Provision sets up the adminPK I module.
func ( a * adminPK I ) Provision ( ctx caddy . Context ) error {
// Provision sets up the adminA PI module.
func ( a * adminA PI ) Provision ( ctx caddy . Context ) error {
a . ctx = ctx
a . log = ctx . Logger ( a )
@ -69,52 +68,128 @@ func (a *adminPKI) Provision(ctx caddy.Context) error {
}
// Routes returns the admin routes for the PKI app.
func ( a * adminPK I ) Routes ( ) [ ] caddy . AdminRoute {
func ( a * adminA PI ) Routes ( ) [ ] caddy . AdminRoute {
return [ ] caddy . AdminRoute {
{
Pattern : adminPKICertificates Endpoint ,
Handler : caddy . AdminHandlerFunc ( a . handleCertificate s ) ,
Pattern : adminPKIEndpointBase ,
Handler : caddy . AdminHandlerFunc ( a . handleAPIEndpoint s ) ,
} ,
}
}
// handleCertificates returns certificate information about a particular
// CA, by its ID. If the CA ID is the default, then the CA will be
// handleAPIEndpoints routes API requests within adminPKIEndpointBase.
func ( a * adminAPI ) handleAPIEndpoints ( w http . ResponseWriter , r * http . Request ) error {
uri := strings . TrimPrefix ( r . URL . Path , "/pki/" )
parts := strings . Split ( uri , "/" )
switch {
case len ( parts ) == 2 && parts [ 0 ] == "ca" && parts [ 1 ] != "" :
return a . handleCAInfo ( w , r )
case len ( parts ) == 3 && parts [ 0 ] == "ca" && parts [ 1 ] != "" && parts [ 2 ] == "certificates" :
return a . handleCACerts ( w , r )
}
return caddy . APIError {
HTTPStatus : http . StatusNotFound ,
Err : fmt . Errorf ( "resource not found: %v" , r . URL . Path ) ,
}
}
// handleCAInfo returns cinformation 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 {
func ( a * adminA PI ) handleCAInfo ( w http . ResponseWriter , r * http . Request ) error {
if r . Method != http . MethodGet {
return caddy . APIError {
HTTPStatus : http . StatusMethodNotAllowed ,
Err : fmt . Errorf ( "method not allowed" ) ,
Err : fmt . Errorf ( "method not allowed: %v" , r . Method ) ,
}
}
ca , err := a . getCAFromAPIRequestPath ( r )
if err != nil {
return err
}
rootCert , interCert , err := rootAndIntermediatePEM ( ca )
if err != nil {
return caddy . APIError {
HTTPStatus : http . StatusInternalServerError ,
Err : fmt . Errorf ( "failed to get root and intermediate cert for CA %s: %v" , ca . ID , err ) ,
}
}
repl := ca . newReplacer ( )
response := caInfo {
ID : ca . ID ,
Name : ca . Name ,
RootCN : repl . ReplaceAll ( ca . RootCommonName , "" ) ,
IntermediateCN : repl . ReplaceAll ( ca . IntermediateCommonName , "" ) ,
RootCert : string ( rootCert ) ,
IntermediateCert : string ( interCert ) ,
}
encoded , err := json . Marshal ( response )
if err != nil {
return caddy . APIError {
HTTPStatus : http . StatusInternalServerError ,
Err : err ,
}
}
// Prep for a JSON response
w . Header ( ) . Set ( "Content-Type" , "application/json" )
enc := json . NewEncoder ( w )
w . Write ( encoded )
idPath := r . URL . Path
return nil
}
// Grab the CA ID from the request path, it should be the 4th segment
parts := strings . Split ( idPath , "/" )
if len ( parts ) < 4 || parts [ 3 ] == "" {
// handleCACerts returns cinformation 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 * adminAPI ) handleCACerts ( w http . ResponseWriter , r * http . Request ) error {
if r . Method != http . MethodGet {
return caddy . APIError {
HTTPStatus : http . StatusBadRequest ,
Err : fmt . Errorf ( "request path is missing the CA ID" ) ,
HTTPStatus : http . StatusMethodNotAllowed ,
Err : fmt . Errorf ( "method not allowed: %v" , r . Method ) ,
}
}
if parts [ 0 ] != "" || parts [ 1 ] != "pki" || parts [ 2 ] != "certificates" {
ca , err := a . getCAFromAPIRequestPath ( r )
if err != nil {
return err
}
rootCert , interCert , err := rootAndIntermediatePEM ( ca )
if err != nil {
return caddy . APIError {
HTTPStatus : http . StatusInternalServerError ,
Err : fmt . Errorf ( "failed to get root and intermediate cert for CA %s: %v" , ca . ID , err ) ,
}
}
w . Header ( ) . Set ( "Content-Type" , "application/pem-certificate-chain" )
_ , err = w . Write ( interCert )
if err == nil {
w . Write ( rootCert )
}
return nil
}
func ( a * adminAPI ) getCAFromAPIRequestPath ( r * http . Request ) ( * CA , error ) {
// Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/<ca>)
id := strings . Split ( r . URL . Path , "/" ) [ 3 ]
if id == "" {
return nil , caddy . APIError {
HTTPStatus : http . StatusBadRequest ,
Err : fmt . Errorf ( "malformed object path" ) ,
Err : fmt . Errorf ( "missing CA in path" ) ,
}
}
id := parts [ 3 ]
// Find the CA by ID, if PKI is configured
var ca * CA
ok := false
var ok bool
if a . pkiApp != nil {
ca , ok = a . pkiApp . CAs [ id ]
}
@ -127,7 +202,7 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er
// if they actually requested the local CA ID.
if ! ok {
if id != DefaultCAID {
return caddy . APIError {
return nil , caddy . APIError {
HTTPStatus : http . StatusNotFound ,
Err : fmt . Errorf ( "no certificate authority configured with id: %s" , id ) ,
}
@ -138,57 +213,43 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er
ca = new ( CA )
err := ca . Provision ( a . ctx , id , a . log )
if err != nil {
return caddy . APIError {
return nil , 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 ,
} ) )
return ca , nil
}
// Build the response
response := CAInfo {
ID : ca . ID ,
Name : ca . Name ,
Root : rootPem ,
Intermediate : interPem ,
func rootAndIntermediatePEM ( ca * CA ) ( root , inter [ ] byte , err error ) {
root , err = pemEncodeCert ( ca . RootCertificate ( ) . Raw )
if err != nil {
return
}
// Encode and write the JSON response
err := enc . Encode ( response )
inter , err = pemEncodeCert ( ca . IntermediateCertificate ( ) . Raw )
if err != nil {
return caddy . APIError {
HTTPStatus : http . StatusInternalServerError ,
Err : err ,
}
return
}
return nil
return
}
// 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" `
// caInfo is the response structure for the CA info API endpoint.
type caInfo struct {
ID string ` json:"id" `
Name string ` json:"name" `
RootCN string ` json:"root_common_name" `
IntermediateCN string ` json:"intermediate_common_name" `
RootCert string ` json:"root_certificate" `
IntermediateCert string ` json:"intermediate_certificate" `
}
const adminPKICertificatesEndpoint = "/pki/certificates/"
// adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist.
const adminPKIEndpointBase = "/pki/"
// Interface guards
var (
_ caddy . AdminRouter = ( * adminPK I ) ( nil )
_ caddy . Provisioner = ( * adminPK I ) ( nil )
_ caddy . AdminRouter = ( * adminA PI ) ( nil )
_ caddy . Provisioner = ( * adminA PI ) ( nil )
)