From bf3e0df9d056765ea2930a11108a013a8cba50a5 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 19 Jan 2021 22:29:32 +0000 Subject: [PATCH 1/2] Support hexadecimal and (base64 encoded) der certificate encodings for application_certificate and service_principal_certificate --- docs/resources/application_certificate.md | 8 +- .../service_principal_certificate.md | 8 +- internal/helpers/aadgraph/credentials.go | 53 +++++++++++- .../application_certificate_resource_test.go | 85 +++++++++++++++++-- ...ice_principal_certificate_resource_test.go | 85 +++++++++++++++++-- 5 files changed, 220 insertions(+), 19 deletions(-) diff --git a/docs/resources/application_certificate.md b/docs/resources/application_certificate.md index ca271eb52c..c14dd4b56b 100644 --- a/docs/resources/application_certificate.md +++ b/docs/resources/application_certificate.md @@ -28,15 +28,19 @@ resource "azuread_application_certificate" "example" { The following arguments are supported: * `application_object_id` - (Required) The Object ID of the Application for which this Certificate should be created. Changing this field forces a new resource to be created. +* `encoding` - (Optional) Specifies the encoding used for the supplied certificate data. Must be one of `pem`, `base64` or `hex`. Defaults to `pem`. + +-> **NOTE:** The `hex` encoding option is useful for consuming certificate data from the [azurerm_key_vault_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_certificate) resource. + * `end_date` - (Optional) The End Date which the Certificate is valid until, formatted as a RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). Changing this field forces a new resource to be created. * `end_date_relative` - (Optional) A relative duration for which the Certificate is valid until, for example `240h` (10 days) or `2400h30m`. Changing this field forces a new resource to be created. -~> **NOTE:** One of `end_date` or `end_date_relative` must be set. The maximum duration is one year. +~> **NOTE:** One of `end_date` or `end_date_relative` must be set. The maximum duration is enforced by Azure AD. * `key_id` - (Optional) A GUID used to uniquely identify this Certificate. If not specified a GUID will be created. Changing this field forces a new resource to be created. * `start_date` - (Optional) The Start Date which the Certificate is valid from, formatted as a RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date is used. Changing this field forces a new resource to be created. * `type` - (Required) The type of key/certificate. Must be one of `AsymmetricX509Cert` or `Symmetric`. Changing this fields forces a new resource to be created. -* `value` - (Required) The Certificate for this Service Principal. +* `value` - (Required) The certificate data, which can be PEM encoded, base64 encoded DER or hexadecimal encoded DER. See also the `encoding` argument. ## Attributes Reference diff --git a/docs/resources/service_principal_certificate.md b/docs/resources/service_principal_certificate.md index 8f0ed8b485..3d427a8c2d 100644 --- a/docs/resources/service_principal_certificate.md +++ b/docs/resources/service_principal_certificate.md @@ -31,16 +31,20 @@ resource "azuread_service_principal_certificate" "example" { The following arguments are supported: +* `encoding` - (Optional) Specifies the encoding used for the supplied certificate data. Must be one of `pem`, `base64` or `hex`. Defaults to `pem`. + +-> **NOTE:** The `hex` encoding option is useful for consuming certificate data from the [azurerm_key_vault_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_certificate) resource. + * `end_date` - (Optional) The End Date which the Certificate is valid until, formatted as a RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). Changing this field forces a new resource to be created. * `end_date_relative` - (Optional) A relative duration for which the Certificate is valid until, for example `240h` (10 days) or `2400h30m`. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". Changing this field forces a new resource to be created. -~> **NOTE:** One of `end_date` or `end_date_relative` must be set. The maximum duration is one year. +~> **NOTE:** One of `end_date` or `end_date_relative` must be set. The maximum duration is enforced by Azure AD. * `key_id` - (Optional) A GUID used to uniquely identify this Certificate. If not specified a GUID will be created. Changing this field forces a new resource to be created. * `service_principal_id` - (Required) The ID of the Service Principal for which this certificate should be created. Changing this field forces a new resource to be created. * `start_date` - (Optional) The Start Date which the Certificate is valid from, formatted as a RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date is used. Changing this field forces a new resource to be created. * `type` - (Required) The type of key/certificate. Must be one of `AsymmetricX509Cert` or `Symmetric`. Changing this fields forces a new resource to be created. -* `value` - (Required) The Certificate for this Service Principal. +* `value` - (Required) The certificate data, which can be PEM encoded, base64 encoded DER or hexadecimal encoded DER. See also the `encoding` argument. ## Attributes Reference diff --git a/internal/helpers/aadgraph/credentials.go b/internal/helpers/aadgraph/credentials.go index 2eb62e033d..c1f38801b6 100644 --- a/internal/helpers/aadgraph/credentials.go +++ b/internal/helpers/aadgraph/credentials.go @@ -3,8 +3,11 @@ package aadgraph import ( "context" "encoding/base64" + "encoding/hex" + "encoding/pem" "errors" "fmt" + "strings" "time" "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" @@ -46,6 +49,18 @@ func CertificateResourceSchema(idAttribute string) map[string]*schema.Schema { }, false), }, + "encoding": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "pem", + ValidateFunc: validation.StringInSlice([]string{ + "base64", + "hex", + "pem", + }, false), + }, + "value": { Type: schema.TypeString, Required: true, @@ -291,7 +306,43 @@ func WaitForPasswordCredentialReplication(ctx context.Context, keyId string, tim func KeyCredentialForResource(d *schema.ResourceData) (*graphrbac.KeyCredential, error) { keyType := d.Get("type").(string) value := d.Get("value").(string) - encodedValue := base64.StdEncoding.EncodeToString([]byte(value)) + + var encodedValue string + encoding := d.Get("encoding").(string) + switch encoding { + case "base64": + der, err := base64.StdEncoding.DecodeString(strings.TrimSpace(value)) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 certificate data") + } + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: der, + } + pemVal := pem.EncodeToMemory(&block) + if pemVal == nil { + return nil, fmt.Errorf("failed to PEM-encode certificate") + } + encodedValue = base64.StdEncoding.EncodeToString(pemVal) + case "hex": + bytesVal := []byte(strings.TrimSpace(value)) + der := make([]byte, hex.DecodedLen(len(bytesVal))) + _, err := hex.Decode(der, bytesVal) + if err != nil { + return nil, fmt.Errorf("failed to decode hexadecimal certificate data: %+v", err) + } + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: der, + } + pemVal := pem.EncodeToMemory(&block) + if pemVal == nil { + return nil, fmt.Errorf("failed to PEM-encode certificate") + } + encodedValue = base64.StdEncoding.EncodeToString(pemVal) + case "pem": + encodedValue = base64.StdEncoding.EncodeToString([]byte(value)) + } // errors should be handled by the validation var keyId string diff --git a/internal/services/applications/application_certificate_resource_test.go b/internal/services/applications/application_certificate_resource_test.go index 1d128dc748..9b758c13d2 100644 --- a/internal/services/applications/application_certificate_resource_test.go +++ b/internal/services/applications/application_certificate_resource_test.go @@ -17,7 +17,7 @@ import ( "github.com/terraform-providers/terraform-provider-azuread/internal/utils" ) -const testCertificateApplication string = `-----BEGIN CERTIFICATE----- +const applicationCertificatePem string = `-----BEGIN CERTIFICATE----- MIIDGjCCAgICCQDAQlCA1jw1BjANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJV UzELMAkGA1UECAwCQ0ExFzAVBgNVBAoMDkhhc2hpQ29ycCwgSW5jMRowGAYDVQQD DBFoYXNoaWNvcnB0ZXN0LmNvbTAeFw0yMDA1MzEyMDI2MTFaFw0yMTA1MzEyMDI2 @@ -37,6 +37,10 @@ HraQzsK7BNxC5NSwwirT95JH+Xd8rvWu+bCveJz3mnZ3sgolCoxL6Hv1uD2UOZb5 rCHdW31vp5PYNJaSkYL0j259Ogb8crkIzDr3Z8YF -----END CERTIFICATE-----` +const applicationCertificateBase64 string = `MIIDLDCCAhSgAwIBAgIQLSZ4E7hXTw+nb8YavHIoLjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw10ZXN0cmcta3YtYXBwMB4XDTIxMDExOTE4MjczMVoXDTIyMDExOTE4MzczMVowGDEWMBQGA1UEAxMNdGVzdHJnLWt2LWFwcDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+HfZqmm57ngIYuzQBWnH2Yw/7u4h5xJ+F4/7U5nGABcUJQOMH+bXpZUz6LpwaXfQ70l8zmEPQv2qIfDs8TzcH0DOi2mOgM8eQoaUkOUeu4AXBBcRcVgTURH5HkSbEYMsxyaiinrvn5+KoQJcgVj8dZdcN+YxZr+ZgTaHGxjirTJEt6aGt+zr2gsZi8m8qGAQuIJbhPvBUk36VmriEIQR3ReigjT0yRCwBezsXL7EZ+WEdZB6p2UFGkXLq7coSkEA9UHLB0pMtLn74RbN6S395VnW4Vk3fgSfinysfIdro5UChC9R6OA9pWSgR0dxRw5AO0JMU8YHZnsajedpGREUUCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgG+MAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUFQUOvXHBPa0ZpNNwARFn0+k4dD4wHQYDVR0OBBYEFBUFDr1xwT2tGaTTcAERZ9PpOHQ+MA0GCSqGSIb3DQEBCwUAA4IBAQCf1LwnUVoBoYluey2kTn5rdI4As0pMg9nfec8xAWh3BjbYTjElcce+IP73TLzTPLe1lR2PlY/QcHuvfx8Orkm4JLBHrIUEcDh+G12qjMKU1GYtSUFj/QYwAPvesjryO0ow2XP+JgTj4yxVyXTcYfwT4t7gBlV50B0BXY/us3Mu/laczlN+xIonIPzIX5ZZQZBwDNmc1EPUjSN7KZ9AzvrMB6EcXYrP7IM7xEJzhCb/zgdIgGYGp0sMxOb8EZnMnIYmForwLbvryAZ3iN1RPCmxjXPRex1IfWxUY1PhCLjU3LUch6aHHx3YYp/GMg8j5DlfyD4WtIxqIUpJP5uE/e8Q` + +const applicationCertificateHex string = `3082032C30820214A00302010202102D267813B8574F0FA76FC61ABC72282E300D06092A864886F70D01010B05003018311630140603550403130D7465737472672D6B762D617070301E170D3231303131393138323733315A170D3232303131393138333733315A3018311630140603550403130D7465737472672D6B762D61707030820122300D06092A864886F70D01010105000382010F003082010A0282010100BF877D9AA69B9EE780862ECD00569C7D98C3FEEEE21E7127E178FFB5399C600171425038C1FE6D7A59533E8BA706977D0EF497CCE610F42FDAA21F0ECF13CDC1F40CE8B698E80CF1E42869490E51EBB80170417117158135111F91E449B11832CC726A28A7AEF9F9F8AA1025C8158FC75975C37E63166BF998136871B18E2AD3244B7A686B7ECEBDA0B198BC9BCA86010B8825B84FBC1524DFA566AE2108411DD17A28234F4C910B005ECEC5CBEC467E58475907AA765051A45CBABB7284A4100F541CB074A4CB4B9FBE116CDE92DFDE559D6E159377E049F8A7CAC7C876BA39502842F51E8E03DA564A0474771470E403B424C53C607667B1A8DE76919111450203010001A3723070300E0603551D0F0101FF0404030201BE30090603551D130402300030130603551D25040C300A06082B06010505070301301F0603551D2304183016801415050EBD71C13DAD19A4D370011167D3E938743E301D0603551D0E0416041415050EBD71C13DAD19A4D370011167D3E938743E300D06092A864886F70D01010B050003820101009FD4BC27515A01A1896E7B2DA44E7E6B748E00B34A4C83D9DF79CF310168770636D84E312571C7BE20FEF74CBCD33CB7B5951D8F958FD0707BAF7F1F0EAE49B824B047AC850470387E1B5DAA8CC294D4662D494163FD063000FBDEB23AF23B4A30D973FE2604E3E32C55C974DC61FC13E2DEE0065579D01D015D8FEEB3732EFE569CCE537EC48A2720FCC85F96594190700CD99CD443D48D237B299F40CEFACC07A11C5D8ACFEC833BC442738426FFCE0748806606A74B0CC4E6FC1199CC9C8626168AF02DBBEBC8067788DD513C29B18D73D17B1D487D6C546353E108B8D4DCB51C87A6871F1DD8629FC6320F23E4395FC83E16B48C6A214A493F9B84FDEF10` + type ApplicationCertificateResource struct{} func TestAccApplicationCertificate_basic(t *testing.T) { @@ -52,7 +56,7 @@ func TestAccApplicationCertificate_basic(t *testing.T) { check.That(data.ResourceName).Key("key_id").Exists(), ), }, - data.ImportStep("end_date_relative", "value"), + data.ImportStep("encoding", "end_date_relative", "value"), }) } @@ -70,7 +74,41 @@ func TestAccApplicationCertificate_complete(t *testing.T) { check.That(data.ResourceName).Key("key_id").Exists(), ), }, - data.ImportStep("end_date_relative", "value"), + data.ImportStep("encoding", "end_date_relative", "value"), + }) +} + +func TestAccApplicationCertificate_base64Cert(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_certificate", "test") + endDate := time.Now().AddDate(0, 3, 27).UTC().Format(time.RFC3339) + r := ApplicationCertificateResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.base64Cert(data, endDate), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("key_id").Exists(), + ), + }, + data.ImportStep("encoding", "end_date_relative", "value"), + }) +} + +func TestAccApplicationCertificate_hexCert(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_certificate", "test") + endDate := time.Now().AddDate(0, 3, 27).UTC().Format(time.RFC3339) + r := ApplicationCertificateResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.hexCert(data, endDate), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("key_id").Exists(), + ), + }, + data.ImportStep("encoding", "end_date_relative", "value"), }) } @@ -87,7 +125,7 @@ func TestAccApplicationCertificate_relativeEndDate(t *testing.T) { check.That(data.ResourceName).Key("end_date").Exists(), ), }, - data.ImportStep("end_date_relative", "value"), + data.ImportStep("encoding", "end_date_relative", "value"), }) } @@ -155,7 +193,7 @@ resource "azuread_application_certificate" "test" { %[3]s EOT } -`, r.template(data), endDate, testCertificateApplication) +`, r.template(data), endDate, applicationCertificatePem) } func (r ApplicationCertificateResource) complete(data acceptance.TestData, startDate, endDate string) string { @@ -168,11 +206,44 @@ resource "azuread_application_certificate" "test" { type = "AsymmetricX509Cert" start_date = "%[3]s" end_date = "%[4]s" + encoding = "pem" value = < Date: Wed, 20 Jan 2021 00:05:28 +0000 Subject: [PATCH 2/2] azuread_application_certificate: add more examples --- docs/resources/application_certificate.md | 85 +++++++++++++++++++ .../service_principal_certificate.md | 22 +++++ 2 files changed, 107 insertions(+) diff --git a/docs/resources/application_certificate.md b/docs/resources/application_certificate.md index c14dd4b56b..a37027fe53 100644 --- a/docs/resources/application_certificate.md +++ b/docs/resources/application_certificate.md @@ -10,6 +10,8 @@ Manages a Certificate associated with an Application within Azure Active Directo ## Example Usage +*Using a PEM certificate* + ```hcl resource "azuread_application" "example" { name = "example" @@ -23,6 +25,89 @@ resource "azuread_application_certificate" "example" { } ``` +*Using a DER certificate* + +```hcl +resource "azuread_application" "example" { + name = "example" +} + +resource "azuread_application_certificate" "example" { + application_object_id = azuread_application.example.id + type = "AsymmetricX509Cert" + encoding = "base64" + value = base64encode(file("cert.der")) + end_date = "2021-05-01T01:02:03Z" +} +``` + +### Using a certificate from Azure Key Vault + +```hcl +resource "azurerm_key_vault_certificate" "example" { + name = "generated-cert" + key_vault_id = azurerm_key_vault.example.id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + days_before_expiry = 30 + } + } + + secret_properties { + content_type = "application/x-pkcs12" + } + + x509_certificate_properties { + extended_key_usage = ["1.3.6.1.5.5.7.3.2"] + + key_usage = [ + "dataEncipherment", + "digitalSignature", + "keyCertSign", + "keyEncipherment", + ] + + subject_alternative_names { + dns_names = ["internal.contoso.com", "domain.hello.world"] + } + + subject = "CN=${azuread_application.example.name}" + validity_in_months = 12 + } + } +} + +resource "azuread_application" "example" { + name = "example" +} + +resource "azuread_application_certificate" "example" { + application_object_id = azuread_application.example.id + type = "AsymmetricX509Cert" + encoding = "hex" + value = azurerm_key_vault_certificate.example.certificate_data + end_date = azurerm_key_vault_certificate.example.certificate_attribute[0].expires + start_date = azurerm_key_vault_certificate.example.certificate_attribute[0].not_before +} +``` + ## Argument Reference The following arguments are supported: diff --git a/docs/resources/service_principal_certificate.md b/docs/resources/service_principal_certificate.md index 3d427a8c2d..a9375a0ed6 100644 --- a/docs/resources/service_principal_certificate.md +++ b/docs/resources/service_principal_certificate.md @@ -10,6 +10,8 @@ Manages a Certificate associated with a Service Principal within Azure Active Di ## Example Usage +*Using a PEM certificate* + ```hcl resource "azuread_application" "example" { name = "example" @@ -27,6 +29,26 @@ resource "azuread_service_principal_certificate" "example" { } ``` +*Using a DER certificate* + +```hcl +resource "azuread_application" "example" { + name = "example" +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id +} + +resource "azuread_service_principal_certificate" "example" { + service_principal_id = azuread_service_principal.example.id + type = "AsymmetricX509Cert" + encoding = "base64" + value = base64encode(file("cert.der")) + end_date = "2021-05-01T01:02:03Z" +} +``` + ## Argument Reference The following arguments are supported: