-
Notifications
You must be signed in to change notification settings - Fork 874
/
Copy pathclient_certificate_credential.go
224 lines (211 loc) · 8.59 KB
/
client_certificate_credential.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"io/ioutil"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"golang.org/x/crypto/pkcs12"
)
// ClientCertificateCredentialOptions contain optional parameters that can be used when configuring a ClientCertificateCredential.
// All zero-value fields will be initialized with their default values.
type ClientCertificateCredentialOptions struct {
// The password required to decrypt the private key. Leave empty if there is no password.
Password string
// Set to true to include x5c header in client claims when acquiring a token to enable
// SubjectName and Issuer based authentication for ClientCertificateCredential.
SendCertificateChain bool
// The host of the Azure Active Directory authority. The default is AzurePublicCloud.
// Leave empty to allow overriding the value from the AZURE_AUTHORITY_HOST environment variable.
AuthorityHost string
// HTTPClient sets the transport for making HTTP requests
// Leave this as nil to use the default HTTP transport
HTTPClient azcore.Transport
// Retry configures the built-in retry policy behavior
Retry azcore.RetryOptions
// Telemetry configures the built-in telemetry policy behavior
Telemetry azcore.TelemetryOptions
// Logging configures the built-in logging policy behavior.
Logging azcore.LogOptions
}
// ClientCertificateCredential enables authentication of a service principal to Azure Active Directory using a certificate that is assigned to its App Registration. More information
// on how to configure certificate authentication can be found here:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-azure-ad
type ClientCertificateCredential struct {
client *aadIdentityClient
tenantID string // The Azure Active Directory tenant (directory) ID of the service principal
clientID string // The client (application) ID of the service principal
cert *certContents // The contents of the certificate file
sendCertificateChain bool // Determines whether to include the certificate chain in the claims to retreive a token
}
var _ azcore.TokenCredential = (*ClientCertificateCredential)(nil)
// NewClientCertificateCredential creates an instance of ClientCertificateCredential with the details needed to authenticate against Azure Active Directory with the specified certificate.
// tenantID: The Azure Active Directory tenant (directory) ID of the service principal.
// clientID: The client (application) ID of the service principal.
// certificatePath: The path to the client certificate used to authenticate the client. Supported formats are PEM and PFX.
// options: ClientCertificateCredentialOptions that can be used to provide additional configurations for the credential, such as the certificate password.
func NewClientCertificateCredential(tenantID string, clientID string, certificatePath string, options *ClientCertificateCredentialOptions) (*ClientCertificateCredential, error) {
if !validTenantID(tenantID) {
return nil, &CredentialUnavailableError{credentialType: "Client Certificate Credential", message: tenantIDValidationErr}
}
_, err := os.Stat(certificatePath)
if err != nil {
credErr := &CredentialUnavailableError{credentialType: "Client Certificate Credential", message: "Certificate file not found in path: " + certificatePath}
logCredentialError(credErr.credentialType, credErr)
return nil, credErr
}
certData, err := ioutil.ReadFile(certificatePath)
if err != nil {
credErr := &CredentialUnavailableError{credentialType: "Client Certificate Credential", message: err.Error()}
logCredentialError(credErr.credentialType, credErr)
return nil, credErr
}
if options == nil {
options = &ClientCertificateCredentialOptions{}
}
var cert *certContents
certificatePath = strings.ToUpper(certificatePath)
if strings.HasSuffix(certificatePath, ".PEM") {
cert, err = extractFromPEMFile(certData, options.Password, options.SendCertificateChain)
} else if strings.HasSuffix(certificatePath, ".PFX") {
cert, err = extractFromPFXFile(certData, options.Password, options.SendCertificateChain)
} else {
err = errors.New("only PEM and PFX files are supported")
}
if err != nil {
credErr := &CredentialUnavailableError{credentialType: "Client Certificate Credential", message: err.Error()}
logCredentialError(credErr.credentialType, credErr)
return nil, credErr
}
authorityHost, err := setAuthorityHost(options.AuthorityHost)
if err != nil {
return nil, err
}
c, err := newAADIdentityClient(authorityHost, pipelineOptions{HTTPClient: options.HTTPClient, Retry: options.Retry, Telemetry: options.Telemetry, Logging: options.Logging})
if err != nil {
return nil, err
}
return &ClientCertificateCredential{tenantID: tenantID, clientID: clientID, cert: cert, sendCertificateChain: options.SendCertificateChain, client: c}, nil
}
// contains decoded cert contents we care about
type certContents struct {
fp fingerprint
pk *rsa.PrivateKey
publicCertificates []string
}
func newCertContents(blocks []*pem.Block, fromPEM bool, sendCertificateChain bool) (*certContents, error) {
cc := certContents{}
// first extract the private key
for _, block := range blocks {
if block.Type == "PRIVATE KEY" {
var key interface{}
var err error
if fromPEM {
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
} else {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
}
if err != nil {
return nil, err
}
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("unexpected private key type")
}
cc.pk = rsaKey
break
}
}
if cc.pk == nil {
return nil, errors.New("missing private key")
}
// now find the certificate with the matching public key of our private key
for _, block := range blocks {
if block.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certKey, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
// keep looking
continue
}
if cc.pk.E != certKey.E || cc.pk.N.Cmp(certKey.N) != 0 {
// keep looking
continue
}
// found a match
fp, err := newFingerprint(block)
if err != nil {
return nil, err
}
cc.fp = fp
break
}
}
if cc.fp == nil {
return nil, errors.New("missing certificate")
}
// now find all the public certificates to send in the x5c header
if sendCertificateChain {
for _, block := range blocks {
if block.Type == "CERTIFICATE" {
cc.publicCertificates = append(cc.publicCertificates, base64.StdEncoding.EncodeToString(block.Bytes))
}
}
}
return &cc, nil
}
func extractFromPEMFile(certData []byte, password string, sendCertificateChain bool) (*certContents, error) {
// TODO: wire up support for password
blocks := []*pem.Block{}
// read all of the PEM blocks
for {
var block *pem.Block
block, certData = pem.Decode(certData)
if block == nil {
break
}
blocks = append(blocks, block)
}
if len(blocks) == 0 {
return nil, errors.New("didn't find any blocks in PEM file")
}
return newCertContents(blocks, true, sendCertificateChain)
}
func extractFromPFXFile(certData []byte, password string, sendCertificateChain bool) (*certContents, error) {
// convert PFX binary data to PEM blocks
blocks, err := pkcs12.ToPEM(certData, password)
if err != nil {
return nil, err
}
if len(blocks) == 0 {
return nil, errors.New("didn't find any blocks in PFX file")
}
return newCertContents(blocks, false, sendCertificateChain)
}
// GetToken obtains a token from Azure Active Directory, using the certificate in the file path.
// scopes: The list of scopes for which the token will have access.
// ctx: controlling the request lifetime.
// Returns an AccessToken which can be used to authenticate service client calls.
func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts azcore.TokenRequestOptions) (*azcore.AccessToken, error) {
tk, err := c.client.authenticateCertificate(ctx, c.tenantID, c.clientID, c.cert, c.sendCertificateChain, opts.Scopes)
if err != nil {
addGetTokenFailureLogs("Client Certificate Credential", err, true)
return nil, err
}
logGetTokenSuccess(c, opts)
return tk, nil
}
// NewAuthenticationPolicy implements the azcore.Credential interface on ClientCertificateCredential.
func (c *ClientCertificateCredential) NewAuthenticationPolicy(options azcore.AuthenticationOptions) azcore.Policy {
return newBearerTokenPolicy(c, options)
}