* Configurable issuer duration and renewBefore [1/3] This is part one of (probably) three parts manually moving the changes from commit 723015174a167d746323f506ab3575cfb243d8bd to the new master. This commit moves the basic functionality of configurable duration while skipping e2e tests and docs. It does not include new work. Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Configurable issuer duration and renewBefore [2/3] This commit moves over most of the e2e testing updates, some things are intentionally left out as they may be obsolete Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Configurable issuer duration and renewBefore [3/3] This commit moves the documentation changes, completely the migration of the original code to the latest master Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerunning all hack scripts with since the massive bazel update Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add missing boilerplate headers Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerun codegen hack Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerunning update-docs hack Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix failing unit tests Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix build errors in e2e tests Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerun update-deps Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Don't recreate the CA issuer, it already exists Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Need to create new issuers for the duration and renew time tests because those fields are set in the issuer, so make sure they are named uniquely Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add duration e2e tests for self-signed issuer Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add duration e2e tests for vault w/ custom mount path Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add validation to disallow acme certificates with duration and renewBefore set and update unit tests to verify Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Update docs to mention duration/renew for self-signed issuer and fix potential parsing errors with rst formatting Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Self-signed issuer was missing duration validation Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix a bug causing certificates with a short enough renew-before w.r.t their duration to be renewed instantly and forever Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Print the exact time until renewal Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Move duration and renwal validation to the issuer validation Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Update e2e tests to work with new validation Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add e2e test for the self-signed issuer Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Redo cert duration and renew before to appear as part of the CSR and not the issuer Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Updating tests to match new duration/renewbefore format Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Update e2e tests to match new format Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Update docs to reflect changing the field from issuers to certificates Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove event firing and replace with a TODO as of discussion on PR Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Run hack scripts Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove the sync unit test since without events there is no way to catch the warnings that it was testing Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Use IssuerOptions RenewBeforeExpiryDuration if certificates dont set a renewBefore value for immediate renewal checks Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Delete check on certificate data length in e2e test for certificate duration as there is no reason it should be there Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Update e2e tests since certificate creation will never generate an event Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerunning hack scripts after big rebase Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix a few problems that slipped through during the rebase Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix an e2e error that resulted from the rebase Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add unit test for the calculateTimeBeforeExpiry function Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Adding back in a bunch of missing error checks Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove unused function Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add missing boilerplate Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove unused constant Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Move log constants to function body Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerun hack scripts Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove mistakenly commited file Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove double-import of util package Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix bad function call in e2e vault issuer Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Change duration and renewBefore to be pointer fields as they are optional Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Remove wrong vault issuer test that got passed the rebase somehow Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Change e2e to use pointer format Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Move e2e cert tests out of issuer test file Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Move e2e self-signed issuer test to new location Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Make sure to check for nil in GenerateTemplate Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Add more empty checks to be safe Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Rerunning hacks after rebase Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix bad function call in new e2e test Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Try not setting duration and renewbefore on acme e2e tests Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Zero checks should really just be replaced by nil tests, zero should be caught as any other too-small value Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fixed a missing nil check that got away Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Change e2e duration test format to use pointer times to better simulate API calls Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix sync unit test to match e2e test format Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Fix vault e2e test Signed-off-by: Max Ehrlich <max.ehr@gmail.com> * Revert changes to Certificate sync function Signed-off-by: James Munnelly <james@munnelly.eu> * Remove selfsigned e2e issuer.go Signed-off-by: James Munnelly <james@munnelly.eu> * Don't use ACME issuer in duration example and tidy up line endings Signed-off-by: James Munnelly <james@munnelly.eu> * Allow renewBefore to be set on ACME certificates Signed-off-by: James Munnelly <james@munnelly.eu> * Update renewBefore ACME docs. Remove unused fields. Signed-off-by: James Munnelly <james@munnelly.eu> * Rename calculateTimeBeforeExpiry to calculateDurationUntilRenew Signed-off-by: James Munnelly <james@munnelly.eu>
272 lines
8.6 KiB
Go
272 lines
8.6 KiB
Go
/*
|
|
Copyright 2018 The Jetstack cert-manager contributors.
|
|
|
|
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 pki
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
|
)
|
|
|
|
// CommonNameForCertificate returns the common name that should be used for the
|
|
// given Certificate resource, by inspecting the CommonName and DNSNames fields.
|
|
func CommonNameForCertificate(crt *v1alpha1.Certificate) string {
|
|
if crt.Spec.CommonName != "" {
|
|
return crt.Spec.CommonName
|
|
}
|
|
if len(crt.Spec.DNSNames) == 0 {
|
|
return ""
|
|
}
|
|
return crt.Spec.DNSNames[0]
|
|
}
|
|
|
|
// DNSNamesForCertificate returns the DNS names that should be used for the
|
|
// given Certificate resource, by inspecting the CommonName and DNSNames fields.
|
|
func DNSNamesForCertificate(crt *v1alpha1.Certificate) []string {
|
|
if len(crt.Spec.DNSNames) == 0 {
|
|
if crt.Spec.CommonName == "" {
|
|
return []string{}
|
|
}
|
|
return []string{crt.Spec.CommonName}
|
|
}
|
|
if crt.Spec.CommonName != "" {
|
|
return removeDuplicates(append([]string{crt.Spec.CommonName}, crt.Spec.DNSNames...))
|
|
}
|
|
return crt.Spec.DNSNames
|
|
}
|
|
|
|
func removeDuplicates(in []string) []string {
|
|
var found []string
|
|
Outer:
|
|
for _, i := range in {
|
|
for _, i2 := range found {
|
|
if i2 == i {
|
|
continue Outer
|
|
}
|
|
}
|
|
found = append(found, i)
|
|
}
|
|
return found
|
|
}
|
|
|
|
const defaultOrganization = "cert-manager"
|
|
|
|
// OrganizationForCertificate will return the Organization to set for the
|
|
// Certificate resource.
|
|
// If an Organization is not specifically set, a default will be used.
|
|
func OrganizationForCertificate(crt *v1alpha1.Certificate) []string {
|
|
if len(crt.Spec.Organization) == 0 {
|
|
return []string{defaultOrganization}
|
|
}
|
|
|
|
return crt.Spec.Organization
|
|
}
|
|
|
|
var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128)
|
|
|
|
// GenerateCSR will generate a new *x509.CertificateRequest template to be used
|
|
// by issuers that utilise CSRs to obtain Certificates.
|
|
// The CSR will not be signed, and should be passed to either EncodeCSR or
|
|
// to the x509.CreateCertificateRequest function.
|
|
func GenerateCSR(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x509.CertificateRequest, error) {
|
|
commonName := CommonNameForCertificate(crt)
|
|
dnsNames := DNSNamesForCertificate(crt)
|
|
organization := OrganizationForCertificate(crt)
|
|
|
|
if len(commonName) == 0 && len(dnsNames) == 0 {
|
|
return nil, fmt.Errorf("no domains specified on certificate")
|
|
}
|
|
|
|
pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &x509.CertificateRequest{
|
|
Version: 3,
|
|
SignatureAlgorithm: sigAlgo,
|
|
PublicKeyAlgorithm: pubKeyAlgo,
|
|
Subject: pkix.Name{
|
|
Organization: organization,
|
|
CommonName: commonName,
|
|
},
|
|
DNSNames: dnsNames,
|
|
// TODO: work out how best to handle extensions/key usages here
|
|
ExtraExtensions: []pkix.Extension{},
|
|
}, nil
|
|
}
|
|
|
|
// GenerateTemplate will create a x509.Certificate for the given Certificate resource.
|
|
// This should create a Certificate template that is equivalent to the CertificateRequest
|
|
// generated by GenerateCSR.
|
|
// The PublicKey field must be populated by the caller.
|
|
func GenerateTemplate(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x509.Certificate, error) {
|
|
commonName := CommonNameForCertificate(crt)
|
|
dnsNames := DNSNamesForCertificate(crt)
|
|
organization := OrganizationForCertificate(crt)
|
|
|
|
if len(commonName) == 0 && len(dnsNames) == 0 {
|
|
return nil, fmt.Errorf("no domains specified on certificate")
|
|
}
|
|
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate serial number: %s", err.Error())
|
|
}
|
|
|
|
certDuration := v1alpha1.DefaultCertificateDuration
|
|
if crt.Spec.Duration != nil {
|
|
certDuration = crt.Spec.Duration.Duration
|
|
}
|
|
|
|
pubKeyAlgo, _, err := SignatureAlgorithm(crt)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyUsages := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
|
if crt.Spec.IsCA {
|
|
keyUsages |= x509.KeyUsageCertSign
|
|
}
|
|
|
|
return &x509.Certificate{
|
|
Version: 3,
|
|
BasicConstraintsValid: true,
|
|
SerialNumber: serialNumber,
|
|
PublicKeyAlgorithm: pubKeyAlgo,
|
|
IsCA: crt.Spec.IsCA,
|
|
Subject: pkix.Name{
|
|
Organization: organization,
|
|
CommonName: commonName,
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(certDuration),
|
|
// see http://golang.org/pkg/crypto/x509/#KeyUsage
|
|
KeyUsage: keyUsages,
|
|
DNSNames: dnsNames,
|
|
}, nil
|
|
}
|
|
|
|
// SignCertificate returns a signed x509.Certificate object for the given
|
|
// *v1alpha1.Certificate crt.
|
|
// publicKey is the public key of the signee, and signerKey is the private
|
|
// key of the signer.
|
|
// It returns a PEM encoded copy of the Certificate as well as a *x509.Certificate
|
|
// which can be used for reading the encoded values.
|
|
func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey interface{}, signerKey interface{}) ([]byte, *x509.Certificate, error) {
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, signerKey)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error creating x509 certificate: %s", err.Error())
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(derBytes)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error decoding DER certificate bytes: %s", err.Error())
|
|
}
|
|
|
|
pemBytes := bytes.NewBuffer([]byte{})
|
|
err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error encoding certificate PEM: %s", err.Error())
|
|
}
|
|
|
|
// don't bundle the CA for selfsigned certificates
|
|
// TODO: better comparison method here? for now we can just compare pointers.
|
|
if issuerCert != template {
|
|
// bundle the CA
|
|
err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: issuerCert.Raw})
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error encoding issuer cetificate PEM: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
return pemBytes.Bytes(), cert, err
|
|
}
|
|
|
|
// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template.
|
|
// It returns a DER encoded signed CSR.
|
|
func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) {
|
|
derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error())
|
|
}
|
|
|
|
return derBytes, nil
|
|
}
|
|
|
|
// EncodeX509 will encode a *x509.Certificate into PEM format.
|
|
func EncodeX509(cert *x509.Certificate) ([]byte, error) {
|
|
caPem := bytes.NewBuffer([]byte{})
|
|
err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return caPem.Bytes(), nil
|
|
}
|
|
|
|
// SignatureAlgorithm will determine the appropriate signature algorithm for
|
|
// the given certificate.
|
|
// Adapted from https://github.com/cloudflare/cfssl/blob/master/csr/csr.go#L102
|
|
func SignatureAlgorithm(crt *v1alpha1.Certificate) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) {
|
|
var sigAlgo x509.SignatureAlgorithm
|
|
var pubKeyAlgo x509.PublicKeyAlgorithm
|
|
switch crt.Spec.KeyAlgorithm {
|
|
case v1alpha1.KeyAlgorithm(""):
|
|
// If keyAlgorithm is not specified, we default to rsa with keysize 2048
|
|
pubKeyAlgo = x509.RSA
|
|
sigAlgo = x509.SHA256WithRSA
|
|
case v1alpha1.RSAKeyAlgorithm:
|
|
pubKeyAlgo = x509.RSA
|
|
switch {
|
|
case crt.Spec.KeySize >= 4096:
|
|
sigAlgo = x509.SHA512WithRSA
|
|
case crt.Spec.KeySize >= 3072:
|
|
sigAlgo = x509.SHA384WithRSA
|
|
case crt.Spec.KeySize >= 2048:
|
|
sigAlgo = x509.SHA256WithRSA
|
|
default:
|
|
return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported rsa keysize specified: %d. min keysize %d", crt.Spec.KeySize, MinRSAKeySize)
|
|
}
|
|
case v1alpha1.ECDSAKeyAlgorithm:
|
|
pubKeyAlgo = x509.ECDSA
|
|
switch crt.Spec.KeySize {
|
|
case 521:
|
|
sigAlgo = x509.ECDSAWithSHA512
|
|
case 384:
|
|
sigAlgo = x509.ECDSAWithSHA384
|
|
case 256:
|
|
sigAlgo = x509.ECDSAWithSHA256
|
|
default:
|
|
return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported ecdsa keysize specified: %d", crt.Spec.KeySize)
|
|
}
|
|
default:
|
|
return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported algorithm specified: %s. should be either 'ecdsa' or 'rsa", crt.Spec.KeyAlgorithm)
|
|
}
|
|
return pubKeyAlgo, sigAlgo, nil
|
|
}
|