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

Implement support for x509 extra extensions #76

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions tls/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ package tls
import (
"crypto/sha1"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"fmt"
"regexp"
"strconv"
"strings"

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

Expand Down Expand Up @@ -122,3 +127,109 @@ var nameSchema *schema.Resource = &schema.Resource{
},
},
}

func extensionFromResourceData(extensionMap map[string]interface{}) (*pkix.Extension, error) {
result := &pkix.Extension{}

// Handle the oid
oidParts := strings.Split(extensionMap["oid"].(string), ".")
oid := make(asn1.ObjectIdentifier, len(oidParts), len(oidParts))
for i, part := range oidParts {
intPart, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("Invalid Extension OID %#v", extensionMap["oid"].(string))
}
oid[i] = intPart
}
result.Id = oid

// Handle the critical flag
result.Critical = extensionMap["critical"].(bool)

// Handle the value
valueField := extensionMap["type"].(string) + "_value"
switch valueField {
case "integer_value":
value := extensionMap["integer_value"].(int)
marshalledValue, err := asn1.Marshal(value)
if err != nil {
return nil, fmt.Errorf("Failed to marshal value %#v", value)
}
result.Value = marshalledValue
case "boolean_value":
value := extensionMap["boolean_value"].(bool)
marshalledValue, err := asn1.Marshal(value)
if err != nil {
return nil, fmt.Errorf("Failed to marshal value %#v", value)
}
result.Value = marshalledValue
case "printable_string_value":
value := extensionMap["printable_string_value"].(string)
marshalledValue, err := asn1.MarshalWithParams(value, "printable")
if err != nil {
return nil, fmt.Errorf("Failed to marshal value %#v", value)
}
result.Value = marshalledValue
case "utf8_string_value":
value := extensionMap["utf8_string_value"].(string)
marshalledValue, err := asn1.MarshalWithParams(value, "utf8")
if err != nil {
return nil, fmt.Errorf("Failed to marshal value %#v", value)
}
result.Value = marshalledValue
}

return result, nil
}

var supportedExtensionTypes = []string{"integer", "boolean", "printable_string", "utf8_string"}

var extensionSchema *schema.Resource = &schema.Resource{
Schema: map[string]*schema.Schema{
"oid": {
Type: schema.TypeString,
Description: "The oid of the extension in dot format",
Required: true,
ForceNew: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`\d+(\.\d+)*`), "Extension oid must use the dot notation"),
},
"critical": {
Type: schema.TypeBool,
Description: "Whether the extension should be treated as critical",
Optional: true,
Default: false,
ForceNew: true,
},
"integer_value": {
Type: schema.TypeInt,
Description: "Fill this field if the extension value should be encoded as an ASN.1 INTEGER",
Optional: true,
ForceNew: true,

Choose a reason for hiding this comment

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

You can use the ConflictsWith attribute to make sure that only one is set:

ConflictsWith: []string{"boolean_value", "printable_string_value", "utf8_string_value"},

Choose a reason for hiding this comment

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

Awesome! Didn't know about this

},
"boolean_value": {
Type: schema.TypeBool,
Description: "Fill this field if the extension value should be encoded as an ASN.1 BOOLEAN",
Optional: true,
ForceNew: true,
},
"printable_string_value": {
Type: schema.TypeString,
Description: "Fill this field if the extension value should be encoded as an ASN.1 PrintableString",
Optional: true,
ForceNew: true,
},
"utf8_string_value": {
Type: schema.TypeString,
Description: "Fill this field if the extension value should be encoded as an ASN.1 UTF8String",
Optional: true,
ForceNew: true,
},
"type": {

Choose a reason for hiding this comment

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

If you have all the ConflictsValue set right, you no longer need this since only one of the values above will be set. The GetOk method should give you the result and ok (whether or not the value has been set by the user)

Choose a reason for hiding this comment

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

This won't work unfortunately. ConflictsWith does not work on nested resources:
hashicorp/terraform-plugin-sdk#71

And I can't call GetOk on a nested resource, because it is returned to me as a map instead of as ResourceData

Type: schema.TypeString,
Description: "The type of the value. One of: " + strings.Join(supportedExtensionTypes[:], ", "),
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(supportedExtensionTypes[:], false),
},
},
}
15 changes: 8 additions & 7 deletions tls/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,22 @@ rpxCHbX0xSJh0s8j7exRHMF8W16DHjjkc265YdWPXWo=

