Skip to content

Commit ddfd537

Browse files
authored
Create design document for MSI v2 In-Memory Key Approach
Add design document for MSI v2 In-Memory Key Approach, detailing goals, .NET reference implementation, Python implementation design, comparison with KeyGuard, and API design.
1 parent 016092e commit ddfd537

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# MSI v2 In-Memory Key Approach — Design Document
2+
3+
## Goal
4+
5+
Implement an MSI v2 path using an **in-memory software RSA key** (no KeyGuard).
6+
The private key is exportable, so standard Python HTTP libraries (`requests`) work
7+
for both token acquisition and resource calls. **No MSAL helper needed.**
8+
9+
This matches .NET's `InMemoryManagedIdentityKeyProvider` — the lowest tier in the
10+
key hierarchy.
11+
12+
---
13+
14+
## .NET Reference Implementation
15+
16+
From [`InMemoryManagedIdentityKeyProvider.cs`](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/InMemoryManagedIdentityKeyProvider.cs):
17+
18+
```csharp
19+
// Portable (non-Windows): pure in-memory RSA
20+
private static RSA CreatePortableRsa()
21+
{
22+
var rsa = RSA.Create();
23+
rsa.KeySize = 2048;
24+
return rsa;
25+
}
26+
27+
// Windows: persisted CNG key with AllowExport
28+
private static RSA CreateWindowsPersistedRsa()
29+
{
30+
var creation = new CngKeyCreationParameters
31+
{
32+
ExportPolicy = CngExportPolicies.AllowExport, // ← EXPORTABLE
33+
Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider
34+
};
35+
string keyName = "MSAL-MTLS-" + Guid.NewGuid().ToString("N");
36+
var key = CngKey.Create(CngAlgorithm.Rsa, keyName, creation);
37+
return new RSACng(key);
38+
}
39+
```
40+
41+
Key points:
42+
- **`AllowExport`** — the key CAN be extracted as bytes
43+
- No VBS/KeyGuard flags — purely software key
44+
- No attestation — MAA not called
45+
- Named + persisted so SChannel can use it (Windows only)
46+
47+
---
48+
49+
## Python Implementation Design
50+
51+
### Key Generation
52+
53+
```python
54+
from cryptography.hazmat.primitives.asymmetric import rsa
55+
from cryptography.hazmat.primitives import serialization
56+
57+
# Generate exportable RSA-2048 key
58+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
59+
60+
# Export as PEM — this is possible because key is in-memory (not KeyGuard)
61+
key_pem = private_key.private_bytes(
62+
encoding=serialization.Encoding.PEM,
63+
format=serialization.PrivateFormat.TraditionalOpenSSL,
64+
encryption_algorithm=serialization.NoEncryption(),
65+
).decode("utf-8")
66+
```
67+
68+
### CSR Building
69+
70+
```python
71+
from cryptography import x509
72+
from cryptography.x509.oid import NameOID
73+
from cryptography.hazmat.primitives.hashes import SHA256
74+
from cryptography.hazmat.primitives.asymmetric.padding import PSS, MGF1
75+
76+
# Build CSR using cryptography library (no manual DER needed)
77+
csr = (
78+
x509.CertificateSigningRequestBuilder()
79+
.subject_name(x509.Name([
80+
x509.NameAttribute(NameOID.COMMON_NAME, client_id),
81+
x509.NameAttribute(NameOID.DOMAIN_COMPONENT, tenant_id),
82+
]))
83+
.add_attribute(cu_id_oid, cu_id_value)
84+
.sign(private_key, SHA256(), padding=PSS(mgf=MGF1(SHA256()), salt_length=32))
85+
)
86+
csr_b64 = base64.b64encode(csr.public_bytes(serialization.Encoding.DER)).decode()
87+
```
88+
89+
### IMDS Calls
90+
91+
Same as KeyGuard path but **no attestation token**:
92+
93+
```python
94+
# Step 1: getplatformmetadata (identical)
95+
meta = http_client.get(imds_base + "/metadata/identity/getplatformmetadata",
96+
params={"cred-api-version": "2.0"}, headers={"Metadata": "true"})
97+
98+
# Step 2: issuecredential — empty attestation_token
99+
cred = http_client.post(imds_base + "/metadata/identity/issuecredential",
100+
params={"cred-api-version": "2.0"},
101+
headers={"Metadata": "true", "Content-Type": "application/json"},
102+
json={"csr": csr_b64, "attestation_token": ""}) # ← empty
103+
```
104+
105+
### Token Acquisition (mTLS)
106+
107+
Since the key is exportable, use `requests` with cert + key PEM:
108+
109+
```python
110+
import requests
111+
import tempfile, os
112+
113+
# Write cert + key to temp files (requests needs file paths)
114+
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as cf:
115+
cf.write(cert_pem)
116+
cert_path = cf.name
117+
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as kf:
118+
kf.write(key_pem)
119+
key_path = kf.name
120+
121+
try:
122+
token_resp = requests.post(
123+
token_endpoint,
124+
cert=(cert_path, key_path),
125+
data={
126+
"grant_type": "client_credentials",
127+
"client_id": client_id,
128+
"scope": scope,
129+
"token_type": "mtls_pop",
130+
},
131+
)
132+
finally:
133+
os.unlink(cert_path)
134+
os.unlink(key_path)
135+
```
136+
137+
### Auth Result
138+
139+
```python
140+
{
141+
"access_token": "eyJ...",
142+
"token_type": "mtls_pop",
143+
"expires_in": 86399,
144+
"cert_pem": "-----BEGIN CERTIFICATE-----\n...",
145+
"key_pem": "-----BEGIN RSA PRIVATE KEY-----\n...", # ← AVAILABLE
146+
"cert_thumbprint_sha256": "abc123...",
147+
}
148+
```
149+
150+
### Resource Call — No Helper Needed!
151+
152+
The caller uses standard `requests`:
153+
154+
```python
155+
result = client.acquire_token_for_client(
156+
resource="https://vault.azure.net",
157+
mtls_proof_of_possession=True,
158+
)
159+
160+
# Write cert+key to temp files (or use in-memory with urllib3)
161+
# ... (same temp file pattern as above)
162+
163+
resp = requests.get(
164+
"https://tokenbinding.vault.azure.net/secrets/boundsecret/?api-version=2015-06-01",
165+
cert=(cert_path, key_path),
166+
headers={
167+
"Authorization": f"{result['token_type']} {result['access_token']}",
168+
"x-ms-tokenboundauth": "true",
169+
},
170+
)
171+
```
172+
173+
---
174+
175+
## Comparison: KeyGuard vs In-Memory
176+
177+
| Aspect | KeyGuard (current PR) | In-Memory (this design) |
178+
|--------|----------------------|------------------------|
179+
| Key type | Non-exportable CNG/VBS | Exportable software RSA |
180+
| Attestation | MAA (proves hardware) | None |
181+
| `key_pem` in result? | ❌ Impossible | ✅ Yes |
182+
| Token acquisition | WinHTTP/SChannel (ctypes) | `requests` + cert/key PEM |
183+
| Resource call | `mtls_http_request()` helper | Standard `requests` |
184+
| Helper needed? | **Yes** | **No** |
185+
| Platform | Windows + Credential Guard | **Any** (cross-platform) |
186+
| Dependencies | `msal-key-attestation`, ctypes | `cryptography` (already used) |
187+
| Security | ★★★★★ | ★★☆☆☆ |
188+
189+
---
190+
191+
## API Design
192+
193+
```python
194+
# KeyGuard + attestation (high security, helper required)
195+
result = client.acquire_token_for_client(
196+
resource=...,
197+
mtls_proof_of_possession=True,
198+
with_attestation_support=True, # ← KeyGuard path
199+
)
200+
# result has cert_pem, cert_der_b64 but NO key_pem
201+
# Must use: mtls_http_request() for resource calls
202+
203+
# In-memory (lower security, no helper needed)
204+
result = client.acquire_token_for_client(
205+
resource=...,
206+
mtls_proof_of_possession=True,
207+
# with_attestation_support=False (default) ← In-memory path
208+
)
209+
# result has cert_pem AND key_pem
210+
# Standard: requests.get(url, cert=(cert, key)) just works
211+
```
212+
213+
---
214+
215+
## Implementation Effort
216+
217+
| Component | KeyGuard (done) | In-Memory (new) |
218+
|-----------|----------------|-----------------|
219+
| Key generation | NCrypt via ctypes | `cryptography.rsa.generate_private_key()` |
220+
| CSR building | Manual DER builder (500+ LOC) | `cryptography.x509.CertificateSigningRequestBuilder` (~20 LOC) |
221+
| IMDS calls | Shared | Shared |
222+
| Token acquisition | WinHTTP/SChannel via ctypes | `requests.post(cert=...)` |
223+
| Platform | Windows only | Cross-platform |
224+
| Complexity | High (ctypes, Win32 APIs) | Low (pure Python) |
225+
226+
The in-memory path is **significantly simpler** — most of the complexity in `msi_v2.py`
227+
(NCrypt, Crypt32, WinHTTP, manual DER) is specifically for KeyGuard. The in-memory path
228+
can be implemented with `cryptography` + `requests` in ~200 lines.

0 commit comments

Comments
 (0)