Skip to content

Commit 4e8c729

Browse files
committed
Fix issue binding config involving certificates
1 parent 6a1e8b8 commit 4e8c729

File tree

5 files changed

+154
-138
lines changed

5 files changed

+154
-138
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Globalization;
2+
using System.Security.Cryptography;
3+
using System.Security.Cryptography.X509Certificates;
4+
5+
// ReSharper disable UnusedAutoPropertyAccessor.Global
6+
7+
namespace DotPulsar.Internal;
8+
9+
// Inspired from https://github.com/dotnet/aspnetcore/blob/449abac6f1ca12fa0ad557a872c219fcdfae09f3/src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs#L13
10+
11+
/// <summary>
12+
/// Represents a client certificate.
13+
/// </summary>
14+
public sealed class PulsarClientCertificate
15+
{
16+
/// <summary>
17+
/// The path to the certificate file.
18+
/// </summary>
19+
public string? Path {
20+
get;
21+
set;
22+
}
23+
24+
/// <summary>
25+
/// The path to the certificate key file.
26+
/// </summary>
27+
public string? KeyPath {
28+
get;
29+
set;
30+
}
31+
32+
/// <summary>
33+
/// The password for the certificate key file.
34+
/// </summary>
35+
public string? Password {
36+
get;
37+
init;
38+
}
39+
40+
public X509Certificate2? Load() {
41+
if (!string.IsNullOrEmpty(Path)) {
42+
var fullChain = new X509Certificate2Collection();
43+
fullChain.ImportFromPemFile(Path);
44+
45+
if (KeyPath != null) {
46+
#pragma warning disable CA2000
47+
var certificate = new X509Certificate2(Path);
48+
#pragma warning restore CA2000
49+
try {
50+
certificate = LoadCertificateKey(certificate, KeyPath, Password);
51+
} catch {
52+
certificate.Dispose();
53+
throw;
54+
}
55+
56+
if (OperatingSystem.IsWindows()) {
57+
try {
58+
certificate = PersistKey(certificate);
59+
} catch {
60+
certificate.Dispose();
61+
throw;
62+
}
63+
}
64+
65+
return certificate;
66+
}
67+
68+
return new X509Certificate2(Path, Password);
69+
}
70+
71+
return null;
72+
73+
static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string? password) {
74+
const string RSAOid = "1.2.840.113549.1.1.1";
75+
const string DSAOid = "1.2.840.10040.4.1";
76+
const string ECDsaOid = "1.2.840.10045.2.1";
77+
78+
var keyText = File.ReadAllText(keyPath);
79+
switch (certificate.PublicKey.Oid.Value) {
80+
case RSAOid: {
81+
using var rsa = RSA.Create();
82+
ImportKeyFromFile(rsa, keyText, password);
83+
return certificate.CopyWithPrivateKey(rsa);
84+
}
85+
case ECDsaOid: {
86+
using var ecdsa = ECDsa.Create();
87+
ImportKeyFromFile(ecdsa, keyText, password);
88+
return certificate.CopyWithPrivateKey(ecdsa);
89+
}
90+
case DSAOid: {
91+
using var dsa = DSA.Create();
92+
ImportKeyFromFile(dsa, keyText, password);
93+
return certificate.CopyWithPrivateKey(dsa);
94+
}
95+
default:
96+
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unknown algorithm for certificate with public key type '{0}'", certificate.PublicKey.Oid.Value));
97+
}
98+
}
99+
100+
static void ImportKeyFromFile(AsymmetricAlgorithm asymmetricAlgorithm, string keyText, string? password) {
101+
if (password == null) {
102+
asymmetricAlgorithm.ImportFromPem(keyText);
103+
} else {
104+
asymmetricAlgorithm.ImportFromEncryptedPem(keyText, password);
105+
}
106+
}
107+
108+
static X509Certificate2 PersistKey(X509Certificate2 fullCertificate) {
109+
// We need to force the key to be persisted.
110+
// See https://github.com/dotnet/runtime/issues/23749
111+
var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
112+
return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
113+
}
114+
}
115+
}

