mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(backend/security): pin client cert chain trust to embedded Intune root (v2)
Re-applies 7e21591b after the chain-validation outage and adds the missing
trust anchors. The first attempt embedded only the 2030 root and the 2026
intermediate; current production device certs chain via the 2026 root +
the 2028 intermediate, so chain.Build() returned "unable to get local
issuer certificate" for every legitimate agent and broke enrolment.
What is different in v2:
- Bundle expanded with both Intune roots (exp 2026-08-12 and 2030-09-15)
and both currently-issuing MDM Device CA intermediates (exp 2026-05-05
and 2028-09-15). Real device certs chain via 2028-intermediate -> 2030-
root today; the 2026 generation is kept for any older device certs
still in flight.
- Loader now globs intune-root-*.pem and intune-intermediate-*.pem from
embedded resources and dedupes by thumbprint, so a future Intune root
rotation is a drop-in PEM with no code change. Legacy generic-named
files (intune-root-ca.pem, intune-intermediate-ca.pem) are duplicates
of the new ones and harmlessly de-dup at load.
- Adds a happy-path integration test against a real Intune-issued device
cert (public-key only, copied from a test VM). This is the regression
test that would have caught the v1 outage and gates any future bundle
drift in CI.
- Fixes a pre-existing TZ bug in the expiry check: X509Certificate2's
NotBefore/NotAfter return Local time, were being compared raw against
DateTime.UtcNow. Surfaced by the integration test on a CEST dev box;
unobservable in prod because Linux Flex Consumption runs in UTC.
Trust model is unchanged from v1: X509ChainTrustMode.CustomRootTrust
pinned to the embedded Intune roots, OS trust store ignored, fail-closed
when no roots are loaded. Self-signed-with-Intune-DN bypass remains shut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,4 +26,10 @@
|
|||||||
<ProjectReference Include="..\..\Shared\AutopilotMonitor.Shared\AutopilotMonitor.Shared.csproj" />
|
<ProjectReference Include="..\..\Shared\AutopilotMonitor.Shared\AutopilotMonitor.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="device-cert-sample.pem">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using AutopilotMonitor.Functions.Security;
|
||||||
|
|
||||||
|
namespace AutopilotMonitor.Functions.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for CertificateValidator chain-trust enforcement.
|
||||||
|
///
|
||||||
|
/// The validator pins trust to embedded Intune root certs via X509ChainTrustMode.CustomRootTrust.
|
||||||
|
/// Two layers of coverage:
|
||||||
|
/// - Happy path: a real Intune-issued device cert (public-key only, copied from a test VM)
|
||||||
|
/// must validate successfully. This guards against rollouts where the embedded root/intermediate
|
||||||
|
/// bundle drifts from the chain Microsoft is actually issuing.
|
||||||
|
/// - Negative path: self-signed leaf certs (with or without Intune-shaped DNs) must NOT validate.
|
||||||
|
/// </summary>
|
||||||
|
public class CertificateValidatorTests
|
||||||
|
{
|
||||||
|
private const string DeviceSamplePem = "device-cert-sample.pem";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithRealIntuneDeviceCert_ReturnsValid()
|
||||||
|
{
|
||||||
|
// Real device cert (public-key only) issued by "Microsoft Intune MDM Device CA".
|
||||||
|
// This is the regression test that would have caught the previous outage where the
|
||||||
|
// embedded root bundle didn't match the chain Microsoft uses for current devices.
|
||||||
|
var pemPath = ResolveSamplePath();
|
||||||
|
Assert.True(File.Exists(pemPath), $"Sample cert not found at {pemPath}");
|
||||||
|
|
||||||
|
var pem = File.ReadAllText(pemPath);
|
||||||
|
using var cert = X509Certificate2.CreateFromPem(pem);
|
||||||
|
var b64 = Convert.ToBase64String(cert.Export(X509ContentType.Cert));
|
||||||
|
|
||||||
|
var result = CertificateValidator.ValidateCertificate(b64);
|
||||||
|
|
||||||
|
Assert.True(result.IsValid, $"Real device cert rejected: {result.ErrorMessage}");
|
||||||
|
Assert.False(string.IsNullOrEmpty(result.Thumbprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithEmptyBase64_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
var result = CertificateValidator.ValidateCertificate("");
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Contains("No certificate", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithNull_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
var result = CertificateValidator.ValidateCertificate(null);
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithMalformedBase64_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
var result = CertificateValidator.ValidateCertificate("not-base64!!!");
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithSelfSignedCertImpersonatingIntune_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
// Regression test: a self-signed leaf with Subject CN="Microsoft Intune MDM Device CA"
|
||||||
|
// and Client-Auth EKU previously passed validation under
|
||||||
|
// AllowUnknownCertificateAuthority + DN substring match. Under CustomRootTrust the
|
||||||
|
// chain has no signature link to a pinned Intune root and must be rejected.
|
||||||
|
var b64 = CreateSelfSignedCertBase64(
|
||||||
|
subjectCn: "Microsoft Intune MDM Device CA",
|
||||||
|
includeClientAuthEku: true);
|
||||||
|
|
||||||
|
var result = CertificateValidator.ValidateCertificate(b64);
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Contains("chain validation failed", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithSelfSignedCertImpersonatingMdmDeviceCa_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
var b64 = CreateSelfSignedCertBase64(
|
||||||
|
subjectCn: "MDM Device CA",
|
||||||
|
includeClientAuthEku: true);
|
||||||
|
|
||||||
|
var result = CertificateValidator.ValidateCertificate(b64);
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithSelfSignedNonIntuneCert_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
var b64 = CreateSelfSignedCertBase64(
|
||||||
|
subjectCn: "test-device.contoso.example",
|
||||||
|
includeClientAuthEku: true);
|
||||||
|
|
||||||
|
var result = CertificateValidator.ValidateCertificate(b64);
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCertificate_WithExpiredSelfSignedCert_ReturnsInvalid()
|
||||||
|
{
|
||||||
|
var b64 = CreateSelfSignedCertBase64(
|
||||||
|
subjectCn: "Microsoft Intune MDM Device CA",
|
||||||
|
includeClientAuthEku: true,
|
||||||
|
notBefore: DateTimeOffset.UtcNow.AddYears(-2),
|
||||||
|
notAfter: DateTimeOffset.UtcNow.AddYears(-1));
|
||||||
|
|
||||||
|
var result = CertificateValidator.ValidateCertificate(b64);
|
||||||
|
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Contains("expired", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSamplePath()
|
||||||
|
{
|
||||||
|
var assemblyDir = Path.GetDirectoryName(typeof(CertificateValidatorTests).Assembly.Location)!;
|
||||||
|
return Path.Combine(assemblyDir, DeviceSamplePem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSelfSignedCertBase64(
|
||||||
|
string subjectCn,
|
||||||
|
bool includeClientAuthEku,
|
||||||
|
DateTimeOffset? notBefore = null,
|
||||||
|
DateTimeOffset? notAfter = null)
|
||||||
|
{
|
||||||
|
using var rsa = RSA.Create(2048);
|
||||||
|
var req = new CertificateRequest(
|
||||||
|
$"CN={subjectCn}",
|
||||||
|
rsa,
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
|
if (includeClientAuthEku)
|
||||||
|
{
|
||||||
|
// 1.3.6.1.5.5.7.3.2 = Client Authentication
|
||||||
|
req.CertificateExtensions.Add(
|
||||||
|
new X509EnhancedKeyUsageExtension(
|
||||||
|
new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") },
|
||||||
|
critical: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
var nb = notBefore ?? DateTimeOffset.UtcNow.AddDays(-1);
|
||||||
|
var na = notAfter ?? DateTimeOffset.UtcNow.AddYears(1);
|
||||||
|
using var cert = req.CreateSelfSigned(nb, na);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(cert.Export(X509ContentType.Cert));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEeTCCA2GgAwIBAgIQ3CvOdsqKrJFKrCZd9mi7RjANBgkqhkiG9w0BAQsFADAp
|
||||||
|
MScwJQYDVQQDEx5NaWNyb3NvZnQgSW50dW5lIE1ETSBEZXZpY2UgQ0EwHhcNMjYw
|
||||||
|
NTA4MTIxNjQ5WhcNMjcwNTA4MDAxNjIyWjAvMS0wKwYDVQQDEyQwN2JjMDE2Ny05
|
||||||
|
MDYxLTQzYjctYjI0My0wYmI2YWVkYTczNmUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQDQR7R0I80KYq0j/ix1RxPmCgiHFr3a6cUVfcMtJnrGYQUg/Ngh
|
||||||
|
PGigNyV/CxQ2qfExAjXh1OJ112NYo+Hhrwgl901r42s1bvU8JCXiXWERG87pnCv8
|
||||||
|
LQ4sRLAW91bz3phhKT5I8gk4uPCJZqTjpZ+/cpdU3CrK5bS8lb23M+s5IizcNAtR
|
||||||
|
DCTOBXc8My9Yy5ExZZ41uo+xOcP+RQ4yp5bxTzzT2gx5BVsc1QA9f4mNzdMQ8dyC
|
||||||
|
ejygU+UwiCJsVTKIFtqhLIrgi5Bszuq2611wma2nKjfRUlJQITGCGCccCegKTQNO
|
||||||
|
eaiYUoS/Hj9lh+gAej6OmCJCoomKWtB6lgUTAgMBAAGjggGVMIIBkTAdBgNVHQ4E
|
||||||
|
FgQU762JgixoNCU2IZEnzm7sMy3ZSTAwHwYDVR0jBBgwFoAUjrJY+zZQjryKKzg8
|
||||||
|
w0YEkXJJJRowHgYIKoZIhvcUBQsEEgQQarzWQl5UmUGVKayL/NSeijAeBggqhkiG
|
||||||
|
9xQFDgQSBBCvwU21IFNgT7XUgh4M8qNZMA8GCCqGSIb3FAUPBAMCAQIwDwYIKoZI
|
||||||
|
hvcUBRAEAwIBDTAPBggqhkiG9xQFEQQDAgEOMA8GCCqGSIb3FAUSBAMCAQEwDwYI
|
||||||
|
KoZIhvcUBRMEAwIBADAWBggqhkiG9xQFFwQKAghAAAAAAAABEDAuBggqhkiG9xQF
|
||||||
|
GAQiBCCVN3VWIg9eIC3z9Bjd96ocQSWOMgtwdKFx2nalSqHDvzAcBggqhkiG9xQF
|
||||||
|
BAQQZwG8B2GQt0OyQwu2rtpzbjAeBggqhkiG9xQFBgQSBBA9hd7edOJrQJ0NRrb+
|
||||||
|
ZbRAMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoG
|
||||||
|
CCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQBzgrcC8ky3A/U5iWPJApoH9T99
|
||||||
|
izrA1SZQ6KNBpSfZPCeRAOtL+H8kxnFf5kIX//9qp00TcBlXAAIw9kXwSeCCWrfx
|
||||||
|
76eFDJrbffyG11TN8L/LOwBaaFAnkOD+yKe/gvMYUyEdQFhDF9pPRr2uUV5L7RYe
|
||||||
|
jZ8RojKl8oid6dlp4ZcY7k25WaZOI/UR03+I4PKl8/m8fkKWPcHY07uPxsdwWyIe
|
||||||
|
Y1VQMborLHe8df2ZxS0wEfAtfjdahk8qLV6uw2VzJI0/S5HZDCAc9agGEKUh8KGC
|
||||||
|
xCKndl1riMHjZZ6sNP/fzVZDI+6cM4yYV6yxraAyEOK5iyq/2QIGQa3NjScl
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Security\Certificates\intune-intermediate-ca.pem" />
|
<EmbeddedResource Include="Security\Certificates\intune-root-*.pem" />
|
||||||
<EmbeddedResource Include="Security\Certificates\intune-root-ca.pem" />
|
<EmbeddedResource Include="Security\Certificates\intune-intermediate-*.pem" />
|
||||||
<EmbeddedResource Include="..\..\..\rules\dist\gather-rules.json" LogicalName="AutopilotMonitor.Functions.Rules.gather-rules.json" />
|
<EmbeddedResource Include="..\..\..\rules\dist\gather-rules.json" LogicalName="AutopilotMonitor.Functions.Rules.gather-rules.json" />
|
||||||
<EmbeddedResource Include="..\..\..\rules\dist\analyze-rules.json" LogicalName="AutopilotMonitor.Functions.Rules.analyze-rules.json" />
|
<EmbeddedResource Include="..\..\..\rules\dist\analyze-rules.json" LogicalName="AutopilotMonitor.Functions.Rules.analyze-rules.json" />
|
||||||
<EmbeddedResource Include="..\..\..\rules\dist\ime-log-patterns.json" LogicalName="AutopilotMonitor.Functions.Rules.ime-log-patterns.json" />
|
<EmbeddedResource Include="..\..\..\rules\dist\ime-log-patterns.json" LogicalName="AutopilotMonitor.Functions.Rules.ime-log-patterns.json" />
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ namespace AutopilotMonitor.Functions.Security
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Security measures:
|
/// Security measures:
|
||||||
/// - Full X.509 chain validation using X509Chain.Build()
|
/// - X.509 chain trust pinned to embedded Intune root certificate(s) via
|
||||||
|
/// X509ChainTrustMode.CustomRootTrust (OS trust store is ignored)
|
||||||
/// - No revocation checking (Intune certificates have no CRL/OCSP endpoints)
|
/// - No revocation checking (Intune certificates have no CRL/OCSP endpoints)
|
||||||
/// - Validates entire chain contains Microsoft Intune CA
|
|
||||||
/// - Prevents self-signed certificate attacks
|
/// - Prevents self-signed certificate attacks
|
||||||
/// - Validates Enhanced Key Usage for Client Authentication
|
/// - Validates Enhanced Key Usage for Client Authentication
|
||||||
/// - In-memory cache for validated certificates (5 minute TTL)
|
/// - In-memory cache for validated certificates (5 minute TTL)
|
||||||
@@ -29,8 +29,16 @@ namespace AutopilotMonitor.Functions.Security
|
|||||||
private static readonly ConcurrentDictionary<string, CachedValidationResult> _validationCache = new();
|
private static readonly ConcurrentDictionary<string, CachedValidationResult> _validationCache = new();
|
||||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
// Lazily loaded Intune CA certs from embedded resources
|
// Lazily loaded Intune CA certs from embedded PEM resources, split by role:
|
||||||
private static readonly Lazy<X509Certificate2[]> _intuneCaCerts = new(LoadIntuneCaCertificates);
|
// roots -> pinned trust anchors (chain.ChainPolicy.CustomTrustStore)
|
||||||
|
// intermediates -> bridge candidates (chain.ChainPolicy.ExtraStore)
|
||||||
|
// Files are picked up by name prefix from Security/Certificates/, so adding a
|
||||||
|
// future Intune root or intermediate is a drop-in (no code change).
|
||||||
|
// Duplicates (same thumbprint) are deduped at load time.
|
||||||
|
private static readonly Lazy<X509Certificate2[]> _intuneRootCerts =
|
||||||
|
new(() => LoadIntuneCerts("intune-root-"));
|
||||||
|
private static readonly Lazy<X509Certificate2[]> _intuneIntermediateCerts =
|
||||||
|
new(() => LoadIntuneCerts("intune-intermediate-"));
|
||||||
|
|
||||||
private class CachedValidationResult
|
private class CachedValidationResult
|
||||||
{
|
{
|
||||||
@@ -89,43 +97,66 @@ namespace AutopilotMonitor.Functions.Security
|
|||||||
|
|
||||||
logger?.LogDebug($"Certificate validation cache MISS for thumbprint {thumbprint}, performing full validation");
|
logger?.LogDebug($"Certificate validation cache MISS for thumbprint {thumbprint}, performing full validation");
|
||||||
|
|
||||||
// 1. Check expiry
|
// 1. Check expiry. X509Certificate2.NotBefore/NotAfter return Local time on .NET;
|
||||||
|
// normalize to UTC before comparing to DateTime.UtcNow or the result is wrong on
|
||||||
|
// hosts whose TZ != UTC (e.g. test runs on a CEST dev box).
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
if (now < certificate.NotBefore || now > certificate.NotAfter)
|
var notBeforeUtc = certificate.NotBefore.ToUniversalTime();
|
||||||
|
var notAfterUtc = certificate.NotAfter.ToUniversalTime();
|
||||||
|
if (now < notBeforeUtc || now > notAfterUtc)
|
||||||
{
|
{
|
||||||
return new CertificateValidationResult
|
return new CertificateValidationResult
|
||||||
{
|
{
|
||||||
IsValid = false,
|
IsValid = false,
|
||||||
ErrorMessage = $"Certificate expired or not yet valid (NotBefore={certificate.NotBefore}, NotAfter={certificate.NotAfter})",
|
ErrorMessage = $"Certificate expired or not yet valid (NotBefore={notBeforeUtc:u}, NotAfter={notAfterUtc:u})",
|
||||||
Thumbprint = thumbprint
|
Thumbprint = thumbprint
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Validate certificate chain (without revocation check - Intune certs have no CRL/OCSP)
|
// 2. Validate certificate chain against pinned Intune roots (no revocation - no CRL/OCSP).
|
||||||
//
|
//
|
||||||
// On Linux (Azure Functions flex consumption plan) the OS trust store does not contain
|
// CustomTrustStore + CustomRootTrust pins trust to the embedded Intune root cert(s)
|
||||||
// Microsoft Intune CAs. We load them from embedded PEM resources into ExtraStore and set
|
// only and ignores the OS trust store entirely - works on Linux/Flex Consumption
|
||||||
// AllowUnknownCertificateAuthority so chain.Build() can resolve the issuer without the OS
|
// (no machine store) and replaces a previous AllowUnknownCertificateAuthority
|
||||||
// store. The issuer identity is still enforced in step 3.
|
// workaround which was bypassable by self-signed leaf certs whose Subject contained
|
||||||
|
// "Microsoft Intune".
|
||||||
|
var roots = _intuneRootCerts.Value;
|
||||||
|
var intermediates = _intuneIntermediateCerts.Value;
|
||||||
|
|
||||||
|
if (roots.Length == 0)
|
||||||
|
{
|
||||||
|
logger?.LogError("No embedded Intune root certificates loaded - failing closed");
|
||||||
|
return new CertificateValidationResult
|
||||||
|
{
|
||||||
|
IsValid = false,
|
||||||
|
ErrorMessage = "Server misconfiguration: no Intune trust anchors available",
|
||||||
|
Thumbprint = thumbprint
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
using var chain = new X509Chain();
|
using var chain = new X509Chain();
|
||||||
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||||
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
|
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
|
||||||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
|
||||||
chain.ChainPolicy.VerificationTime = DateTime.UtcNow;
|
chain.ChainPolicy.VerificationTime = DateTime.UtcNow;
|
||||||
chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(10);
|
chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
||||||
|
|
||||||
// Load embedded Intune CA certs into ExtraStore
|
foreach (var root in roots)
|
||||||
var caCerts = _intuneCaCerts.Value;
|
chain.ChainPolicy.CustomTrustStore.Add(root);
|
||||||
logger?.LogDebug("Loading {Count} embedded Intune CA certificate(s) into chain ExtraStore", caCerts.Length);
|
foreach (var intermediate in intermediates)
|
||||||
foreach (var caCert in caCerts)
|
chain.ChainPolicy.ExtraStore.Add(intermediate);
|
||||||
chain.ChainPolicy.ExtraStore.Add(caCert);
|
|
||||||
|
logger?.LogDebug(
|
||||||
|
"Chain build with {RootCount} pinned Intune root(s) + {IntermediateCount} intermediate(s)",
|
||||||
|
roots.Length, intermediates.Length);
|
||||||
|
|
||||||
var chainValid = chain.Build(certificate);
|
var chainValid = chain.Build(certificate);
|
||||||
|
|
||||||
if (!chainValid)
|
if (!chainValid)
|
||||||
{
|
{
|
||||||
var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation));
|
var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation));
|
||||||
logger?.LogWarning($"Certificate chain validation failed: {chainErrors}");
|
logger?.LogWarning("Certificate chain validation failed: {ChainErrors}", chainErrors);
|
||||||
|
|
||||||
return new CertificateValidationResult
|
return new CertificateValidationResult
|
||||||
{
|
{
|
||||||
@@ -135,29 +166,12 @@ namespace AutopilotMonitor.Functions.Security
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Verify issuer chain contains Microsoft Intune CA
|
// chainValid == true under CustomRootTrust proves the leaf is transitively signed
|
||||||
var hasValidIssuer = chain.ChainElements
|
// by one of the pinned Intune roots. The previous Subject/Issuer DN substring check
|
||||||
.Cast<X509ChainElement>()
|
// is intentionally removed - it added no security on top of root pinning and would
|
||||||
.Any(element =>
|
// false-negative if Microsoft renames the CA.
|
||||||
element.Certificate.Issuer.Contains("Microsoft Intune", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
element.Certificate.Issuer.Contains("MDM Device CA", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
element.Certificate.Subject.Contains("Microsoft Intune", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
element.Certificate.Subject.Contains("MDM Device CA", StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (!hasValidIssuer)
|
// 3. Check Enhanced Key Usage for Client Authentication (1.3.6.1.5.5.7.3.2)
|
||||||
{
|
|
||||||
var chainInfo = string.Join(" -> ", chain.ChainElements.Cast<X509ChainElement>().Select(e => e.Certificate.Subject));
|
|
||||||
logger?.LogWarning($"Certificate chain does not contain Microsoft Intune CA: {chainInfo}");
|
|
||||||
|
|
||||||
return new CertificateValidationResult
|
|
||||||
{
|
|
||||||
IsValid = false,
|
|
||||||
ErrorMessage = $"Certificate not issued by Microsoft Intune CA",
|
|
||||||
Thumbprint = thumbprint
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Check Enhanced Key Usage for Client Authentication (1.3.6.1.5.5.7.3.2)
|
|
||||||
var hasClientAuth = certificate.Extensions
|
var hasClientAuth = certificate.Extensions
|
||||||
.OfType<X509EnhancedKeyUsageExtension>()
|
.OfType<X509EnhancedKeyUsageExtension>()
|
||||||
.Any(ext => ext.EnhancedKeyUsages
|
.Any(ext => ext.EnhancedKeyUsages
|
||||||
@@ -207,45 +221,59 @@ namespace AutopilotMonitor.Functions.Security
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads Intune CA certificates from embedded PEM resources.
|
/// Loads all certs whose embedded resource filename starts with the given prefix.
|
||||||
/// Files: Security/Certificates/intune-intermediate-ca.pem
|
/// Multiple PEM blocks per file are supported (cross-signed bundles). Duplicates
|
||||||
/// Security/Certificates/intune-root-ca.pem
|
/// are deduped by thumbprint, so a file accidentally duplicated under another name
|
||||||
|
/// is harmless. Roles are filename-segregated:
|
||||||
|
/// intune-root-*.pem -> pinned trust anchors (CustomTrustStore)
|
||||||
|
/// intune-intermediate-*.pem -> chain bridge candidates (ExtraStore)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static X509Certificate2[] LoadIntuneCaCertificates()
|
private static X509Certificate2[] LoadIntuneCerts(string fileNamePrefix)
|
||||||
{
|
{
|
||||||
|
const string resourceNamespace = "AutopilotMonitor.Functions.Security.Certificates.";
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
var resourceNames = new[]
|
var resourcePrefix = resourceNamespace + fileNamePrefix;
|
||||||
{
|
|
||||||
"AutopilotMonitor.Functions.Security.Certificates.intune-intermediate-ca.pem",
|
|
||||||
"AutopilotMonitor.Functions.Security.Certificates.intune-root-ca.pem"
|
|
||||||
};
|
|
||||||
|
|
||||||
List<X509Certificate2> certs = [];
|
var resourceNames = assembly.GetManifestResourceNames()
|
||||||
|
.Where(n => n.StartsWith(resourcePrefix, StringComparison.Ordinal)
|
||||||
|
&& n.EndsWith(".pem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(n => n, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
const string begin = "-----BEGIN CERTIFICATE-----";
|
||||||
|
const string end = "-----END CERTIFICATE-----";
|
||||||
|
var seenThumbprints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var certs = new List<X509Certificate2>();
|
||||||
|
|
||||||
foreach (var resourceName in resourceNames)
|
foreach (var resourceName in resourceNames)
|
||||||
{
|
{
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
if (stream == null)
|
if (stream == null) continue;
|
||||||
{
|
|
||||||
// Resource not found - not fatal, chain validation will fall back to OS store
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
var pem = reader.ReadToEnd();
|
var pem = reader.ReadToEnd();
|
||||||
|
|
||||||
// Skip placeholder files that haven't been filled with a real cert yet
|
|
||||||
if (pem.Contains("PLACEHOLDER_REPLACE_WITH_BASE64"))
|
if (pem.Contains("PLACEHOLDER_REPLACE_WITH_BASE64"))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
try
|
int idx = 0;
|
||||||
|
while ((idx = pem.IndexOf(begin, idx, StringComparison.Ordinal)) >= 0)
|
||||||
{
|
{
|
||||||
var cert = X509Certificate2.CreateFromPem(pem);
|
var endIdx = pem.IndexOf(end, idx, StringComparison.Ordinal);
|
||||||
certs.Add(cert);
|
if (endIdx < 0) break;
|
||||||
}
|
var block = pem.Substring(idx, endIdx + end.Length - idx);
|
||||||
catch
|
try
|
||||||
{
|
{
|
||||||
// Malformed PEM - skip silently, chain validation continues with OS store
|
var cert = X509Certificate2.CreateFromPem(block);
|
||||||
|
if (seenThumbprints.Add(cert.Thumbprint))
|
||||||
|
certs.Add(cert);
|
||||||
|
else
|
||||||
|
cert.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Malformed block - skip; chain.Build() will fail closed if no roots remain.
|
||||||
|
}
|
||||||
|
idx = endIdx + end.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEfTCCAmWgAwIBAgIQQt4O1ZNyWaBPqw1mUq1F8jANBgkqhkiG9w0BAQsFADA4
|
||||||
|
MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBB
|
||||||
|
dXRob3JpdHkwHhcNMjMwNTA1MDAwMDAwWhcNMjYwNTA1MDAwMDAwWjApMScwJQYD
|
||||||
|
VQQDEx5NaWNyb3NvZnQgSW50dW5lIE1ETSBEZXZpY2UgQ0EwggEiMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4IBDwAwggEKAoIBAQDYOy/IV8f+BmH5y9/zJUv5oROn3MEQua39r03K
|
||||||
|
7fgAqwEsAFtU/4ZWnFbriFSx0xZsDkZxxUy9BlOQZ+2sr/eJ2j6uI6CocR4woRkt
|
||||||
|
JR6pQ5EXdsJb9lZOfV9+q6PwLnAIBQTTpUq+XtE8T1MwwGQ5NWW1JqQMDPo3nR0T
|
||||||
|
h14tDQNrLtCPe7c2f+qaG2x+uRvWGMgTmxtGpYiipQB8upnInAZZ5wVdhu+/GMZg
|
||||||
|
TSYJvn+3Px33NOB+0mpVanq7CwzyXA+dqGio8sZJDfOS1fPYDKqPj52l3rct3doa
|
||||||
|
2o98gPHWaDs7Rj+GiAKeidUkN3uhQUyNfi8+YJEHb8OVGjEFAgMBAAGjgZEwgY4w
|
||||||
|
HQYDVR0OBBYEFC5aTr/lv8IEGDqYYmHaaj3AsHw4MB8GA1UdIwQYMBaAFCtJzrEH
|
||||||
|
JUyd5gqT85C5MPfmRJ+SMBAGCSsGAQQBgjcVAQQDAgEAMA4GA1UdDwEB/wQEAwIB
|
||||||
|
hjAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMA0G
|
||||||
|
CSqGSIb3DQEBCwUAA4ICAQCoci1EyidjS6xqi3S37/TJdIGqM0OcvTsi12WPIVxA
|
||||||
|
MFRaF638dgu9FrRpTDCquc06lBHtIfBWCU7VFkGIGGIZ54L4en/dGi0ZBXjv4vfp
|
||||||
|
RQ+LsoJZnYtCF3MbvlPXfQr3nk1k6m7c1utsuSzL0VgeEJJ++5KXlMRHrFdlXXm5
|
||||||
|
N8MJNw78I/e7sbwNZxvA4UdxnEpqHeKDj1+nppANJDKrobIp9WzmYCFA8Qu3bX/N
|
||||||
|
g9OnxLnpGi8mCIvPCkPu3oo1RtHxjf/tQtdSE4V1/8mggYLNNftjWk6ElqA+gRGY
|
||||||
|
gJCssJrfG/bRYk+9ZQjavu3oG0B+SD7j3UeAOQqoOaPi8faF95tpgpVfJqQ6bm9w
|
||||||
|
qBdODWfE4TYIPd0QG5nsYr86JThEtsL36U9j74wC33+4YHV4UviUiycyQiQQCSIF
|
||||||
|
xSY2u9cLpFO4rjM+Qe/n8KWQURTW8i3wUFReVrMp9l+y70GklU/HHkXv8SDtFi6Q
|
||||||
|
0+aK2duUhbHr+8P2nPuPNetfryVsdE+RVL4pFWEfIvbOKOI5EQj3Bh4/XkbhQtZk
|
||||||
|
+FR7VkUIm5A9Kp+OzsQADkYhWE1N4xL/ysfRWmOcjB1MT1m7Zfie3YsR7j+rY4NL
|
||||||
|
b+snuHqqpYRDSZpngZAJgRYHI1xSUxOk+zJ8XyHG+eyeZdfwlrWLKuMJEFw4pczs
|
||||||
|
0Q==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEfTCCAmWgAwIBAgIQ6w2J4bS8K69IHzmhs1IQ2TANBgkqhkiG9w0BAQsFADA4
|
||||||
|
MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBB
|
||||||
|
dXRob3JpdHkwHhcNMjUwOTE1MDAwMDAwWhcNMjgwOTE1MDAwMDAwWjApMScwJQYD
|
||||||
|
VQQDEx5NaWNyb3NvZnQgSW50dW5lIE1ETSBEZXZpY2UgQ0EwggEiMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4IBDwAwggEKAoIBAQDQJdsb4/U/bHNQZxCYN+9luarjMFhbBsBgGUnf
|
||||||
|
SZw3IzfvXuwvg/hyctwPDGjNh83L+sVDMXuHJ7yRtCtCcI/bUutR8eplET9GZJjt
|
||||||
|
yINcf373QkCJRn5NwRU7UNuy19LbCQmYWxbgEORgrTV/pQAMFULc5hjEtyXnMe+e
|
||||||
|
VNocKsjXn0FnJCT5IOqZZpTIa6Lb6T52jWX3JAU//4DaVl8JWMkZ8r4UptYUfWQ0
|
||||||
|
uCdvfEpb3+M3tlWT0LWfoSQ7EpLxrW1zCCEHsEAnhC4m7Dvk9rw9GiYwl+UCSU03
|
||||||
|
RkjXXSssHOqcIkhaBigzLCIOKy0apa+7iWc/2rnnTf73PGVxAgMBAAGjgZEwgY4w
|
||||||
|
HQYDVR0OBBYEFI6yWPs2UI68iis4PMNGBJFySSUaMB8GA1UdIwQYMBaAFLI/34iA
|
||||||
|
ekb9SNronyGSCloICRftMBAGCSsGAQQBgjcVAQQDAgEAMA4GA1UdDwEB/wQEAwIB
|
||||||
|
hjAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMA0G
|
||||||
|
CSqGSIb3DQEBCwUAA4ICAQApSLtPpgvRt/wu1gRll2PHYXm984XTGVePkfcZd7Sh
|
||||||
|
FfG3G21SnAoVYL8jRwgIwqOKpdE07dq+09tELdP2EBJff4uhdHyBJxC40lzJeOK5
|
||||||
|
ClzHMhdKq/HQ0dnbdfqqr5b54mkEuQubcZFCtd2e+3hcMi0gaHbKhUO53RhU32yt
|
||||||
|
Hy980mcnlfOzrhTyzY7hDZIkWO5mTqp0Ygxlb4w1nh4/PVXWnZPFKd6d2Nbgj34n
|
||||||
|
olNMa53R7xFGKj2o1NMTS2ITjTSo0wzdifo41keNIIIjGrPokaINIuDJyvUNDZuR
|
||||||
|
vcpNXSpQ83/hAo5F9kmgnetGXxJYUoTJRw1WAGE/33lXxKD9/5BN1CJjwZlBVt6j
|
||||||
|
7gVaoELMn64t3pw1zrd+iycCmXW0RmZpLDUEO99SlXKp2N2+qPizh+3Z8RsQBp+u
|
||||||
|
obiPbOcpBRTGdj9j90+JvGJSTrohLn6ViRIuiqYKjzFPEKehmFV/yOt40i0MEQW0
|
||||||
|
kXoApaYPOhlEY2pdDZARsRcJf8Qa3cVJPqbfgb2gg5kcyjtgZYibEnsYCQJcY0+J
|
||||||
|
lemmBQCSEIPqYV33ydz8MUKBweaAREediHGhdQVMmEmuxUTIqxTv2ADcxUSjgQHE
|
||||||
|
JXxiQKCWL91HVoetR9NDWHFZBNCdBcZ3TGPX/BoH/a8duNW1RHficqDVOm9pDdDU
|
||||||
|
Bw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Reference in New Issue
Block a user