Skip to content

Commit 3ef6559

Browse files
authored
Add ML-DSA-87 (apple#357)
Add ML-DSA-87 ### Checklist - [X] I've run tests to see all new and existing tests pass - [X] I've followed the code style of the rest of the project - [X] I've read the [Contribution Guidelines](CONTRIBUTING.md) - [X] I've updated the documentation if necessary #### If you've made changes to `gyb` files - [X] I've run `.script/generate_boilerplate_files_with_gyb` and included updated generated files in a commit of this pull request ### Motivation: See apple#355 ### Modifications: Add the "87" parameter set in MLDSA_boring.swift.gyb, add tests for `MLDSA87` and update the test vectors for both ML-DSA 65 and 87 ### Result: `MLDSA87` is now available
1 parent 010a448 commit 3ef6559

File tree

8 files changed

+1506
-19
lines changed

8 files changed

+1506
-19
lines changed

Sources/_CryptoExtras/Docs.docc/index.md

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

1717
- ``_RSA``
18+
- ``MLKEM768``
19+
- ``MLKEM1024``
1820
- ``MLDSA65``
21+
- ``MLDSA87``
1922

2023
### Key derivation functions
2124

Sources/_CryptoExtras/MLDSA/MLDSA_boring.swift

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,312 @@ extension MLDSA65 {
326326
private static let signatureByteCount = Int(MLDSA65_SIGNATURE_BYTES)
327327
}
328328

329+
/// A module-lattice-based digital signature algorithm that provides security against quantum computing attacks.
330+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
331+
public enum MLDSA87 {}
332+
333+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
334+
extension MLDSA87 {
335+
/// A ML-DSA-87 private key.
336+
public struct PrivateKey: Sendable {
337+
private var backing: Backing
338+
339+
/// Initialize a ML-DSA-87 private key from a random seed.
340+
public init() throws {
341+
self.backing = try Backing()
342+
}
343+
344+
/// Initialize a ML-DSA-87 private key from a seed.
345+
///
346+
/// - Parameter seedRepresentation: The seed to use to generate the private key.
347+
///
348+
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long.
349+
public init(seedRepresentation: some DataProtocol) throws {
350+
self.backing = try Backing(seedRepresentation: seedRepresentation)
351+
}
352+
353+
/// The seed from which this private key was generated.
354+
public var seedRepresentation: Data {
355+
self.backing.seed
356+
}
357+
358+
/// The public key associated with this private key.
359+
public var publicKey: PublicKey {
360+
self.backing.publicKey
361+
}
362+
363+
/// Generate a signature for the given data.
364+
///
365+
/// - Parameter data: The message to sign.
366+
///
367+
/// - Returns: The signature of the message.
368+
public func signature<D: DataProtocol>(for data: D) throws -> Data {
369+
let context: Data? = nil
370+
return try self.backing.signature(for: data, context: context)
371+
}
372+
373+
/// Generate a signature for the given data.
374+
///
375+
/// - Parameters:
376+
/// - data: The message to sign.
377+
/// - context: The context to use for the signature.
378+
///
379+
/// - Returns: The signature of the message.
380+
public func signature<D: DataProtocol, C: DataProtocol>(for data: D, context: C) throws -> Data {
381+
try self.backing.signature(for: data, context: context)
382+
}
383+
384+
/// The size of the private key in bytes.
385+
static let byteCount = Backing.byteCount
386+
387+
fileprivate final class Backing {
388+
fileprivate var key: MLDSA87_private_key
389+
var seed: Data
390+
391+
/// Initialize a ML-DSA-87 private key from a random seed.
392+
init() throws {
393+
// We have to initialize all members before `self` is captured by the closure
394+
self.key = .init()
395+
self.seed = Data()
396+
397+
self.seed = try withUnsafeTemporaryAllocation(
398+
of: UInt8.self,
399+
capacity: MLDSA.seedByteCount
400+
) { seedPtr in
401+
try withUnsafeTemporaryAllocation(
402+
of: UInt8.self,
403+
capacity: MLDSA87.PublicKey.Backing.byteCount
404+
) { publicKeyPtr in
405+
guard
406+
CCryptoBoringSSL_MLDSA87_generate_key(
407+
publicKeyPtr.baseAddress,
408+
seedPtr.baseAddress,
409+
&self.key
410+
) == 1
411+
else {
412+
throw CryptoKitError.internalBoringSSLError()
413+
}
414+
415+
return Data(bytes: seedPtr.baseAddress!, count: MLDSA.seedByteCount)
416+
}
417+
}
418+
}
419+
420+
/// Initialize a ML-DSA-87 private key from a seed.
421+
///
422+
/// - Parameter seedRepresentation: The seed to use to generate the private key.
423+
///
424+
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long.
425+
init(seedRepresentation: some DataProtocol) throws {
426+
guard seedRepresentation.count == MLDSA.seedByteCount else {
427+
throw CryptoKitError.incorrectKeySize
428+
}
429+
430+
self.key = .init()
431+
self.seed = Data(seedRepresentation)
432+
433+
guard
434+
self.seed.withUnsafeBytes({ seedPtr in
435+
CCryptoBoringSSL_MLDSA87_private_key_from_seed(
436+
&self.key,
437+
seedPtr.baseAddress,
438+
MLDSA.seedByteCount
439+
)
440+
}) == 1
441+
else {
442+
throw CryptoKitError.internalBoringSSLError()
443+
}
444+
}
445+
446+
/// The public key associated with this private key.
447+
var publicKey: PublicKey {
448+
PublicKey(privateKeyBacking: self)
449+
}
450+
451+
/// Generate a signature for the given data.
452+
///
453+
/// - Parameters:
454+
/// - data: The message to sign.
455+
/// - context: The context to use for the signature.
456+
///
457+
/// - Returns: The signature of the message.
458+
func signature<D: DataProtocol, C: DataProtocol>(for data: D, context: C?) throws -> Data {
459+
var signature = Data(repeating: 0, count: MLDSA87.signatureByteCount)
460+
461+
let rc: CInt = signature.withUnsafeMutableBytes { signaturePtr in
462+
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
463+
return bytes.withUnsafeBytes { dataPtr in
464+
context.withUnsafeBytes { contextPtr in
465+
CCryptoBoringSSL_MLDSA87_sign(
466+
signaturePtr.baseAddress,
467+
&self.key,
468+
dataPtr.baseAddress,
469+
dataPtr.count,
470+
contextPtr.baseAddress,
471+
contextPtr.count
472+
)
473+
}
474+
}
475+
}
476+
477+
guard rc == 1 else {
478+
throw CryptoKitError.internalBoringSSLError()
479+
}
480+
481+
return signature
482+
}
483+
484+
/// The size of the private key in bytes.
485+
static let byteCount = Int(MLDSA87_PRIVATE_KEY_BYTES)
486+
}
487+
}
488+
}
489+
490+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
491+
extension MLDSA87 {
492+
/// A ML-DSA-87 public key.
493+
public struct PublicKey: Sendable {
494+
private var backing: Backing
495+
496+
fileprivate init(privateKeyBacking: PrivateKey.Backing) {
497+
self.backing = Backing(privateKeyBacking: privateKeyBacking)
498+
}
499+
500+
/// Initialize a ML-DSA-87 public key from a raw representation.
501+
///
502+
/// - Parameter rawRepresentation: The public key bytes.
503+
///
504+
/// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size.
505+
public init(rawRepresentation: some DataProtocol) throws {
506+
self.backing = try Backing(rawRepresentation: rawRepresentation)
507+
}
508+
509+
/// The raw binary representation of the public key.
510+
public var rawRepresentation: Data {
511+
self.backing.rawRepresentation
512+
}
513+
514+
/// Verify a signature for the given data.
515+
///
516+
/// - Parameters:
517+
/// - signature: The signature to verify.
518+
/// - data: The message to verify the signature against.
519+
///
520+
/// - Returns: `true` if the signature is valid, `false` otherwise.
521+
public func isValidSignature<S: DataProtocol, D: DataProtocol>(_ signature: S, for data: D) -> Bool {
522+
let context: Data? = nil
523+
return self.backing.isValidSignature(signature, for: data, context: context)
524+
}
525+
526+
/// Verify a signature for the given data.
527+
///
528+
/// - Parameters:
529+
/// - signature: The signature to verify.
530+
/// - data: The message to verify the signature against.
531+
/// - context: The context to use for the signature verification.
532+
///
533+
/// - Returns: `true` if the signature is valid, `false` otherwise.
534+
public func isValidSignature<S: DataProtocol, D: DataProtocol, C: DataProtocol>(
535+
_ signature: S,
536+
for data: D,
537+
context: C
538+
) -> Bool {
539+
self.backing.isValidSignature(signature, for: data, context: context)
540+
}
541+
542+
/// The size of the public key in bytes.
543+
static let byteCount = Backing.byteCount
544+
545+
fileprivate final class Backing {
546+
private var key: MLDSA87_public_key
547+
548+
init(privateKeyBacking: PrivateKey.Backing) {
549+
self.key = .init()
550+
CCryptoBoringSSL_MLDSA87_public_from_private(&self.key, &privateKeyBacking.key)
551+
}
552+
553+
/// Initialize a ML-DSA-87 public key from a raw representation.
554+
///
555+
/// - Parameter rawRepresentation: The public key bytes.
556+
///
557+
/// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size.
558+
init(rawRepresentation: some DataProtocol) throws {
559+
guard rawRepresentation.count == MLDSA87.PublicKey.Backing.byteCount else {
560+
throw CryptoKitError.incorrectKeySize
561+
}
562+
563+
self.key = .init()
564+
565+
let bytes: ContiguousBytes =
566+
rawRepresentation.regions.count == 1
567+
? rawRepresentation.regions.first!
568+
: Array(rawRepresentation)
569+
try bytes.withUnsafeBytes { rawBuffer in
570+
try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in
571+
var cbs = CBS(data: buffer.baseAddress, len: buffer.count)
572+
guard CCryptoBoringSSL_MLDSA87_parse_public_key(&self.key, &cbs) == 1 else {
573+
throw CryptoKitError.internalBoringSSLError()
574+
}
575+
}
576+
}
577+
}
578+
579+
/// The raw binary representation of the public key.
580+
var rawRepresentation: Data {
581+
var cbb = CBB()
582+
// The following BoringSSL functions can only fail on allocation failure, which we define as impossible.
583+
CCryptoBoringSSL_CBB_init(&cbb, MLDSA87.PublicKey.Backing.byteCount)
584+
defer { CCryptoBoringSSL_CBB_cleanup(&cbb) }
585+
CCryptoBoringSSL_MLDSA87_marshal_public_key(&cbb, &self.key)
586+
return Data(bytes: CCryptoBoringSSL_CBB_data(&cbb), count: CCryptoBoringSSL_CBB_len(&cbb))
587+
}
588+
589+
/// Verify a signature for the given data.
590+
///
591+
/// - Parameters:
592+
/// - signature: The signature to verify.
593+
/// - data: The message to verify the signature against.
594+
/// - context: The context to use for the signature verification.
595+
///
596+
/// - Returns: `true` if the signature is valid, `false` otherwise.
597+
func isValidSignature<S: DataProtocol, D: DataProtocol, C: DataProtocol>(
598+
_ signature: S,
599+
for data: D,
600+
context: C?
601+
) -> Bool {
602+
let signatureBytes: ContiguousBytes =
603+
signature.regions.count == 1 ? signature.regions.first! : Array(signature)
604+
return signatureBytes.withUnsafeBytes { signaturePtr in
605+
let dataBytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
606+
let rc: CInt = dataBytes.withUnsafeBytes { dataPtr in
607+
context.withUnsafeBytes { contextPtr in
608+
CCryptoBoringSSL_MLDSA87_verify(
609+
&self.key,
610+
signaturePtr.baseAddress,
611+
signaturePtr.count,
612+
dataPtr.baseAddress,
613+
dataPtr.count,
614+
contextPtr.baseAddress,
615+
contextPtr.count
616+
)
617+
}
618+
}
619+
return rc == 1
620+
}
621+
}
622+
623+
/// The size of the public key in bytes.
624+
static let byteCount = Int(MLDSA87_PUBLIC_KEY_BYTES)
625+
}
626+
}
627+
}
628+
629+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
630+
extension MLDSA87 {
631+
/// The size of the signature in bytes.
632+
private static let signatureByteCount = Int(MLDSA87_SIGNATURE_BYTES)
633+
}
634+
329635
private enum MLDSA {
330636
/// The size of the seed in bytes.
331637
fileprivate static let seedByteCount = 32

Sources/_CryptoExtras/MLDSA/MLDSA_boring.swift.gyb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import Crypto
2121
import Foundation
2222
%{
23-
parameter_sets = ["65"]
23+
parameter_sets = ["65", "87"]
2424
}%
2525
% for parameter_set in parameter_sets:
2626

0 commit comments

Comments
 (0)