Skip to content

Commit 4b284c3

Browse files
authored
[MSI v2] Cache MSI v2 cert (#5557)
* init * pr comments * pr comments
1 parent 57f2815 commit 4b284c3

File tree

11 files changed

+2135
-124
lines changed

11 files changed

+2135
-124
lines changed

src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.IO;
6-
using System.Threading.Tasks;
7+
using System.Security.Cryptography.X509Certificates;
78
using System.Threading;
8-
using Microsoft.Identity.Client.Internal;
9+
using System.Threading.Tasks;
910
using Microsoft.Identity.Client.ApiConfig.Parameters;
10-
using Microsoft.Identity.Client.PlatformsCommon.Shared;
1111
using Microsoft.Identity.Client.Core;
12+
using Microsoft.Identity.Client.Internal;
1213
using Microsoft.Identity.Client.ManagedIdentity.V2;
13-
using System.Security.Cryptography.X509Certificates;
14+
using Microsoft.Identity.Client.PlatformsCommon.Shared;
1415

1516
namespace Microsoft.Identity.Client.ManagedIdentity
1617
{
@@ -30,6 +31,9 @@ internal class ManagedIdentityClient
3031
internal static void ResetSourceForTest()
3132
{
3233
s_sourceName = ManagedIdentitySource.None;
34+
35+
// Clear cert caches so each test starts fresh
36+
ImdsV2ManagedIdentitySource.ResetCertCacheForTest();
3337
}
3438

3539
internal async Task<ManagedIdentityResponse> SendTokenRequestForManagedIdentityAsync(
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Cryptography.X509Certificates;
6+
using System.Threading;
7+
8+
namespace Microsoft.Identity.Client.ManagedIdentity.V2
9+
{
10+
/// <summary>
11+
/// In-memory entry owned by the cache. Disposing the entry disposes the certificate it owns.
12+
/// </summary>
13+
internal sealed class CertificateCacheEntry : IDisposable
14+
{
15+
private int _disposed;
16+
17+
/// <summary>
18+
/// Represents the minimum remaining lifetime for an operation or resource.
19+
/// </summary>
20+
public static readonly TimeSpan MinRemainingLifetime = TimeSpan.FromHours(24);
21+
22+
/// <summary>
23+
/// certificate+endpoint+clientId cache entry.
24+
/// </summary>
25+
/// <param name="certificate"></param>
26+
/// <param name="notAfterUtc"></param>
27+
/// <param name="endpoint"></param>
28+
/// <param name="clientId"></param>
29+
/// <exception cref="ArgumentNullException"></exception>
30+
public CertificateCacheEntry(X509Certificate2 certificate, DateTimeOffset notAfterUtc, string endpoint, string clientId)
31+
{
32+
Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
33+
NotAfterUtc = notAfterUtc;
34+
Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
35+
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
36+
}
37+
38+
/// <summary>
39+
/// certificate owned by this entry.
40+
/// </summary>
41+
public X509Certificate2 Certificate { get; }
42+
/// <summary>
43+
/// notAfterUtc of the certificate.
44+
/// </summary>
45+
public DateTimeOffset NotAfterUtc { get; }
46+
/// <summary>
47+
/// endpoint associated with this certificate.
48+
/// </summary>
49+
public string Endpoint { get; }
50+
/// <summary>
51+
/// clientId associated with this certificate.
52+
/// </summary>
53+
public string ClientId { get; }
54+
55+
/// <summary>Whether this entry has been disposed.</summary>
56+
public bool IsDisposed => Volatile.Read(ref _disposed) != 0;
57+
58+
/// <summary>
59+
/// is expired at the specified time.
60+
/// </summary>
61+
/// <param name="nowUtc"></param>
62+
/// <returns></returns>
63+
public bool IsExpiredUtc(DateTimeOffset nowUtc) => nowUtc >= (NotAfterUtc - MinRemainingLifetime);
64+
65+
/// <summary>
66+
/// dispose the entry and its certificate.
67+
/// </summary>
68+
public void Dispose()
69+
{
70+
if (Interlocked.Exchange(ref _disposed, 1) != 0)
71+
{
72+
return; // already disposed
73+
}
74+
75+
// Idempotent due to the atomic guard
76+
Certificate.Dispose();
77+
}
78+
}
79+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Cryptography.X509Certificates;
6+
7+
namespace Microsoft.Identity.Client.ManagedIdentity.V2
8+
{
9+
/// <summary>
10+
/// Immutable snapshot of a cached certificate and its associated metadata.
11+
/// </summary>
12+
internal readonly struct CertificateCacheValue
13+
{
14+
public CertificateCacheValue(X509Certificate2 certificate, string endpoint, string clientId)
15+
{
16+
if (certificate == null) throw new ArgumentNullException(nameof(certificate));
17+
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));
18+
if (clientId == null) throw new ArgumentNullException(nameof(clientId));
19+
20+
Certificate = certificate;
21+
Endpoint = endpoint;
22+
ClientId = clientId;
23+
}
24+
25+
/// <summary>The certificate (clone owned by the caller).</summary>
26+
public X509Certificate2 Certificate { get; }
27+
28+
/// <summary>The base endpoint to use with this certificate.</summary>
29+
public string Endpoint { get; }
30+
31+
/// <summary>The canonical client id to be posted to the mTLS token endpoint.</summary>
32+
public string ClientId { get; }
33+
}
34+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Cryptography.X509Certificates;
6+
using Microsoft.Identity.Client.Core;
7+
8+
namespace Microsoft.Identity.Client.ManagedIdentity.V2
9+
{
10+
/// <summary>
11+
/// Process-local cache for an mTLS certificate and its endpoint.
12+
/// Expiration is based solely on certificate.NotAfter.
13+
/// </summary>
14+
internal interface ICertificateCache
15+
{
16+
/// <summary>
17+
/// Try to get a cached certificate+endpoint+clientId for the specified cacheKey.
18+
/// Returns true and non-null outputs if found and not expired.
19+
/// </summary>
20+
bool TryGet(
21+
string cacheKey,
22+
out CertificateCacheValue value,
23+
ILoggerAdapter logger = null);
24+
25+
/// <summary>
26+
/// Insert or replace the cached certificate+endpoint+clientId for cacheKey.
27+
/// </summary>
28+
void Set(
29+
string cacheKey,
30+
in CertificateCacheValue value,
31+
ILoggerAdapter logger = null);
32+
33+
/// <summary>Remove an entry if present.</summary>
34+
bool Remove(string cacheKey, ILoggerAdapter logger = null);
35+
36+
/// <summary>Clear all entries.</summary>
37+
void Clear(ILoggerAdapter logger = null);
38+
}
39+
}

0 commit comments

Comments
 (0)