mirror of
https://github.com/prometheus/prometheus.git
synced 2026-06-09 08:32:26 -04:00
Add Azure AD certificate-based authentication for remote write (#2)
* Initial plan * Add Azure AD certificate-based authentication support Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com> * Update documentation for certificate-based authentication Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com> * Address code review feedback - improve error messages and format detection Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com> * Replace third-party go-pkcs12 with official golang.org/x/crypto/pkcs12 Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com> * Extract certificate parsing to common util/certutil package Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com> * Fix linting errors: import ordering and comment formatting Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>
This commit is contained in:
parent
8937cbd395
commit
81c2ad71b4
14 changed files with 454 additions and 3 deletions
|
|
@ -3416,6 +3416,18 @@ azuread:
|
|||
[ client_secret: <string> ]
|
||||
[ tenant_id: <string> ] ]
|
||||
|
||||
# Azure Certificate-based authentication.
|
||||
[ certificate:
|
||||
client_id: <string>
|
||||
tenant_id: <string>
|
||||
certificate_path: <file_name>
|
||||
# Optional path to private key file if separate from certificate
|
||||
[ certificate_key_path: <file_name> ]
|
||||
# Optional password for password-protected certificate files (PFX/PKCS12)
|
||||
[ certificate_password: <secret> ]
|
||||
# Whether to send the certificate chain in the x5c header
|
||||
[ send_certificate_chain: <boolean> | default = false ] ]
|
||||
|
||||
# Azure SDK auth.
|
||||
# See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication
|
||||
[ sdk:
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -239,7 +239,7 @@ require (
|
|||
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import (
|
|||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/regexp"
|
||||
|
||||
"github.com/prometheus/prometheus/util/certutil"
|
||||
)
|
||||
|
||||
// Clouds.
|
||||
|
|
@ -87,6 +89,30 @@ type SDKConfig struct {
|
|||
TenantID string `yaml:"tenant_id,omitempty"`
|
||||
}
|
||||
|
||||
// CertificateConfig is used to store azure certificate-based authentication config values.
|
||||
type CertificateConfig struct {
|
||||
// ClientID is the clientId of the azure active directory application that is being used to authenticate.
|
||||
ClientID string `yaml:"client_id,omitempty"`
|
||||
|
||||
// TenantID is the tenantId of the azure active directory application that is being used to authenticate.
|
||||
TenantID string `yaml:"tenant_id,omitempty"`
|
||||
|
||||
// CertificatePath is the path to the certificate file (PEM or PFX format).
|
||||
CertificatePath string `yaml:"certificate_path,omitempty"`
|
||||
|
||||
// CertificateKeyPath is the path to the private key file (PEM format).
|
||||
// This is optional and only needed if the certificate and key are in separate files.
|
||||
CertificateKeyPath string `yaml:"certificate_key_path,omitempty"`
|
||||
|
||||
// CertificatePassword is the password for the certificate file (for PFX files).
|
||||
// This is optional and only needed if the certificate file is password-protected.
|
||||
CertificatePassword string `yaml:"certificate_password,omitempty"`
|
||||
|
||||
// SendCertificateChain controls whether to include x5c header in assertion to support
|
||||
// subject name / issuer-based authentication.
|
||||
SendCertificateChain bool `yaml:"send_certificate_chain,omitempty"`
|
||||
}
|
||||
|
||||
// AzureADConfig is used to store the config values.
|
||||
type AzureADConfig struct { //nolint:revive // exported.
|
||||
// ManagedIdentity is the managed identity that is being used to authenticate.
|
||||
|
|
@ -101,6 +127,9 @@ type AzureADConfig struct { //nolint:revive // exported.
|
|||
// SDK is the SDK config that is being used to authenticate.
|
||||
SDK *SDKConfig `yaml:"sdk,omitempty"`
|
||||
|
||||
// Certificate is the certificate config that is being used to authenticate.
|
||||
Certificate *CertificateConfig `yaml:"certificate,omitempty"`
|
||||
|
||||
// Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina.
|
||||
Cloud string `yaml:"cloud,omitempty"`
|
||||
|
||||
|
|
@ -150,9 +179,12 @@ func (c *AzureADConfig) Validate() error {
|
|||
if c.SDK != nil {
|
||||
authenticators++
|
||||
}
|
||||
if c.Certificate != nil {
|
||||
authenticators++
|
||||
}
|
||||
|
||||
if authenticators == 0 {
|
||||
return errors.New("must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth or Azure SDK in the Azure AD config")
|
||||
return errors.New("must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth, Azure Certificate or Azure SDK in the Azure AD config")
|
||||
}
|
||||
if authenticators > 1 {
|
||||
return errors.New("cannot provide multiple authentication methods in the Azure AD config")
|
||||
|
|
@ -214,6 +246,25 @@ func (c *AzureADConfig) Validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
if c.Certificate != nil {
|
||||
if c.Certificate.ClientID == "" {
|
||||
return errors.New("must provide an Azure Certificate client_id in the Azure AD config")
|
||||
}
|
||||
if c.Certificate.TenantID == "" {
|
||||
return errors.New("must provide an Azure Certificate tenant_id in the Azure AD config")
|
||||
}
|
||||
if c.Certificate.CertificatePath == "" {
|
||||
return errors.New("must provide an Azure Certificate certificate_path in the Azure AD config")
|
||||
}
|
||||
|
||||
if _, err := uuid.Parse(c.Certificate.ClientID); err != nil {
|
||||
return errors.New("the provided Azure Certificate client_id is invalid")
|
||||
}
|
||||
if _, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", c.Certificate.TenantID); err != nil {
|
||||
return errors.New("the provided Azure Certificate tenant_id is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if c.Scope != "" {
|
||||
if matched, err := regexp.MatchString("^[\\w\\s:/.\\-]+$", c.Scope); err != nil || !matched {
|
||||
return errors.New("the provided scope contains invalid characters")
|
||||
|
|
@ -324,6 +375,21 @@ func newTokenCredential(cfg *AzureADConfig) (azcore.TokenCredential, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if cfg.Certificate != nil {
|
||||
certificateConfig := &CertificateConfig{
|
||||
ClientID: cfg.Certificate.ClientID,
|
||||
TenantID: cfg.Certificate.TenantID,
|
||||
CertificatePath: cfg.Certificate.CertificatePath,
|
||||
CertificateKeyPath: cfg.Certificate.CertificateKeyPath,
|
||||
CertificatePassword: cfg.Certificate.CertificatePassword,
|
||||
SendCertificateChain: cfg.Certificate.SendCertificateChain,
|
||||
}
|
||||
cred, err = newCertificateTokenCredential(clientOpts, certificateConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
|
|
@ -366,6 +432,39 @@ func newSDKTokenCredential(clientOpts *azcore.ClientOptions, sdkConfig *SDKConfi
|
|||
return azidentity.NewDefaultAzureCredential(opts)
|
||||
}
|
||||
|
||||
// newCertificateTokenCredential returns new certificate-based token credential.
|
||||
func newCertificateTokenCredential(clientOpts *azcore.ClientOptions, certConfig *CertificateConfig) (azcore.TokenCredential, error) {
|
||||
// Use certutil to parse the certificate files
|
||||
certData, err := certutil.ParseCertificateFiles(
|
||||
certConfig.CertificatePath,
|
||||
certConfig.CertificateKeyPath,
|
||||
certConfig.CertificatePassword,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(certData.Certificates) == 0 {
|
||||
return nil, errors.New("no certificates found in certificate file")
|
||||
}
|
||||
if certData.PrivateKey == nil {
|
||||
return nil, errors.New("no private key found")
|
||||
}
|
||||
|
||||
opts := &azidentity.ClientCertificateCredentialOptions{
|
||||
ClientOptions: *clientOpts,
|
||||
SendCertificateChain: certConfig.SendCertificateChain,
|
||||
}
|
||||
|
||||
return azidentity.NewClientCertificateCredential(
|
||||
certConfig.TenantID,
|
||||
certConfig.ClientID,
|
||||
certData.Certificates,
|
||||
certData.PrivateKey,
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
// newTokenProvider helps to fetch accessToken for different types of credential. This also takes care of
|
||||
// refreshing the accessToken before expiry. This accessToken is attached to the Authorization header while making requests.
|
||||
func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ func TestAzureAdConfig(t *testing.T) {
|
|||
// Missing managedidentity or oauth field.
|
||||
{
|
||||
filename: "testdata/azuread_bad_configmissing.yaml",
|
||||
err: "must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth or Azure SDK in the Azure AD config",
|
||||
err: "must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth, Azure Certificate or Azure SDK in the Azure AD config",
|
||||
},
|
||||
// Invalid managedidentity client id.
|
||||
{
|
||||
|
|
@ -231,6 +231,43 @@ func TestAzureAdConfig(t *testing.T) {
|
|||
{
|
||||
filename: "testdata/azuread_good_oauth_customscope.yaml",
|
||||
},
|
||||
// Valid certificate config.
|
||||
{
|
||||
filename: "testdata/azuread_good_certificate.yaml",
|
||||
},
|
||||
// Valid certificate config with separate key file.
|
||||
{
|
||||
filename: "testdata/azuread_good_certificate_with_key.yaml",
|
||||
},
|
||||
// Valid certificate config with PFX.
|
||||
{
|
||||
filename: "testdata/azuread_good_certificate_pfx.yaml",
|
||||
},
|
||||
// Missing certificate client id.
|
||||
{
|
||||
filename: "testdata/azuread_bad_certificate_missingclientid.yaml",
|
||||
err: "must provide an Azure Certificate client_id in the Azure AD config",
|
||||
},
|
||||
// Missing certificate tenant id.
|
||||
{
|
||||
filename: "testdata/azuread_bad_certificate_missingtenantid.yaml",
|
||||
err: "must provide an Azure Certificate tenant_id in the Azure AD config",
|
||||
},
|
||||
// Missing certificate path.
|
||||
{
|
||||
filename: "testdata/azuread_bad_certificate_missingpath.yaml",
|
||||
err: "must provide an Azure Certificate certificate_path in the Azure AD config",
|
||||
},
|
||||
// Invalid certificate client id.
|
||||
{
|
||||
filename: "testdata/azuread_bad_certificate_invalidclientid.yaml",
|
||||
err: "the provided Azure Certificate client_id is invalid",
|
||||
},
|
||||
// Invalid config when both certificate and oauth is provided.
|
||||
{
|
||||
filename: "testdata/azuread_bad_certificate_oauth.yaml",
|
||||
err: "cannot provide multiple authentication methods in the Azure AD config",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := loadAzureAdConfig(c.filename)
|
||||
|
|
|
|||
5
storage/remote/azuread/testdata/azuread_bad_certificate_invalidclientid.yaml
vendored
Normal file
5
storage/remote/azuread/testdata/azuread_bad_certificate_invalidclientid.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: invalid-client-id
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
certificate_path: /path/to/certificate.pem
|
||||
4
storage/remote/azuread/testdata/azuread_bad_certificate_missingclientid.yaml
vendored
Normal file
4
storage/remote/azuread/testdata/azuread_bad_certificate_missingclientid.yaml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
certificate_path: /path/to/certificate.pem
|
||||
4
storage/remote/azuread/testdata/azuread_bad_certificate_missingpath.yaml
vendored
Normal file
4
storage/remote/azuread/testdata/azuread_bad_certificate_missingpath.yaml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
4
storage/remote/azuread/testdata/azuread_bad_certificate_missingtenantid.yaml
vendored
Normal file
4
storage/remote/azuread/testdata/azuread_bad_certificate_missingtenantid.yaml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
certificate_path: /path/to/certificate.pem
|
||||
9
storage/remote/azuread/testdata/azuread_bad_certificate_oauth.yaml
vendored
Normal file
9
storage/remote/azuread/testdata/azuread_bad_certificate_oauth.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
certificate_path: /path/to/certificate.pem
|
||||
oauth:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
client_secret: Cl1ent$ecret!
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
5
storage/remote/azuread/testdata/azuread_good_certificate.yaml
vendored
Normal file
5
storage/remote/azuread/testdata/azuread_good_certificate.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
certificate_path: /path/to/certificate.pem
|
||||
6
storage/remote/azuread/testdata/azuread_good_certificate_pfx.yaml
vendored
Normal file
6
storage/remote/azuread/testdata/azuread_good_certificate_pfx.yaml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
certificate_path: /path/to/certificate.pfx
|
||||
certificate_password: P@ssw0rd!
|
||||
7
storage/remote/azuread/testdata/azuread_good_certificate_with_key.yaml
vendored
Normal file
7
storage/remote/azuread/testdata/azuread_good_certificate_with_key.yaml
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
cloud: AzurePublic
|
||||
certificate:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
certificate_path: /path/to/certificate.pem
|
||||
certificate_key_path: /path/to/key.pem
|
||||
send_certificate_chain: true
|
||||
204
util/certutil/certutil.go
Normal file
204
util/certutil/certutil.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package certutil provides utilities for loading and parsing X.509 certificates
|
||||
// and private keys from various formats (PEM, PKCS12/PFX).
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
// CertificateData represents parsed certificate data including certificates and private key.
|
||||
type CertificateData struct {
|
||||
// Certificates is a slice of X.509 certificates (leaf certificate first, followed by any intermediates)
|
||||
Certificates []*x509.Certificate
|
||||
// PrivateKey is the private key associated with the leaf certificate
|
||||
PrivateKey any
|
||||
}
|
||||
|
||||
// ParseCertificateFiles loads and parses certificate(s) and private key from files.
|
||||
// It supports both PEM and PKCS12/PFX formats.
|
||||
//
|
||||
// Parameters:
|
||||
// - certPath: Path to the certificate file (required)
|
||||
// - keyPath: Path to a separate private key file (optional, only used for PEM format)
|
||||
// - password: Password for PKCS12/PFX files (optional, only used for PKCS12 format)
|
||||
//
|
||||
// Returns the parsed certificate data or an error.
|
||||
func ParseCertificateFiles(certPath, keyPath, password string) (*CertificateData, error) {
|
||||
// Read certificate file
|
||||
certData, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to read certificate file " + certPath + ": " + err.Error())
|
||||
}
|
||||
|
||||
// Detect format: check if PEM format first
|
||||
if isPEMFormat(certData) {
|
||||
return parsePEMData(certData, keyPath, certPath)
|
||||
}
|
||||
|
||||
// Must be PKCS12/PFX format
|
||||
return parsePKCS12Data(certData, password, certPath)
|
||||
}
|
||||
|
||||
// isPEMFormat checks if data is in PEM format.
|
||||
func isPEMFormat(data []byte) bool {
|
||||
block, _ := pem.Decode(data)
|
||||
return block != nil
|
||||
}
|
||||
|
||||
// parsePEMData parses PEM-encoded certificate and private key data.
|
||||
func parsePEMData(certData []byte, keyPath, certPath string) (*CertificateData, error) {
|
||||
var certs []*x509.Certificate
|
||||
var privateKey any
|
||||
|
||||
// Parse certificates and keys from certificate file
|
||||
rest := certData
|
||||
for {
|
||||
var block *pem.Block
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
|
||||
switch block.Type {
|
||||
case "CERTIFICATE":
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse certificate from " + certPath + ": " + err.Error())
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
case "PRIVATE KEY":
|
||||
if privateKey == nil { // Only take the first private key
|
||||
var err error
|
||||
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse PKCS8 private key from " + certPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
case "RSA PRIVATE KEY":
|
||||
if privateKey == nil { // Only take the first private key
|
||||
var err error
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse RSA private key from " + certPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
case "EC PRIVATE KEY":
|
||||
if privateKey == nil { // Only take the first private key
|
||||
var err error
|
||||
privateKey, err = x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse EC private key from " + certPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no private key found in main file and separate key file is provided, read it
|
||||
if privateKey == nil && keyPath != "" {
|
||||
keyData, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to read private key file " + keyPath + ": " + err.Error())
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(keyData)
|
||||
if block == nil {
|
||||
return nil, errors.New("failed to decode PEM private key from " + keyPath)
|
||||
}
|
||||
|
||||
switch block.Type {
|
||||
case "PRIVATE KEY":
|
||||
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse PKCS8 private key from " + keyPath + ": " + err.Error())
|
||||
}
|
||||
case "RSA PRIVATE KEY":
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse RSA private key from " + keyPath + ": " + err.Error())
|
||||
}
|
||||
case "EC PRIVATE KEY":
|
||||
privateKey, err = x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse EC private key from " + keyPath + ": " + err.Error())
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported private key type in " + keyPath + ": " + block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
return &CertificateData{
|
||||
Certificates: certs,
|
||||
PrivateKey: privateKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parsePKCS12Data parses PKCS12/PFX-encoded certificate and private key data.
|
||||
func parsePKCS12Data(certData []byte, password, certPath string) (*CertificateData, error) {
|
||||
// Convert PKCS12 to PEM blocks
|
||||
pemBlocks, err := pkcs12.ToPEM(certData, password)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse PFX certificate " + certPath + ": " + err.Error())
|
||||
}
|
||||
|
||||
var certs []*x509.Certificate
|
||||
var privateKey any
|
||||
|
||||
// Parse PEM blocks to extract certificates and private key
|
||||
for _, block := range pemBlocks {
|
||||
switch block.Type {
|
||||
case "CERTIFICATE":
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse certificate from PFX " + certPath + ": " + err.Error())
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
case "PRIVATE KEY":
|
||||
// PKCS8 private key
|
||||
if privateKey == nil { // Only take the first private key
|
||||
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse PKCS8 private key from PFX " + certPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
case "RSA PRIVATE KEY":
|
||||
// PKCS1 RSA private key
|
||||
if privateKey == nil { // Only take the first private key
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse RSA private key from PFX " + certPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
case "EC PRIVATE KEY":
|
||||
// EC private key
|
||||
if privateKey == nil { // Only take the first private key
|
||||
privateKey, err = x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse EC private key from PFX " + certPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &CertificateData{
|
||||
Certificates: certs,
|
||||
PrivateKey: privateKey,
|
||||
}, nil
|
||||
}
|
||||
55
util/certutil/certutil_test.go
Normal file
55
util/certutil/certutil_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestIsPEMFormat tests the PEM format detection.
|
||||
func TestIsPEMFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid PEM",
|
||||
data: []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIBkTCB+wIJAKHHCgVZU6pfMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNVBAYTAlVT
|
||||
MB4XDTE5MDEwMTAwMDAwMFoXDTIwMDEwMTAwMDAwMFowDTELMAkGA1UEBhMCVVMw
|
||||
-----END CERTIFICATE-----`),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not PEM",
|
||||
data: []byte("random binary data"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
data: []byte(""),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isPEMFormat(tt.data)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isPEMFormat() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue