Skip to content

Commit

Permalink
refactor: Update certificate authentication options
Browse files Browse the repository at this point in the history
Update the `client_certificate_base64` and `client_certificate_file_path` attributes in the `M365Provider` schema to support both PEM and PKCS#12 certificate formats. The changes include:
- Updating the description to clarify the supported formats and usage
- Adding support for encrypted PKCS#12 certificates with the `client_certificate_password` attribute

These updates enhance the flexibility and security of certificate authentication in the M365 provider.
  • Loading branch information
ShocOne committed Jul 26, 2024
1 parent 7ad9520 commit 3893498
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 138 deletions.
133 changes: 98 additions & 35 deletions internal/helpers/cert.go
Original file line number Diff line number Diff line change
@@ -1,56 +1,119 @@
package helpers

import (
"context"
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"os"

pkcs12 "software.sslmate.com/src/go-pkcs12"
"github.com/hashicorp/terraform-plugin-log/tflog"
"golang.org/x/crypto/pkcs12"
)

// GetCertificatesAndKeyFromCertOrFilePath takes either a base64-encoded certificate or a file path to a PKCS#12 file,
// decodes it, and returns the certificates and private key.
func GetCertificatesAndKeyFromCertOrFilePath(certOrFilePath string, password string) ([]*x509.Certificate, interface{}, error) {
certData, err := base64.StdEncoding.DecodeString(certOrFilePath)
if err == nil {
key, cert, err := pkcs12.Decode(certData, password)
if err == nil {
return []*x509.Certificate{cert}, key, nil
// ParseCertificateData reads and parses the certificate data, extracting the certificate and private key.
// It first tries to parse the data as PEM. If that fails, it assumes PKCS#12 format and tries to decode it.
func ParseCertificateData(ctx context.Context, certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) {
var certs []*x509.Certificate
var key crypto.PrivateKey
var err error
var certType string

// Try to parse as PEM
blocks := []*pem.Block{}
for {
var block *pem.Block
block, certData = pem.Decode(certData)
if block == nil {
break
}
blocks = append(blocks, block)
}

file, err := os.Open(certOrFilePath)
if err != nil {
return nil, nil, errors.New("could not open file or decode base64 input")
for _, block := range blocks {
switch block.Type {
case "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
tflog.Error(ctx, "Failed to parse PEM certificate", map[string]interface{}{
"error": err,
})
return nil, nil, err
}
certs = append(certs, cert)
certType = "PEM"
case "ENCRYPTED PRIVATE KEY":
decryptedKey, err := x509.DecryptPEMBlock(block, password)
if err != nil {
tflog.Error(ctx, "Failed to decrypt PEM private key", map[string]interface{}{
"error": err,
})
return nil, nil, err
}
key, err = x509.ParsePKCS8PrivateKey(decryptedKey)
if err != nil {
tflog.Error(ctx, "Failed to parse decrypted PEM private key", map[string]interface{}{
"error": err,
})
return nil, nil, err
}
case "PRIVATE KEY":
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
}
if err != nil {
tflog.Error(ctx, "Failed to parse PEM private key", map[string]interface{}{
"error": err,
})
return nil, nil, err
}
case "RSA PRIVATE KEY":
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
tflog.Error(ctx, "Failed to parse PEM RSA private key", map[string]interface{}{
"error": err,
})
return nil, nil, err
}
}
}
defer file.Close()

pfxData, err := io.ReadAll(file)
if err != nil {
return nil, nil, errors.New("could not read file content")
}
// If PEM parsing failed, try to decode as PKCS#12
if len(certs) == 0 || key == nil {
tflog.Debug(ctx, "Attempting to parse as PKCS#12")
privateKey, certificate, err := pkcs12.Decode(certData, string(password))
if err != nil {
tflog.Error(ctx, "Failed to parse PKCS#12 data", map[string]interface{}{
"error": err,
})
return nil, nil, err
}

key, cert, err := pkcs12.Decode(pfxData, password)
if err != nil {
return nil, nil, err
certs = append(certs, certificate)
key = privateKey
certType = "PKCS#12"
}

return []*x509.Certificate{cert}, key, nil
}

// ConvertBase64ToCert takes a base64 encoded PKCS#12 file, decodes it, and returns the certificate.
func ConvertBase64ToCert(base64PfxData string, password string) (*x509.Certificate, error) {
pfxData, err := base64.StdEncoding.DecodeString(base64PfxData)
if err != nil {
return nil, err
if len(certs) == 0 {
tflog.Error(ctx, "No certificates found")
return nil, nil, errors.New("no certificates found")
}
if key == nil {
tflog.Error(ctx, "No private key found")
return nil, nil, errors.New("no private key found")
}

_, cert, err := pkcs12.Decode(pfxData, password)
if err != nil {
return nil, err
// Check that the private key is of the expected RSA type
if _, ok := key.(*rsa.PrivateKey); !ok {
tflog.Error(ctx, "Private key is not of RSA type")
return nil, nil, errors.New("private key is not of RSA type")
}

return cert, nil
tflog.Info(ctx, "Certificate and private key parsed successfully", map[string]interface{}{
"certificateType": certType,
})

return certs, key, nil
}
140 changes: 59 additions & 81 deletions internal/provider/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,106 +7,84 @@ import (

"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

func logDebugInfo(ctx context.Context, req provider.ConfigureRequest, data M365ProviderModel) {
if !data.DebugMode.ValueBool() {
return
}

tflog.Info(ctx, "==== M365ProviderModel Debug Information ====")
fmt.Println("\n==== M365ProviderModel Debug Information ====")

logEnvironmentVariables(ctx)
logSchemaValues(ctx, req)
logProviderDataModel(ctx, data)
var config M365ProviderModel
req.Config.Get(ctx, &config)

tflog.Info(ctx, "========================================")
}
logValueSource("Cloud", []string{"M365_CLOUD", "AZURE_CLOUD"}, config.Cloud, data.Cloud)
logValueSource("Tenant ID", []string{"M365_TENANT_ID"}, config.TenantID, data.TenantID)
logValueSource("Auth Method", []string{"M365_AUTH_METHOD"}, config.AuthMethod, data.AuthMethod)
logValueSource("Client ID", []string{"M365_CLIENT_ID"}, config.ClientID, data.ClientID)
logValueSource("Client Secret", []string{"M365_CLIENT_SECRET"}, config.ClientSecret, data.ClientSecret)
logValueSource("Client Certificate Base64", []string{"M365_CLIENT_CERTIFICATE_BASE64"}, config.ClientCertificateBase64, data.ClientCertificateBase64)
logValueSource("Client Certificate File Path", []string{"M365_CLIENT_CERTIFICATE_FILE_PATH"}, config.ClientCertificateFilePath, data.ClientCertificateFilePath)
logValueSource("Client Certificate Password", []string{"M365_CLIENT_CERTIFICATE_PASSWORD"}, config.ClientCertificatePassword, data.ClientCertificatePassword)
logValueSource("Username", []string{"M365_USERNAME"}, config.Username, data.Username)
logValueSource("Password", []string{"M365_PASSWORD"}, config.Password, data.Password)
logValueSource("Redirect URL", []string{"M365_REDIRECT_URL"}, config.RedirectURL, data.RedirectURL)
logBoolValueSource("Use Proxy", []string{"M365_USE_PROXY"}, config.UseProxy, data.UseProxy)
logValueSource("Proxy URL", []string{"M365_PROXY_URL"}, config.ProxyURL, data.ProxyURL)
logBoolValueSource("Enable Chaos", []string{"M365_ENABLE_CHAOS"}, config.EnableChaos, data.EnableChaos)
logBoolValueSource("Telemetry Optout", []string{"M365_TELEMETRY_OPTOUT"}, config.TelemetryOptout, data.TelemetryOptout)
logBoolValueSource("Debug Mode", []string{"M365_DEBUG_MODE"}, config.DebugMode, data.DebugMode)

func logEnvironmentVariables(ctx context.Context) {
tflog.Info(ctx, "==== Environment Variables ====")
envVars := []string{
"M365_CLOUD", "M365_TENANT_ID", "M365_AUTH_METHOD", "M365_CLIENT_ID",
"M365_CLIENT_SECRET", "M365_CLIENT_CERTIFICATE_BASE64", "M365_CLIENT_CERTIFICATE_FILE_PATH",
"M365_CLIENT_CERTIFICATE_PASSWORD", "M365_USERNAME", "M365_PASSWORD",
"M365_REDIRECT_URL", "M365_USE_PROXY", "M365_PROXY_URL", "M365_ENABLE_CHAOS",
"M365_TELEMETRY_OPTOUT", "M365_DEBUG_MODE",
}
fmt.Println("========================================")
}

func logValueSource(name string, envVars []string, configValue, dataValue types.String) {
var source, value string
for _, env := range envVars {
value := os.Getenv(env)
if isSecretValue(env) && value != "" {
value = "[REDACTED]"
if v := os.Getenv(env); v != "" {
source = "Environment Variable"
value = v
break
}
tflog.Info(ctx, fmt.Sprintf("%s: %s", env, value))
}
}

func logSchemaValues(ctx context.Context, req provider.ConfigureRequest) {
tflog.Info(ctx, "==== Values Set in Schema ====")
var config M365ProviderModel
diags := req.Config.Get(ctx, &config)
if diags.HasError() {
tflog.Error(ctx, "Error retrieving schema values", map[string]interface{}{"diagnostics": diags.Errors()})
return
if source == "" {
if !configValue.IsNull() && !configValue.IsUnknown() {
source = "HCL Configuration"
value = configValue.ValueString()
} else {
source = "HCL Default"
value = dataValue.ValueString()
}
}

logSchemaValue(ctx, "Tenant ID", config.TenantID)
logSchemaValue(ctx, "Auth Method", config.AuthMethod)
logSchemaValue(ctx, "Client ID", config.ClientID)
logSchemaValue(ctx, "Client Secret", config.ClientSecret)
logSchemaValue(ctx, "Client Certificate Base64", config.ClientCertificateBase64)
logSchemaValue(ctx, "Client Certificate File Path", config.ClientCertificateFilePath)
logSchemaValue(ctx, "Client Certificate Password", config.ClientCertificatePassword)
logSchemaValue(ctx, "Username", config.Username)
logSchemaValue(ctx, "Password", config.Password)
logSchemaValue(ctx, "Redirect URL", config.RedirectURL)
logSchemaValue(ctx, "Use Proxy", config.UseProxy)
logSchemaValue(ctx, "Proxy URL", config.ProxyURL)
logSchemaValue(ctx, "Cloud", config.Cloud)
logSchemaValue(ctx, "Enable Chaos", config.EnableChaos)
logSchemaValue(ctx, "Telemetry Optout", config.TelemetryOptout)
logSchemaValue(ctx, "Debug Mode", config.DebugMode)
}

func logProviderDataModel(ctx context.Context, data M365ProviderModel) {
tflog.Info(ctx, "==== Values Mapped to Provider Data Model ====")
tflog.Info(ctx, fmt.Sprintf("Tenant ID Length: %d", len(data.TenantID.ValueString())))
tflog.Info(ctx, fmt.Sprintf("Auth Method: %s", data.AuthMethod.ValueString()))
tflog.Info(ctx, fmt.Sprintf("Client ID Length: %d", len(data.ClientID.ValueString())))
tflog.Info(ctx, fmt.Sprintf("Client Secret Length: %d", len(data.ClientSecret.ValueString())))
tflog.Info(ctx, fmt.Sprintf("Client Certificate Base64 Length: %d", len(data.ClientCertificateBase64.ValueString())))
tflog.Info(ctx, fmt.Sprintf("Client Certificate File Path: %s", data.ClientCertificateFilePath.ValueString()))
tflog.Info(ctx, fmt.Sprintf("Client Certificate Password Set: %t", data.ClientCertificatePassword.ValueString() != ""))
tflog.Info(ctx, fmt.Sprintf("Username Set: %t", data.Username.ValueString() != ""))
tflog.Info(ctx, fmt.Sprintf("Password Set: %t", data.Password.ValueString() != ""))
tflog.Info(ctx, fmt.Sprintf("Redirect URL: %s", data.RedirectURL.ValueString()))
tflog.Info(ctx, fmt.Sprintf("Use Proxy: %t", data.UseProxy.ValueBool()))
tflog.Info(ctx, fmt.Sprintf("Proxy URL: %s", data.ProxyURL.ValueString()))
tflog.Info(ctx, fmt.Sprintf("Cloud: %s", data.Cloud.ValueString()))
tflog.Info(ctx, fmt.Sprintf("Enable Chaos: %t", data.EnableChaos.ValueBool()))
tflog.Info(ctx, fmt.Sprintf("Telemetry Optout: %t", data.TelemetryOptout.ValueBool()))
tflog.Info(ctx, fmt.Sprintf("Debug Mode: %t", data.DebugMode.ValueBool()))
fmt.Printf("%s: %s (Source: %s)\n", name, maskSensitiveValue(value), source)
}

func logSchemaValue(ctx context.Context, name string, value interface{}) {
switch v := value.(type) {
case types.String:
tflog.Info(ctx, fmt.Sprintf("%s: %t", name, !v.IsNull() && !v.IsUnknown()))
case types.Bool:
tflog.Info(ctx, fmt.Sprintf("%s: %t", name, !v.IsNull() && !v.IsUnknown()))
default:
tflog.Info(ctx, fmt.Sprintf("%s: Unknown type", name))
func logBoolValueSource(name string, envVars []string, configValue, dataValue types.Bool) {
var source string
var value bool
for _, env := range envVars {
if v := os.Getenv(env); v != "" {
source = "Environment Variable"
value = v == "true" || v == "1"
break
}
}
if source == "" {
if !configValue.IsNull() && !configValue.IsUnknown() {
source = "HCL Configuration"
value = configValue.ValueBool()
} else {
source = "HCL Default"
value = dataValue.ValueBool()
}
}
fmt.Printf("%s: %t (Source: %s)\n", name, value, source)
}

func isSecretValue(envVar string) bool {
secretVars := []string{"M365_CLIENT_SECRET", "M365_CLIENT_CERTIFICATE_BASE64", "M365_CLIENT_CERTIFICATE_PASSWORD", "M365_PASSWORD"}
for _, secretVar := range secretVars {
if envVar == secretVar {
return true
}
func maskSensitiveValue(value string) string {
if value != "" {
return value
}
return false
return ""
}
37 changes: 20 additions & 17 deletions internal/provider/entra.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package provider

import (
"context"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"os"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
Expand Down Expand Up @@ -106,27 +106,30 @@ func obtainCredential(ctx context.Context, data M365ProviderModel, clientOptions
"client_id": data.ClientID.ValueString(),
})

var certs []*x509.Certificate
var key interface{}
var err error

if !data.ClientCertificateBase64.IsNull() {
tflog.Debug(ctx, "Using base64 encoded client certificate")
certs, key, err = helpers.GetCertificatesAndKeyFromCertOrFilePath(data.ClientCertificateBase64.ValueString(), data.ClientCertificatePassword.ValueString())
} else if !data.ClientCertificateFilePath.IsNull() {
tflog.Debug(ctx, "Using client certificate file path")
certs, key, err = helpers.GetCertificatesAndKeyFromCertOrFilePath(data.ClientCertificateFilePath.ValueString(), data.ClientCertificatePassword.ValueString())
} else {
return nil, fmt.Errorf("either 'client_certificate' or 'client_certificate_file_path' must be provided for client_certificate authentication")
if data.ClientCertificateFilePath.IsNull() {
return nil, fmt.Errorf("'client_certificate_file_path' must be provided for client_certificate authentication")
}

tflog.Debug(ctx, "Using client certificate file path")
certData, err := os.ReadFile(data.ClientCertificateFilePath.ValueString())
if err != nil {
return nil, fmt.Errorf("failed to get certificates and key: %s", err.Error())
return nil, fmt.Errorf("failed to read certificate file: %v", err)
}

return azidentity.NewClientCertificateCredential(data.TenantID.ValueString(), data.ClientID.ValueString(), certs, key, &azidentity.ClientCertificateCredentialOptions{
ClientOptions: clientOptions,
})
password := []byte(data.ClientCertificatePassword.ValueString())
certs, key, err := helpers.ParseCertificateData(ctx, certData, password)
if err != nil {
return nil, fmt.Errorf("failed to parse certificates: %v", err)
}

return azidentity.NewClientCertificateCredential(
data.TenantID.ValueString(),
data.ClientID.ValueString(),
certs,
key,
&azidentity.ClientCertificateCredentialOptions{
ClientOptions: clientOptions,
})
case "interactive_browser":
tflog.Debug(ctx, "Obtaining Interactive Browser Credential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
Expand Down
Loading

0 comments on commit 3893498

Please sign in to comment.