provider/tls: add locally signed certificates
This allows you to generate and sign certificates using a local CA.
This commit is contained in:
parent
2dbd9b47e4
commit
309e697a52
|
@ -13,9 +13,10 @@ import (
|
||||||
func Provider() terraform.ResourceProvider {
|
func Provider() terraform.ResourceProvider {
|
||||||
return &schema.Provider{
|
return &schema.Provider{
|
||||||
ResourcesMap: map[string]*schema.Resource{
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
"tls_private_key": resourcePrivateKey(),
|
"tls_private_key": resourcePrivateKey(),
|
||||||
"tls_self_signed_cert": resourceSelfSignedCert(),
|
"tls_locally_signed_cert": resourceLocallySignedCert(),
|
||||||
"tls_cert_request": resourceCertRequest(),
|
"tls_self_signed_cert": resourceSelfSignedCert(),
|
||||||
|
"tls_cert_request": resourceCertRequest(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,3 +34,62 @@ DrUJcPbKUfF4VBqmmwwkpwT938Hr/iCcS6kE3hqXiN9a5XJb4vnk2FdZNPS9hf2J
|
||||||
rpxCHbX0xSJh0s8j7exRHMF8W16DHjjkc265YdWPXWo=
|
rpxCHbX0xSJh0s8j7exRHMF8W16DHjjkc265YdWPXWo=
|
||||||
-----END RSA PRIVATE KEY-----
|
-----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-----
|
||||||
|
`
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pemCertReqType = "CERTIFICATE REQUEST"
|
||||||
|
|
||||||
func resourceCertRequest() *schema.Resource {
|
func resourceCertRequest() *schema.Resource {
|
||||||
return &schema.Resource{
|
return &schema.Resource{
|
||||||
Create: CreateCertRequest,
|
Create: CreateCertRequest,
|
||||||
|
@ -71,19 +73,9 @@ func resourceCertRequest() *schema.Resource {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
|
func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
|
||||||
keyAlgoName := d.Get("key_algorithm").(string)
|
key, err := parsePrivateKey(d, "private_key_pem", "key_algorithm")
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode private_key_pem: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
subjectConfs := d.Get("subject").([]interface{})
|
subjectConfs := d.Get("subject").([]interface{})
|
||||||
|
@ -117,7 +109,7 @@ func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Errorf("Error creating certificate request: %s", err)
|
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.SetId(hashForState(string(certReqBytes)))
|
||||||
d.Set("cert_request_pem", certReqPem)
|
d.Set("cert_request_pem", certReqPem)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,169 +1,72 @@
|
||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"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 {
|
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{
|
return &schema.Resource{
|
||||||
Create: CreateSelfSignedCert,
|
Create: CreateSelfSignedCert,
|
||||||
Delete: DeleteSelfSignedCert,
|
Delete: DeleteCertificate,
|
||||||
Read: ReadSelfSignedCert,
|
Read: ReadCertificate,
|
||||||
|
Schema: s,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
|
func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
|
||||||
keyAlgoName := d.Get("key_algorithm").(string)
|
key, err := parsePrivateKey(d, "private_key_pem", "key_algorithm")
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode private_key_pem: %s", err)
|
return 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subjectConfs := d.Get("subject").([]interface{})
|
subjectConfs := d.Get("subject").([]interface{})
|
||||||
|
@ -177,24 +80,10 @@ func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := x509.Certificate{
|
cert := x509.Certificate{
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: *subject,
|
Subject: *subject,
|
||||||
NotBefore: notBefore,
|
|
||||||
NotAfter: notAfter,
|
|
||||||
BasicConstraintsValid: true,
|
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{})
|
dnsNamesI := d.Get("dns_names").([]interface{})
|
||||||
for _, nameI := range dnsNamesI {
|
for _, nameI := range dnsNamesI {
|
||||||
cert.DNSNames = append(cert.DNSNames, nameI.(string))
|
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)
|
cert.IPAddresses = append(cert.IPAddresses, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.Get("is_ca_certificate").(bool) {
|
return createCertificate(d, &cert, &cert, publicKey(key), key)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
Loading…
Reference in New Issue