Skip to content

Commit

Permalink
Merge pull request #386 from hashicorp/feature/app-sp-cert-encodings
Browse files Browse the repository at this point in the history
Support hexadecimal and base64 encoded DER certificates
  • Loading branch information
manicminer authored Jan 28, 2021
2 parents 49a2b33 + 6b7af52 commit 8863d0b
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 19 deletions.
93 changes: 91 additions & 2 deletions docs/resources/application_certificate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,20 +25,107 @@ 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:

* `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

Expand Down
30 changes: 28 additions & 2 deletions docs/resources/service_principal_certificate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,20 +29,44 @@ 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:

* `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

Expand Down
53 changes: 52 additions & 1 deletion internal/helpers/aadgraph/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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"),
})
}

Expand All @@ -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"),
})
}

Expand All @@ -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"),
})
}

Expand Down Expand Up @@ -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 {
Expand All @@ -168,11 +206,44 @@ resource "azuread_application_certificate" "test" {
type = "AsymmetricX509Cert"
start_date = "%[3]s"
end_date = "%[4]s"
encoding = "pem"
value = <<EOT
%[5]s
EOT
}
`, r.template(data), data.RandomID, startDate, endDate, testCertificateApplication)
`, r.template(data), data.RandomID, startDate, endDate, applicationCertificatePem)
}

func (r ApplicationCertificateResource) base64Cert(data acceptance.TestData, endDate string) string {
return fmt.Sprintf(`
%[1]s
resource "azuread_application_certificate" "test" {
application_object_id = azuread_application.test.id
type = "AsymmetricX509Cert"
end_date = "%[2]s"
encoding = "base64"
value = <<EOT
%[3]s
EOT
}
`, r.template(data), endDate, applicationCertificateBase64)
}

func (r ApplicationCertificateResource) hexCert(data acceptance.TestData, endDate string) string {
return fmt.Sprintf(`
%[1]s
resource "azuread_application_certificate" "test" {
application_object_id = azuread_application.test.id
type = "AsymmetricX509Cert"
end_date = "%[2]s"
encoding = "hex"
value = <<EOT
%[3]s
EOT
}
`, r.template(data), endDate, applicationCertificateHex)
}

func (r ApplicationCertificateResource) relativeEndDate(data acceptance.TestData) string {
Expand All @@ -187,7 +258,7 @@ resource "azuread_application_certificate" "test" {
%[2]s
EOT
}
`, r.template(data), testCertificateApplication)
`, r.template(data), applicationCertificatePem)
}

func (r ApplicationCertificateResource) requiresImport(data acceptance.TestData, endDate string) string {
Expand Down
Loading

0 comments on commit 8863d0b

Please sign in to comment.