var testCertRequest = `
-----BEGIN CERTIFICATE REQUEST-----
MIICnjCCAgcCAQAwgcUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UE
MIIC0jCCAjsCAQAwgcUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UE
BxMNUGlyYXRlIEhhcmJvcjEZMBcGA1UECRMQNTg3OSBDb3R0b24gTGluazETMBEG
A1UEERMKOTU1NTktMTIyNzEVMBMGA1UEChMMRXhhbXBsZSwgSW5jMSgwJgYDVQQL
Ex9EZXBhcnRtZW50IG9mIFRlcnJhZm9ybSBUZXN0aW5nMRQwEgYDVQQDEwtleGFt
cGxlLmNvbTEKMAgGA1UEBRMBMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
zy2quNw/QufepKPckIqVlH9hW0YRbNfA98AQwalyyKD+1t65uUtvdvyYjT2vpl++
iGBReNes0wRhvSecOHeFRa0Tp/CLjFqh4HV7AlH9rMzexJyXFQbAc08cVqs+A419
9eb25vzxTi5SswtDYLOIdYg4gg/R5NjGo2bhZyYqo9UCAwEAAaCBlzCBlAYJKoZI
hvcNAQkOMYGGMIGDMIGABgNVHREEeTB3ggtleGFtcGxlLmNvbYILZXhhbXBsZS5u
9eb25vzxTi5SswtDYLOIdYg4gg/R5NjGo2bhZyYqo9UCAwEAAaCByzCByAYJKoZI
hvcNAQkOMYG6MIG3MIGABgNVHREEeTB3ggtleGFtcGxlLmNvbYILZXhhbXBsZS5u
ZXSHBH8AAAGHBH8AAAKGJnNwaWZmZTovL2V4YW1wbGUtdHJ1c3QtZG9tYWluL3dv
cmtsb2FkhidzcGlmZmU6Ly9leGFtcGxlLXRydXN0LWRvbWFpbi93b3JrbG9hZDIw
DQYJKoZIhvcNAQELBQADgYEAx1/DOJKVylgc47ptL3PlyUOyChLLRKpo9ExREdgF
bYBYT5Zx2EYWdQ+wc6qnMGzEr8TnGodYKxdF2awjTX5s0Cz4UgE5Q07yttLWIZwy
ynTNwKyKaFWqB0r8hTuh60yRA5iBUNrQrpjVS6RuadFXep4fUV1mleVdUWFupzhr
9FY=
DAYFKgMEBQYEAwIBETAQBgUrBQcJCwQHEwVoZWxsbzASBgUCBAYICgQJDAfFoeKE
osKjMA0GCSqGSIb3DQEBCwUAA4GBAFAKTZc/TIOmERrshpOvfpQHu1sZMSvCR4LH
HrozVI38y9+lMw5Z1MnQTRITbmLfU3RwbKhmOyWmkN4rJc1pDtmwIVekgoDgoCng
W5HEVhQhScwur6/T9OzbZXzz6sXY6kL9hcUugwAhbXUAGdfkK1fziyJBAdM5uJQt
EPkuWG6W
-----END CERTIFICATE REQUEST-----
`

Expand Down
16 changes: 16 additions & 0 deletions tls/resource_cert_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ func resourceCertRequest() *schema.Resource {
},
},

"extension": {
Type: schema.TypeList,
Optional: true,
Description: "Extension to add to the certificate, can have multiple",
ForceNew: true,
Elem: extensionSchema,
},

"key_algorithm": {
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -126,6 +134,14 @@ func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
}
certReq.URIs = append(certReq.URIs, uri)
}
extensionsI := d.Get("extension").([]interface{})
for _, extensionI := range extensionsI {
extension, err := extensionFromResourceData(extensionI.(map[string]interface{}))
if err != nil {
return err
}
certReq.ExtraExtensions = append(certReq.ExtraExtensions, *extension)
}

certReqBytes, err := x509.CreateCertificateRequest(rand.Reader, &certReq, key)
if err != nil {
Expand Down
51 changes: 50 additions & 1 deletion tls/resource_cert_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tls

import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"strings"
Expand Down Expand Up @@ -43,7 +45,25 @@ func TestCertRequest(t *testing.T) {
uris = [
"spiffe://example-trust-domain/workload",
"spiffe://example-trust-domain/workload2",
]
]

Choose a reason for hiding this comment

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

formatting seems weird here


extension {
oid = "1.2.3.4.5.6"
integer_value = 17
type = "integer"
}

extension {
oid = "1.3.5.7.9.11"
printable_string_value = "hello"
type = "printable_string"
}

extension {
oid = "0.2.4.6.8.10"
utf8_string_value = "š™£"
type = "utf8_string"
}

key_algorithm = "RSA"
private_key_pem = <<EOT
Expand Down Expand Up @@ -128,6 +148,35 @@ EOT
return fmt.Errorf("incorrect URI 1: expected %v, got %v", expected, got)
}

