Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented tls_x509_crl resource #73

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
- docker
language: go
go:
- "1.11.x"
- "1.14.x"

env:
- GO111MODULE=on GOFLAGS=-mod=vendor
Expand Down
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build: fmtcheck
go install

test: fmtcheck
go test -i $(TEST) || exit 1
go test $(TEST) || exit 1
echo $(TEST) | \
xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module github.com/terraform-providers/terraform-provider-tls

go 1.14

require (
github.com/hashicorp/terraform-plugin-sdk v1.0.0
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586
Expand Down
1 change: 1 addition & 0 deletions tls/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func Provider() terraform.ResourceProvider {
"tls_locally_signed_cert": resourceLocallySignedCert(),
"tls_self_signed_cert": resourceSelfSignedCert(),
"tls_cert_request": resourceCertRequest(),
"tls_x509_crl": resourceCertRevocationList(),
},
DataSourcesMap: map[string]*schema.Resource{
"tls_public_key": dataSourcePublicKey(),
Expand Down
153 changes: 153 additions & 0 deletions tls/resource_cert_revocation_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package tls

import (
"crypto/rand"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func resourceCertRevocationList() *schema.Resource {
s := map[string]*schema.Schema{
"certs_to_revoke": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
Description: "PEM-encoded certificates to be revoked",
ForceNew: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While technically correct, it would be better, if we would update the list of certificates in the CRL, rather than create the new one every time, as then the expiry date will be updated too for example.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... but I'm quite unsure on how to handle that lifecycle 🤔 , for instance the Id of the resource is calculated as a hash of the CRL PEM content (in the same way we do for locally signed certificates and other resources in this provider). When a CRL is "updated" actually what happens is that a new version of it is created, basically a new CRL itself. I was checking https://tools.ietf.org/html/rfc5280#section-5.1 trying to find a field that we can use to identify a CRL independently of the version, but I couldn't find any. But I can miss something, do you have any idea around this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure if I understand the problem. If you set the ID only in Create function and then don't touch it in Update, then what's the problem? I don't think there is a need to change the ID.

Aside from that, I think this is completely fine for the initial implementation. It can always be improved later 👍

StateFunc: func(v interface{}) string {
return hashForState(strings.Join(v.([]string), ","))
},
},
"early_renewal_hours": {
Type: schema.TypeInt,
Optional: true,
Default: 0,
Description: "Number of hours before the CRL expiry when a new CRL will be generated",
},
"validity_period_hours": {
Type: schema.TypeInt,
Required: true,
Description: "Number of hours that the CRL will remain valid for",
ForceNew: true,
},
"validity_start_time": {
Type: schema.TypeString,
Computed: true,
},

"validity_end_time": {
Type: schema.TypeString,
Computed: true,
},
"ready_for_renewal": {
Type: schema.TypeBool,
Computed: true,
},
"crl_pem": {
Type: schema.TypeString,
Computed: true,
},
"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))
},
},
"ca_private_key_pem": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "PEM-encoded CA private key used to sign the CRL",
ForceNew: true,
Sensitive: true,
StateFunc: func(v interface{}) string {
return hashForState(v.(string))
},
},
"ca_key_algorithm": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Name of the algorithm used by the CA private key",
ForceNew: true,
},
}

return &schema.Resource{
Create: CreateCRL,
Delete: DeleteCRL,
Read: ReadCRL,
Update: UpdateCRL,
CustomizeDiff: CustomizeCertificateDiff,
Schema: s,
}
}

func CreateCRL(d *schema.ResourceData, meta interface{}) error {
notBefore := now()

certsToRevoke := make([]pkix.RevokedCertificate, 0)
for _, vi := range d.Get("certs_to_revoke").([]interface{}) {
certificate, err := decodeCertificateFromBytes([]byte(vi.(string)))
if err != nil {
return fmt.Errorf("failed to parse %q field: %w", "certs_to_revoke", err)
}
certsToRevoke = append(certsToRevoke, pkix.RevokedCertificate{
SerialNumber: certificate.SerialNumber,
RevocationTime: notBefore,
})
}
caKey, err := parsePrivateKey(d, "ca_private_key_pem", "ca_key_algorithm")
if err != nil {
return fmt.Errorf("failed to parse %q field: %w", "ca_private_key_pem", err)
}
caCert, err := parseCertificate(d, "ca_cert_pem")
if err != nil {
return fmt.Errorf("failed to parse %q field: %w", "ca_cert_pem", err)
}

notAfter := notBefore.Add(time.Duration(d.Get("validity_period_hours").(int)) * time.Hour)
validFromBytes, err := notBefore.MarshalText()
if err != nil {
return err
}
validToBytes, err := notAfter.MarshalText()
if err != nil {
return err
}

crlBytes, err := caCert.CreateCRL(rand.Reader, caKey, certsToRevoke, notBefore, notAfter)
if err != nil {
return fmt.Errorf("failed to create crl: %w", err)
}

crlPem := string(pem.EncodeToMemory(&pem.Block{Type: "X509 CRL", Bytes: crlBytes}))

d.SetId(hashForState(string(crlBytes)))
d.Set("crl_pem", crlPem)
d.Set("ready_for_renewal", false)
d.Set("validity_start_time", string(validFromBytes))
d.Set("validity_end_time", string(validToBytes))
return nil
}

func DeleteCRL(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

func ReadCRL(d *schema.ResourceData, meta interface{}) error {
return nil
}

func UpdateCRL(d *schema.ResourceData, meta interface{}) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function can be removed for now, if we don't support updates.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was kept because of the early_renewal_hours field. If we make the resource non-updatable that field needs to be flagged with ForceNew: true. Does it make sense?

return nil
}
Loading