diff --git a/builtin/providers/tls/provider.go b/builtin/providers/tls/provider.go index 69dfa0ded..e6c1d6198 100644 --- a/builtin/providers/tls/provider.go +++ b/builtin/providers/tls/provider.go @@ -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(), }, } } diff --git a/builtin/providers/tls/provider_test.go b/builtin/providers/tls/provider_test.go index 31b014733..7dc7af0d2 100644 --- a/builtin/providers/tls/provider_test.go +++ b/builtin/providers/tls/provider_test.go @@ -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----- +` diff --git a/builtin/providers/tls/resource_cert_request.go b/builtin/providers/tls/resource_cert_request.go index ac1f70071..7dd1430c6 100644 --- a/builtin/providers/tls/resource_cert_request.go +++ b/builtin/providers/tls/resource_cert_request.go @@ -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) diff --git a/builtin/providers/tls/resource_certificate.go b/builtin/providers/tls/resource_certificate.go new file mode 100644 index 000000000..bfdc6eea7 --- /dev/null +++ b/builtin/providers/tls/resource_certificate.go @@ -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 +} diff --git a/builtin/providers/tls/resource_locally_signed_cert.go b/builtin/providers/tls/resource_locally_signed_cert.go new file mode 100644 index 000000000..39c90022f --- /dev/null +++ b/builtin/providers/tls/resource_locally_signed_cert.go @@ -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) +} diff --git a/builtin/providers/tls/resource_locally_signed_cert_test.go b/builtin/providers/tls/resource_locally_signed_cert_test.go new file mode 100644 index 000000000..7e9688d12 --- /dev/null +++ b/builtin/providers/tls/resource_locally_signed_cert_test.go @@ -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 = < (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 + }, + }, + }, + }) +} diff --git a/builtin/providers/tls/resource_self_signed_cert.go b/builtin/providers/tls/resource_self_signed_cert.go index 405535245..29e04154d 100644 --- a/builtin/providers/tls/resource_self_signed_cert.go +++ b/builtin/providers/tls/resource_self_signed_cert.go @@ -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) } diff --git a/builtin/providers/tls/util.go b/builtin/providers/tls/util.go new file mode 100644 index 000000000..b1ff32e5b --- /dev/null +++ b/builtin/providers/tls/util.go @@ -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 +} diff --git a/website/source/docs/providers/tls/r/locally_signed_cert.html.md b/website/source/docs/providers/tls/r/locally_signed_cert.html.md new file mode 100644 index 000000000..c052c5ff9 --- /dev/null +++ b/website/source/docs/providers/tls/r/locally_signed_cert.html.md @@ -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.