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