Skip to content

Commit cedae37

Browse files
fpseverinoLukasa
andauthored
Add ML-DSA post-quantum signatures to _CryptoExtras (#267)
Add support for ML-DSA post-quantum digital signatures inside `_CryptoExtras`. ### Motivation: With the advent of quantum computing, the mathematical foundations on which the cryptographic protocols in use today are based have been questioned, as they can easily be circumvented and violated by quantum computers. While waiting for the creation of quantum computers that work at full capacity, and to protect network communications from "[Harvest Now, Decrypt Later](https://en.wikipedia.org/wiki/Harvest_now,_decrypt_later)" attacks, the cryptographic community is working on post-quantum cryptography algorithms, which work on the traditional computers we use today, but are resistant to future attacks by quantum computers. One of these algorithms is ML-DSA (AKA Dilithium), a module lattice-based signature scheme standardized by NIST in [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final), that is available inside BoringSSL. By including ML-DSA inside Swift Crypto, we can get closer to normalizing quantum secure algorithms and start implementing them into our apps and libraries to make them quantum-proof. ### Modifications: Added a `MLDSA65` enum inside the `_CryptoExtras` module with corresponding `PrivateKey`, `PublicKey` and `Signature` structs that use BoringSSL methods to produce and verify ML-DSA-65 digital signatures, with the code style of other signature schemes in the library. Added tests that cover use cases of the ML-DSA scheme, including [test vectors taken from the BoringSSL repo](https://boringssl.googlesource.com/boringssl/+/refs/heads/master/crypto/mldsa/mldsa_nist_keygen_tests.txt) (extracted from a `.txt` file and encoded in JSON). ### Result: ML-DSA-65 digital signatures can be created and verified with Swift Crypto. --------- Co-authored-by: Cory Benfield <[email protected]>
1 parent 0411996 commit cedae37

File tree

9 files changed

+1715
-0
lines changed

9 files changed

+1715
-0
lines changed

Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "CCryptoBoringSSL_hrss.h"
4545
#include "CCryptoBoringSSL_md4.h"
4646
#include "CCryptoBoringSSL_md5.h"
47+
#include "CCryptoBoringSSL_mldsa.h"
4748
#include "CCryptoBoringSSL_obj_mac.h"
4849
#include "CCryptoBoringSSL_objects.h"
4950
#include "CCryptoBoringSSL_opensslv.h"

Sources/_CryptoExtras/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_library(_CryptoExtras
4141
"Key Derivation/PBKDF2/PBKDF2.swift"
4242
"Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift"
4343
"Key Derivation/Scrypt/Scrypt.swift"
44+
"MLDSA/MLDSA65_boring.swift"
4445
"OPRFs/OPRF.swift"
4546
"OPRFs/OPRFClient.swift"
4647
"OPRFs/OPRFServer.swift"
@@ -58,6 +59,7 @@ add_library(_CryptoExtras
5859
"Util/Error.swift"
5960
"Util/I2OSP.swift"
6061
"Util/IntegerEncoding.swift"
62+
"Util/Optional+withUnsafeBytes.swift"
6163
"Util/PEMDocument.swift"
6264
"Util/PrettyBytes.swift"
6365
"Util/SubjectPublicKeyInfo.swift"

Sources/_CryptoExtras/Docs.docc/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Provides additional cryptographic APIs that are not available in CryptoKit (and
1515
### Public key cryptography
1616

1717
- ``_RSA``
18+
- ``MLDSA65``
1819

1920
### Key derivation functions
2021

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@_implementationOnly import CCryptoBoringSSL
16+
import Crypto
17+
import Foundation
18+
19+
/// A module-lattice-based digital signature algorithm that provides security against quantum computing attacks.
20+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
21+
public enum MLDSA65 {}
22+
23+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
24+
extension MLDSA65 {
25+
/// A ML-DSA-65 private key.
26+
public struct PrivateKey: Sendable {
27+
private var backing: Backing
28+
29+
/// Initialize a ML-DSA-65 private key from a random seed.
30+
public init() throws {
31+
self.backing = try Backing()
32+
}
33+
34+
/// Initialize a ML-DSA-65 private key from a seed.
35+
///
36+
/// - Parameter seedRepresentation: The seed to use to generate the private key.
37+
///
38+
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long.
39+
public init(seedRepresentation: some DataProtocol) throws {
40+
self.backing = try Backing(seedRepresentation: seedRepresentation)
41+
}
42+
43+
/// The seed from which this private key was generated.
44+
public var seedRepresentation: Data {
45+
self.backing.seed
46+
}
47+
48+
/// The public key associated with this private key.
49+
public var publicKey: PublicKey {
50+
self.backing.publicKey
51+
}
52+
53+
/// Generate a signature for the given data.
54+
///
55+
/// - Parameter data: The message to sign.
56+
///
57+
/// - Returns: The signature of the message.
58+
public func signature<D: DataProtocol>(for data: D) throws -> Data {
59+
let context: Data? = nil
60+
return try self.backing.signature(for: data, context: context)
61+
}
62+
63+
/// Generate a signature for the given data.
64+
///
65+
/// - Parameters:
66+
/// - data: The message to sign.
67+
/// - context: The context to use for the signature.
68+
///
69+
/// - Returns: The signature of the message.
70+
public func signature<D: DataProtocol, C: DataProtocol>(for data: D, context: C) throws -> Data {
71+
try self.backing.signature(for: data, context: context)
72+
}
73+
74+
/// The size of the private key in bytes.
75+
static let byteCount = Backing.byteCount
76+
77+
fileprivate final class Backing {
78+
fileprivate var key: MLDSA65_private_key
79+
var seed: Data
80+
81+
/// Initialize a ML-DSA-65 private key from a random seed.
82+
init() throws {
83+
// We have to initialize all members before `self` is captured by the closure
84+
self.key = .init()
85+
self.seed = Data()
86+
87+
self.seed = try withUnsafeTemporaryAllocation(
88+
of: UInt8.self,
89+
capacity: MLDSA65.seedByteCount
90+
) { seedPtr in
91+
try withUnsafeTemporaryAllocation(
92+
of: UInt8.self,
93+
capacity: MLDSA65.PublicKey.Backing.byteCount
94+
) { publicKeyPtr in
95+
guard
96+
CCryptoBoringSSL_MLDSA65_generate_key(
97+
publicKeyPtr.baseAddress,
98+
seedPtr.baseAddress,
99+
&self.key
100+
) == 1
101+
else {
102+
throw CryptoKitError.internalBoringSSLError()
103+
}
104+
105+
return Data(bytes: seedPtr.baseAddress!, count: MLDSA65.seedByteCount)
106+
}
107+
}
108+
}
109+
110+
/// Initialize a ML-DSA-65 private key from a seed.
111+
///
112+
/// - Parameter seedRepresentation: The seed to use to generate the private key.
113+
///
114+
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long.
115+
init(seedRepresentation: some DataProtocol) throws {
116+
guard seedRepresentation.count == MLDSA65.seedByteCount else {
117+
throw CryptoKitError.incorrectKeySize
118+
}
119+
120+
self.key = .init()
121+
self.seed = Data(seedRepresentation)
122+
123+
guard
124+
self.seed.withUnsafeBytes({ seedPtr in
125+
CCryptoBoringSSL_MLDSA65_private_key_from_seed(
126+
&self.key,
127+
seedPtr.baseAddress,
128+
MLDSA65.seedByteCount
129+
)
130+
}) == 1
131+
else {
132+
throw CryptoKitError.internalBoringSSLError()
133+
}
134+
}
135+
136+
/// The public key associated with this private key.
137+
var publicKey: PublicKey {
138+
PublicKey(privateKeyBacking: self)
139+
}
140+
141+
/// Generate a signature for the given data.
142+
///
143+
/// - Parameters:
144+
/// - data: The message to sign.
145+
/// - context: The context to use for the signature.
146+
///
147+
/// - Returns: The signature of the message.
148+
func signature<D: DataProtocol, C: DataProtocol>(for data: D, context: C?) throws -> Data {
149+
var signature = Data(repeating: 0, count: MLDSA65.signatureByteCount)
150+
151+
let rc: CInt = signature.withUnsafeMutableBytes { signaturePtr in
152+
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
153+
return bytes.withUnsafeBytes { dataPtr in
154+
context.withUnsafeBytes { contextPtr in
155+
CCryptoBoringSSL_MLDSA65_sign(
156+
signaturePtr.baseAddress,
157+
&self.key,
158+
dataPtr.baseAddress,
159+
dataPtr.count,
160+
contextPtr.baseAddress,
161+
contextPtr.count
162+
)
163+
}
164+
}
165+
}
166+
167+
guard rc == 1 else {
168+
throw CryptoKitError.internalBoringSSLError()
169+
}
170+
171+
return signature
172+
}
173+
174+
/// The size of the private key in bytes.
175+
static let byteCount = 4032
176+
}
177+
}
178+
}
179+
180+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
181+
extension MLDSA65 {
182+
/// A ML-DSA-65 public key.
183+
public struct PublicKey: Sendable {
184+
private var backing: Backing
185+
186+
fileprivate init(privateKeyBacking: PrivateKey.Backing) {
187+
self.backing = Backing(privateKeyBacking: privateKeyBacking)
188+
}
189+
190+
/// Initialize a ML-DSA-65 public key from a raw representation.
191+
///
192+
/// - Parameter rawRepresentation: The public key bytes.
193+
///
194+
/// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size.
195+
public init(rawRepresentation: some DataProtocol) throws {
196+
self.backing = try Backing(rawRepresentation: rawRepresentation)
197+
}
198+
199+
/// The raw binary representation of the public key.
200+
public var rawRepresentation: Data {
201+
self.backing.rawRepresentation
202+
}
203+
204+
/// Verify a signature for the given data.
205+
///
206+
/// - Parameters:
207+
/// - signature: The signature to verify.
208+
/// - data: The message to verify the signature against.
209+
///
210+
/// - Returns: `true` if the signature is valid, `false` otherwise.
211+
public func isValidSignature<S: DataProtocol, D: DataProtocol>(_ signature: S, for data: D) -> Bool {
212+
let context: Data? = nil
213+
return self.backing.isValidSignature(signature, for: data, context: context)
214+
}
215+
216+
/// Verify a signature for the given data.
217+
///
218+
/// - Parameters:
219+
/// - signature: The signature to verify.
220+
/// - data: The message to verify the signature against.
221+
/// - context: The context to use for the signature verification.
222+
///
223+
/// - Returns: `true` if the signature is valid, `false` otherwise.
224+
public func isValidSignature<S: DataProtocol, D: DataProtocol, C: DataProtocol>(
225+
_ signature: S,
226+
for data: D,
227+
context: C
228+
) -> Bool {
229+
self.backing.isValidSignature(signature, for: data, context: context)
230+
}
231+
232+
/// The size of the public key in bytes.
233+
static let byteCount = Backing.byteCount
234+
235+
fileprivate final class Backing {
236+
private var key: MLDSA65_public_key
237+
238+
init(privateKeyBacking: PrivateKey.Backing) {
239+
self.key = .init()
240+
CCryptoBoringSSL_MLDSA65_public_from_private(&self.key, &privateKeyBacking.key)
241+
}
242+
243+
/// Initialize a ML-DSA-65 public key from a raw representation.
244+
///
245+
/// - Parameter rawRepresentation: The public key bytes.
246+
///
247+
/// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size.
248+
init(rawRepresentation: some DataProtocol) throws {
249+
guard rawRepresentation.count == MLDSA65.PublicKey.Backing.byteCount else {
250+
throw CryptoKitError.incorrectKeySize
251+
}
252+
253+
self.key = .init()
254+
255+
let bytes: ContiguousBytes =
256+
rawRepresentation.regions.count == 1
257+
? rawRepresentation.regions.first!
258+
: Array(rawRepresentation)
259+
try bytes.withUnsafeBytes { rawBuffer in
260+
try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in
261+
var cbs = CBS(data: buffer.baseAddress, len: buffer.count)
262+
guard CCryptoBoringSSL_MLDSA65_parse_public_key(&self.key, &cbs) == 1 else {
263+
throw CryptoKitError.internalBoringSSLError()
264+
}
265+
}
266+
}
267+
}
268+
269+
/// The raw binary representation of the public key.
270+
var rawRepresentation: Data {
271+
var cbb = CBB()
272+
// The following BoringSSL functions can only fail on allocation failure, which we define as impossible.
273+
CCryptoBoringSSL_CBB_init(&cbb, MLDSA65.PublicKey.Backing.byteCount)
274+
defer { CCryptoBoringSSL_CBB_cleanup(&cbb) }
275+
CCryptoBoringSSL_MLDSA65_marshal_public_key(&cbb, &self.key)
276+
return Data(bytes: CCryptoBoringSSL_CBB_data(&cbb), count: CCryptoBoringSSL_CBB_len(&cbb))
277+
}
278+
279+
/// Verify a signature for the given data.
280+
///
281+
/// - Parameters:
282+
/// - signature: The signature to verify.
283+
/// - data: The message to verify the signature against.
284+
/// - context: The context to use for the signature verification.
285+
///
286+
/// - Returns: `true` if the signature is valid, `false` otherwise.
287+
func isValidSignature<S: DataProtocol, D: DataProtocol, C: DataProtocol>(
288+
_ signature: S,
289+
for data: D,
290+
context: C?
291+
) -> Bool {
292+
let signatureBytes: ContiguousBytes =
293+
signature.regions.count == 1 ? signature.regions.first! : Array(signature)
294+
return signatureBytes.withUnsafeBytes { signaturePtr in
295+
let dataBytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
296+
let rc: CInt = dataBytes.withUnsafeBytes { dataPtr in
297+
context.withUnsafeBytes { contextPtr in
298+
CCryptoBoringSSL_MLDSA65_verify(
299+
&self.key,
300+
signaturePtr.baseAddress,
301+
signaturePtr.count,
302+
dataPtr.baseAddress,
303+
dataPtr.count,
304+
contextPtr.baseAddress,
305+
contextPtr.count
306+
)
307+
}
308+
}
309+
return rc == 1
310+
}
311+
}
312+
313+
/// The size of the public key in bytes.
314+
static let byteCount = 1952
315+
}
316+
}
317+
}
318+
319+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
320+
extension MLDSA65 {
321+
/// The size of the seed in bytes.
322+
private static let seedByteCount = 32
323+
324+
/// The size of the signature in bytes.
325+
private static let signatureByteCount = 3309
326+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
extension Optional where Wrapped: DataProtocol {
18+
func withUnsafeBytes<ReturnValue>(_ body: (UnsafeRawBufferPointer) throws -> ReturnValue) rethrows -> ReturnValue {
19+
if let self {
20+
let bytes: ContiguousBytes = self.regions.count == 1 ? self.regions.first! : Array(self)
21+
return try bytes.withUnsafeBytes { try body($0) }
22+
} else {
23+
return try body(UnsafeRawBufferPointer(start: nil, count: 0))
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)