-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBclCryptoProvider.cs
More file actions
247 lines (219 loc) · 8.89 KB
/
Copy pathBclCryptoProvider.cs
File metadata and controls
247 lines (219 loc) · 8.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
using System.Security.Cryptography;
namespace PostQuantum.FileFormat.Crypto;
/// <summary>
/// Crypto provider that goes directly to the .NET 10 BCL native PQC
/// primitives (<c>System.Security.Cryptography.MLKem</c> /
/// <c>System.Security.Cryptography.MLDsa</c>) for ML-KEM-768 and
/// ML-DSA-87, and falls back to BouncyCastle for everything else
/// (X25519, Ed25519, and the FIPS 204 deterministic-signing path which
/// the BCL does not yet expose).
///
/// The previous reflection bridge that reached these types from a net8.0
/// build was removed in the same change that dropped the net8.0 target
/// frame. The decision rationale: no consumer ships against net8.0, the
/// reflection layer was strictly worse than a compile-time reference for
/// every reviewer who looked at it, and keeping it preserved the weakest
/// side-channel link in the project for no current upside.
///
/// Why the BCL native path matters for the side-channel posture:
/// BouncyCastle's managed ML-KEM and ML-DSA implementations are not
/// written to be constant-time; the BCL implementations are platform-
/// backed (Linux OpenSSL 3.5+ via libcrypto; Windows CNG on 11 /
/// Server 2025) and are expected to provide stronger constant-time
/// guarantees on the platforms where
/// <c>System.Security.Cryptography.MLKem.IsSupported</c> is true. See
/// <c>docs/SIDE-CHANNEL-POSTURE.md</c> for the full discussion.
/// </summary>
public sealed class BclCryptoProvider : ICryptoProvider
{
private readonly BouncyCastleCryptoProvider _fallback = new();
internal void SetInjectableRandomness(TestSupport.InjectableRandomness? randomness)
{
_fallback.SetInjectableRandomness(randomness);
}
public static bool IsSupported => BclMlKemBridge.MlKem768Supported || BclMlDsaBridge.MlDsa87Supported;
public static bool MlKem768UsesBcl => BclMlKemBridge.MlKem768Supported;
public static bool MlDsa87UsesBcl => BclMlDsaBridge.MlDsa87Supported;
public (byte[] sk, byte[] pk) X25519GenerateKeyPair() => _fallback.X25519GenerateKeyPair();
public byte[] X25519DeriveSharedSecret(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> peerPk) =>
_fallback.X25519DeriveSharedSecret(sk, peerPk);
public (byte[] sk, byte[] pk) MlKem768GenerateKeyPair() =>
BclMlKemBridge.MlKem768Supported
? BclMlKemBridge.GenerateKeyPair()
: _fallback.MlKem768GenerateKeyPair();
public (byte[] sharedSecret, byte[] ciphertext) MlKem768Encapsulate(ReadOnlySpan<byte> peerPk) =>
BclMlKemBridge.MlKem768Supported
? BclMlKemBridge.Encapsulate(peerPk)
: _fallback.MlKem768Encapsulate(peerPk);
public byte[] MlKem768Decapsulate(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> ciphertext) =>
BclMlKemBridge.MlKem768Supported
? BclMlKemBridge.Decapsulate(sk, ciphertext)
: _fallback.MlKem768Decapsulate(sk, ciphertext);
public (byte[] sk, byte[] pk) Ed25519GenerateKeyPair() => _fallback.Ed25519GenerateKeyPair();
public byte[] Ed25519Sign(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> message) =>
_fallback.Ed25519Sign(sk, message);
public bool Ed25519Verify(ReadOnlySpan<byte> pk, ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature) =>
_fallback.Ed25519Verify(pk, message, signature);
public (byte[] sk, byte[] pk) MlDsa87GenerateKeyPair() =>
BclMlDsaBridge.MlDsa87Supported
? BclMlDsaBridge.GenerateKeyPair()
: _fallback.MlDsa87GenerateKeyPair();
public byte[] MlDsa87Sign(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> message) =>
BclMlDsaBridge.MlDsa87Supported
? BclMlDsaBridge.Sign(sk, message)
: _fallback.MlDsa87Sign(sk, message);
// Deterministic FIPS 204 signing path. The .NET BCL MLDsa surface does
// not currently expose an "rnd = 0" knob in a stable API, so we always
// delegate to the BouncyCastle fallback for this path. Deterministic
// signing is used for vector regeneration and explicit-repeatability
// callers; both are fine with the BC backend.
public byte[] MlDsa87SignDeterministic(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> message) =>
_fallback.MlDsa87SignDeterministic(sk, message);
public bool MlDsa87Verify(ReadOnlySpan<byte> pk, ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature) =>
BclMlDsaBridge.MlDsa87Supported
? BclMlDsaBridge.Verify(pk, message, signature)
: _fallback.MlDsa87Verify(pk, message, signature);
}
/// <summary>
/// Thin compile-time wrapper around
/// <c>System.Security.Cryptography.MLKem</c> (ML-KEM-768).
/// </summary>
internal static class BclMlKemBridge
{
public static bool MlKem768Supported { get; } = ProbeSupported();
private static bool ProbeSupported()
{
if (!MLKem.IsSupported)
{
return false;
}
// Liveness probe: actually generate-and-dispose a 768 key. Catches
// the case where MLKem is exposed at compile time but the platform
// provider does not support the parameter set at runtime.
try
{
using var _ = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
return true;
}
catch (PlatformNotSupportedException)
{
return false;
}
catch (CryptographicException)
{
return false;
}
}
public static (byte[] sk, byte[] pk) GenerateKeyPair()
{
using var instance = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
var pk = instance.ExportEncapsulationKey();
var sk = instance.ExportDecapsulationKey();
return (sk, pk);
}
public static (byte[] sharedSecret, byte[] ciphertext) Encapsulate(ReadOnlySpan<byte> peerPk)
{
var pkBytes = peerPk.ToArray();
using var instance = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, pkBytes);
try
{
var ct = new byte[MLKemAlgorithm.MLKem768.CiphertextSizeInBytes];
var ss = new byte[MLKemAlgorithm.MLKem768.SharedSecretSizeInBytes];
instance.Encapsulate(ct, ss);
return (ss, ct);
}
finally
{
SecureZero.Clear(pkBytes);
}
}
public static byte[] Decapsulate(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> ciphertext)
{
var skBytes = sk.ToArray();
var ctBytes = ciphertext.ToArray();
try
{
using var instance = MLKem.ImportDecapsulationKey(MLKemAlgorithm.MLKem768, skBytes);
return instance.Decapsulate(ctBytes);
}
finally
{
SecureZero.Clear(skBytes);
SecureZero.Clear(ctBytes);
}
}
}
/// <summary>
/// Thin compile-time wrapper around
/// <c>System.Security.Cryptography.MLDsa</c> (ML-DSA-87).
/// </summary>
internal static class BclMlDsaBridge
{
public static bool MlDsa87Supported { get; } = ProbeSupported();
private static bool ProbeSupported()
{
if (!MLDsa.IsSupported)
{
return false;
}
try
{
using var _ = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87);
return true;
}
catch (PlatformNotSupportedException)
{
return false;
}
catch (CryptographicException)
{
return false;
}
}
public static (byte[] sk, byte[] pk) GenerateKeyPair()
{
using var instance = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87);
var pk = instance.ExportMLDsaPublicKey();
var sk = instance.ExportMLDsaPrivateKey();
return (sk, pk);
}
public static byte[] Sign(ReadOnlySpan<byte> sk, ReadOnlySpan<byte> message)
{
var skBytes = sk.ToArray();
var msgBytes = message.ToArray();
try
{
using var instance = MLDsa.ImportMLDsaPrivateKey(MLDsaAlgorithm.MLDsa87, skBytes);
// Empty context per the PQF spec (ML-DSA used without a context string).
return instance.SignData(msgBytes, Array.Empty<byte>());
}
finally
{
SecureZero.Clear(skBytes);
SecureZero.Clear(msgBytes);
}
}
public static bool Verify(ReadOnlySpan<byte> pk, ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature)
{
var pkBytes = pk.ToArray();
var msgBytes = message.ToArray();
var sigBytes = signature.ToArray();
try
{
using var instance = MLDsa.ImportMLDsaPublicKey(MLDsaAlgorithm.MLDsa87, pkBytes);
return instance.VerifyData(msgBytes, sigBytes, Array.Empty<byte>());
}
catch (CryptographicException)
{
// BCL throws on malformed inputs; treat as verify-fail to match
// the BouncyCastle path's swallow-and-return-false behaviour.
return false;
}
finally
{
SecureZero.Clear(pkBytes);
SecureZero.Clear(msgBytes);
SecureZero.Clear(sigBytes);
}
}
}