client_certificate_credential.go

  1//go:build go1.18
  2// +build go1.18
  3
  4// Copyright (c) Microsoft Corporation. All rights reserved.
  5// Licensed under the MIT License.
  6
  7package azidentity
  8
  9import (
 10	"context"
 11	"crypto"
 12	"crypto/x509"
 13	"encoding/pem"
 14	"errors"
 15
 16	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
 17	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 18	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 19	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
 20	"golang.org/x/crypto/pkcs12"
 21)
 22
 23const credNameCert = "ClientCertificateCredential"
 24
 25// ClientCertificateCredentialOptions contains optional parameters for ClientCertificateCredential.
 26type ClientCertificateCredentialOptions struct {
 27	azcore.ClientOptions
 28
 29	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
 30	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
 31	// application is registered.
 32	AdditionallyAllowedTenants []string
 33
 34	// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
 35	// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
 36	// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
 37	// the application responsible for ensuring the configured authority is valid and trustworthy.
 38	DisableInstanceDiscovery bool
 39
 40	// SendCertificateChain controls whether the credential sends the public certificate chain in the x5c
 41	// header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication.
 42	// Defaults to False.
 43	SendCertificateChain bool
 44
 45	// tokenCachePersistenceOptions enables persistent token caching when not nil.
 46	tokenCachePersistenceOptions *tokenCachePersistenceOptions
 47}
 48
 49// ClientCertificateCredential authenticates a service principal with a certificate.
 50type ClientCertificateCredential struct {
 51	client *confidentialClient
 52}
 53
 54// NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults. See
 55// [ParseCertificates] for help loading a certificate.
 56func NewClientCertificateCredential(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *ClientCertificateCredentialOptions) (*ClientCertificateCredential, error) {
 57	if len(certs) == 0 {
 58		return nil, errors.New("at least one certificate is required")
 59	}
 60	if options == nil {
 61		options = &ClientCertificateCredentialOptions{}
 62	}
 63	cred, err := confidential.NewCredFromCert(certs, key)
 64	if err != nil {
 65		return nil, err
 66	}
 67	msalOpts := confidentialClientOptions{
 68		AdditionallyAllowedTenants:   options.AdditionallyAllowedTenants,
 69		ClientOptions:                options.ClientOptions,
 70		DisableInstanceDiscovery:     options.DisableInstanceDiscovery,
 71		SendX5C:                      options.SendCertificateChain,
 72		tokenCachePersistenceOptions: options.tokenCachePersistenceOptions,
 73	}
 74	c, err := newConfidentialClient(tenantID, clientID, credNameCert, cred, msalOpts)
 75	if err != nil {
 76		return nil, err
 77	}
 78	return &ClientCertificateCredential{client: c}, nil
 79}
 80
 81// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
 82func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
 83	var err error
 84	ctx, endSpan := runtime.StartSpan(ctx, credNameCert+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
 85	defer func() { endSpan(err) }()
 86	tk, err := c.client.GetToken(ctx, opts)
 87	return tk, err
 88}
 89
 90// ParseCertificates loads certificates and a private key, in PEM or PKCS#12 format, for use with [NewClientCertificateCredential].
 91// Pass nil for password if the private key isn't encrypted. This function has limitations, for example it can't decrypt keys in
 92// PEM format or PKCS#12 certificates that use SHA256 for message authentication. If you encounter such limitations, consider
 93// using another module to load the certificate and private key.
 94func ParseCertificates(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) {
 95	var blocks []*pem.Block
 96	var err error
 97	if len(password) == 0 {
 98		blocks, err = loadPEMCert(certData)
 99	}
100	if len(blocks) == 0 || err != nil {
101		blocks, err = loadPKCS12Cert(certData, string(password))
102	}
103	if err != nil {
104		return nil, nil, err
105	}
106	var certs []*x509.Certificate
107	var pk crypto.PrivateKey
108	for _, block := range blocks {
109		switch block.Type {
110		case "CERTIFICATE":
111			c, err := x509.ParseCertificate(block.Bytes)
112			if err != nil {
113				return nil, nil, err
114			}
115			certs = append(certs, c)
116		case "PRIVATE KEY":
117			if pk != nil {
118				return nil, nil, errors.New("certData contains multiple private keys")
119			}
120			pk, err = x509.ParsePKCS8PrivateKey(block.Bytes)
121			if err != nil {
122				pk, err = x509.ParsePKCS1PrivateKey(block.Bytes)
123			}
124			if err != nil {
125				return nil, nil, err
126			}
127		case "RSA PRIVATE KEY":
128			if pk != nil {
129				return nil, nil, errors.New("certData contains multiple private keys")
130			}
131			pk, err = x509.ParsePKCS1PrivateKey(block.Bytes)
132			if err != nil {
133				return nil, nil, err
134			}
135		}
136	}
137	if len(certs) == 0 {
138		return nil, nil, errors.New("found no certificate")
139	}
140	if pk == nil {
141		return nil, nil, errors.New("found no private key")
142	}
143	return certs, pk, nil
144}
145
146func loadPEMCert(certData []byte) ([]*pem.Block, error) {
147	blocks := []*pem.Block{}
148	for {
149		var block *pem.Block
150		block, certData = pem.Decode(certData)
151		if block == nil {
152			break
153		}
154		blocks = append(blocks, block)
155	}
156	if len(blocks) == 0 {
157		return nil, errors.New("didn't find any PEM blocks")
158	}
159	return blocks, nil
160}
161
162func loadPKCS12Cert(certData []byte, password string) ([]*pem.Block, error) {
163	blocks, err := pkcs12.ToPEM(certData, password)
164	if err != nil {
165		return nil, err
166	}
167	if len(blocks) == 0 {
168		// not mentioning PKCS12 in this message because we end up here when certData is garbage
169		return nil, errors.New("didn't find any certificate content")
170	}
171	return blocks, err
172}
173
174var _ azcore.TokenCredential = (*ClientCertificateCredential)(nil)