client_certificate_credential.go raw

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