From 92682fff18b94c6bbcf2df1dcd31e0b56a4d650e Mon Sep 17 00:00:00 2001 From: AbdulRahman AlHamali Date: Thu, 9 Apr 2020 23:01:48 -0400 Subject: [PATCH 1/2] implement x509 extra extensions --- tls/provider.go | 111 ++++++ tls/provider_test.go | 15 +- tls/resource_cert_request.go | 16 + tls/resource_cert_request_test.go | 51 ++- tls/resource_locally_signed_cert.go | 1 + tls/resource_locally_signed_cert_test.go | 33 ++ tls/resource_self_signed_cert.go | 16 + tls/resource_self_signed_cert_test.go | 52 ++- .../helper/structure/expand_json.go | 11 + .../helper/structure/flatten_json.go | 16 + .../helper/structure/normalize_json.go | 24 ++ .../helper/structure/suppress_json_diff.go | 21 ++ .../helper/validation/validation.go | 341 ++++++++++++++++++ 13 files changed, 699 insertions(+), 9 deletions(-) create mode 100644 vendor/github.com/hashicorp/terraform-plugin-sdk/helper/structure/expand_json.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-sdk/helper/structure/flatten_json.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-sdk/helper/structure/normalize_json.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-sdk/helper/structure/suppress_json_diff.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-sdk/helper/validation/validation.go diff --git a/tls/provider.go b/tls/provider.go index 1f941979..c31caf21 100644 --- a/tls/provider.go +++ b/tls/provider.go @@ -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" ) @@ -121,3 +126,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, + }, + "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": { + Type: schema.TypeString, + Description: "The type of the value. One of: " + strings.Join(supportedExtensionTypes[:], ", "), + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(supportedExtensionTypes[:], false), + }, + }, +} diff --git a/tls/provider_test.go b/tls/provider_test.go index b2f73cac..ae7b0e88 100644 --- a/tls/provider_test.go +++ b/tls/provider_test.go @@ -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----- ` diff --git a/tls/resource_cert_request.go b/tls/resource_cert_request.go index a1edb3b9..5f105c58 100644 --- a/tls/resource_cert_request.go +++ b/tls/resource_cert_request.go @@ -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, @@ -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 { diff --git a/tls/resource_cert_request_test.go b/tls/resource_cert_request_test.go index 5cf7022c..1cbb1ec1 100644 --- a/tls/resource_cert_request_test.go +++ b/tls/resource_cert_request_test.go @@ -2,6 +2,8 @@ package tls import ( "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "fmt" "strings" @@ -43,7 +45,25 @@ func TestCertRequest(t *testing.T) { uris = [ "spiffe://example-trust-domain/workload", "spiffe://example-trust-domain/workload2", - ] + ] + + 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 = < max { + es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) + return + } + + return + } +} + +// IntAtLeast returns a SchemaValidateFunc which tests if the provided value +// is of type int and is at least min (inclusive) +func IntAtLeast(min int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v < min { + es = append(es, fmt.Errorf("expected %s to be at least (%d), got %d", k, min, v)) + return + } + + return + } +} + +// IntAtMost returns a SchemaValidateFunc which tests if the provided value +// is of type int and is at most max (inclusive) +func IntAtMost(max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v > max { + es = append(es, fmt.Errorf("expected %s to be at most (%d), got %d", k, max, v)) + return + } + + return + } +} + +// IntInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type int and matches the value of an element in the valid slice +func IntInSlice(valid []int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be an integer", k)) + return + } + + for _, validInt := range valid { + if v == validInt { + return + } + } + + es = append(es, fmt.Errorf("expected %s to be one of %v, got %d", k, valid, v)) + return + } +} + +// StringInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type string and matches the value of an element in the valid slice +// will test with in lower case if ignoreCase is true +func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + for _, str := range valid { + if v == str || (ignoreCase && strings.ToLower(v) == strings.ToLower(str)) { + return + } + } + + es = append(es, fmt.Errorf("expected %s to be one of %v, got %s", k, valid, v)) + return + } +} + +// StringLenBetween returns a SchemaValidateFunc which tests if the provided value +// is of type string and has length between min and max (inclusive) +func StringLenBetween(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + if len(v) < min || len(v) > max { + es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, min, max, v)) + } + return + } +} + +// StringMatch returns a SchemaValidateFunc which tests if the provided value +// matches a given regexp. Optionally an error message can be provided to +// return something friendlier than "must match some globby regexp". +func StringMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + v, ok := i.(string) + if !ok { + return nil, []error{fmt.Errorf("expected type of %s to be string", k)} + } + + if ok := r.MatchString(v); !ok { + if message != "" { + return nil, []error{fmt.Errorf("invalid value for %s (%s)", k, message)} + + } + return nil, []error{fmt.Errorf("expected value of %s to match regular expression %q", k, r)} + } + return nil, nil + } +} + +// NoZeroValues is a SchemaValidateFunc which tests if the provided value is +// not a zero value. It's useful in situations where you want to catch +// explicit zero values on things like required fields during validation. +func NoZeroValues(i interface{}, k string) (s []string, es []error) { + if reflect.ValueOf(i).Interface() == reflect.Zero(reflect.TypeOf(i)).Interface() { + switch reflect.TypeOf(i).Kind() { + case reflect.String: + es = append(es, fmt.Errorf("%s must not be empty", k)) + case reflect.Int, reflect.Float64: + es = append(es, fmt.Errorf("%s must not be zero", k)) + default: + // this validator should only ever be applied to TypeString, TypeInt and TypeFloat + panic(fmt.Errorf("can't use NoZeroValues with %T attribute %s", i, k)) + } + } + return +} + +// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value +// is of type string, is in valid CIDR network notation, and has significant bits between min and max (inclusive) +func CIDRNetwork(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + _, ipnet, err := net.ParseCIDR(v) + if err != nil { + es = append(es, fmt.Errorf( + "expected %s to contain a valid CIDR, got: %s with err: %s", k, v, err)) + return + } + + if ipnet == nil || v != ipnet.String() { + es = append(es, fmt.Errorf( + "expected %s to contain a valid network CIDR, expected %s, got %s", + k, ipnet, v)) + } + + sigbits, _ := ipnet.Mask.Size() + if sigbits < min || sigbits > max { + es = append(es, fmt.Errorf( + "expected %q to contain a network CIDR with between %d and %d significant bits, got: %d", + k, min, max, sigbits)) + } + + return + } +} + +// SingleIP returns a SchemaValidateFunc which tests if the provided value +// is of type string, and in valid single IP notation +func SingleIP() schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + ip := net.ParseIP(v) + if ip == nil { + es = append(es, fmt.Errorf( + "expected %s to contain a valid IP, got: %s", k, v)) + } + return + } +} + +// IPRange returns a SchemaValidateFunc which tests if the provided value +// is of type string, and in valid IP range notation +func IPRange() schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + ips := strings.Split(v, "-") + if len(ips) != 2 { + es = append(es, fmt.Errorf( + "expected %s to contain a valid IP range, got: %s", k, v)) + return + } + ip1 := net.ParseIP(ips[0]) + ip2 := net.ParseIP(ips[1]) + if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 { + es = append(es, fmt.Errorf( + "expected %s to contain a valid IP range, got: %s", k, v)) + } + return + } +} + +// ValidateJsonString is a SchemaValidateFunc which tests to make sure the +// supplied string is valid JSON. +func ValidateJsonString(v interface{}, k string) (ws []string, errors []error) { + if _, err := structure.NormalizeJsonString(v); err != nil { + errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err)) + } + return +} + +// ValidateListUniqueStrings is a ValidateFunc that ensures a list has no +// duplicate items in it. It's useful for when a list is needed over a set +// because order matters, yet the items still need to be unique. +func ValidateListUniqueStrings(v interface{}, k string) (ws []string, errors []error) { + for n1, v1 := range v.([]interface{}) { + for n2, v2 := range v.([]interface{}) { + if v1.(string) == v2.(string) && n1 != n2 { + errors = append(errors, fmt.Errorf("%q: duplicate entry - %s", k, v1.(string))) + } + } + } + return +} + +// ValidateRegexp returns a SchemaValidateFunc which tests to make sure the +// supplied string is a valid regular expression. +func ValidateRegexp(v interface{}, k string) (ws []string, errors []error) { + if _, err := regexp.Compile(v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: %s", k, err)) + } + return +} + +// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses +// as time.RFC3339 format +func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { + if _, err := time.Parse(time.RFC3339, v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: invalid RFC3339 timestamp", k)) + } + return +} + +// FloatBetween returns a SchemaValidateFunc which tests if the provided value +// is of type float64 and is between min and max (inclusive). +func FloatBetween(min, max float64) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(float64) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be float64", k)) + return + } + + if v < min || v > max { + es = append(es, fmt.Errorf("expected %s to be in the range (%f - %f), got %f", k, min, max, v)) + return + } + + return + } +} From 434f6f1d2d831701a143349fb60ec56738ba5ffe Mon Sep 17 00:00:00 2001 From: AbdulRahman AlHamali Date: Thu, 9 Apr 2020 23:09:37 -0400 Subject: [PATCH 2/2] add the new dependencies to modules.txt --- vendor/modules.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vendor/modules.txt b/vendor/modules.txt index 8f5f82e5..d8d70d81 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -127,6 +127,8 @@ github.com/hashicorp/terraform-config-inspect/tfconfig # github.com/hashicorp/terraform-plugin-sdk v1.0.0 github.com/hashicorp/terraform-plugin-sdk/plugin github.com/hashicorp/terraform-plugin-sdk/helper/schema +github.com/hashicorp/terraform-plugin-sdk/helper/structure +github.com/hashicorp/terraform-plugin-sdk/helper/validation github.com/hashicorp/terraform-plugin-sdk/terraform github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema github.com/hashicorp/terraform-plugin-sdk/internal/helper/plugin