// Convert the Extension structure to one that is easier to query
var extra_extensions = map[string]*pkix.Extension{}
for index, value := range csr.Extensions {
extra_extensions[value.Id.String()] = &csr.Extensions[index]
}
if extra_extensions["1.2.3.4.5.6"] == nil {
return fmt.Errorf("Extension 1.2.3.4.5.6 was not added")
}
var integer_extension_value int
asn1.Unmarshal(extra_extensions["1.2.3.4.5.6"].Value, &integer_extension_value)
if expected, got := 17, integer_extension_value; got != expected {
return fmt.Errorf("Incorrect value for extension 1.2.3.4.5.6: expected %v, got %v", expected, got)
}
if extra_extensions["1.3.5.7.9.11"] == nil {
return fmt.Errorf("Extension 1.3.5.7.9.11 was not added")
}
var printable_string_extension_value string
asn1.Unmarshal(extra_extensions["1.3.5.7.9.11"].Value, &printable_string_extension_value)
if expected, got := "hello", printable_string_extension_value; got != expected {
return fmt.Errorf("Incorrect value for extension 1.3.5.7.9.11: expected %v, got %v", expected, got)
}
if extra_extensions["0.2.4.6.8.10"] == nil {
return fmt.Errorf("Extension 0.2.4.6.8.10 was not added")
}
var utf8_string_extension_value string
asn1.Unmarshal(extra_extensions["0.2.4.6.8.10"].Value, &utf8_string_extension_value)
if expected, got := "š™£", utf8_string_extension_value; got != expected {
return fmt.Errorf("Incorrect value for extension 0.2.4.6.8.10: expected %v, got %v", expected, got)
}
return nil
},
},
Expand Down
1 change: 1 addition & 0 deletions tls/resource_locally_signed_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func CreateLocallySignedCert(d *schema.ResourceData, meta interface{}) error {
IPAddresses: certReq.IPAddresses,
URIs: certReq.URIs,
BasicConstraintsValid: true,
ExtraExtensions: certReq.Extensions, // pass the all the pre-existing extension requests
}

return createCertificate(d, &cert, caCert, certReq.PublicKey, caKey)
Expand Down
33 changes: 33 additions & 0 deletions tls/resource_locally_signed_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tls

import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
Expand Down Expand Up @@ -104,6 +106,37 @@ func TestLocallySignedCert(t *testing.T) {
return fmt.Errorf("incorrect KeyUsage: expected %v, got %v", expected, got)
}

// Test that the Extensions in the request have been added to the certificate
// Convert the Extension structure to one that is easier to query
var extra_extensions = map[string]*pkix.Extension{}
for index, value := range cert.Extensions {
extra_extensions[value.Id.String()] = &cert.Extensions[index]
}
if extra_extensions["1.2.3.4.5.6"] == nil {
return fmt.Errorf("Extension 1.2.3.4.5.6 was not added")
}
var integer_extension_value int
asn1.Unmarshal(extra_extensions["1.2.3.4.5.6"].Value, &integer_extension_value)
if expected, got := 17, integer_extension_value; got != expected {
return fmt.Errorf("Incorrect value for extension 1.2.3.4.5.6: expected %v, got %v", expected, got)
}
if extra_extensions["1.3.5.7.9.11"] == nil {
return fmt.Errorf("Extension 1.3.5.7.9.11 was not added")
}
var printable_string_extension_value string
asn1.Unmarshal(extra_extensions["1.3.5.7.9.11"].Value, &printable_string_extension_value)
if expected, got := "hello", printable_string_extension_value; got != expected {
return fmt.Errorf("Incorrect value for extension 1.3.5.7.9.11: expected %v, got %v", expected, got)
}
if extra_extensions["0.2.4.6.8.10"] == nil {
return fmt.Errorf("Extension 0.2.4.6.8.10 was not added")
}
var utf8_string_extension_value string
asn1.Unmarshal(extra_extensions["0.2.4.6.8.10"].Value, &utf8_string_extension_value)
if expected, got := "š™£", utf8_string_extension_value; got != expected {
return fmt.Errorf("Incorrect value for extension 0.2.4.6.8.10: 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()
Expand Down
16 changes: 16 additions & 0 deletions tls/resource_self_signed_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ func resourceSelfSignedCert() *schema.Resource {
},
}

s["extension"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "Extension to add to the certificate, can have multiple",
ForceNew: true,
Elem: extensionSchema,
}

s["key_algorithm"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -121,6 +129,14 @@ func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
}
cert.URIs = append(cert.URIs, uri)
}
extensionsI := d.Get("extension").([]interface{})
for _, extensionI := range extensionsI {
extension, err := extensionFromResourceData(extensionI.(map[string]interface{}))
if err != nil {
return err
}
cert.ExtraExtensions = append(cert.ExtraExtensions, *extension)
}

return createCertificate(d, &cert, &cert, publicKey(key), key)
}
Loading