Browse Source
* pki: Initial commit of PKI app (WIP) (see #2502 and #3021) * pki: Ability to use root/intermediates, and sign with root * pki: Fix benign misnamings left over from copy+paste * pki: Only install root if not already trusted * Make HTTPS port the default; all names use auto-HTTPS; bug fixes * Fix build - what happened to our CI tests?? * Fix go.modmaster
Matt Holt
5 years ago
committed by
GitHub
21 changed files with 1292 additions and 175 deletions
@ -0,0 +1,334 @@ |
|||
// Copyright 2015 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 ( |
|||
"crypto/x509" |
|||
"encoding/json" |
|||
"fmt" |
|||
"path" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/caddyserver/caddy/v2" |
|||
"github.com/caddyserver/certmagic" |
|||
"go.uber.org/zap" |
|||
) |
|||
|
|||
// CA describes a certificate authority, which consists of
|
|||
// root/signing certificates and various settings pertaining
|
|||
// to the issuance of certificates and trusting them.
|
|||
type CA struct { |
|||
// The user-facing name of the certificate authority.
|
|||
Name string `json:"name,omitempty"` |
|||
|
|||
// The name to put in the CommonName field of the
|
|||
// root certificate.
|
|||
RootCommonName string `json:"root_common_name,omitempty"` |
|||
|
|||
// The name to put in the CommonName field of the
|
|||
// intermediate certificates.
|
|||
IntermediateCommonName string `json:"intermediate_common_name,omitempty"` |
|||
|
|||
// Whether Caddy will attempt to install the CA's root
|
|||
// into the system trust store, as well as into Java
|
|||
// and Mozilla Firefox trust stores. Default: true.
|
|||
InstallTrust *bool `json:"install_trust,omitempty"` |
|||
|
|||
Root *KeyPair `json:"root,omitempty"` |
|||
Intermediate *KeyPair `json:"intermediate,omitempty"` |
|||
|
|||
// Optionally configure a separate storage module associated with this
|
|||
// issuer, instead of using Caddy's global/default-configured storage.
|
|||
// This can be useful if you want to keep your signing keys in a
|
|||
// separate location from your leaf certificates.
|
|||
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` |
|||
|
|||
id string |
|||
storage certmagic.Storage |
|||
root, inter *x509.Certificate |
|||
interKey interface{} // TODO: should we just store these as crypto.Signer?
|
|||
mu *sync.RWMutex |
|||
|
|||
rootCertPath string // mainly used for logging purposes if trusting
|
|||
log *zap.Logger |
|||
} |
|||
|
|||
// Provision sets up the CA.
|
|||
func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { |
|||
ca.mu = new(sync.RWMutex) |
|||
ca.log = log.Named("ca." + id) |
|||
|
|||
if id == "" { |
|||
return fmt.Errorf("CA ID is required (use 'local' for the default CA)") |
|||
} |
|||
ca.mu.Lock() |
|||
ca.id = id |
|||
ca.mu.Unlock() |
|||
|
|||
if ca.StorageRaw != nil { |
|||
val, err := ctx.LoadModule(ca, "StorageRaw") |
|||
if err != nil { |
|||
return fmt.Errorf("loading storage module: %v", err) |
|||
} |
|||
cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() |
|||
if err != nil { |
|||
return fmt.Errorf("creating storage configuration: %v", err) |
|||
} |
|||
ca.storage = cmStorage |
|||
} |
|||
if ca.storage == nil { |
|||
ca.storage = ctx.Storage() |
|||
} |
|||
|
|||
if ca.Name == "" { |
|||
ca.Name = defaultCAName |
|||
} |
|||
if ca.RootCommonName == "" { |
|||
ca.RootCommonName = defaultRootCommonName |
|||
} |
|||
if ca.IntermediateCommonName == "" { |
|||
ca.IntermediateCommonName = defaultIntermediateCommonName |
|||
} |
|||
|
|||
// load the certs and key that will be used for signing
|
|||
var rootCert, interCert *x509.Certificate |
|||
var rootKey, interKey interface{} |
|||
var err error |
|||
if ca.Root != nil { |
|||
if ca.Root.Format == "" || ca.Root.Format == "pem_file" { |
|||
ca.rootCertPath = ca.Root.Certificate |
|||
} |
|||
rootCert, rootKey, err = ca.Root.Load() |
|||
} else { |
|||
ca.rootCertPath = "storage:" + ca.storageKeyRootCert() |
|||
rootCert, rootKey, err = ca.loadOrGenRoot() |
|||
} |
|||
if err != nil { |
|||
return err |
|||
} |
|||
if ca.Intermediate != nil { |
|||
interCert, interKey, err = ca.Intermediate.Load() |
|||
} else { |
|||
interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey) |
|||
} |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
ca.mu.Lock() |
|||
ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey |
|||
ca.mu.Unlock() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// ID returns the CA's ID, as given by the user in the config.
|
|||
func (ca CA) ID() string { |
|||
return ca.id |
|||
} |
|||
|
|||
// RootCertificate returns the CA's root certificate (public key).
|
|||
func (ca CA) RootCertificate() *x509.Certificate { |
|||
ca.mu.RLock() |
|||
defer ca.mu.RUnlock() |
|||
return ca.root |
|||
} |
|||
|
|||
// RootKey returns the CA's root private key. Since the root key is
|
|||
// not cached in memory long-term, it needs to be loaded from storage,
|
|||
// which could yield an error.
|
|||
func (ca CA) RootKey() (interface{}, error) { |
|||
_, rootKey, err := ca.loadOrGenRoot() |
|||
return rootKey, err |
|||
} |
|||
|
|||
// IntermediateCertificate returns the CA's intermediate
|
|||
// certificate (public key).
|
|||
func (ca CA) IntermediateCertificate() *x509.Certificate { |
|||
ca.mu.RLock() |
|||
defer ca.mu.RUnlock() |
|||
return ca.inter |
|||
} |
|||
|
|||
// IntermediateKey returns the CA's intermediate private key.
|
|||
func (ca CA) IntermediateKey() interface{} { |
|||
ca.mu.RLock() |
|||
defer ca.mu.RUnlock() |
|||
return ca.interKey |
|||
} |
|||
|
|||
func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { |
|||
rootCertPEM, err := ca.storage.Load(ca.storageKeyRootCert()) |
|||
if err != nil { |
|||
if _, ok := err.(certmagic.ErrNotExist); !ok { |
|||
return nil, nil, fmt.Errorf("loading root cert: %v", err) |
|||
} |
|||
|
|||
// TODO: should we require that all or none of the assets are required before overwriting anything?
|
|||
rootCert, rootKey, err = ca.genRoot() |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("generating root: %v", err) |
|||
} |
|||
} |
|||
|
|||
if rootCert == nil { |
|||
rootCert, err = pemDecodeSingleCert(rootCertPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err) |
|||
} |
|||
} |
|||
if rootKey == nil { |
|||
rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey()) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("loading root key: %v", err) |
|||
} |
|||
rootKey, err = pemDecodePrivateKey(rootKeyPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("decoding root key: %v", err) |
|||
} |
|||
} |
|||
|
|||
return rootCert, rootKey, nil |
|||
} |
|||
|
|||
func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { |
|||
repl := ca.newReplacer() |
|||
|
|||
rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, "")) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("generating CA root: %v", err) |
|||
} |
|||
rootCertPEM, err := pemEncodeCert(rootCert.Raw) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("encoding root certificate: %v", err) |
|||
} |
|||
err = ca.storage.Store(ca.storageKeyRootCert(), rootCertPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("saving root certificate: %v", err) |
|||
} |
|||
rootKeyPEM, err := pemEncodePrivateKey(rootKey) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("encoding root key: %v", err) |
|||
} |
|||
err = ca.storage.Store(ca.storageKeyRootKey(), rootKeyPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("saving root key: %v", err) |
|||
} |
|||
|
|||
return rootCert, rootKey, nil |
|||
} |
|||
|
|||
func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { |
|||
interCertPEM, err := ca.storage.Load(ca.storageKeyIntermediateCert()) |
|||
if err != nil { |
|||
if _, ok := err.(certmagic.ErrNotExist); !ok { |
|||
return nil, nil, fmt.Errorf("loading intermediate cert: %v", err) |
|||
} |
|||
|
|||
// TODO: should we require that all or none of the assets are required before overwriting anything?
|
|||
interCert, interKey, err = ca.genIntermediate(rootCert, rootKey) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err) |
|||
} |
|||
} |
|||
|
|||
if interCert == nil { |
|||
interCert, err = pemDecodeSingleCert(interCertPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err) |
|||
} |
|||
} |
|||
|
|||
if interKey == nil { |
|||
interKeyPEM, err := ca.storage.Load(ca.storageKeyIntermediateKey()) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("loading intermediate key: %v", err) |
|||
} |
|||
interKey, err = pemDecodePrivateKey(interKeyPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("decoding intermediate key: %v", err) |
|||
} |
|||
} |
|||
|
|||
return interCert, interKey, nil |
|||
} |
|||
|
|||
func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { |
|||
repl := ca.newReplacer() |
|||
|
|||
rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey()) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("loading root key to sign new intermediate: %v", err) |
|||
} |
|||
rootKey, err = pemDecodePrivateKey(rootKeyPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("decoding root key: %v", err) |
|||
} |
|||
interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("generating CA intermediate: %v", err) |
|||
} |
|||
interCertPEM, err := pemEncodeCert(interCert.Raw) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("encoding intermediate certificate: %v", err) |
|||
} |
|||
err = ca.storage.Store(ca.storageKeyIntermediateCert(), interCertPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err) |
|||
} |
|||
interKeyPEM, err := pemEncodePrivateKey(interKey) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("encoding intermediate key: %v", err) |
|||
} |
|||
err = ca.storage.Store(ca.storageKeyIntermediateKey(), interKeyPEM) |
|||
if err != nil { |
|||
return nil, nil, fmt.Errorf("saving intermediate key: %v", err) |
|||
} |
|||
|
|||
return interCert, interKey, nil |
|||
} |
|||
|
|||
func (ca CA) storageKeyCAPrefix() string { |
|||
return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.id)) |
|||
} |
|||
func (ca CA) storageKeyRootCert() string { |
|||
return path.Join(ca.storageKeyCAPrefix(), "root.crt") |
|||
} |
|||
func (ca CA) storageKeyRootKey() string { |
|||
return path.Join(ca.storageKeyCAPrefix(), "root.key") |
|||
} |
|||
func (ca CA) storageKeyIntermediateCert() string { |
|||
return path.Join(ca.storageKeyCAPrefix(), "intermediate.crt") |
|||
} |
|||
func (ca CA) storageKeyIntermediateKey() string { |
|||
return path.Join(ca.storageKeyCAPrefix(), "intermediate.key") |
|||
} |
|||
|
|||
func (ca CA) newReplacer() *caddy.Replacer { |
|||
repl := caddy.NewReplacer() |
|||
repl.Set("pki.ca.name", ca.Name) |
|||
return repl |
|||
} |
|||
|
|||
const ( |
|||
defaultCAID = "local" |
|||
defaultCAName = "Caddy Local Authority" |
|||
defaultRootCommonName = "{pki.ca.name} - {time.now.year} ECC Root" |
|||
defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate" |
|||
|
|||
defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10 |
|||
defaultIntermediateLifetime = 24 * time.Hour * 7 |
|||
) |
@ -0,0 +1,50 @@ |
|||
// Copyright 2015 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 ( |
|||
"crypto/x509" |
|||
"time" |
|||
|
|||
"github.com/smallstep/cli/crypto/x509util" |
|||
) |
|||
|
|||
func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey interface{}, err error) { |
|||
rootProfile, err := x509util.NewRootProfile(commonName) |
|||
if err != nil { |
|||
return |
|||
} |
|||
rootProfile.Subject().NotAfter = time.Now().Add(defaultRootLifetime) // TODO: make configurable
|
|||
return newCert(rootProfile) |
|||
} |
|||
|
|||
func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey interface{}) (cert *x509.Certificate, privateKey interface{}, err error) { |
|||
interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey) |
|||
if err != nil { |
|||
return |
|||
} |
|||
interProfile.Subject().NotAfter = time.Now().Add(defaultIntermediateLifetime) // TODO: make configurable
|
|||
return newCert(interProfile) |
|||
} |
|||
|
|||
func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey interface{}, err error) { |
|||
certBytes, err := profile.CreateCertificate() |
|||
if err != nil { |
|||
return |
|||
} |
|||
privateKey = profile.SubjectPrivateKey() |
|||
cert, err = x509.ParseCertificate(certBytes) |
|||
return |
|||
} |
@ -0,0 +1,89 @@ |
|||
// Copyright 2015 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 ( |
|||
"flag" |
|||
"fmt" |
|||
"os" |
|||
"path/filepath" |
|||
|
|||
"github.com/caddyserver/caddy/v2" |
|||
caddycmd "github.com/caddyserver/caddy/v2/cmd" |
|||
"github.com/smallstep/truststore" |
|||
) |
|||
|
|||
func init() { |
|||
caddycmd.RegisterCommand(caddycmd.Command{ |
|||
Name: "untrust", |
|||
Func: cmdUntrust, |
|||
Usage: "[--ca <id> | --cert <path>]", |
|||
Short: "Untrusts a locally-trusted CA certificate", |
|||
Long: ` |
|||
Untrusts a root certificate from the local trust store(s). Intended |
|||
for development environments only. |
|||
|
|||
This command uninstalls trust; it does not necessarily delete the |
|||
root certificate from trust stores entirely. Thus, repeatedly |
|||
trusting and untrusting new certificates can fill up trust databases. |
|||
|
|||
This command does not delete or modify certificate files. |
|||
|
|||
Specify which certificate to untrust either by the ID of its CA with |
|||
the --ca flag, or the direct path to the certificate file with the |
|||
--cert flag. If the --ca flag is used, only the default storage paths |
|||
are assumed (i.e. using --ca flag with custom storage backends or file |
|||
paths will not work). |
|||
|
|||
If no flags are specified, --ca=local is assumed.`, |
|||
Flags: func() *flag.FlagSet { |
|||
fs := flag.NewFlagSet("untrust", flag.ExitOnError) |
|||
fs.String("ca", "", "The ID of the CA to untrust") |
|||
fs.String("cert", "", "The path to the CA certificate to untrust") |
|||
return fs |
|||
}(), |
|||
}) |
|||
} |
|||
|
|||
func cmdUntrust(fs caddycmd.Flags) (int, error) { |
|||
ca := fs.String("ca") |
|||
cert := fs.String("cert") |
|||
|
|||
if ca != "" && cert != "" { |
|||
return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments") |
|||
} |
|||
if ca == "" && cert == "" { |
|||
ca = defaultCAID |
|||
} |
|||
if ca != "" { |
|||
cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt") |
|||
} |
|||
|
|||
// sanity check, make sure cert file exists first
|
|||
_, err := os.Stat(cert) |
|||
if err != nil { |
|||
return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err) |
|||
} |
|||
|
|||
err = truststore.UninstallFile(cert, |
|||
truststore.WithDebug(), |
|||
truststore.WithFirefox(), |
|||
truststore.WithJava()) |
|||
if err != nil { |
|||
return caddy.ExitCodeFailedStartup, err |
|||
} |
|||
|
|||
return caddy.ExitCodeSuccess, nil |
|||
} |
@ -0,0 +1,155 @@ |
|||
// Copyright 2015 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 ( |
|||
"bytes" |
|||
"crypto" |
|||
"crypto/ecdsa" |
|||
"crypto/ed25519" |
|||
"crypto/rsa" |
|||
"crypto/x509" |
|||
"encoding/pem" |
|||
"fmt" |
|||
"io/ioutil" |
|||
"strings" |
|||
) |
|||
|
|||
func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { |
|||
pemBlock, remaining := pem.Decode(pemDER) |
|||
if pemBlock == nil { |
|||
return nil, fmt.Errorf("no PEM block found") |
|||
} |
|||
if len(remaining) > 0 { |
|||
return nil, fmt.Errorf("input contained more than a single PEM block") |
|||
} |
|||
if pemBlock.Type != "CERTIFICATE" { |
|||
return nil, fmt.Errorf("expected PEM block type to be CERTIFICATE, but got '%s'", pemBlock.Type) |
|||
} |
|||
return x509.ParseCertificate(pemBlock.Bytes) |
|||
} |
|||
|
|||
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.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}) |
|||
return buf.Bytes(), err |
|||
} |
|||
|
|||
func trusted(cert *x509.Certificate) bool { |
|||
chains, err := cert.Verify(x509.VerifyOptions{}) |
|||
return len(chains) > 0 && err == nil |
|||
} |
|||
|
|||
// KeyPair represents a public-private key pair, where the
|
|||
// public key is also called a certificate.
|
|||
type KeyPair struct { |
|||
Certificate string `json:"certificate,omitempty"` |
|||
PrivateKey string `json:"private_key,omitempty"` |
|||
Format string `json:"format,omitempty"` |
|||
} |
|||
|
|||
// Load loads the certificate and key.
|
|||
func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) { |
|||
switch kp.Format { |
|||
case "", "pem_file": |
|||
certData, err := ioutil.ReadFile(kp.Certificate) |
|||
if err != nil { |
|||
return nil, nil, err |
|||
} |
|||
keyData, err := ioutil.ReadFile(kp.PrivateKey) |
|||
if err != nil { |
|||
return nil, nil, err |
|||
} |
|||
|
|||
cert, err := pemDecodeSingleCert(certData) |
|||
if err != nil { |
|||
return nil, nil, err |
|||
} |
|||
key, err := pemDecodePrivateKey(keyData) |
|||
if err != nil { |
|||
return nil, nil, err |
|||
} |
|||
|
|||
return cert, key, nil |
|||
|
|||
default: |
|||
return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format) |
|||
} |
|||
} |
@ -0,0 +1,99 @@ |
|||
// Copyright 2015 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 ( |
|||
"crypto/x509" |
|||
"fmt" |
|||
"time" |
|||
|
|||
"go.uber.org/zap" |
|||
) |
|||
|
|||
func (p *PKI) maintenance() { |
|||
ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable
|
|||
defer ticker.Stop() |
|||
|
|||
for { |
|||
select { |
|||
case <-ticker.C: |
|||
p.renewCerts() |
|||
case <-p.ctx.Done(): |
|||
return |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (p *PKI) renewCerts() { |
|||
for _, ca := range p.CAs { |
|||
err := p.renewCertsForCA(ca) |
|||
if err != nil { |
|||
p.log.Error("renewing intermediate certificates", |
|||
zap.Error(err), |
|||
zap.String("ca", ca.id)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (p *PKI) renewCertsForCA(ca *CA) error { |
|||
ca.mu.Lock() |
|||
defer ca.mu.Unlock() |
|||
|
|||
log := p.log.With(zap.String("ca", ca.id)) |
|||
|
|||
// only maintain the root if it's not manually provided in the config
|
|||
if ca.Root == nil { |
|||
if needsRenewal(ca.root) { |
|||
// TODO: implement root renewal (use same key)
|
|||
log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)", |
|||
zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), |
|||
) |
|||
} |
|||
} |
|||
|
|||
// only maintain the intermediate if it's not manually provided in the config
|
|||
if ca.Intermediate == nil { |
|||
if needsRenewal(ca.inter) { |
|||
log.Info("intermediate expires soon; renewing", |
|||
zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), |
|||
) |
|||
|
|||
rootCert, rootKey, err := ca.loadOrGenRoot() |
|||
if err != nil { |
|||
return fmt.Errorf("loading root key: %v", err) |
|||
} |
|||
interCert, interKey, err := ca.genIntermediate(rootCert, rootKey) |
|||
if err != nil { |
|||
return fmt.Errorf("generating new certificate: %v", err) |
|||
} |
|||
ca.inter, ca.interKey = interCert, interKey |
|||
|
|||
log.Info("renewed intermediate", |
|||
zap.Time("new_expiration", ca.inter.NotAfter), |
|||
) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func needsRenewal(cert *x509.Certificate) bool { |
|||
lifetime := cert.NotAfter.Sub(cert.NotBefore) |
|||
renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio) |
|||
renewalWindowStart := cert.NotAfter.Add(-renewalWindow) |
|||
return time.Now().After(renewalWindowStart) |
|||
} |
|||
|
|||
const renewalWindowRatio = 0.2 // TODO: make configurable
|
@ -0,0 +1,117 @@ |
|||
// Copyright 2015 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 ( |
|||
"fmt" |
|||
|
|||
"github.com/caddyserver/caddy/v2" |
|||
"github.com/smallstep/truststore" |
|||
"go.uber.org/zap" |
|||
) |
|||
|
|||
func init() { |
|||
caddy.RegisterModule(PKI{}) |
|||
} |
|||
|
|||
// PKI provides Public Key Infrastructure facilities for Caddy.
|
|||
type PKI struct { |
|||
// The CAs to manage. Each CA is keyed by an ID that is used
|
|||
// to uniquely identify it from other CAs. The default CA ID
|
|||
// is "local".
|
|||
CAs map[string]*CA `json:"certificate_authorities,omitempty"` |
|||
|
|||
ctx caddy.Context |
|||
log *zap.Logger |
|||
} |
|||
|
|||
// CaddyModule returns the Caddy module information.
|
|||
func (PKI) CaddyModule() caddy.ModuleInfo { |
|||
return caddy.ModuleInfo{ |
|||
ID: "pki", |
|||
New: func() caddy.Module { return new(PKI) }, |
|||
} |
|||
} |
|||
|
|||
// Provision sets up the configuration for the PKI app.
|
|||
func (p *PKI) Provision(ctx caddy.Context) error { |
|||
p.ctx = ctx |
|||
p.log = ctx.Logger(p) |
|||
|
|||
// if this app is initialized at all, ensure there's
|
|||
// at least a default CA that can be used
|
|||
if len(p.CAs) == 0 { |
|||
p.CAs = map[string]*CA{defaultCAID: new(CA)} |
|||
} |
|||
|
|||
for caID, ca := range p.CAs { |
|||
err := ca.Provision(ctx, caID, p.log) |
|||
if err != nil { |
|||
return fmt.Errorf("provisioning CA '%s': %v", caID, err) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Start starts the PKI app.
|
|||
func (p *PKI) Start() error { |
|||
// install roots to trust store, if not disabled
|
|||
for _, ca := range p.CAs { |
|||
if ca.InstallTrust != nil && !*ca.InstallTrust { |
|||
ca.log.Warn("root certificate trust store installation disabled; clients will show warnings without intervention", |
|||
zap.String("path", ca.rootCertPath)) |
|||
continue |
|||
} |
|||
|
|||
// avoid password prompt if already trusted
|
|||
if trusted(ca.root) { |
|||
ca.log.Info("root certificate is already trusted by system", |
|||
zap.String("path", ca.rootCertPath)) |
|||
continue |
|||
} |
|||
|
|||
ca.log.Warn("trusting root certificate (you might be prompted for password)", |
|||
zap.String("path", ca.rootCertPath)) |
|||
|
|||
err := truststore.Install(ca.root, |
|||
truststore.WithDebug(), |
|||
truststore.WithFirefox(), |
|||
truststore.WithJava(), |
|||
) |
|||
if err != nil { |
|||
return fmt.Errorf("adding root certificate to trust store: %v", err) |
|||
} |
|||
} |
|||
|
|||
// see if root/intermediates need renewal...
|
|||
p.renewCerts() |
|||
|
|||
// ...and keep them renewed
|
|||
go p.maintenance() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Stop stops the PKI app.
|
|||
func (p *PKI) Stop() error { |
|||
return nil |
|||
} |
|||
|
|||
// Interface guards
|
|||
var ( |
|||
_ caddy.Provisioner = (*PKI)(nil) |
|||
_ caddy.App = (*PKI)(nil) |
|||
) |
@ -0,0 +1,199 @@ |
|||
// Copyright 2015 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 caddytls |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"crypto" |
|||
"crypto/x509" |
|||
"encoding/pem" |
|||
"fmt" |
|||
"time" |
|||
|
|||
"github.com/caddyserver/caddy/v2" |
|||
"github.com/caddyserver/caddy/v2/modules/caddypki" |
|||
"github.com/caddyserver/certmagic" |
|||
"github.com/smallstep/certificates/authority" |
|||
"github.com/smallstep/certificates/authority/provisioner" |
|||
"github.com/smallstep/cli/crypto/x509util" |
|||
) |
|||
|
|||
func init() { |
|||
caddy.RegisterModule(InternalIssuer{}) |
|||
} |
|||
|
|||
// InternalIssuer is a certificate issuer that generates
|
|||
// certificates internally using a locally-configured
|
|||
// CA which can be customized using the `pki` app.
|
|||
type InternalIssuer struct { |
|||
// The ID of the CA to use for signing. The default
|
|||
// CA ID is "local". The CA can be configured with the
|
|||
// `pki` app.
|
|||
CA string `json:"ca,omitempty"` |
|||
|
|||
// The validity period of certificates.
|
|||
Lifetime caddy.Duration `json:"lifetime,omitempty"` |
|||
|
|||
// If true, the root will be the issuer instead of
|
|||
// the intermediate. This is NOT recommended and should
|
|||
// only be used when devices/clients do not properly
|
|||
// validate certificate chains.
|
|||
SignWithRoot bool `json:"sign_with_root,omitempty"` |
|||
|
|||
ca *caddypki.CA |
|||
} |
|||
|
|||
// CaddyModule returns the Caddy module information.
|
|||
func (InternalIssuer) CaddyModule() caddy.ModuleInfo { |
|||
return caddy.ModuleInfo{ |
|||
ID: "tls.issuance.internal", |
|||
New: func() caddy.Module { return new(InternalIssuer) }, |
|||
} |
|||
} |
|||
|
|||
// Provision sets up the issuer.
|
|||
func (li *InternalIssuer) Provision(ctx caddy.Context) error { |
|||
// get a reference to the configured CA
|
|||
appModule, err := ctx.App("pki") |
|||
if err != nil { |
|||
return err |
|||
} |
|||
pkiApp := appModule.(*caddypki.PKI) |
|||
if li.CA == "" { |
|||
li.CA = defaultInternalCAName |
|||
} |
|||
ca, ok := pkiApp.CAs[li.CA] |
|||
if !ok { |
|||
return fmt.Errorf("no certificate authority configured with id: %s", li.CA) |
|||
} |
|||
li.ca = ca |
|||
|
|||
// set any other default values
|
|||
if li.Lifetime == 0 { |
|||
li.Lifetime = caddy.Duration(defaultInternalCertLifetime) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// IssuerKey returns the unique issuer key for the
|
|||
// confgured CA endpoint.
|
|||
func (li InternalIssuer) IssuerKey() string { |
|||
return li.ca.ID() |
|||
} |
|||
|
|||
// Issue issues a certificate to satisfy the CSR.
|
|||
func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { |
|||
// prepare the signing authority
|
|||
// TODO: eliminate placeholders / needless values
|
|||
cfg := &authority.Config{ |
|||
Address: "placeholder_Address:1", |
|||
Root: []string{"placeholder_Root"}, |
|||
IntermediateCert: "placeholder_IntermediateCert", |
|||
IntermediateKey: "placeholder_IntermediateKey", |
|||
DNSNames: []string{"placeholder_DNSNames"}, |
|||
AuthorityConfig: &authority.AuthConfig{ |
|||
Provisioners: provisioner.List{}, |
|||
}, |
|||
} |
|||
|
|||
// get the root certificate and the issuer cert+key
|
|||
rootCert := li.ca.RootCertificate() |
|||
var issuerCert *x509.Certificate |
|||
var issuerKey interface{} |
|||
if li.SignWithRoot { |
|||
issuerCert = rootCert |
|||
var err error |
|||
issuerKey, err = li.ca.RootKey() |
|||
if err != nil { |
|||
return nil, fmt.Errorf("loading signing key: %v", err) |
|||
} |
|||
} else { |
|||
issuerCert = li.ca.IntermediateCertificate() |
|||
issuerKey = li.ca.IntermediateKey() |
|||
} |
|||
|
|||
auth, err := authority.New(cfg, |
|||
authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)), |
|||
authority.WithX509RootCerts(rootCert), |
|||
) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("initializing certificate authority: %v", err) |
|||
} |
|||
|
|||
// ensure issued certificate does not expire later than its issuer
|
|||
lifetime := time.Duration(li.Lifetime) |
|||
if time.Now().Add(lifetime).After(issuerCert.NotAfter) { |
|||
// TODO: log this
|
|||
lifetime = issuerCert.NotAfter.Sub(time.Now()) |
|||
} |
|||
|
|||
certChain, err := auth.Sign(csr, provisioner.Options{}, |
|||
profileDefaultDuration(li.Lifetime), |
|||
) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
var buf bytes.Buffer |
|||
for _, cert := range certChain { |
|||
err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
} |
|||
|
|||
return &certmagic.IssuedCertificate{ |
|||
Certificate: buf.Bytes(), |
|||
}, nil |
|||
} |
|||
|
|||
// TODO: borrowing from https://github.com/smallstep/certificates/blob/806abb6232a5691198b891d76b9898ea7f269da0/authority/provisioner/sign_options.go#L191-L211
|
|||
// as per https://github.com/smallstep/certificates/issues/198.
|
|||
// profileDefaultDuration is a wrapper against x509util.WithOption to conform
|
|||
// the SignOption interface.
|
|||
type profileDefaultDuration time.Duration |
|||
|
|||
// TODO: is there a better way to set cert lifetimes than copying from the smallstep libs?
|
|||
func (d profileDefaultDuration) Option(so provisioner.Options) x509util.WithOption { |
|||
var backdate time.Duration |
|||
notBefore := so.NotBefore.Time() |
|||
if notBefore.IsZero() { |
|||
notBefore = time.Now().Truncate(time.Second) |
|||
backdate = -1 * so.Backdate |
|||
} |
|||
notAfter := so.NotAfter.RelativeTime(notBefore) |
|||
return func(p x509util.Profile) error { |
|||
fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(d)) |
|||
if err := fn(p); err != nil { |
|||
return err |
|||
} |
|||
crt := p.Subject() |
|||
crt.NotBefore = crt.NotBefore.Add(backdate) |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
const ( |
|||
defaultInternalCAName = "local" |
|||
defaultInternalCertLifetime = 12 * time.Hour |
|||
) |
|||
|
|||
// Interface guards
|
|||
var ( |
|||
_ caddy.Provisioner = (*InternalIssuer)(nil) |
|||
_ certmagic.Issuer = (*InternalIssuer)(nil) |
|||
) |
Loading…
Reference in new issue