provider/tls: add locally signed certificates

This allows you to generate and sign certificates using a local CA.
This commit is contained in:
Silas Sewell 2015-11-15 11:45:05 -05:00
parent 2dbd9b47e4
commit 309e697a52
9 changed files with 765 additions and 232 deletions

View File

@ -13,9 +13,10 @@ import (
func Provider() terraform.ResourceProvider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"tls_private_key": resourcePrivateKey(),
"tls_self_signed_cert": resourceSelfSignedCert(),
"tls_cert_request": resourceCertRequest(),
"tls_private_key": resourcePrivateKey(),
"tls_locally_signed_cert": resourceLocallySignedCert(),
"tls_self_signed_cert": resourceSelfSignedCert(),
"tls_cert_request": resourceCertRequest(),
},
}
}

View File

@ -34,3 +34,62 @@ DrUJcPbKUfF4VBqmmwwkpwT938Hr/iCcS6kE3hqXiN9a5XJb4vnk2FdZNPS9hf2J
rpxCHbX0xSJh0s8j7exRHMF8W16DHjjkc265YdWPXWo=
-----END RSA PRIVATE KEY-----
`
var testCertRequest = `
-----BEGIN CERTIFICATE REQUEST-----
MIICYDCCAckCAQAwgcUxFDASBgNVBAMMC2V4YW1wbGUuY29tMQswCQYDVQQGEwJV
UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVBpcmF0ZSBIYXJib3IxGTAXBgNVBAkM
EDU4NzkgQ290dG9uIExpbmsxEzARBgNVBBEMCjk1NTU5LTEyMjcxFTATBgNVBAoM
DEV4YW1wbGUsIEluYzEoMCYGA1UECwwfRGVwYXJ0bWVudCBvZiBUZXJyYWZvcm0g
VGVzdGluZzEKMAgGA1UEBRMBMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
qLFq7Tpmlt0uDCCn5bA/oTj4v16/pXXaD+Ice2bS4rBH2UUM2gca5U4j8QCxrIxh
91mBvloE4VS5xrIGotAwoMgwK3E2md5kzQJToDve/hm8JNOcms+OAOjfjajPc40e
+ue9roT8VjWGU0wz7ttQNuao56GXYr5kOpcfiZMs7RcCAwEAAaBaMFgGCSqGSIb3
DQEJDjFLMEkwLwYDVR0RBCgwJoILZXhhbXBsZS5jb22CC2V4YW1wbGUubmV0hwR/
AAABhwR/AAACMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMA0GCSqGSIb3DQEBBQUA
A4GBAGEDWUYnGygtnvScamz3o4PuVMFubBfqIdWCu02hBgzL3Hi3/UkOEsV028GM
M3YMB+it7U8eDdT2XjzBDlvpxWT1hXWnmJFu6z6B8N/JFk8fOkaP7U6YjZlG5N9m
L1A4WtQz0SgXcnIujKisqIaymYrvpANnm4IsqTKsnwZD7CsQ
-----END CERTIFICATE REQUEST-----
`
var testCAPrivateKey = `
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC7QNFtw54heoD9KL2s2Qr7utKZFM/8GXYHh3Y5/Zis9USlJ7Mc
Lorbmm9Lopnr5zUBZULAxAgX51X0FbifK8Re3JIZvpFRyxNw8aWYBnOk/sX7UhUH
pI139dSAhkNAMkRQd1ySpDP+4okCptgZPs7h0bXwoYmWMNFKlaRZHuAQLQIDAQAB
AoGAQ/YwjLAU8n2t1zQ0M0nLDLYvvVOqcQskpXLq2/1Irm2OborMHQxfZXjVsBPh
3ZbazBjec2wyq8pQjfhcO5j8+fj9zLtRNDpWEa9t/VDky0MSGezQyLL1J5+htFDJ
JDCkKK441IWKGCMC31hoVP6PvE/3G2+vWAkrkT4U7ekLQVkCQQD1/RKMxDFJ57Qr
Zlu1y72dnGLsGqoxeNaco6G5JXAEEcWTx8qXghKQX0uHxooeRYQRupOGLBo1Js1p
/AZDR8inAkEAwt/J0GDsojV89RbpJ0h7C1kcxNULooCYQZs/rmJcVXSs6pUIIFdI
oYQIEGnRsfQUPo6EUUGMKh8sSEjF6R8nCwJBAMKYuoT7a9aAYwp2RhTSIaW+oo8P
JRZP9s8hr31tPWkqufeHdSBYOOFXUcQObxM1gR4ZUD0zRGRJ1vSB+F5fOj8CQEuG
HZnTpoHrBuWZnnyp+33XaG3kP2EYQ2nRuClmV3CLCmTTo1WdXjmyiMmLqUg1Vw8z
fpZbN+4vLKNLCOCjQScCQDWmNDrie4Omd5wWKV5B+LVZO8/xMlub6IEioZpMfDGZ
q1Ov/Qw2ge3yumfO+6GzKG0k13yYEn1AcatF5lP8BYY=
-----END RSA PRIVATE KEY-----
`
var testCACert = `
-----BEGIN CERTIFICATE-----
MIIDVTCCAr6gAwIBAgIJALLsVgWAcCvxMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNV
BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNUGlyYXRlIEhhcmJvcjEVMBMG
A1UEChMMRXhhbXBsZSwgSW5jMSEwHwYDVQQLExhEZXBhcnRtZW50IG9mIENBIFRl
c3RpbmcxDTALBgNVBAMTBHJvb3QwHhcNMTUxMTE0MTY1MTQ0WhcNMTUxMjE0MTY1
MTQ0WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVBpcmF0
ZSBIYXJib3IxFTATBgNVBAoTDEV4YW1wbGUsIEluYzEhMB8GA1UECxMYRGVwYXJ0
bWVudCBvZiBDQSBUZXN0aW5nMQ0wCwYDVQQDEwRyb290MIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQC7QNFtw54heoD9KL2s2Qr7utKZFM/8GXYHh3Y5/Zis9USl
J7McLorbmm9Lopnr5zUBZULAxAgX51X0FbifK8Re3JIZvpFRyxNw8aWYBnOk/sX7
UhUHpI139dSAhkNAMkRQd1ySpDP+4okCptgZPs7h0bXwoYmWMNFKlaRZHuAQLQID
AQABo4HgMIHdMB0GA1UdDgQWBBQyrsMhTd85ATqm9vNybTtAbwnGkDCBrQYDVR0j
BIGlMIGigBQyrsMhTd85ATqm9vNybTtAbwnGkKF/pH0wezELMAkGA1UEBhMCVVMx
CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1QaXJhdGUgSGFyYm9yMRUwEwYDVQQKEwxF
eGFtcGxlLCBJbmMxITAfBgNVBAsTGERlcGFydG1lbnQgb2YgQ0EgVGVzdGluZzEN
MAsGA1UEAxMEcm9vdIIJALLsVgWAcCvxMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
AQEFBQADgYEAuJ7JGZlSzbQOuAFz2t3c1pQzUIiS74blFbg6RPvNPSSjoBg3Ly61
FbliR8P3qiSWA/X03/XSMTH1XkHU8re+P0uILUzLJkKBkdHJfdwfk8kifDjdO14+
tffPaqAEFUkwhbiQUoj9aeTOOS6kEjbMV6+o7fsz5pPUHbj/l4idys0=
-----END CERTIFICATE-----
`

View File

@ -10,6 +10,8 @@ import (
"github.com/hashicorp/terraform/helper/schema"
)
const pemCertReqType = "CERTIFICATE REQUEST"
func resourceCertRequest() *schema.Resource {
return &schema.Resource{
Create: CreateCertRequest,
@ -71,19 +73,9 @@ func resourceCertRequest() *schema.Resource {
}
func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
keyAlgoName := d.Get("key_algorithm").(string)
var keyFunc keyParser
var ok bool
if keyFunc, ok = keyParsers[keyAlgoName]; !ok {
return fmt.Errorf("invalid key_algorithm %#v", keyAlgoName)
}
keyBlock, _ := pem.Decode([]byte(d.Get("private_key_pem").(string)))
if keyBlock == nil {
return fmt.Errorf("no PEM block found in private_key_pem")
}
key, err := keyFunc(keyBlock.Bytes)
key, err := parsePrivateKey(d, "private_key_pem", "key_algorithm")
if err != nil {
return fmt.Errorf("failed to decode private_key_pem: %s", err)
return err
}
subjectConfs := d.Get("subject").([]interface{})
@ -117,7 +109,7 @@ func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
if err != nil {
fmt.Errorf("Error creating certificate request: %s", err)
}
certReqPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: certReqBytes}))
certReqPem := string(pem.EncodeToMemory(&pem.Block{Type: pemCertReqType, Bytes: certReqBytes}))
d.SetId(hashForState(string(certReqBytes)))
d.Set("cert_request_pem", certReqPem)

View File

@ -0,0 +1,210 @@
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 {
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
}

View File

@ -0,0 +1,79 @@
package tls
import (
"crypto/x509"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceLocallySignedCert() *schema.Resource {
s := resourceCertificateCommonSchema()
s["cert_request_pem"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "PEM-encoded certificate request",
ForceNew: true,
StateFunc: func(v interface{}) string {
return hashForState(v.(string))
},
}
s["ca_key_algorithm"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Name of the algorithm used to generate the certificate's private key",
ForceNew: true,
}
s["ca_private_key_pem"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "PEM-encoded CA private key used to sign the certificate",
ForceNew: true,
StateFunc: func(v interface{}) string {
return hashForState(v.(string))
},
}
s["ca_cert_pem"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "PEM-encoded CA certificate",
ForceNew: true,
StateFunc: func(v interface{}) string {
return hashForState(v.(string))
},
}
return &schema.Resource{
Create: CreateLocallySignedCert,
Delete: DeleteCertificate,
Read: ReadCertificate,
Schema: s,
}
}
func CreateLocallySignedCert(d *schema.ResourceData, meta interface{}) error {
certReq, err := parseCertificateRequest(d, "cert_request_pem")
if err != nil {
return err
}
caKey, err := parsePrivateKey(d, "ca_private_key_pem", "ca_key_algorithm")
if err != nil {
return err
}
caCert, err := parseCertificate(d, "ca_cert_pem")
if err != nil {
return err
}
cert := x509.Certificate{
Subject: certReq.Subject,
DNSNames: certReq.DNSNames,
IPAddresses: certReq.IPAddresses,
BasicConstraintsValid: true,
}
return createCertificate(d, &cert, caCert, certReq.PublicKey, caKey)
}

View File

@ -0,0 +1,162 @@
package tls
import (
"bytes"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"
"testing"
"time"
r "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestLocallySignedCert(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
r.TestStep{
Config: fmt.Sprintf(`
resource "tls_locally_signed_cert" "test" {
cert_request_pem = <<EOT
%s
EOT
validity_period_hours = 1
allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
"client_auth",
]
ca_key_algorithm = "RSA"
ca_cert_pem = <<EOT
%s
EOT
ca_private_key_pem = <<EOT
%s
EOT
}
output "cert_pem" {
value = "${tls_locally_signed_cert.test.cert_pem}"
}
`, testCertRequest, testCACert, testCAPrivateKey),
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["cert_pem"]
if !strings.HasPrefix(got, "-----BEGIN CERTIFICATE----") {
return fmt.Errorf("key is missing cert PEM preamble")
}
block, _ := pem.Decode([]byte(got))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("error parsing cert: %s", err)
}
if expected, got := "2", cert.Subject.SerialNumber; got != expected {
return fmt.Errorf("incorrect subject serial number: expected %v, got %v", expected, got)
}
if expected, got := "example.com", cert.Subject.CommonName; got != expected {
return fmt.Errorf("incorrect subject common name: expected %v, got %v", expected, got)
}
if expected, got := "Example, Inc", cert.Subject.Organization[0]; got != expected {
return fmt.Errorf("incorrect subject organization: expected %v, got %v", expected, got)
}
if expected, got := "Department of Terraform Testing", cert.Subject.OrganizationalUnit[0]; got != expected {
return fmt.Errorf("incorrect subject organizational unit: expected %v, got %v", expected, got)
}
if expected, got := "5879 Cotton Link", cert.Subject.StreetAddress[0]; got != expected {
return fmt.Errorf("incorrect subject street address: expected %v, got %v", expected, got)
}
if expected, got := "Pirate Harbor", cert.Subject.Locality[0]; got != expected {
return fmt.Errorf("incorrect subject locality: expected %v, got %v", expected, got)
}
if expected, got := "CA", cert.Subject.Province[0]; got != expected {
return fmt.Errorf("incorrect subject province: expected %v, got %v", expected, got)
}
if expected, got := "US", cert.Subject.Country[0]; got != expected {
return fmt.Errorf("incorrect subject country: expected %v, got %v", expected, got)
}
if expected, got := "95559-1227", cert.Subject.PostalCode[0]; got != expected {
return fmt.Errorf("incorrect subject postal code: expected %v, got %v", expected, got)
}
if expected, got := 2, len(cert.DNSNames); got != expected {
return fmt.Errorf("incorrect number of DNS names: expected %v, got %v", expected, got)
}
if expected, got := "example.com", cert.DNSNames[0]; got != expected {
return fmt.Errorf("incorrect DNS name 0: expected %v, got %v", expected, got)
}
if expected, got := "example.net", cert.DNSNames[1]; got != expected {
return fmt.Errorf("incorrect DNS name 0: expected %v, got %v", expected, got)
}
if expected, got := 2, len(cert.IPAddresses); got != expected {
return fmt.Errorf("incorrect number of IP addresses: expected %v, got %v", expected, got)
}
if expected, got := "127.0.0.1", cert.IPAddresses[0].String(); got != expected {
return fmt.Errorf("incorrect IP address 0: expected %v, got %v", expected, got)
}
if expected, got := "127.0.0.2", cert.IPAddresses[1].String(); got != expected {
return fmt.Errorf("incorrect IP address 0: expected %v, got %v", expected, got)
}
if expected, got := []byte{50, 174, 195, 33, 77, 223, 57, 1, 58, 166, 246, 243, 114, 109, 59, 64, 111, 9, 198, 144}, cert.AuthorityKeyId; !bytes.Equal(got, expected) {
return fmt.Errorf("incorrect AuthorityKeyId: expected %v, got %v", expected, got)
}
if expected, got := 2, len(cert.ExtKeyUsage); got != expected {
return fmt.Errorf("incorrect number of ExtKeyUsage: expected %v, got %v", expected, got)
}
if expected, got := x509.ExtKeyUsageServerAuth, cert.ExtKeyUsage[0]; got != expected {
return fmt.Errorf("incorrect ExtKeyUsage[0]: expected %v, got %v", expected, got)
}
if expected, got := x509.ExtKeyUsageClientAuth, cert.ExtKeyUsage[1]; got != expected {
return fmt.Errorf("incorrect ExtKeyUsage[1]: expected %v, got %v", expected, got)
}
if expected, got := x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature, cert.KeyUsage; got != expected {
return fmt.Errorf("incorrect KeyUsage: expected %v, got %v", expected, got)
}
// This time checking is a bit sloppy to avoid inconsistent test results
// depending on the power of the machine running the tests.
now := time.Now()
if cert.NotBefore.After(now) {
return fmt.Errorf("certificate validity begins in the future")
}
if now.Sub(cert.NotBefore) > (2 * time.Minute) {
return fmt.Errorf("certificate validity begins more than two minutes in the past")
}
if cert.NotAfter.Sub(cert.NotBefore) != time.Hour {
return fmt.Errorf("certificate validity is not one hour")
}
caBlock, _ := pem.Decode([]byte(testCACert))
caCert, err := x509.ParseCertificate(caBlock.Bytes)
if err != nil {
return fmt.Errorf("error parsing ca cert: %s", err)
}
certPool := x509.NewCertPool()
// Verify certificate
_, err = cert.Verify(x509.VerifyOptions{Roots: certPool})
if err == nil {
return errors.New("incorrectly verified certificate")
} else if _, ok := err.(x509.UnknownAuthorityError); !ok {
return fmt.Errorf("incorrect verify error: expected UnknownAuthorityError, got %v", err)
}
certPool.AddCert(caCert)
if _, err = cert.Verify(x509.VerifyOptions{Roots: certPool}); err != nil {
return fmt.Errorf("verify failed: %s", err)
}
return nil
},
},
},
})
}

View File

@ -1,169 +1,72 @@
package tls
import (
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"net"
"time"
"github.com/hashicorp/terraform/helper/schema"
)
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,
}
func resourceSelfSignedCert() *schema.Resource {
s := resourceCertificateCommonSchema()
s["subject"] = &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: nameSchema,
ForceNew: true,
}
s["dns_names"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "List of DNS names to use as subjects of the certificate",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
}
s["ip_addresses"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "List of IP addresses to use as subjects of the certificate",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
}
s["key_algorithm"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Name of the algorithm to use to generate the certificate's private key",
ForceNew: true,
}
s["private_key_pem"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "PEM-encoded private key that the certificate will belong to",
ForceNew: true,
StateFunc: func(v interface{}) string {
return hashForState(v.(string))
},
}
return &schema.Resource{
Create: CreateSelfSignedCert,
Delete: DeleteSelfSignedCert,
Read: ReadSelfSignedCert,
Schema: map[string]*schema.Schema{
"dns_names": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "List of DNS names to use as subjects of the certificate",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"ip_addresses": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "List of IP addresses to use as subjects of the certificate",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"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,
},
},
"key_algorithm": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Name of the algorithm to use to generate the certificate's private key",
ForceNew: true,
},
"private_key_pem": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "PEM-encoded private key that the certificate will belong to",
ForceNew: true,
StateFunc: func(v interface{}) string {
return hashForState(v.(string))
},
},
"subject": &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: nameSchema,
ForceNew: true,
},
"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,
},
},
Delete: DeleteCertificate,
Read: ReadCertificate,
Schema: s,
}
}
func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
keyAlgoName := d.Get("key_algorithm").(string)
var keyFunc keyParser
var ok bool
if keyFunc, ok = keyParsers[keyAlgoName]; !ok {
return fmt.Errorf("invalid key_algorithm %#v", keyAlgoName)
}
keyBlock, _ := pem.Decode([]byte(d.Get("private_key_pem").(string)))
if keyBlock == nil {
return fmt.Errorf("no PEM block found in private_key_pem")
}
key, err := keyFunc(keyBlock.Bytes)
key, err := parsePrivateKey(d, "private_key_pem", "key_algorithm")
if err != nil {
return fmt.Errorf("failed to decode private_key_pem: %s", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(time.Duration(d.Get("validity_period_hours").(int)) * time.Hour)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %s", err)
return err
}
subjectConfs := d.Get("subject").([]interface{})
@ -177,24 +80,10 @@ func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
}
cert := x509.Certificate{
SerialNumber: serialNumber,
Subject: *subject,
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
}
keyUsesI := d.Get("allowed_uses").([]interface{})
for _, keyUseI := range keyUsesI {
keyUse := keyUseI.(string)
if usage, ok := keyUsages[keyUse]; ok {
cert.KeyUsage |= usage
}
if usage, ok := extKeyUsages[keyUse]; ok {
cert.ExtKeyUsage = append(cert.ExtKeyUsage, usage)
}
}
dnsNamesI := d.Get("dns_names").([]interface{})
for _, nameI := range dnsNamesI {
cert.DNSNames = append(cert.DNSNames, nameI.(string))
@ -208,58 +97,5 @@ func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
cert.IPAddresses = append(cert.IPAddresses, ip)
}
if d.Get("is_ca_certificate").(bool) {
cert.IsCA = true
}
certBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, publicKey(key), key)
if err != nil {
fmt.Errorf("Error creating certificate: %s", err)
}
certPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}))
validFromBytes, err := notBefore.MarshalText()
if err != nil {
return fmt.Errorf("error serializing validity_start_time: %s", err)
}
validToBytes, err := notAfter.MarshalText()
if err != nil {
return fmt.Errorf("error serializing validity_end_time: %s", err)
}
d.SetId(serialNumber.String())
d.Set("cert_pem", certPem)
d.Set("validity_start_time", string(validFromBytes))
d.Set("validity_end_time", string(validToBytes))
return nil
}
func DeleteSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func ReadSelfSignedCert(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
return createCertificate(d, &cert, &cert, publicKey(key), key)
}

View File

@ -0,0 +1,76 @@
package tls
import (
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/hashicorp/terraform/helper/schema"
)
func decodePEM(d *schema.ResourceData, pemKey, pemType string) (*pem.Block, error) {
block, _ := pem.Decode([]byte(d.Get(pemKey).(string)))
if block == nil {
return nil, fmt.Errorf("no PEM block found in %s", pemKey)
}
if pemType != "" && block.Type != pemType {
return nil, fmt.Errorf("invalid PEM type in %s: %s", pemKey, block.Type)
}
return block, nil
}
func parsePrivateKey(d *schema.ResourceData, pemKey, algoKey string) (interface{}, error) {
algoName := d.Get(algoKey).(string)
keyFunc, ok := keyParsers[algoName]
if !ok {
return nil, fmt.Errorf("invalid %s: %#v", algoKey, algoName)
}
block, err := decodePEM(d, pemKey, "")
if err != nil {
return nil, err
}
key, err := keyFunc(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to decode %s: %s", pemKey, err)
}
return key, nil
}
func parseCertificate(d *schema.ResourceData, pemKey string) (*x509.Certificate, error) {
block, err := decodePEM(d, pemKey, "")
if err != nil {
return nil, err
}
certs, err := x509.ParseCertificates(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %s", pemKey, err)
}
if len(certs) < 1 {
return nil, fmt.Errorf("no certificates found in %s", pemKey)
}
if len(certs) > 1 {
return nil, fmt.Errorf("multiple certificates found in %s", pemKey)
}
return certs[0], nil
}
func parseCertificateRequest(d *schema.ResourceData, pemKey string) (*x509.CertificateRequest, error) {
block, err := decodePEM(d, pemKey, pemCertReqType)
if err != nil {
return nil, err
}
certReq, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %s", pemKey, err)
}
return certReq, nil
}

View File

@ -0,0 +1,118 @@
---
layout: "tls"
page_title: "TLS: tls_locally_signed_cert"
sidebar_current: "docs-tls-resourse-locally-signed-cert"
description: |-
Creates a locally-signed TLS certificate in PEM format.
---
# tls\_locally\_signed\_cert
Generates a TLS ceritifcate using a *Certificate Signing Request* (CSR) and
signs it with a provided certificate authority (CA) private key.
Locally-signed certificates are generally only trusted by client software when
setup to use the provided CA. They are normally used in development environments
or when deployed internally to an organization.
## Example Usage
```
resource "tls_locally_signed_cert" "example" {
cert_request_pem = "${file(\"cert_request.pem\")}"
ca_key_algorithm = "ECDSA"
ca_private_key_pem = "${file(\"ca_private_key.pem\")}"
ca_cert_pem = "${file(\"ca_cert.pem\")}"
validity_period_hours = 12
allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
]
}
```
## Argument Reference
The following arguments are supported:
* `cert_request_pem` - (Required) PEM-encoded request certificate data.
* `ca_key_algorithm` - (Required) The name of the algorithm for the key provided
in `ca_private_key_pem`.
* `ca_private_key_pem` - (Required) PEM-encoded private key data for the CA.
This can be read from a separate file using the ``file`` interpolation
function.
* `ca_cert_pem` - (Required) PEM-encoded certificate data for the CA.
* `validity_period_hours` - (Required) The number of hours after initial issuing that the
certificate will become invalid.
* `allowed_uses` - (Required) List of keywords each describing a use that is permitted
for the issued certificate. The valid keywords are listed below.
* `early_renewal_hours` - (Optional) If set, the resource will consider the certificate to
have expired the given number of hours before its actual expiry time. This can be useful
to deploy an updated certificate in advance of the expiration of the current certificate.
Note however that the old certificate remains valid until its true expiration time, since
this resource does not (and cannot) support certificate revocation. Note also that this
advance update can only be performed should the Terraform configuration be applied during the
early renewal period.
* `is_ca_certificate` - (Optional) Boolean controlling whether the CA flag will be set in the
generated certificate. Defaults to `false`, meaning that the certificate does not represent
a certificate authority.
The `allowed_uses` list accepts the following keywords, combining the set of flags defined by
both [Key Usage](https://tools.ietf.org/html/rfc5280#section-4.2.1.3) and
[Extended Key Usage](https://tools.ietf.org/html/rfc5280#section-4.2.1.12) in
[RFC5280](https://tools.ietf.org/html/rfc5280):
* `digital_signature`
* `content_commitment`
* `key_encipherment`
* `data_encipherment`
* `key_agreement`
* `cert_signing`
* `encipher_only`
* `decipher_only`
* `any_extended`
* `server_auth`
* `client_auth`
* `code_signing`
* `email_protection`
* `ipsec_end_system`
* `ipsec_tunnel`
* `ipsec_user`
* `timestamping`
* `ocsp_signing`
* `microsoft_server_gated_crypto`
* `netscape_server_gated_crypto`
## Attributes Reference
The following attributes are exported:
* `cert_pem` - The certificate data in PEM format.
* `validity_start_time` - The time after which the certificate is valid, as an
[RFC3339](https://tools.ietf.org/html/rfc3339) timestamp.
* `validity_end_time` - The time until which the certificate is invalid, as an
[RFC3339](https://tools.ietf.org/html/rfc3339) timestamp.
## Automatic Renewal
This resource considers its instances to have been deleted after either their validity
periods ends or the early renewal period is reached. At this time, applying the
Terraform configuration will cause a new certificate to be generated for the instance.
Therefore in a development environment with frequent deployments it may be convenient
to set a relatively-short expiration time and use early renewal to automatically provision
a new certificate when the current one is about to expire.
The creation of a new certificate may of course cause dependent resources to be updated
or replaced, depending on the lifecycle rules applying to those resources.