src/DotPulsar.Extensions.DependencyInjection/PulsarClientOptions.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
using System.Security.Cryptography.X509Certificates;
21
using DotPulsar.Abstractions;
2+
using DotPulsar.Internal;
3+
34
// ReSharper disable UnusedAutoPropertyAccessor.Global
45

56
namespace DotPulsar;
67

78
public class PulsarClientOptions
89
{
10+
internal const string SectionName = "Pulsar";
11+
912
/// <summary>
1013
/// The service URL for the Pulsar cluster. The default is "pulsar://localhost:6650".
1114
/// </summary>
@@ -17,7 +20,7 @@ public Uri? ServiceUrl {
1720
/// <summary>
1821
/// Authenticate using a client certificate. This is optional.
1922
/// </summary>
20-
public X509Certificate2? AuthenticateUsingClientCertificate {
23+
public PulsarClientCertificate? AuthenticateUsingClientCertificate {
2124
get;
2225
set;
2326
}
@@ -73,7 +76,7 @@ public TimeSpan? RetryInterval {
7376
/// <summary>
7477
/// Add a trusted certificate authority. This is optional.
7578
/// </summary>
76-
public X509Certificate2? TrustedCertificateAuthority {
79+
public PulsarClientCertificate? TrustedCertificateAuthority {
7780
get;
7881
set;
7982
}
@@ -109,8 +112,11 @@ public void Apply(IPulsarClientBuilder builder) {
109112
builder.ServiceUrl(ServiceUrl);
110113
}
111114

112-
if (AuthenticateUsingClientCertificate != null) {
113-
builder.AuthenticateUsingClientCertificate(AuthenticateUsingClientCertificate);
115+
#pragma warning disable CA2000
116+
var authenticateUsingClientCertificate = AuthenticateUsingClientCertificate?.Load();
117+
#pragma warning restore CA2000
118+
if (authenticateUsingClientCertificate != null) {
119+
builder.AuthenticateUsingClientCertificate(authenticateUsingClientCertificate);
114120
builder.ConnectionSecurity(EncryptionPolicy.PreferEncrypted);
115121
}
116122

@@ -139,8 +145,11 @@ public void Apply(IPulsarClientBuilder builder) {
139145
builder.RetryInterval(RetryInterval.Value);
140146
}
141147

142-
if (TrustedCertificateAuthority != null) {
143-
builder.TrustedCertificateAuthority(TrustedCertificateAuthority);
148+
#pragma warning disable CA2000
149+
var trustedCertificateAuthority = TrustedCertificateAuthority?.Load();
150+
#pragma warning restore CA2000
151+
if (trustedCertificateAuthority != null) {
152+
builder.TrustedCertificateAuthority(trustedCertificateAuthority);
144153
}
145154

146155
if (VerifyCertificateAuthority != null) {
Lines changed: 21 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using System.Globalization;
2-
using System.Security.Cryptography;
3-
using System.Security.Cryptography.X509Certificates;
41
using DotPulsar;
52
using DotPulsar.Abstractions;
63
using Microsoft.Extensions.Configuration;
@@ -12,6 +9,16 @@ namespace Microsoft.Extensions.DependencyInjection;
129

1310
public static class PulsarServiceCollectionExtensions
1411
{
12+
public static IServiceCollection AddPulsarOptions(this IServiceCollection services, string sectionName = PulsarClientOptions.SectionName) {
13+
services.AddOptions<PulsarClientOptions>().BindConfiguration(sectionName);
14+
return services;
15+
}
16+
17+
public static IServiceCollection AddPulsarOptions(this IServiceCollection services, IConfigurationSection pulsarSection) {
18+
services.AddOptions<PulsarClientOptions>().Bind(pulsarSection);
19+
return services;
20+
}
21+
1522
public static IServiceCollection AddPulsarClient(this IServiceCollection services, IPulsarClient pulsarClient) {
1623
services.TryAddSingleton(pulsarClient);
1724
return services;
@@ -22,29 +29,18 @@ public static IServiceCollection AddPulsarClient(this IServiceCollection service
2229
return services;
2330
}
2431

25-
public static IServiceCollection AddPulsarClient(this IServiceCollection services, IPulsarClientBuilder pulsarClientBuilder) {
26-
ArgumentNullException.ThrowIfNull(pulsarClientBuilder);
27-
return AddPulsarClient(services, pulsarClientBuilder.Build());
28-
}
29-
30-
public static IServiceCollection AddPulsarClient(this IServiceCollection services) {
31-
services.AddOptions<PulsarClientOptions>().Configure<IConfiguration>(static (opts, config) => BindOptions(opts, config.GetSection("Pulsar")));
32-
return AddPulsarClient(services, static (sp, builder) => {
33-
var options = sp.GetRequiredService<IOptions<PulsarClientOptions>>().Value;
34-
options.Apply(builder);
32+
public static IServiceCollection AddPulsarClient(this IServiceCollection services, PulsarClientOptions? options = null) {
33+
return AddPulsarClient(services, (sp, builder) => {
34+
if (options == null) {
35+
options = sp.GetService<IOptions<PulsarClientOptions>>()?.Value;
36+
}
37+
builder.Configure(options);
3538
});
3639
}
3740

38-
public static IServiceCollection AddPulsarClient(this IServiceCollection services, IConfigurationSection pulsarSection) {
39-
return AddPulsarClient(services, opts => BindOptions(opts, pulsarSection));
40-
}
41-
4241
public static IServiceCollection AddPulsarClient(this IServiceCollection services, Action<PulsarClientOptions> configure) {
4342
services.AddOptions<PulsarClientOptions>().Configure(configure);
44-
return AddPulsarClient(services, static (sp, builder) => {
45-
var options = sp.GetRequiredService<IOptions<PulsarClientOptions>>().Value;
46-
options.Apply(builder);
47-
});
43+
return AddPulsarClient(services);
4844
}
4945

5046
public static IServiceCollection AddPulsarClient(this IServiceCollection services, Action<IPulsarClientBuilder> configure) {
@@ -60,112 +56,9 @@ public static IServiceCollection AddPulsarClient(this IServiceCollection service
6056
});
6157
}
6258

63-
private static void BindOptions(this PulsarClientOptions options, IConfiguration configuration) {
64-
configuration.Bind(options);
65-
66-
options.AuthenticateUsingClientCertificate = BindCertificate(configuration.GetSection(nameof(PulsarClientOptions.AuthenticateUsingClientCertificate)));
67-
options.TrustedCertificateAuthority = BindCertificate(configuration.GetSection(nameof(PulsarClientOptions.TrustedCertificateAuthority)));
68-
69-
static X509Certificate2? BindCertificate(IConfigurationSection configSection) => new CertificateConfig(configSection).LoadCertificate();
70-
}
71-
72-
// Inspired from https://github.com/dotnet/aspnetcore/blob/449abac6f1ca12fa0ad557a872c219fcdfae09f3/src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs#L13
73-
private sealed class CertificateConfig
74-
{
75-
public CertificateConfig(IConfiguration configSection) {
76-
Path = configSection[nameof(Path)];
77-
KeyPath = configSection[nameof(KeyPath)];
78-
Password = configSection[nameof(Password)];
79-
}
80-
81-
public string? Path {
82-
get;
83-
init;
84-
}
85-
86-
public string? KeyPath {
87-
get;
88-
init;
89-
}
90-
91-
public string? Password {
92-
get;
93-
init;
94-
}
95-
96-
public X509Certificate2? LoadCertificate() {
97-
if (!string.IsNullOrEmpty(Path)) {
98-
var fullChain = new X509Certificate2Collection();
99-
fullChain.ImportFromPemFile(Path);
100-
101-
if (KeyPath != null) {
102-
#pragma warning disable CA2000
103-
var certificate = new X509Certificate2(Path);
104-
#pragma warning restore CA2000
105-
try {
106-
certificate = LoadCertificateKey(certificate, KeyPath, Password);
107-
} catch {
108-
certificate.Dispose();
109-
throw;
110-
}
111-
112-
if (OperatingSystem.IsWindows()) {
113-
try {
114-
certificate = PersistKey(certificate);
115-
} catch {
116-
certificate.Dispose();
117-
throw;
118-
}
119-
}
120-
return certificate;
121-
}
122-
123-
return new X509Certificate2(Path, Password);
124-
}
125-
126-
return null;
127-
128-
static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string? password) {
129-
const string RSAOid = "1.2.840.113549.1.1.1";
130-
const string DSAOid = "1.2.840.10040.4.1";
131-
const string ECDsaOid = "1.2.840.10045.2.1";
132-
133-
var keyText = File.ReadAllText(keyPath);
134-
switch (certificate.PublicKey.Oid.Value) {
135-
case RSAOid: {
136-
using var rsa = RSA.Create();
137-
ImportKeyFromFile(rsa, keyText, password);
138-
return certificate.CopyWithPrivateKey(rsa);
139-
}
140-
case ECDsaOid: {
141-
using var ecdsa = ECDsa.Create();
142-
ImportKeyFromFile(ecdsa, keyText, password);
143-
return certificate.CopyWithPrivateKey(ecdsa);
144-
}
145-
case DSAOid: {
146-
using var dsa = DSA.Create();
147-
ImportKeyFromFile(dsa, keyText, password);
148-
return certificate.CopyWithPrivateKey(dsa);
149-
}
150-
default:
151-
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unknown algorithm for certificate with public key type '{0}'", certificate.PublicKey.Oid.Value));
152-
}
153-
}
154-
155-
static void ImportKeyFromFile(AsymmetricAlgorithm asymmetricAlgorithm, string keyText, string? password) {
156-
if (password == null) {
157-
asymmetricAlgorithm.ImportFromPem(keyText);
158-
} else {
159-
asymmetricAlgorithm.ImportFromEncryptedPem(keyText, password);
160-
}
161-
}
162-
163-
static X509Certificate2 PersistKey(X509Certificate2 fullCertificate) {
164-
// We need to force the key to be persisted.
165-
// See https://github.com/dotnet/runtime/issues/23749
166-
var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
167-
return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
168-
}
169-
}
59+
public static IPulsarClientBuilder Configure(this IPulsarClientBuilder pulsarClientBuilder, PulsarClientOptions? options) {
60+
ArgumentNullException.ThrowIfNull(pulsarClientBuilder);
61+
options?.Apply(pulsarClientBuilder);
62+
return pulsarClientBuilder;
17063
}
17164
}

src/DotPulsar.Extensions.DependencyInjection/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dotnet add package DotPulsar.Extensions.DependencyInjection
1414
## Usage
1515

1616
```c#
17-
services.AddPulsarClient();
17+
services.AddPulsarOptions().AddPulsarClient();
1818
```
1919

2020
This will register the `IPulsarClient` as a singleton in the service collection with the default settings. The settings can be configured using the standard `appSettings.json` file with the following configuration keys supported.

test/DotPulsar.Extensions.DependencyInjection.Tests/PulsarServiceCollectionExtensionsTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.ComponentModel;
2-
using System.Security.Cryptography.X509Certificates;
31
using DotPulsar.Abstractions;
42
using Microsoft.Extensions.Configuration;
53
using Microsoft.Extensions.DependencyInjection;
@@ -16,6 +14,7 @@ public async Task can_configure_pulsar_client() {
1614
.AddInMemoryCollection(new[] {
1715
new KeyValuePair<string, string?>("Pulsar:ServiceUrl", serviceUrl.ToString())
1816
}).Build())
17+
.AddPulsarOptions()
1918
.AddPulsarClient()
2019
.BuildServiceProvider();
2120
var client = services.GetRequiredService<IPulsarClient>();

0 commit comments

Comments
 (0)