Add support for self-signed certificates in subscriber
This commit is contained in:
Родитель
b35b3d4b53
Коммит
b32df18702
|
@ -0,0 +1,52 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
{
|
||||
public static class CertificateHelper
|
||||
{
|
||||
public static void ImportCertificate(params X509Certificate2[] certificates)
|
||||
{
|
||||
if (certificates != null)
|
||||
{
|
||||
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
|
||||
StoreLocation storeLocation = StoreLocation.CurrentUser;
|
||||
using (var store = new X509Store(storeName, storeLocation))
|
||||
{
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
foreach (X509Certificate2 cert in certificates)
|
||||
{
|
||||
store.Add(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsCACertificate(X509Certificate2 certificate)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
|
||||
// The keyCertSign bit is asserted when the subject public key is
|
||||
// used for verifying a signature on public key certificates. If the
|
||||
// keyCertSign bit is asserted, then the cA bit in the basic
|
||||
// constraints extension (section 4.2.1.10) MUST also be asserted.
|
||||
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
|
||||
// The cA boolean indicates whether the certified public key belongs to
|
||||
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
|
||||
// the key usage extension MUST NOT be asserted.
|
||||
X509ExtensionCollection extensionCollection = certificate.Extensions;
|
||||
foreach (X509Extension extension in extensionCollection)
|
||||
{
|
||||
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
|
||||
{
|
||||
if (basicConstraintExtension.CertificateAuthority)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class CertificateResponse
|
||||
{
|
||||
public PrivateKey PrivateKey { get; set; }
|
||||
|
||||
public string Certificate { get; set; }
|
||||
|
||||
public DateTime? Expiration { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class IdentityCertificateRequest
|
||||
{
|
||||
public DateTime Expiration { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class PrivateKey
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PrivateKeyType? Type { get; set; }
|
||||
|
||||
public string Ref { get; set; }
|
||||
|
||||
public string Bytes { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public enum PrivateKeyType
|
||||
{
|
||||
Ref = 0,
|
||||
Key = 1,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class ServerCertificateRequest
|
||||
{
|
||||
public string CommonName { get; set; }
|
||||
|
||||
public DateTime Expiration { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class TrustBundleResponse
|
||||
{
|
||||
public string Certificate { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
{
|
||||
public enum PrivateKeyType
|
||||
{
|
||||
[System.Runtime.Serialization.EnumMember(Value = "ref")]
|
||||
Ref = 0,
|
||||
|
||||
[System.Runtime.Serialization.EnumMember(Value = "key")]
|
||||
Key = 1,
|
||||
}
|
||||
|
||||
public class IdentityCertificateRequest
|
||||
{
|
||||
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
|
||||
public DateTime Expiration { get; set; }
|
||||
}
|
||||
|
||||
public class ServerCertificateRequest
|
||||
{
|
||||
[JsonProperty("commonName", Required = Newtonsoft.Json.Required.Always)]
|
||||
public string CommonName { get; set; }
|
||||
|
||||
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
|
||||
public DateTime Expiration { get; set; }
|
||||
}
|
||||
|
||||
public class CertificateResponse
|
||||
{
|
||||
[JsonProperty("privateKey", Required = Newtonsoft.Json.Required.Always)]
|
||||
public PrivateKey PrivateKey { get; set; }
|
||||
|
||||
[JsonProperty("certificate", Required = Newtonsoft.Json.Required.Always)]
|
||||
public string Certificate { get; set; }
|
||||
|
||||
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
|
||||
public DateTime Expiration { get; set; }
|
||||
}
|
||||
|
||||
public class PrivateKey
|
||||
{
|
||||
[JsonProperty("type", Required = Required.Always)]
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public PrivateKeyType Type { get; set; }
|
||||
|
||||
[JsonProperty("ref", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Ref { get; set; }
|
||||
|
||||
[JsonProperty("bytes", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Bytes { get; set; }
|
||||
}
|
||||
|
||||
public class TrustBundleResponse
|
||||
{
|
||||
[Newtonsoft.Json.JsonProperty("certificate", Required = Newtonsoft.Json.Required.Always)]
|
||||
public string Certificate { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
{
|
||||
internal class IoTEdgeConstants
|
||||
{
|
||||
public const string ModuleGenerationId = "IOTEDGE_MODULEGENERATIONID";
|
||||
public const string ModuleId = "IOTEDGE_MODULEID";
|
||||
public const string WorkloadUri = "IOTEDGE_WORKLOADURI";
|
||||
public const string WorkloadApiVersion = "IOTEDGE_APIVERSION";
|
||||
public const string EdgeGatewayHostName = "IOTEDGE_GATEWAYHOSTNAME";
|
||||
public const string UnixScheme = "unix";
|
||||
public const int DefaultServerCertificateValidityInDays = 90;
|
||||
public const int DefaultIdentityCertificateValidityInDays = 7;
|
||||
}
|
||||
}
|
|
@ -1,397 +0,0 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
{
|
||||
public class IoTSecurity
|
||||
{
|
||||
public void ImportCertificate(IEnumerable<X509Certificate2> certificates)
|
||||
{
|
||||
if (certificates != null)
|
||||
{
|
||||
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
|
||||
StoreLocation storeLocation = StoreLocation.CurrentUser;
|
||||
using (var store = new X509Store(storeName, storeLocation))
|
||||
{
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
foreach (X509Certificate2 cert in certificates)
|
||||
{
|
||||
store.Add(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(X509Certificate2, IEnumerable<X509Certificate2>)> GetClientCertificateAsync()
|
||||
{
|
||||
Uri workloadUri = this.GetWorkloadUri();
|
||||
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||
Uri workloadRequestUri = this.GetIdentityCertificateRequestUri(workloadUri);
|
||||
|
||||
int certificateValidityInDays = IoTEdgeConstants.DefaultIdentityCertificateValidityInDays;
|
||||
DateTime expirationTime = DateTime.UtcNow.AddDays(certificateValidityInDays);
|
||||
var identityCertificateRequest = new IdentityCertificateRequest() { Expiration = expirationTime };
|
||||
|
||||
var errorMessage = "Failed to retrieve ClientCertificate from IoTEdge Security Daemon.";
|
||||
|
||||
try
|
||||
{
|
||||
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
|
||||
{
|
||||
string requestString = JsonConvert.SerializeObject(identityCertificateRequest);
|
||||
var content = new StringContent(requestString);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, workloadRequestUri))
|
||||
{
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
httpRequest.Content = content;
|
||||
|
||||
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None))
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responseData);
|
||||
|
||||
IEnumerable<string> rawCerts = this.ParseResponse(cr.Certificate);
|
||||
if (rawCerts.FirstOrDefault() == null)
|
||||
{
|
||||
throw new Exception("Did not receive an identity certificate from IoTEdge daemon!");
|
||||
}
|
||||
|
||||
return this.CreateX509Certificates(rawCerts, cr.PrivateKey.Bytes, moduleId);
|
||||
}
|
||||
|
||||
errorMessage = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Failed to retrieve client certificate from IoTEdge Security Daemon. Reason: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain)> GetServerCertificateAsync()
|
||||
{
|
||||
Uri workloadUri = this.GetWorkloadUri();
|
||||
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||
Uri workloadRequestUri = this.GetServerCertificateRequestUri(workloadUri);
|
||||
|
||||
ServerCertificateRequest scRequest = this.GetServerCertificateRequest(IoTEdgeConstants.DefaultServerCertificateValidityInDays);
|
||||
try
|
||||
{
|
||||
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
|
||||
{
|
||||
string scrString = JsonConvert.SerializeObject(scRequest);
|
||||
var content = new StringContent(scrString);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, workloadRequestUri))
|
||||
{
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
httpRequest.Content = content;
|
||||
|
||||
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responseData);
|
||||
IEnumerable<string> rawCerts = this.ParseResponse(cr.Certificate);
|
||||
if (rawCerts.FirstOrDefault() == null)
|
||||
{
|
||||
throw new Exception($"Failed to retrieve serverCertificate from IoTEdge Security daemon. Reason: Security daemon return empty response.");
|
||||
}
|
||||
|
||||
(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) = this.CreateX509Certificates(rawCerts, cr.PrivateKey.Bytes, moduleId);
|
||||
return (serverCertificate, certificateChain);
|
||||
}
|
||||
|
||||
string errorData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
throw new Exception(errorData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Failed to retrieve server certificate from IoTEdge Security Daemon. Reason: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private Uri GetWorkloadUri() => new Uri(Environment.GetEnvironmentVariable(IoTEdgeConstants.WorkloadUri));
|
||||
|
||||
private string GetIoTEdgeEnvironmentVariable(string envVarName) => Environment.GetEnvironmentVariable(envVarName);
|
||||
|
||||
private Uri GetIdentityCertificateRequestUri(Uri workloadUri)
|
||||
{
|
||||
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
|
||||
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||
|
||||
string urlEncodedModuleId = WebUtility.UrlEncode(moduleId);
|
||||
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
|
||||
|
||||
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
|
||||
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
|
||||
workloadRequestUriBuilder.Append($"/modules/{urlEncodedModuleId}/certificate/identity?api-version={urlEncodedWorkloadApiVersion}");
|
||||
return new Uri(workloadRequestUriBuilder.ToString());
|
||||
}
|
||||
|
||||
private Uri GetServerCertificateRequestUri(Uri workloadUri)
|
||||
{
|
||||
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
|
||||
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||
string moduleGenerationId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleGenerationId);
|
||||
|
||||
string urlEncodedModuleId = WebUtility.UrlEncode(moduleId);
|
||||
string urlEncodedModuleGenerationId = WebUtility.UrlEncode(moduleGenerationId);
|
||||
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
|
||||
|
||||
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
|
||||
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
|
||||
workloadRequestUriBuilder.Append($"/modules/{urlEncodedModuleId}/genid/{urlEncodedModuleGenerationId}/certificate/server?api-version={urlEncodedWorkloadApiVersion}");
|
||||
return new Uri(workloadRequestUriBuilder.ToString());
|
||||
}
|
||||
|
||||
private Uri GetTrustBundleRequestUri(Uri workloadUri)
|
||||
{
|
||||
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
|
||||
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
|
||||
|
||||
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
|
||||
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
|
||||
workloadRequestUriBuilder.Append($"/trust-bundle?api-version={urlEncodedWorkloadApiVersion}");
|
||||
return new Uri(workloadRequestUriBuilder.ToString());
|
||||
}
|
||||
|
||||
private ServerCertificateRequest GetServerCertificateRequest(int validityInDays = 90)
|
||||
{
|
||||
string edgeDeviceHostName = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.EdgeGatewayHostName);
|
||||
DateTime expirationTime = DateTime.UtcNow.AddDays(validityInDays);
|
||||
|
||||
return new ServerCertificateRequest()
|
||||
{
|
||||
CommonName = edgeDeviceHostName,
|
||||
Expiration = expirationTime,
|
||||
};
|
||||
}
|
||||
|
||||
private string GetBaseUrl(Uri workloadUri)
|
||||
{
|
||||
if (workloadUri.Scheme.Equals(IoTEdgeConstants.UnixScheme, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"http://{workloadUri.Segments.Last()}";
|
||||
}
|
||||
|
||||
return workloadUri.OriginalString;
|
||||
}
|
||||
|
||||
private HttpClient GetHttpClient(Uri workloadUri)
|
||||
{
|
||||
if (workloadUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || workloadUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new HttpClient();
|
||||
}
|
||||
else if (workloadUri.Scheme.Equals(IoTEdgeConstants.UnixScheme, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new HttpClient(new HttpUdsMessageHandler(workloadUri));
|
||||
}
|
||||
|
||||
throw new Exception($"Unknow workloadUri schema specified. {workloadUri}");
|
||||
}
|
||||
|
||||
private IList<string> ParseResponse(string certificateChain)
|
||||
{
|
||||
if (string.IsNullOrEmpty(certificateChain))
|
||||
{
|
||||
throw new InvalidOperationException("Trusted certificates can not be null or empty.");
|
||||
}
|
||||
|
||||
// Extract each certificate's string. The final string from the split will either be empty
|
||||
// or a non-certificate entry, so it is dropped.
|
||||
string delimiter = "-----END CERTIFICATE-----";
|
||||
string[] rawCerts = certificateChain.Split(new[] { delimiter }, StringSplitOptions.None);
|
||||
return rawCerts.Take(count: rawCerts.Count() - 1).Select(c => $"{c}{delimiter}").ToList();
|
||||
}
|
||||
|
||||
private (X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) CreateX509Certificates(IEnumerable<string> rawCerts, string privateKey, string moduleId)
|
||||
{
|
||||
string primaryCert = rawCerts.First();
|
||||
RsaPrivateCrtKeyParameters keyParams = null;
|
||||
|
||||
IEnumerable<X509Certificate2> x509CertsChain = this.ConvertToX509(rawCerts.Skip(1));
|
||||
|
||||
IList<X509CertificateEntry> chainCertEntries = new List<X509CertificateEntry>();
|
||||
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
|
||||
// note: the seperator between the certificate and private key is added for safety to delinate the cert and key boundary
|
||||
var sr = new StringReader(primaryCert + "\r\n" + privateKey);
|
||||
var pemReader = new PemReader(sr);
|
||||
object certObject = pemReader.ReadObject();
|
||||
while (certObject != null)
|
||||
{
|
||||
if (certObject is Org.BouncyCastle.X509.X509Certificate x509Cert)
|
||||
{
|
||||
chainCertEntries.Add(new X509CertificateEntry(x509Cert));
|
||||
}
|
||||
|
||||
// when processing certificates generated via openssl certObject type is of AsymmetricCipherKeyPair
|
||||
if (certObject is AsymmetricCipherKeyPair)
|
||||
{
|
||||
certObject = ((AsymmetricCipherKeyPair)certObject).Private;
|
||||
}
|
||||
|
||||
if (certObject is RsaPrivateCrtKeyParameters)
|
||||
{
|
||||
keyParams = (RsaPrivateCrtKeyParameters)certObject;
|
||||
}
|
||||
|
||||
certObject = pemReader.ReadObject();
|
||||
}
|
||||
|
||||
if (keyParams == null)
|
||||
{
|
||||
throw new InvalidOperationException("Private key is required");
|
||||
}
|
||||
|
||||
store.SetKeyEntry(moduleId, new AsymmetricKeyEntry(keyParams), chainCertEntries.ToArray());
|
||||
using (var p12File = new MemoryStream())
|
||||
{
|
||||
store.Save(p12File, Array.Empty<char>(), new SecureRandom());
|
||||
var x509PrimaryCert = new X509Certificate2(p12File.ToArray());
|
||||
return (x509PrimaryCert, x509CertsChain);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<X509Certificate2>> GetTrustBundleAsync()
|
||||
{
|
||||
Uri workloadUri = this.GetWorkloadUri();
|
||||
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
|
||||
{
|
||||
Uri workloadRequestUri = this.GetTrustBundleRequestUri(workloadUri);
|
||||
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, workloadRequestUri))
|
||||
{
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None))
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
TrustBundleResponse trustBundleResponse = JsonConvert.DeserializeObject<TrustBundleResponse>(responseData);
|
||||
IEnumerable<string> rawCerts = this.ParseResponse(trustBundleResponse.Certificate);
|
||||
if (rawCerts.FirstOrDefault() == null)
|
||||
{
|
||||
throw new Exception($"Failed to retrieve trustbundle from security daemon.");
|
||||
}
|
||||
|
||||
return this.ConvertToX509(rawCerts);
|
||||
}
|
||||
|
||||
string errorData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
throw new Exception($"Failed to retrieve trustbundle from security daemon. Reason: {errorData}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ValidateClientCertificateAsync(X509Certificate2 clientCertificate)
|
||||
{
|
||||
// Please add validation more validations as appropriate
|
||||
|
||||
if (this.IsCACertificate(clientCertificate))
|
||||
{
|
||||
throw new Exception("Cannot use CA certificate for client authentication!");
|
||||
}
|
||||
|
||||
IEnumerable<X509Certificate2> trustedCertificates = await this.GetTrustBundleAsync();
|
||||
|
||||
using (X509Chain chain = new X509Chain())
|
||||
{
|
||||
foreach (X509Certificate2 trustedClientCert in trustedCertificates)
|
||||
{
|
||||
chain.ChainPolicy.ExtraStore.Add(trustedClientCert);
|
||||
}
|
||||
|
||||
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// IoTEdge generates a self-signed certificate by default, that is not rooted in a root certificate that is trusted by the trust provider hence this flag is needed
|
||||
// so that build returns true if root terminates in a self-signed certificate
|
||||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||
}
|
||||
|
||||
if (!chain.Build(clientCertificate))
|
||||
{
|
||||
var errorMessageBuilder = new StringBuilder();
|
||||
foreach (X509ChainStatus cs in chain.ChainStatus)
|
||||
{
|
||||
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
|
||||
errorMessageBuilder.AppendLine();
|
||||
}
|
||||
|
||||
throw new Exception($"ClientCertificate is not valid! Reason: Failed chain validation. Details: {errorMessageBuilder}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private X509Certificate2[] ConvertToX509(IEnumerable<string> rawCerts)
|
||||
{
|
||||
return rawCerts
|
||||
.Select(c => Encoding.UTF8.GetBytes(c))
|
||||
.Select(c => new X509Certificate2(c))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private bool IsCACertificate(X509Certificate2 certificate)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
|
||||
// The keyCertSign bit is asserted when the subject public key is
|
||||
// used for verifying a signature on public key certificates. If the
|
||||
// keyCertSign bit is asserted, then the cA bit in the basic
|
||||
// constraints extension (section 4.2.1.10) MUST also be asserted.
|
||||
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
|
||||
// The cA boolean indicates whether the certified public key belongs to
|
||||
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
|
||||
// the key usage extension MUST NOT be asserted.
|
||||
X509ExtensionCollection extensionCollection = certificate.Extensions;
|
||||
foreach (X509Extension extension in extensionCollection)
|
||||
{
|
||||
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
|
||||
{
|
||||
if (basicConstraintExtension.CertificateAuthority)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
[Obsolete("This is a pubternal API that's being made public as a stop-gap measure. It will be removed from the Event Grid SDK nuget package as soon IoT Edge SDK ships with a built-in a security daemon client.")]
|
||||
public sealed class SecurityDaemonClient : IDisposable
|
||||
{
|
||||
private const string UnixScheme = "unix";
|
||||
private const int DefaultServerCertificateValidityInDays = 90;
|
||||
private const int DefaultIdentityCertificateValidityInDays = 7;
|
||||
private readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.None,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
FloatParseHandling = FloatParseHandling.Decimal,
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
Converters = new JsonConverter[] { new StringEnumConverter() },
|
||||
};
|
||||
|
||||
private readonly string moduleGenerationId;
|
||||
private readonly string edgeGatewayHostName;
|
||||
private readonly string workloadApiVersion;
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly Uri getTrustBundleUri;
|
||||
private readonly Uri postIdentityCertificateRequestUri;
|
||||
private readonly Uri postServerCertificateRequestUri;
|
||||
private readonly string asString;
|
||||
|
||||
public SecurityDaemonClient()
|
||||
{
|
||||
this.ModuleId = Environment.GetEnvironmentVariable("IOTEDGE_MODULEID");
|
||||
this.DeviceId = Environment.GetEnvironmentVariable("IOTEDGE_DEVICEID");
|
||||
string iotHubHostName = Environment.GetEnvironmentVariable("IOTEDGE_IOTHUBHOSTNAME");
|
||||
this.IotHubName = iotHubHostName.Split('.').FirstOrDefault();
|
||||
|
||||
this.moduleGenerationId = Environment.GetEnvironmentVariable("IOTEDGE_MODULEGENERATIONID");
|
||||
this.edgeGatewayHostName = Environment.GetEnvironmentVariable("IOTEDGE_GATEWAYHOSTNAME");
|
||||
this.workloadApiVersion = Environment.GetEnvironmentVariable("IOTEDGE_APIVERSION");
|
||||
string workloadUriString = Environment.GetEnvironmentVariable("IOTEDGE_WORKLOADURI");
|
||||
|
||||
Validate.ArgumentNotNullOrEmpty(this.ModuleId, nameof(this.ModuleId));
|
||||
Validate.ArgumentNotNullOrEmpty(this.DeviceId, nameof(this.DeviceId));
|
||||
Validate.ArgumentNotNullOrEmpty(this.IotHubName, nameof(this.IotHubName));
|
||||
Validate.ArgumentNotNullOrEmpty(this.moduleGenerationId, nameof(this.moduleGenerationId));
|
||||
Validate.ArgumentNotNullOrEmpty(this.edgeGatewayHostName, nameof(this.edgeGatewayHostName));
|
||||
Validate.ArgumentNotNullOrEmpty(this.workloadApiVersion, nameof(this.workloadApiVersion));
|
||||
Validate.ArgumentNotNullOrEmpty(workloadUriString, nameof(workloadUriString));
|
||||
|
||||
var workloadUri = new Uri(workloadUriString);
|
||||
|
||||
string baseUrlForRequests;
|
||||
if (workloadUri.Scheme.Equals(SecurityDaemonClient.UnixScheme, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
baseUrlForRequests = $"http://{workloadUri.Segments.Last()}";
|
||||
this.httpClient = new HttpClient(new HttpUdsMessageHandler(workloadUri));
|
||||
}
|
||||
else if (workloadUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
|
||||
workloadUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
baseUrlForRequests = workloadUriString;
|
||||
this.httpClient = new HttpClient();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown workloadUri scheme specified. {workloadUri}");
|
||||
}
|
||||
|
||||
baseUrlForRequests = baseUrlForRequests.TrimEnd();
|
||||
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
string encodedApiVersion = UrlEncoder.Default.Encode(this.workloadApiVersion);
|
||||
string encodedModuleId = UrlEncoder.Default.Encode(this.ModuleId);
|
||||
string encodedModuleGenerationId = UrlEncoder.Default.Encode(this.moduleGenerationId);
|
||||
|
||||
this.getTrustBundleUri = new Uri($"{baseUrlForRequests}/trust-bundle?api-version={encodedApiVersion}");
|
||||
this.postIdentityCertificateRequestUri = new Uri($"{baseUrlForRequests}/modules/{encodedModuleId}/certificate/identity?api-version={encodedApiVersion}");
|
||||
this.postServerCertificateRequestUri = new Uri($"{baseUrlForRequests}/modules/{encodedModuleId}/genid/{encodedModuleGenerationId}/certificate/server?api-version={encodedApiVersion}");
|
||||
|
||||
var settings = new
|
||||
{
|
||||
this.ModuleId,
|
||||
this.DeviceId,
|
||||
IotHubHostName = iotHubHostName,
|
||||
ModuleGenerationId = this.moduleGenerationId,
|
||||
GatewayHostName = this.edgeGatewayHostName,
|
||||
WorkloadUri = workloadUriString,
|
||||
WorkloadApiVersion = this.workloadApiVersion,
|
||||
};
|
||||
this.asString = $"{nameof(SecurityDaemonClient)}{JsonConvert.SerializeObject(settings, Formatting.None, this.jsonSettings)}";
|
||||
}
|
||||
|
||||
public string IotHubName { get; }
|
||||
|
||||
public string DeviceId { get; }
|
||||
|
||||
public string ModuleId { get; }
|
||||
|
||||
public void Dispose() => this.httpClient.Dispose();
|
||||
|
||||
public override string ToString() => this.asString;
|
||||
|
||||
public Task<(X509Certificate2 serverCert, X509Certificate2[] certChain)> GetServerCertificateAsync(CancellationToken token = default)
|
||||
{
|
||||
return this.GetServerCertificateAsync(TimeSpan.FromDays(SecurityDaemonClient.DefaultServerCertificateValidityInDays), token);
|
||||
}
|
||||
|
||||
public async Task<(X509Certificate2 serverCert, X509Certificate2[] certChain)> GetServerCertificateAsync(TimeSpan validity, CancellationToken token = default)
|
||||
{
|
||||
var request = new ServerCertificateRequest
|
||||
{
|
||||
CommonName = this.edgeGatewayHostName,
|
||||
Expiration = DateTime.UtcNow.Add(validity),
|
||||
};
|
||||
|
||||
string requestString = JsonConvert.SerializeObject(request, Formatting.None, this.jsonSettings);
|
||||
using (var content = new StringContent(requestString, Encoding.UTF8, "application/json"))
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, this.postServerCertificateRequestUri) { Content = content })
|
||||
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
|
||||
{
|
||||
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responsePayload, this.jsonSettings);
|
||||
return this.CreateX509Certificates(cr);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to retrieve server certificate from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' Request={requestString} This={this}");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<(X509Certificate2 identityCert, X509Certificate2[] certChain)> GetIdentityCertificateAsync(CancellationToken token = default)
|
||||
{
|
||||
return this.GetIdentityCertificateAsync(TimeSpan.FromDays(SecurityDaemonClient.DefaultIdentityCertificateValidityInDays), token);
|
||||
}
|
||||
|
||||
public async Task<(X509Certificate2 identityCert, X509Certificate2[] certChain)> GetIdentityCertificateAsync(TimeSpan validity, CancellationToken token = default)
|
||||
{
|
||||
var request = new IdentityCertificateRequest
|
||||
{
|
||||
Expiration = DateTime.UtcNow.Add(validity),
|
||||
};
|
||||
|
||||
string requestString = JsonConvert.SerializeObject(request, Formatting.None, this.jsonSettings);
|
||||
using (var content = new StringContent(requestString, Encoding.UTF8, "application/json"))
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, this.postIdentityCertificateRequestUri) { Content = content })
|
||||
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
|
||||
{
|
||||
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responsePayload, this.jsonSettings);
|
||||
return this.CreateX509Certificates(cr);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to retrieve identity certificate from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' Request={requestString} This={this}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<X509Certificate2[]> GetTrustBundleAsync(CancellationToken token = default)
|
||||
{
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, this.getTrustBundleUri))
|
||||
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
|
||||
{
|
||||
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
|
||||
if (httpResponse.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
TrustBundleResponse trustBundleResponse = JsonConvert.DeserializeObject<TrustBundleResponse>(responsePayload, this.jsonSettings);
|
||||
Validate.ArgumentNotNullOrEmpty(trustBundleResponse.Certificate, nameof(trustBundleResponse.Certificate));
|
||||
|
||||
string[] rawCerts = ParseCertificateResponse(trustBundleResponse.Certificate);
|
||||
if (rawCerts.FirstOrDefault() == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to retrieve the certificate trust bundle from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' Reason='Security daemon returned an empty response' This={this}");
|
||||
}
|
||||
|
||||
return ConvertToX509(rawCerts);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to retrieve the certificate trust bundle from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' This={this}");
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate2[] ConvertToX509(IEnumerable<string> rawCerts) => rawCerts.Select(c => new X509Certificate2(Encoding.UTF8.GetBytes(c))).ToArray();
|
||||
|
||||
private static string[] ParseCertificateResponse(string certificateChain, [CallerMemberName] string callerMemberName = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(certificateChain))
|
||||
{
|
||||
throw new InvalidOperationException($"Trusted certificates can not be null or empty for {callerMemberName}.");
|
||||
}
|
||||
|
||||
// Extract each certificate's string. The final string from the split will either be empty
|
||||
// or a non-certificate entry, so it is dropped.
|
||||
string delimiter = "-----END CERTIFICATE-----";
|
||||
string[] rawCerts = certificateChain.Split(new[] { delimiter }, StringSplitOptions.None);
|
||||
return rawCerts.Take(count: rawCerts.Length - 1).Select(c => $"{c}{delimiter}").ToArray();
|
||||
}
|
||||
|
||||
private (X509Certificate2 primaryCert, X509Certificate2[] certChain) CreateX509Certificates(CertificateResponse cr, [CallerMemberName] string callerMemberName = default)
|
||||
{
|
||||
Validate.ArgumentNotNullOrEmpty(cr.Certificate, nameof(cr.Certificate));
|
||||
Validate.ArgumentNotNull(cr.Expiration, nameof(cr.Expiration));
|
||||
Validate.ArgumentNotNull(cr.PrivateKey, nameof(cr.PrivateKey));
|
||||
Validate.ArgumentNotNull(cr.PrivateKey.Type, nameof(cr.PrivateKey.Type));
|
||||
Validate.ArgumentNotNull(cr.PrivateKey.Bytes, nameof(cr.PrivateKey.Bytes));
|
||||
|
||||
string[] rawCerts = ParseCertificateResponse(cr.Certificate);
|
||||
if (rawCerts.Length == 0 ||
|
||||
string.IsNullOrWhiteSpace(rawCerts[0]))
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to retrieve certificate from IoTEdge Security daemon for {callerMemberName}. Reason: Security daemon returned an empty response.");
|
||||
}
|
||||
|
||||
string primaryCert = rawCerts[0];
|
||||
X509Certificate2[] certChain = ConvertToX509(rawCerts.Skip(1));
|
||||
|
||||
RsaPrivateCrtKeyParameters keyParams = null;
|
||||
|
||||
var chainCertEntries = new List<X509CertificateEntry>();
|
||||
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
|
||||
|
||||
// note: the seperator between the certificate and private key is added for safety to delineate the cert and key boundary
|
||||
using (var sr = new StringReader(primaryCert + "\r\n" + cr.PrivateKey.Bytes))
|
||||
{
|
||||
var pemReader = new PemReader(sr);
|
||||
object certObject;
|
||||
while ((certObject = pemReader.ReadObject()) != null)
|
||||
{
|
||||
if (certObject is Org.BouncyCastle.X509.X509Certificate x509Cert)
|
||||
{
|
||||
chainCertEntries.Add(new X509CertificateEntry(x509Cert));
|
||||
}
|
||||
|
||||
// when processing certificates generated via openssl certObject type is of AsymmetricCipherKeyPair
|
||||
if (certObject is AsymmetricCipherKeyPair ackp)
|
||||
{
|
||||
certObject = ackp.Private;
|
||||
}
|
||||
|
||||
if (certObject is RsaPrivateCrtKeyParameters rpckp)
|
||||
{
|
||||
keyParams = rpckp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (keyParams == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Private key was not found for {callerMemberName}");
|
||||
}
|
||||
|
||||
store.SetKeyEntry(this.ModuleId, new AsymmetricKeyEntry(keyParams), chainCertEntries.ToArray());
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
store.Save(ms, Array.Empty<char>(), new SecureRandom());
|
||||
var x509PrimaryCert = new X509Certificate2(ms.ToArray());
|
||||
return (x509PrimaryCert, certChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
[Obsolete("This is a pubternal API that's being made public as a stop-gap measure. It will be removed from the Event Grid SDK nuget package as soon IoT Edge SDK ships with a built-in a security daemon client.")]
|
||||
public class SecurityDaemonHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> callback;
|
||||
|
||||
public SecurityDaemonHttpClientFactory(X509Certificate2 identityCertificate)
|
||||
: this(identityCertificate, ServiceCertificateValidationCallback)
|
||||
{
|
||||
}
|
||||
|
||||
public SecurityDaemonHttpClientFactory(X509Certificate2 identityCertificate, Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCallback)
|
||||
{
|
||||
this.IdentityCertificate = identityCertificate;
|
||||
this.callback = serverCertificateCallback;
|
||||
}
|
||||
|
||||
public X509Certificate2 IdentityCertificate { get; }
|
||||
|
||||
public static async Task<SecurityDaemonHttpClientFactory> CreateAsync(CancellationToken token = default)
|
||||
{
|
||||
using (var iotEdgeClient = new SecurityDaemonClient())
|
||||
{
|
||||
(X509Certificate2 identityCertificate, _) = await iotEdgeClient.GetIdentityCertificateAsync(token);
|
||||
return new SecurityDaemonHttpClientFactory(identityCertificate);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2000: DisposeObjectsBeforeLosingScope", Justification = "The HttpClient owns the lifetime of the handler")]
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = this.callback };
|
||||
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||
httpClientHandler.ClientCertificates.Add(this.IdentityCertificate);
|
||||
return new HttpClient(httpClientHandler, disposeHandler: true);
|
||||
}
|
||||
|
||||
private static bool ServiceCertificateValidationCallback(HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors errors)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,16 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class HttpBufferedStream : Stream
|
||||
internal class HttpBufferedStream : Stream
|
||||
{
|
||||
private const char CR = '\r';
|
||||
private const char LF = '\n';
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -9,21 +11,24 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
public class HttpSerializer
|
||||
internal class HttpRequestResponseSerializer
|
||||
{
|
||||
private const char SP = ' ';
|
||||
private const char CR = '\r';
|
||||
private const char LF = '\n';
|
||||
private const char ProtocolVersionSeparator = '/';
|
||||
private const string Protocol = "HTTP";
|
||||
private const char HeaderSeparator = ':';
|
||||
private const string HeaderSeparator = ":";
|
||||
private const string ContentLengthHeaderName = "content-length";
|
||||
|
||||
public byte[] SerializeRequest(HttpRequestMessage request)
|
||||
{
|
||||
this.PreProcessRequest(request);
|
||||
Validate.ArgumentNotNull(request, nameof(request));
|
||||
Validate.ArgumentNotNull(request.RequestUri, nameof(request.RequestUri));
|
||||
|
||||
PreProcessRequest(request);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
// request-line = method SP request-target SP HTTP-version CRLF
|
||||
|
@ -60,14 +65,12 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
|||
public async Task<HttpResponseMessage> DeserializeResponseAsync(HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpResponse = new HttpResponseMessage();
|
||||
|
||||
await this.SetResponseStatusLineAsync(httpResponse, bufferedStream, cancellationToken);
|
||||
await this.SetHeadersAndContentAsync(httpResponse, bufferedStream, cancellationToken);
|
||||
|
||||
await SetResponseStatusLineAsync(httpResponse, bufferedStream, cancellationToken);
|
||||
await SetHeadersAndContentAsync(httpResponse, bufferedStream, cancellationToken);
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
private async Task SetHeadersAndContentAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||
private static async Task SetHeadersAndContentAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||
{
|
||||
IList<string> headers = new List<string>();
|
||||
string line = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||
|
@ -113,7 +116,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
|||
}
|
||||
}
|
||||
|
||||
private async Task SetResponseStatusLineAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||
private static async Task SetResponseStatusLineAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||
{
|
||||
string statusLine = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(statusLine))
|
||||
|
@ -144,7 +147,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
|||
httpResponse.ReasonPhrase = statusLineParts[2];
|
||||
}
|
||||
|
||||
private void PreProcessRequest(HttpRequestMessage request)
|
||||
private static void PreProcessRequest(HttpRequestMessage request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Headers.Host))
|
||||
{
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
@ -7,27 +9,30 @@ using System.Net.Sockets;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
/// <summary>
|
||||
/// Unix domain message handler.
|
||||
/// </summary>
|
||||
public class HttpUdsMessageHandler : HttpMessageHandler
|
||||
internal class HttpUdsMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Uri providerUri;
|
||||
|
||||
public HttpUdsMessageHandler(Uri providerUri)
|
||||
{
|
||||
Validate.ArgumentNotNull(providerUri, nameof(providerUri));
|
||||
this.providerUri = providerUri;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Validate.ArgumentNotNull(request, nameof(request));
|
||||
|
||||
using (Socket socket = await this.GetConnectedSocketAsync())
|
||||
{
|
||||
using (var stream = new HttpBufferedStream(new NetworkStream(socket, true)))
|
||||
{
|
||||
var serializer = new HttpSerializer();
|
||||
var serializer = new HttpRequestResponseSerializer();
|
||||
byte[] requestBytes = serializer.SerializeRequest(request);
|
||||
|
||||
await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken);
|
|
@ -0,0 +1,32 @@
|
|||
// -----------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||
{
|
||||
internal static class Validate
|
||||
{
|
||||
public static void ArgumentNotNull(object value, string paramName)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName, $"The argument {paramName} is null.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ArgumentNotNullOrEmpty(string value, string paramName)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName, $"The argument {paramName} is null.");
|
||||
}
|
||||
else if (value.Length == 0)
|
||||
{
|
||||
throw new ArgumentException(paramName, $"The argument {paramName} is empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ using Microsoft.Extensions.Configuration;
|
|||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.EventGridEdge.Samples.Common.Auth;
|
||||
using Microsoft.Azure.EventGridEdge.IotEdge;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
|
||||
{
|
||||
|
@ -164,8 +164,8 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
|
|||
|
||||
if (gridConfig.ClientAuth.Source.Equals("IoTEdge", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
IoTSecurity iotSecurity = new IoTSecurity();
|
||||
(X509Certificate2 identityCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetClientCertificateAsync();
|
||||
SecurityDaemonClient iotSecurity = new SecurityDaemonClient();
|
||||
(X509Certificate2 identityCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetIdentityCertificateAsync();
|
||||
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain.First(), identityCertificate));
|
||||
}
|
||||
else if (gridConfig.ClientAuth.Source.Equals("BearerToken", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Common\auth\Auth.csproj" />
|
||||
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
|
||||
<ProjectReference Include="..\..\..\SecurityDaemonClient\SecurityDaemonClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright(c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||
{
|
||||
public static class CertificateHelper
|
||||
{
|
||||
[SuppressMessage("Microsoft.Security", "CA5381: DoNotInstallRootCert", Justification = "We're in a docker container, there is no risk to the host machine.")]
|
||||
public static void ImportIntermediateCAs(params X509Certificate2[] certificates)
|
||||
{
|
||||
if (certificates != null)
|
||||
{
|
||||
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
|
||||
StoreLocation storeLocation = StoreLocation.CurrentUser;
|
||||
using (var store = new X509Store(storeName, storeLocation))
|
||||
{
|
||||
Console.WriteLine($"Importing certificate to StoreName:{storeName}, StoreLocation:{storeLocation}");
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
foreach (X509Certificate2 cert in certificates)
|
||||
{
|
||||
store.Add(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ImportCertificate(X509Certificate2 certificate)
|
||||
{
|
||||
if (certificate != null)
|
||||
{
|
||||
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.My : StoreName.Root;
|
||||
StoreLocation storeLocation = StoreLocation.CurrentUser;
|
||||
using (var store = new X509Store(storeName, storeLocation))
|
||||
{
|
||||
Console.WriteLine($"Importing certificate to StoreName:{storeName}, StoreLocation:{storeLocation}");
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
store.Add(certificate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsCACertificate(X509Certificate2 certificate)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
|
||||
// The keyCertSign bit is asserted when the subject public key is
|
||||
// used for verifying a signature on public key certificates. If the
|
||||
// keyCertSign bit is asserted, then the cA bit in the basic
|
||||
// constraints extension (section 4.2.1.10) MUST also be asserted.
|
||||
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
|
||||
// The cA boolean indicates whether the certified public key belongs to
|
||||
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
|
||||
// the key usage extension MUST NOT be asserted.
|
||||
X509ExtensionCollection extensionCollection = certificate.Extensions;
|
||||
foreach (X509Extension extension in extensionCollection)
|
||||
{
|
||||
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
|
||||
{
|
||||
if (basicConstraintExtension.CertificateAuthority)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
|
@ -12,8 +11,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.Azure.EventGridEdge.SDK;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using Microsoft.Azure.EventGridEdge.Samples.Common.Auth;
|
||||
using Microsoft.Azure.EventGridEdge.IotEdge;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||
{
|
||||
|
@ -85,18 +83,24 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
|
||||
private static async Task<SubscriberHost> SetupSubscriberHostAsync(CancellationTokenSource lifetimeCts)
|
||||
{
|
||||
IoTSecurity iotSecurity = new IoTSecurity();
|
||||
using var securityDaemonClient = new SecurityDaemonClient();
|
||||
|
||||
// get server certificate to configure with
|
||||
(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) =
|
||||
await iotSecurity.GetServerCertificateAsync().ConfigureAwait(false);
|
||||
iotSecurity.ImportCertificate(new List<X509Certificate2>() { serverCertificate });
|
||||
iotSecurity.ImportCertificate(certificateChain);
|
||||
Console.WriteLine($"Configure server certificate");
|
||||
(X509Certificate2 serverCert, X509Certificate2[] certChain) =
|
||||
await securityDaemonClient.GetServerCertificateAsync().ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine($"Server Certificate issue is valid from {serverCertificate.NotBefore}, {serverCertificate.NotAfter}");
|
||||
CertificateHelper.ImportCertificate(serverCert);
|
||||
CertificateHelper.ImportIntermediateCAs(serverCert);
|
||||
CertificateHelper.ImportIntermediateCAs(certChain);
|
||||
|
||||
// Configure client trust bundle
|
||||
Console.WriteLine($"Configure client trust bundle");
|
||||
var trustBundle = await securityDaemonClient.GetTrustBundleAsync();
|
||||
CertificateHelper.ImportIntermediateCAs(trustBundle);
|
||||
|
||||
// start subscriber webhost
|
||||
SubscriberHost host = new SubscriberHost(serverCertificate, lifetimeCts);
|
||||
SubscriberHost host = new SubscriberHost(serverCert, lifetimeCts);
|
||||
await host.StartAsync().ConfigureAwait(false);
|
||||
return host;
|
||||
}
|
||||
|
@ -140,10 +144,10 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
|
||||
private static async Task<EventGridEdgeClient> GetEventGridClientAsync(GridConfiguration gridConfig)
|
||||
{
|
||||
IoTSecurity iotSecurity = new IoTSecurity();
|
||||
using var securityDaemonClient = new SecurityDaemonClient();
|
||||
|
||||
// get the client certificate to use when communicating with eventgrid
|
||||
(X509Certificate2 clientCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetClientCertificateAsync().ConfigureAwait(false);
|
||||
(X509Certificate2 clientCertificate, X509Certificate2[] chain) = await securityDaemonClient.GetIdentityCertificateAsync().ConfigureAwait(false);
|
||||
Console.WriteLine($"Client Certificate issue is valid from {clientCertificate.NotBefore}, {clientCertificate.NotAfter}");
|
||||
string[] urlTokens = gridConfig.Url.Split(":");
|
||||
if (urlTokens.Length != 3)
|
||||
|
@ -154,7 +158,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
string baseUrl = urlTokens[0] + ":" + urlTokens[1];
|
||||
int port = int.Parse(urlTokens[2], CultureInfo.InvariantCulture);
|
||||
|
||||
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain.First(), clientCertificate));
|
||||
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain[0], clientCertificate));
|
||||
}
|
||||
|
||||
private static void LogAndBackoff(string topicName, Exception e)
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Common\auth\Auth.csproj" />
|
||||
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
|
||||
<ProjectReference Include="..\..\..\SecurityDaemonClient\SecurityDaemonClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Security;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
@ -35,12 +34,14 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// this is needed because IoTEdge generates a self signed certificate that is not rooted in a root certificate that is trusted by the trust provider.
|
||||
// Kestrel rejects the request automatically because of this. We return true here so that client validation can happen when routing requests.
|
||||
o.AllowAnyClientCertificate();
|
||||
o.CheckCertificateRevocation = false;
|
||||
o.ClientCertificateValidation = (X509Certificate2 arg1, X509Chain arg2, SslPolicyErrors arg3) =>
|
||||
|
||||
// this is needed because IoTEdge generates a self signed certificate that is not rooted in a root certificate that is trusted by the trust provider.
|
||||
// Kestrel rejects the request automatically because of this. We return true here so that client validation can happen when routing requests.
|
||||
o.ClientCertificateValidation = (cert, chain, errors) =>
|
||||
{
|
||||
Console.WriteLine("ClientCertValidation invoked");
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Azure.EventGridEdge.Samples.Common.Auth;
|
||||
using Microsoft.Azure.EventGridEdge.IotEdge;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||
|
@ -16,7 +20,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
public class HostStartup : IStartup
|
||||
{
|
||||
private readonly EventsHandler eventsHandler = new EventsHandler();
|
||||
private readonly IoTSecurity iotSecurity = new IoTSecurity();
|
||||
private readonly SecurityDaemonClient iotSecurity = new SecurityDaemonClient();
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
|
@ -44,7 +48,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
}
|
||||
|
||||
Console.WriteLine("Validating client certificate...");
|
||||
await iotSecurity.ValidateClientCertificateAsync(clientCert);
|
||||
await this.ValidateClientCertificateAsync(clientCert);
|
||||
}
|
||||
|
||||
// TODO: Verify it is eventgrid instance indeed!
|
||||
|
@ -56,5 +60,72 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
|||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateClientCertificateAsync(X509Certificate2 clientCertificate)
|
||||
{
|
||||
// Please add validation more validations as appropriate
|
||||
|
||||
if (this.IsCACertificate(clientCertificate))
|
||||
{
|
||||
throw new Exception("Cannot use CA certificate for client authentication!");
|
||||
}
|
||||
|
||||
IEnumerable<X509Certificate2> trustedCertificates = await this.iotSecurity.GetTrustBundleAsync();
|
||||
|
||||
using (X509Chain chain = new X509Chain())
|
||||
{
|
||||
foreach (X509Certificate2 trustedClientCert in trustedCertificates)
|
||||
{
|
||||
chain.ChainPolicy.ExtraStore.Add(trustedClientCert);
|
||||
}
|
||||
|
||||
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// IoTEdge generates a self-signed certificate by default, that is not rooted in a root certificate that is trusted by the trust provider hence this flag is needed
|
||||
// so that build returns true if root terminates in a self-signed certificate
|
||||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||
}
|
||||
|
||||
if (!chain.Build(clientCertificate))
|
||||
{
|
||||
var errorMessageBuilder = new StringBuilder();
|
||||
foreach (X509ChainStatus cs in chain.ChainStatus)
|
||||
{
|
||||
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
|
||||
errorMessageBuilder.AppendLine();
|
||||
}
|
||||
|
||||
throw new Exception($"ClientCertificate is not valid! Reason: Failed chain validation. Details: {errorMessageBuilder}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCACertificate(X509Certificate2 certificate)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
|
||||
// The keyCertSign bit is asserted when the subject public key is
|
||||
// used for verifying a signature on public key certificates. If the
|
||||
// keyCertSign bit is asserted, then the cA bit in the basic
|
||||
// constraints extension (section 4.2.1.10) MUST also be asserted.
|
||||
|
||||
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
|
||||
// The cA boolean indicates whether the certified public key belongs to
|
||||
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
|
||||
// the key usage extension MUST NOT be asserted.
|
||||
X509ExtensionCollection extensionCollection = certificate.Extensions;
|
||||
foreach (X509Extension extension in extensionCollection)
|
||||
{
|
||||
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
|
||||
{
|
||||
if (basicConstraintExtension.CertificateAuthority)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecurityDaemonClient", "Sec
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{34DA2417-E843-402A-BB71-D8385A8735F0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Auth", "Common\auth\Auth.csproj", "{4F4E7F1E-4488-4426-99D3-81B37310E0FA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -55,10 +53,6 @@ Global
|
|||
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -70,7 +64,6 @@ Global
|
|||
{3814D171-75B4-4324-8478-DBCCC9CBB5E3} = {1A29FBDF-CDBA-4BD0-9FE5-D40D95FF988D}
|
||||
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A} = {AEE9042C-0BC5-4302-9582-9AFA340EF555}
|
||||
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8} = {AEE9042C-0BC5-4302-9582-9AFA340EF555}
|
||||
{4F4E7F1E-4488-4426-99D3-81B37310E0FA} = {34DA2417-E843-402A-BB71-D8385A8735F0}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {475D2ECB-88BD-47E1-B351-D17F2ACF95B8}
|
||||
|
|
Загрузка…
Ссылка в новой задаче