Skip to content

Commit e2cac03

Browse files
authored
Support Subject Name/Issuer authentication (#13350)
1 parent 6930938 commit e2cac03

File tree

4 files changed

+139
-60
lines changed

4 files changed

+139
-60
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
- `DefaultAzureCredential` allows specifying the client ID of a user-assigned
99
managed identity via keyword argument `managed_identity_client_id`
1010
([#12991](https://github.com/Azure/azure-sdk-for-python/issues/12991))
11+
- `CertificateCredential` supports Subject Name/Issuer authentication when
12+
created with `send_certificate=True`. The async `CertificateCredential`
13+
(`azure.identity.aio.CertificateCredential`) will support this in a
14+
future version.
15+
([#10816](https://github.com/Azure/azure-sdk-for-python/issues/10816))
1116

1217
## 1.4.0 (2020-08-10)
1318
### Added

sdk/identity/azure-identity/azure/identity/_credentials/certificate.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class CertificateCredential(ClientCredentialBase):
2929
:keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate
3030
requires a different encoding, pass appropriately encoded bytes instead.
3131
:paramtype password: str or bytes
32+
:keyword bool send_certificate: if True, the credential will send public certificate material with token requests.
33+
This is required to use Subject Name/Issuer (SNI) authentication. Defaults to False.
3234
:keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache. Defaults to
3335
False.
3436
:keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache when encryption
@@ -54,9 +56,30 @@ def __init__(self, tenant_id, client_id, certificate_path, **kwargs):
5456

5557
# TODO: msal doesn't formally support passwords (but soon will); the below depends on an implementation detail
5658
private_key = serialization.load_pem_private_key(pem_bytes, password=password, backend=default_backend())
59+
client_credential = {"private_key": private_key, "thumbprint": hexlify(fingerprint).decode("utf-8")}
60+
if kwargs.pop("send_certificate", False):
61+
try:
62+
# the JWT needs the whole chain but load_pem_x509_certificate deserializes only the signing cert
63+
chain = extract_cert_chain(pem_bytes)
64+
client_credential["public_certificate"] = six.ensure_str(chain)
65+
except ValueError as ex:
66+
# we shouldn't land here, because load_pem_private_key should have raised when given a malformed file
67+
message = 'Found no PEM encoded certificate in "{}"'.format(certificate_path)
68+
six.raise_from(ValueError(message), ex)
69+
5770
super(CertificateCredential, self).__init__(
58-
client_id=client_id,
59-
client_credential={"private_key": private_key, "thumbprint": hexlify(fingerprint).decode("utf-8")},
60-
tenant_id=tenant_id,
61-
**kwargs
71+
client_id=client_id, client_credential=client_credential, tenant_id=tenant_id, **kwargs
6272
)
73+
74+
75+
def extract_cert_chain(pem_bytes):
76+
# type: (bytes) -> bytes
77+
"""Extract a certificate chain from a PEM file's bytes, removing line breaks."""
78+
79+
# if index raises ValueError, there's no PEM-encoded cert
80+
start = pem_bytes.index(b"-----BEGIN CERTIFICATE-----")
81+
footer = b"-----END CERTIFICATE-----"
82+
end = pem_bytes.rindex(footer)
83+
chain = pem_bytes[start:end + len(footer) + 1]
84+
85+
return b"".join(chain.splitlines())
Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,81 @@
1-
-----BEGIN PRIVATE KEY-----
2-
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDL1hG+JYCfIPp3
3-
tlZ05J4pYIJ3Ckfs432bE3rYuWlR2w9KqdjWkKxuAxpjJ+T+uoqVaT3BFMfi4ZRY
4-
OCI69s4+lP3DwR8uBCp9xyVkF8thXfS3iui0liGDviVBoBJJWvjDFU8a/Hseg+Qf
5-
oxAb6tx0kEc7V3ozBLWoIDJjfwJ3NdsLZGVtAC34qCWeEIvS97CDA4g3Kc6hYJIr
6-
Aa7pxHzo/Nd0U3e7z+DlBcJV7dY6TZUyjBVTpzppWe+XQEOfKsjkDNykHEC1C1bC
7-
lG0u7unS7QOBMd6bOGkeL+Bc+n22slTzs5amsbDLNuobSaUsFt9vgD5jRD6FwhpX
8-
wj/Ek0F7AgMBAAECggEAblU3UWdXUcs2CCqIbcl52wfEVs8X05/n01MeAcWKvqYG
9-
hvGcz7eLvhir5dQoXcF3VhybMrIe6C4WcBIiZSxGwxU+rwEP8YaLwX1UPfOrQM7s
10-
sZTdFTLWfUslO3p7q300fdRA92iG9COMDZvkElh0cBvQksxs9sSr149l9vk+ymtC
11-
uBhZtHG6Ki0BIMBNC9jGUqDuOatXl/dkK4tNjXrNJT7tVwzPaqnNALIWl6B+k9oQ
12-
m1oNhSH2rvs9tw2ITXfIoIk9KdOMjQVUD43wKOaz0hNZhUsb1OFuls7UtRzaFcZH
13-
rMd/M8DtA104QTTlHK+XS7r+nqdv7+ZyB+suTdM+oQKBgQDxCrJZU3hJ0eJ4VYhK
14-
xGDfVGNpYxNkQ4CDB9fwRNbFr/Ck3kgzfE9QxTx1pJOolVmfuFmk9B86in4UNy91
15-
KdaqT79AU5RdOBXNN6tuMbLC0AVqe8sZq+1vWVVwbCstffxEMmyW1Ju/FLYPl2Zp
16-
e5P96dBh5B3mXrQtpDJ0RkxxaQKBgQDYfE6tQQnQSs2ewD6ae8Mu6j8ueDlVoZ37
17-
vze1QdBasR26xu2H8XBt3u41zc524BwQsB1GE1tnC8ZylrqwVEayK4FesSQRCO6o
18-
yK8QSdb06I5J4TaN+TppCDPLzstOh0Dmxp+iFUGoErb7AEOLAJ/VebhF9kBZObL/
19-
HYy4Es+bQwKBgHW/4vYuB3IQXNCp/+V+X1BZ+iJOaves3gekekF+b2itFSKFD8JO
20-
9LQhVfKmTheptdmHhgtF0keXxhV8C+vxX1Ndl7EF41FSh5vzmQRAtPHkCvFEviex
21-
TFD70/gSb1lO1UA/Xbqk69yBcprVPAtFejss0EYx2MVj+CLftmIEwW0ZAoGBAIMG
22-
EVQ45eikLXjkn78+Iq7VZbIJX6IdNBH29I+GqsUJJ5Yw6fh6P3KwF3qG+mvmTfYn
23-
sUAFXS+r58rYwVsRVsxlGmKmUc7hmhibhaEVH72QtvWuEiexbRG+viKfIVuA7t39
24-
3wXpWZiQ4yBdU4Pgt9wrVEU7ukyGaHiReOa7s90jAoGAJc0K7smn98YutQQ+g2ur
25-
ybfnsl0YdsksaP2S2zvZUmNevKPrgnaIDDabOlhYYga+AK1G3FQ7/nefUgiIg1Nd
26-
kr+T6Q4osS3xHB6Az9p/jaF4R2KaWN2nNVCn7ecsmPxDdM7k1vLxaT26vwO9OP5f
27-
YU/5CeIzrfA5nQyPZkOXZBk=
28-
-----END PRIVATE KEY-----
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEowIBAAKCAQEAunkGHWyBYbIp6G97dwFeMhB/7c/y1SPlABi6cUJ6hp7gFeRm
3+
Nwl4gDvBmY8e8t6ANQxn3vv3HOp/QZmFl7Cr8aSjvD0JAT2CBbQ/O/Lgzb+5FaGR
4+
vBFbBJ4AcXeHnzJ4ilsCrTJXtIWfo497uAHePQ7F3AtC9vLlf3kOoc7EIkdJ00Cf
5+
+EKjTbU4UhgBUq+zqPMc8QTUyYXvgb8AxPCTJAktL9tiVpsthmK0SsOEZUiscL/U
6+
Ga/N4EonCklD1AAgWHye0bl0kDhzjJSHAuKBrQ6zLIRs6+9OB6Pg4gcmH+Rup5H2
7+
dSO09N/YBCiiJZTSlqockB3oym2t5z9et2SiNwIDAQABAoIBAQCKzivPG0X0AztO
8+
2i19mHcVrVKNI44POnjsaXvfcyzhqMIFic7MiTA5xEGInRDcmOO2mVV4lvaLf8La
9+
gfz/vXNAnN2E8aoSUkbHGDU52sGcZmrPv0VMSV8HQNXzoJZD2r3/v19urVq79fuv
10+
NM9TWZCkwqpl8bwXNxe+m85YhCFboY9G543qmuXzKAQLoSupT0e4eIo2IGp7eJYK
11+
5J/wtlEumUdhsKo1ajLojDgsgPKfrCyvsmO+bj1dRKGXVLO2SL2pFVCjjHF4SP3q
12+
1WX39beu61Zu+kGthDgj5muHgH06FtnWoHLIUrRmYpM+ezCxQHdRWz7AYjheeE7q
13+
QqJv1PqBAoGBAOlb/gzsps+rInE+LQoEzVj8osILI4NxIpNc6+iG81dEi+zQABX/
14+
bHV6hXGGceozVcX4B+V7f08PlZIAgM3IDqfy0fH2pwEQahJ8a3MwzCgR66RxYlkX
15+
E8czkoz0pcHW58FnLLlWXpHRALTtqoPP5LnWs0SmoNvcHZ9yjJ6tvpRlAoGBAMyQ
16+
fytsyla1ujO0l/kuLFG7gndeOc96SutH3V17lZ1pN0efHyk2aglOnl6YsdPKLZvZ
17+
3ghj01HV0Q0f//xpftduuA7gdgDzSG1irXsxEidfVxX7RsPxX6cx8dhYnuk5rz5E
18+
XyTko7zTpr+A4XMnq6+JNSSCIE+CVYcYf/hyemxrAoGAeC9py4xCaWgxR/OGzMcm
19+
X3NV++wysSqebRkJYuvF/icOjbuen7W6TVL50Ts2BjHENj6FCpqtObHEDbr2m4Uy
20+
jysPF7g50OF8T+MGkAAM1YJNQ5cl2M564DhefPwvNoMRP1l8/kNOV3k2DPjuvg5f
21+
NZsvHudWp4VZOFqNs9e19MUCgYAjewCDoKfrqDN2mmEtmAOZ3YMAfzhZsyVhb6KG
22+
f1Pw7HnpE0FNXaHAoYE4eRWG3W9Rs9Ud8WqKrCJJO36j4gxdA1grRGVTPt8WEeJz
23+
FozGhXPOXTnl7GyhzDjdRGmznAy4KRWziXCY5MDsQEdaOMw/cvXjsio2gC2jc+1m
24+
QzzWpwKBgHzszJ5s6vcWElox4Yc1elQ8xniPpo3RtfXZOLX8xA4eR9yQawah1zd6
25+
ChfeYbHVfq007s+RWGTb+KYQ6ic9nkW464qmVxHGBatUo9+MR4Gk8blANoAfHxdV
26+
g6JNgT2kIGu9IEwoD6XQldC/v24bvFSesyGRHNdI4mUG+hhU4aNw
27+
-----END RSA PRIVATE KEY-----
2928
-----BEGIN CERTIFICATE-----
30-
MIIDazCCAlOgAwIBAgIUF2VIP4+AnEtb52KTCHbo4+fESfswDQYJKoZIhvcNAQEL
31-
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
32-
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTEwMzAyMjQ2MjBaFw0yMjA4
33-
MTkyMjQ2MjBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
34-
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
35-
AQUAA4IBDwAwggEKAoIBAQDL1hG+JYCfIPp3tlZ05J4pYIJ3Ckfs432bE3rYuWlR
36-
2w9KqdjWkKxuAxpjJ+T+uoqVaT3BFMfi4ZRYOCI69s4+lP3DwR8uBCp9xyVkF8th
37-
XfS3iui0liGDviVBoBJJWvjDFU8a/Hseg+QfoxAb6tx0kEc7V3ozBLWoIDJjfwJ3
38-
NdsLZGVtAC34qCWeEIvS97CDA4g3Kc6hYJIrAa7pxHzo/Nd0U3e7z+DlBcJV7dY6
39-
TZUyjBVTpzppWe+XQEOfKsjkDNykHEC1C1bClG0u7unS7QOBMd6bOGkeL+Bc+n22
40-
slTzs5amsbDLNuobSaUsFt9vgD5jRD6FwhpXwj/Ek0F7AgMBAAGjUzBRMB0GA1Ud
41-
DgQWBBT6Mf9uXFB67bY2PeW3GCTKfkO7vDAfBgNVHSMEGDAWgBT6Mf9uXFB67bY2
42-
PeW3GCTKfkO7vDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCZ
43-
1+kTISX85v9/ag7glavaPFUYsOSOOofl8gSzov7L01YL+srq7tXdvZmWrjQ/dnOY
44-
h18rp9rb24vwIYxNioNG/M2cW1jBJwEGsDPOwdPV1VPcRmmUJW9kY130gRHBCd/N
45-
qB7dIkcQnpNsxPIIWI+sRQp73U0ijhOByDnCNHLHon6vbfFTwkO1XggmV5BdZ3uQ
46-
JNJyckILyNzlhmf6zhonMp4lVzkgxWsAm2vgdawd6dmBa+7Avb2QK9s+IdUSutFh
47-
DgW2L12Obgh12Y4sf1iKQXA0RbZ2k+XQIz8EKZa7vJQY0ciYXSgB/BV3a96xX3cx
48-
LIPL8Vam8Ytkopi3gsGA
49-
-----END CERTIFICATE-----
29+
MIID7zCCAdcCAQEwDQYJKoZIhvcNAQEFBQAwPjELMAkGA1UEBhMCVVMxDDAKBgNV
30+
BAoMA3h5ejEMMAoGA1UECwwDYWJjMRMwEQYDVQQDDApJTlRFUklNLUNOMCAXDTIw
31+
MDgyMTE3MTA0M1oYDzMzODkwODA0MTcxMDQzWjA7MQswCQYDVQQGEwJVUzEMMAoG
32+
A1UECgwDeHl6MQwwCgYDVQQLDANhYmMxEDAOBgNVBAMMB1VTRVItQ04wggEiMA0G
33+
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6eQYdbIFhsinob3t3AV4yEH/tz/LV
34+
I+UAGLpxQnqGnuAV5GY3CXiAO8GZjx7y3oA1DGfe+/cc6n9BmYWXsKvxpKO8PQkB
35+
PYIFtD878uDNv7kVoZG8EVsEngBxd4efMniKWwKtMle0hZ+jj3u4Ad49DsXcC0L2
36+
8uV/eQ6hzsQiR0nTQJ/4QqNNtThSGAFSr7Oo8xzxBNTJhe+BvwDE8JMkCS0v22JW
37+
my2GYrRKw4RlSKxwv9QZr83gSicKSUPUACBYfJ7RuXSQOHOMlIcC4oGtDrMshGzr
38+
704Ho+DiByYf5G6nkfZ1I7T039gEKKIllNKWqhyQHejKba3nP163ZKI3AgMBAAEw
39+
DQYJKoZIhvcNAQEFBQADggIBADfitSfjlYa2inBKlpWN8VT0DPm5uw8EHuwLymCM
40+
WYrQMCuQVE2xYoqCSmXj6KLFt8ycgxHsthdkAzXxDhawaKjz2UFp6nszmUA4xfvS
41+
mxLSajwzK/KMBkjdFL7TM+TTBJ1bleDbmoJvDiUeQwisbb1Uh8b3v/jpBwoiamm8
42+
Y4Ca5A15SeBUvAt0/Mc4XJfZ/Ts+LBAPevI9ZyU7C5JZky1q41KPklEHfFZKQRfP
43+
cTyTYYvlPoq57C8XPDs6r50EV3B6Z8MN21OB6MVGi8BOY/c7a2h1ZOhxNyBnJuQX
44+
w4meJthoKcHUnAs8YCrEoQKayMqPH0Vdhaii/gx4jAgh4PNyIZz5cAst+ybPtQj4
45+
i7LFEWjxis+NLQMHhyE4fIGIkEjzU0uGDugifheIwKALqYEgMDrcoolwvGMdPxGo
46+
Qps7tkad5vZV9d9+tTbI+DMB16Y51S04/u1dGFz3jSrDVF08PznJc99VB69OReiC
47+
K17n8Xyox/VAaYsRFbOAJpLRWwcnotDpFQbgiLrmXxNOoiWPNbQsQzaQx7cR9okQ
48+
v5RTpFAkrdjadhMsXFFiQh+axlaGD368ZGAj5ZoyOiXkV88tNCtyP/RDgW5ftQQ7
49+
fdv05bNXhDfLgEgQvVSDfClDL1hKukLmLQS3ILfB4FlM/XmE+FW/qgo9aSx2XIbx
50+
E4ie
51+
-----END CERTIFICATE-----
52+
-----BEGIN CERTIFICATE-----
53+
MIIFGTCCAwGgAwIBAgIUBpOlpNN/cgasvozVw6mfa04+ZC0wDQYJKoZIhvcNAQEL
54+
BQAwOzELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA3h6eTEMMAoGA1UECwwDYWJjMRAw
55+
DgYDVQQDDAdST09ULUNOMCAXDTIwMDgyMTE3MTAyNVoYDzMzODkwODA0MTcxMDI1
56+
WjA+MQswCQYDVQQGEwJVUzEMMAoGA1UECgwDeHl6MQwwCgYDVQQLDANhYmMxEzAR
57+
BgNVBAMMCklOVEVSSU0tQ04wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
58+
AQCr+Tblr4DhX3Xahbei00OJnUgRw6FMsnyROZ170Lx0YNcOrRJ9PuaOZiYXY2Hm
59+
t71o/PZjMtmiYMIxFaiMnql/dCca777l+uBmlwFOR8bquBWiLStmPpvf7Kh5GZNw
60+
XvLGAhk/oxG0O9Pa3OfrlD5vrn/UEGJBu0C+c6ZSLyRk8RjAh8ZbUvnDhhQw3PoK
61+
MQSmFK8BN8X34elu7kq0j7nS0D6Mt7eS40oYeHEaQDdBGl8f7rcqC3RjJ/b/F9wA
62+
+CsKaps6TvpxE7ln9Y3+0yscgeRbyHW0zem6U7MMvVnK/znuNY90Wmajbea7SUj6
63+
nGZpLGS1TqS4H5rn9U1N1WCSyFukTpAQLCPQHeUrSiHKa9Ye5KuC6u2ZXgy0qpGj
64+
nMLu+7746wemi7jN06yZjEmDVneMNCxjLYs4ZhuhiTEItlZpR0VBugNbKo2mJw2U
65+
UesizB3AzQkqGOKp70y74yC+ykLkR5vRNyY3MENJ+W83U1haS7C1rhqFV4eXflVe
66+
EHl8tj7p4KrfhSPr0Rd12UIWDXkYUpCAPlDMdEa9+SDAyuSnkN4P1fAeuzG01jeJ
67+
bnsrWgs3gH3KaGBcPTV4tOTavilGNYDvHZbN9XpYZoZQoPrDZc61M5Ol/cxBahkO
68+
n4aDyhpx5hHnSs7VQuHnjeMUxt3J5HqrXPvaf6uPYNT8KQIDAQABoxAwDjAMBgNV
69+
HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCHCxFqJwfVMI9kMvwlj+sxd4Q5
70+
KuyWxlXRfzpYZ/6JCUq7VBceRVJ87KytMNCyq61rd3Jhb8ssoMCENB68HYhIFUGz
71+
GR92AAc6LTh2Y3vQAg640Cz2vLCGnqnlbIslYV6fzxYqgSopR5wJ4D/kJ9w7NSrC
72+
paN6bS8Olv//tN6RSnvEMJZdXFA40xFin6qT8Op3nrysEE7Z84wPG9Wj2DXskX6v
73+
bZenCEgl1/Ezif5IEgJcYdRkXtYPp6JNbVV+KjDTIMEaUVMpGMGefrt22E+4nSa3
74+
qFvcbzYEKeANe9IAxdPzeWiQ2U90PqWFYCA9sOVsrlSwrup+yYXl0yhTxKY67NCX
75+
gyVtZRnzawv0AVFsfCOT4V0wJSuUz4BV6sH7kl2C7FW3zqYVdFEDigbUNsEEh/jF
76+
3JiAtgNbpJ8TtiCFrCI4g9Jepa3polVPzDD8mLtkWWnfSBN/28cxa2jiUlfQxB39
77+
kyqu4rWbm01lyucJxVgJzH0SGyEM5OvF/OIOU3Q7UIXEcZSX3m4Xo59+v6ZNDwKL
78+
PcFDNK+PL3WNYfdexQCSAbLm1gkUrVIqvidpCSSVv5oWwTM5m7rbA16Hlu4Ea2ep
79+
Pl7I9YXXXnIEFqLYZDnCJglcXmlt6OjI8D3w0TRWHb6bFqubDP417sJDX1S6udN5
80+
wOnOIqg0ZZcqfvpxXA==
81+
-----END CERTIFICATE-----

sdk/identity/azure-identity/tests/test_certificate_credential.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ def test_authority(authority):
109109

110110

111111
@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS)
112-
def test_request_body(cert_path, cert_password):
112+
@pytest.mark.parametrize("send_certificate", (True, False))
113+
def test_request_body(cert_path, cert_password, send_certificate):
113114
access_token = "***"
114115
authority = "authority.com"
115116
client_id = "client-id"
@@ -124,18 +125,24 @@ def mock_send(request, **kwargs):
124125
assert request.body["scope"] == expected_scope
125126

126127
with open(cert_path, "rb") as cert_file:
127-
validate_jwt(request, client_id, cert_file.read())
128+
validate_jwt(request, client_id, cert_file.read(), expect_x5c=send_certificate)
128129

129-
return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token})
130+
return mock_response(json_payload=build_aad_response(access_token=access_token))
130131

131132
cred = CertificateCredential(
132-
tenant_id, client_id, cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority
133+
tenant_id,
134+
client_id,
135+
cert_path,
136+
password=cert_password,
137+
transport=Mock(send=mock_send),
138+
authority=authority,
139+
send_certificate=send_certificate,
133140
)
134141
token = cred.get_token(expected_scope)
135142
assert token.token == access_token
136143

137144

138-
def validate_jwt(request, client_id, pem_bytes):
145+
def validate_jwt(request, client_id, pem_bytes, expect_x5c=False):
139146
"""Validate the request meets AAD's expectations for a client credential grant using a certificate, as documented
140147
at https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
141148
"""
@@ -146,16 +153,28 @@ def validate_jwt(request, client_id, pem_bytes):
146153
jwt = six.ensure_str(request.body["client_assertion"])
147154
header, payload, signature = (urlsafeb64_decode(s) for s in jwt.split("."))
148155
signed_part = jwt[: jwt.rfind(".")]
156+
149157
claims = json.loads(payload.decode("utf-8"))
158+
assert claims["aud"] == request.url
159+
assert claims["iss"] == claims["sub"] == client_id
150160

151161
deserialized_header = json.loads(header.decode("utf-8"))
152162
assert deserialized_header["alg"] == "RS256"
153163
assert deserialized_header["typ"] == "JWT"
164+
if expect_x5c:
165+
# x5c should have all the certs in the PEM file, in order, minus headers and footers
166+
pem_lines = pem_bytes.decode("utf-8").splitlines()
167+
header = "-----BEGIN CERTIFICATE-----"
168+
assert len(deserialized_header["x5c"]) == pem_lines.count(header)
169+
170+
# concatenate the PEM file's certs, removing headers and footers
171+
chain_start = pem_lines.index(header)
172+
pem_chain_content = "".join(line for line in pem_lines[chain_start:] if not line.startswith("-" * 5))
173+
assert "".join(deserialized_header["x5c"]) == pem_chain_content, "JWT's x5c claim contains unexpected content"
174+
else:
175+
assert "x5c" not in deserialized_header
154176
assert urlsafeb64_decode(deserialized_header["x5t"]) == cert.fingerprint(hashes.SHA1()) # nosec
155177

156-
assert claims["aud"] == request.url
157-
assert claims["iss"] == claims["sub"] == client_id
158-
159178
cert.public_key().verify(signature, signed_part.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
160179

161180

0 commit comments

Comments
 (0)