diff --git a/docs/resources/cert_request.md b/docs/resources/cert_request.md index 230a1737..d20c70e9 100644 --- a/docs/resources/cert_request.md +++ b/docs/resources/cert_request.md @@ -45,7 +45,8 @@ resource "tls_cert_request" "example" { - `ip_addresses` (List of String) List of IP addresses for which a certificate is being requested (i.e. certificate subjects). - `subject` (Block List) The subject for which a certificate is being requested. The acceptable arguments are all optional and their naming is based upon [Issuer Distinguished Names (RFC5280)](https://tools.ietf.org/html/rfc5280#section-4.1.2.4) section. (see [below for nested schema](#nestedblock--subject)) - `uris` (List of String) List of URIs for which a certificate is being requested (i.e. certificate subjects). - +- `allowed_uses` (List of String) List of key usages for the certificate singing request. Accepted values: `any_extended`, `client_auth`, `code_signing`, `email_protection`, `ipsec_end_system`, `ipsec_tunnel`, `ipsec_user`, `microsoft_commercial_code_signing`, `microsoft_kernel_code_signing`, `microsoft_server_gated_crypto`, `netscape_server_gated_crypto`, `ocsp_signing`, `server_auth`, `timestamping`. +- ### Read-Only - `cert_request_pem` (String) The certificate request data in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format. **NOTE**: the [underlying](https://pkg.go.dev/encoding/pem#Encode) [libraries](https://pkg.go.dev/golang.org/x/crypto/ssh#MarshalAuthorizedKey) that generate this value append a `\n` at the end of the PEM. In case this disrupts your use case, we recommend using [`trimspace()`](https://www.terraform.io/language/functions/trimspace). diff --git a/internal/provider/common_cert.go b/internal/provider/common_cert.go index 72d081e0..b9b86d1f 100644 --- a/internal/provider/common_cert.go +++ b/internal/provider/common_cert.go @@ -59,6 +59,23 @@ var extendedKeyUsages = map[string]x509.ExtKeyUsage{ "microsoft_kernel_code_signing": x509.ExtKeyUsageMicrosoftKernelCodeSigning, } +var extendedKeyUsageOIDs = map[string]asn1.ObjectIdentifier{ + "any_extended": asn1.ObjectIdentifier{2, 5, 29, 37, 0}, + "server_auth": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1}, + "client_auth": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2}, + "code_signing": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3}, + "email_protection": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4}, + "ipsec_end_system": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}, + "ipsec_tunnel": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}, + "ipsec_user": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}, + "timestamping": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8}, + "ocsp_signing": asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9}, + "microsoft_server_gated_crypto": asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}, + "netscape_server_gated_crypto": asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}, + "microsoft_commercial_code_signing": asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}, + "microsoft_kernel_code_signing": asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}, +} + // supportedKeyUsagesStr returns a slice with all the keys in keyUsages and extendedKeyUsages. func supportedKeyUsagesStr() []string { res := make([]string, 0, len(keyUsages)+len(extendedKeyUsages)) @@ -74,6 +91,18 @@ func supportedKeyUsagesStr() []string { return res } +// supportedEtendedKeyUsagesStr returns a slice with all the keys in extraExtensions. +func supportedEtendedKeyUsagesStr() []string { + res := make([]string, 0, len(extendedKeyUsages)) + + for k := range extendedKeyUsages { + res = append(res, k) + } + sort.Strings(res) + + return res +} + // generateSubjectKeyID generates a SHA-1 hash of the subject public key. func generateSubjectKeyID(pubKey crypto.PublicKey) ([]byte, error) { var pubKeyBytes []byte diff --git a/internal/provider/models.go b/internal/provider/models.go index 474f888d..0166f9b8 100644 --- a/internal/provider/models.go +++ b/internal/provider/models.go @@ -44,6 +44,7 @@ type certRequestResourceModel struct { DNSNames types.List `tfsdk:"dns_names"` IPAddresses types.List `tfsdk:"ip_addresses"` URIs types.List `tfsdk:"uris"` + AllowedUses types.List `tfsdk:"allowed_uses"` PrivateKeyPEM types.String `tfsdk:"private_key_pem"` KeyAlgorithm types.String `tfsdk:"key_algorithm"` CertRequestPEM types.String `tfsdk:"cert_request_pem"` diff --git a/internal/provider/resource_cert_request.go b/internal/provider/resource_cert_request.go index 8754665a..ac13db99 100644 --- a/internal/provider/resource_cert_request.go +++ b/internal/provider/resource_cert_request.go @@ -7,10 +7,13 @@ import ( "context" "crypto/rand" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "fmt" "net" "net/url" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -87,6 +90,20 @@ func (r *certRequestResource) Schema(_ context.Context, req resource.SchemaReque }, Description: "List of URIs for which a certificate is being requested (i.e. certificate subjects).", }, + "allowed_uses": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(supportedEtendedKeyUsagesStr()...), + ), + }, + Description: "List of key usages for the certificate singing request. " + + fmt.Sprintf("Accepted values: `%s`.", strings.Join(supportedEtendedKeyUsagesStr(), "`, `")), + }, // Computed attributes "key_algorithm": schema.StringAttribute{ @@ -305,6 +322,38 @@ func (r *certRequestResource) Create(ctx context.Context, req resource.CreateReq } } + // Add AllowedUses if provided + if !newState.AllowedUses.IsNull() && !newState.AllowedUses.IsUnknown() && len(newState.AllowedUses.Elements()) > 0 { + tflog.Debug(ctx, "Adding key usages in certificate request", map[string]interface{}{ + "extraExtensions": newState.AllowedUses, + }) + + var extKeyUsages []string + res.Diagnostics.Append(newState.AllowedUses.ElementsAs(ctx, &extKeyUsages, false)...) + if res.Diagnostics.HasError() { + return + } + + var extKeyUsageOIDs []asn1.ObjectIdentifier + for _, extKeyUsageStr := range extKeyUsages { + extKeyUsageOID, ok := extendedKeyUsageOIDs[extKeyUsageStr] + if !ok { + res.Diagnostics.AddError("Invalid key usage", fmt.Sprintf("%#v unsupported", extKeyUsageStr)) + return + } + extKeyUsageOIDs = append(extKeyUsageOIDs, extKeyUsageOID) + } + asn1ExtKeyUsages, err := asn1.Marshal(extKeyUsageOIDs) + if err != nil { + res.Diagnostics.AddError("Error creating key usage list for certificate request", err.Error()) + return + } + certReq.ExtraExtensions = append(certReq.ExtraExtensions, pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 5, 29, 37}, + Value: asn1ExtKeyUsages, + }) + } + // Generate `Certificate Request` tflog.Debug(ctx, "Generating certificate request", map[string]interface{}{ "certReq": certReq,