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)