terraform/builtin/providers/tls/resource_certificate.go

211 lines
6.0 KiB
Go

package tls
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"math/big"
"time"
"github.com/hashicorp/terraform/helper/schema"
)
const pemCertType = "CERTIFICATE"
var keyUsages map[string]x509.KeyUsage = map[string]x509.KeyUsage{
"digital_signature": x509.KeyUsageDigitalSignature,
"content_commitment": x509.KeyUsageContentCommitment,
"key_encipherment": x509.KeyUsageKeyEncipherment,
"data_encipherment": x509.KeyUsageDataEncipherment,
"key_agreement": x509.KeyUsageKeyAgreement,
"cert_signing": x509.KeyUsageCertSign,
"crl_signing": x509.KeyUsageCRLSign,
"encipher_only": x509.KeyUsageEncipherOnly,
"decipher_only": x509.KeyUsageDecipherOnly,
}
var extKeyUsages map[string]x509.ExtKeyUsage = map[string]x509.ExtKeyUsage{
"any_extended": x509.ExtKeyUsageAny,
"server_auth": x509.ExtKeyUsageServerAuth,
"client_auth": x509.ExtKeyUsageClientAuth,
"code_signing": x509.ExtKeyUsageCodeSigning,
"email_protection": x509.ExtKeyUsageEmailProtection,
"ipsec_end_system": x509.ExtKeyUsageIPSECEndSystem,
"ipsec_tunnel": x509.ExtKeyUsageIPSECTunnel,
"ipsec_user": x509.ExtKeyUsageIPSECUser,
"timestamping": x509.ExtKeyUsageTimeStamping,
"ocsp_signing": x509.ExtKeyUsageOCSPSigning,
"microsoft_server_gated_crypto": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
"netscape_server_gated_crypto": x509.ExtKeyUsageNetscapeServerGatedCrypto,
}
// rsaPublicKey reflects the ASN.1 structure of a PKCS#1 public key.
type rsaPublicKey struct {
N *big.Int
E int
}
// generateSubjectKeyID generates a SHA-1 hash of the subject public key.
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
var publicKeyBytes []byte
var err error
switch pub := pub.(type) {
case *rsa.PublicKey:
publicKeyBytes, err = asn1.Marshal(rsaPublicKey{N: pub.N, E: pub.E})
if err != nil {
return nil, err
}
case *ecdsa.PublicKey:
publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y)
default:
return nil, errors.New("only RSA and ECDSA public keys supported")
}
hash := sha1.Sum(publicKeyBytes)
return hash[:], nil
}
func resourceCertificateCommonSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
"validity_period_hours": &schema.Schema{
Type: schema.TypeInt,
Required: true,
Description: "Number of hours that the certificate will remain valid for",
ForceNew: true,
},
"early_renewal_hours": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
Description: "Number of hours before the certificates expiry when a new certificate will be generated",
ForceNew: true,
},
"is_ca_certificate": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Whether the generated certificate will be usable as a CA certificate",
ForceNew: true,
},
"allowed_uses": &schema.Schema{
Type: schema.TypeList,
Required: true,
Description: "Uses that are allowed for the certificate",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"cert_pem": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"validity_start_time": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"validity_end_time": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
}
}
func createCertificate(d *schema.ResourceData, template, parent *x509.Certificate, pub crypto.PublicKey, priv interface{}) error {
var err error
template.NotBefore = time.Now()
template.NotAfter = template.NotBefore.Add(time.Duration(d.Get("validity_period_hours").(int)) * time.Hour)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
template.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %s", err)
}
keyUsesI := d.Get("allowed_uses").([]interface{})
for _, keyUseI := range keyUsesI {
keyUse := keyUseI.(string)
if usage, ok := keyUsages[keyUse]; ok {
template.KeyUsage |= usage
}
if usage, ok := extKeyUsages[keyUse]; ok {
template.ExtKeyUsage = append(template.ExtKeyUsage, usage)
}
}
if d.Get("is_ca_certificate").(bool) {
template.IsCA = true
template.SubjectKeyId, err = generateSubjectKeyID(pub)
if err != nil {
return fmt.Errorf("failed to set subject key identifier: %s", err)
}
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv)
if err != nil {
return fmt.Errorf("error creating certificate: %s", err)
}
certPem := string(pem.EncodeToMemory(&pem.Block{Type: pemCertType, Bytes: certBytes}))
validFromBytes, err := template.NotBefore.MarshalText()
if err != nil {
return fmt.Errorf("error serializing validity_start_time: %s", err)
}
validToBytes, err := template.NotAfter.MarshalText()
if err != nil {
return fmt.Errorf("error serializing validity_end_time: %s", err)
}
d.SetId(template.SerialNumber.String())
d.Set("cert_pem", certPem)
d.Set("validity_start_time", string(validFromBytes))
d.Set("validity_end_time", string(validToBytes))
return nil
}
func DeleteCertificate(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func ReadCertificate(d *schema.ResourceData, meta interface{}) error {
endTimeStr := d.Get("validity_end_time").(string)
endTime := time.Now()
err := endTime.UnmarshalText([]byte(endTimeStr))
if err != nil {
// If end time is invalid then we'll just throw away the whole
// thing so we can generate a new one.
d.SetId("")
return nil
}
earlyRenewalPeriod := time.Duration(-d.Get("early_renewal_hours").(int)) * time.Hour
endTime = endTime.Add(earlyRenewalPeriod)
if time.Now().After(endTime) {
// Treat an expired certificate as not existing, so we'll generate
// a new one with the next plan.
d.SetId("")
}
return nil
}