Skip to content

Add ML-DSA post-quantum signatures to _CryptoExtras #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b42638c
Initial commit
fpseverino Sep 30, 2024
ef1c321
Complete implementation
fpseverino Oct 1, 2024
e7015bc
Use `some` instead of generics
fpseverino Oct 2, 2024
dad4590
Add NIST test vectors
fpseverino Oct 2, 2024
f595a7d
Remove unnecessary `PublicKey` init, make `bytesCount` private, make …
fpseverino Oct 3, 2024
58351d1
Use `copyBytes(to:)`, move `cbsPointer` inside the closure, remove er…
fpseverino Oct 3, 2024
dee0c3d
Move `CBB` from heap to stack
fpseverino Oct 3, 2024
9d417d4
Move `CBS` pointer inside closure for `PublicKey` too
fpseverino Oct 3, 2024
d5650a1
Don't escape context pointers
fpseverino Oct 3, 2024
a5075ff
Create `Backing`s for the keys
fpseverino Oct 3, 2024
891b5a1
Remove `CryptoMLDSAError`
fpseverino Oct 3, 2024
b856f48
Merge pull request #4 from apple/main
fpseverino Oct 6, 2024
8569b02
Small improvements
fpseverino Oct 8, 2024
e85aa8e
Fix access control
fpseverino Oct 8, 2024
114339f
First set of requested changes
fpseverino Oct 18, 2024
d39ef72
Update Sources/_CryptoExtras/MLDSA/MLDSA_boring.swift
fpseverino Oct 18, 2024
b3c8328
Update from seed init
fpseverino Oct 18, 2024
4e29fb1
Make pointer private and change DER to raw representation
fpseverino Oct 18, 2024
0334c64
Add `withUnsafeBytes` function for `ContiguousBytes?`
fpseverino Oct 18, 2024
36b37cc
Move `Optional.withUnsafeBytes` to a separate file
fpseverino Oct 20, 2024
530d87f
Add Wycheproof tests
fpseverino Oct 21, 2024
a7049c3
Simplify tests
fpseverino Oct 21, 2024
acd486a
Add license header to `Optional+withUnsafeBytes.swift`
fpseverino Oct 21, 2024
5950937
Stack-allocate `CBS` and make `var rawRepresentation` non-throwing
fpseverino Oct 22, 2024
e34c7e3
Adapt custom `withUnsafeBytes` to `DataProtocol`
fpseverino Oct 22, 2024
b0dc44b
Refactor MLDSA private key initialization and seed handling
fpseverino Oct 23, 2024
e3776c2
CBB cleanup and remove superfluous seed check
fpseverino Oct 25, 2024
b6494ff
Check if seed is exactly 32 bytes long
fpseverino Oct 25, 2024
545f5e7
Store keys and seed in their own format
fpseverino Oct 30, 2024
aff3763
Remove some `withUnsafeTemporaryAllocation`
fpseverino Oct 30, 2024
06154e4
Fix DocC
fpseverino Oct 30, 2024
0ac196b
Don't use Array for seed initialization
fpseverino Nov 8, 2024
654953e
Merge branch 'main' into ml-dsa
fpseverino Nov 18, 2024
4834aa5
Update CMake and license headers
fpseverino Nov 20, 2024
ffe2688
Merge branch 'main' into ml-dsa
fpseverino Nov 27, 2024
b0839bc
Merge branch 'main' into ml-dsa
fpseverino Nov 30, 2024
aae10bf
Swift Format
fpseverino Nov 30, 2024
c820d7a
Change `MLDSA` to `MLDSA65`
fpseverino Dec 12, 2024
0213029
Small renaming in tests
fpseverino Dec 12, 2024
7f4a578
Merge branch 'main' into ml-dsa
fpseverino Dec 12, 2024
9b1b317
Merge branch 'main' into ml-dsa
fpseverino Dec 14, 2024
e61fe0b
Merge branch 'main' into ml-dsa
fpseverino Dec 16, 2024
c879a71
Merge branch 'main' into ml-dsa
fpseverino Dec 22, 2024
1efae6c
Add `MLDSA65` to CryptoExtras DocC landing page
fpseverino Dec 26, 2024
7e3966e
Remove `Signature` struct
fpseverino Jan 3, 2025
653f094
Rename constants with the style of the rest of the project
fpseverino Jan 3, 2025
7742c64
Merge branch 'main' into ml-dsa
fpseverino Apr 3, 2025
39b7677
Merge branch 'main' into ml-dsa
fpseverino Apr 24, 2025
6479b62
Add `@available` attribute due to `CryptoKitError`
fpseverino Apr 24, 2025
d94f10c
Make`key` in `Backing` private/fileprivate
fpseverino Apr 24, 2025
a6ce825
Rename `seed` to `seedRepresentation`
fpseverino Apr 24, 2025
88c5246
Split `isValidSignature`
fpseverino Apr 24, 2025
06128fe
Split `isValidSignature` only in the public struct
fpseverino Apr 24, 2025
ba04f1b
Split `signature` function
fpseverino Apr 24, 2025
547d497
Update DocC
fpseverino Apr 24, 2025
f2c3601
Make `context` non-optional in public API
fpseverino Apr 30, 2025
451158b
Try fixing test on Windows
fpseverino Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "CCryptoBoringSSL_hrss.h"
#include "CCryptoBoringSSL_md4.h"
#include "CCryptoBoringSSL_md5.h"
#include "CCryptoBoringSSL_mldsa.h"
#include "CCryptoBoringSSL_obj_mac.h"
#include "CCryptoBoringSSL_objects.h"
#include "CCryptoBoringSSL_opensslv.h"
Expand Down
2 changes: 2 additions & 0 deletions Sources/_CryptoExtras/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ add_library(_CryptoExtras
"Key Derivation/PBKDF2/PBKDF2.swift"
"Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift"
"Key Derivation/Scrypt/Scrypt.swift"
"MLDSA/MLDSA65_boring.swift"
"OPRFs/OPRF.swift"
"OPRFs/OPRFClient.swift"
"OPRFs/OPRFServer.swift"
Expand All @@ -58,6 +59,7 @@ add_library(_CryptoExtras
"Util/Error.swift"
"Util/I2OSP.swift"
"Util/IntegerEncoding.swift"
"Util/Optional+withUnsafeBytes.swift"
"Util/PEMDocument.swift"
"Util/PrettyBytes.swift"
"Util/SubjectPublicKeyInfo.swift"
Expand Down
1 change: 1 addition & 0 deletions Sources/_CryptoExtras/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Provides additional cryptographic APIs that are not available in CryptoKit (and
### Public key cryptography

- ``_RSA``
- ``MLDSA65``

### Key derivation functions

Expand Down
326 changes: 326 additions & 0 deletions Sources/_CryptoExtras/MLDSA/MLDSA65_boring.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

@_implementationOnly import CCryptoBoringSSL
import Crypto
import Foundation

/// A module-lattice-based digital signature algorithm that provides security against quantum computing attacks.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
public enum MLDSA65 {}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension MLDSA65 {
/// A ML-DSA-65 private key.
public struct PrivateKey: Sendable {
private var backing: Backing

/// Initialize a ML-DSA-65 private key from a random seed.
public init() throws {
self.backing = try Backing()
}

/// Initialize a ML-DSA-65 private key from a seed.
///
/// - Parameter seedRepresentation: The seed to use to generate the private key.
///
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long.
public init(seedRepresentation: some DataProtocol) throws {
self.backing = try Backing(seedRepresentation: seedRepresentation)
}

/// The seed from which this private key was generated.
public var seedRepresentation: Data {
self.backing.seed
}

/// The public key associated with this private key.
public var publicKey: PublicKey {
self.backing.publicKey
}

/// Generate a signature for the given data.
///
/// - Parameter data: The message to sign.
///
/// - Returns: The signature of the message.
public func signature<D: DataProtocol>(for data: D) throws -> Data {
let context: Data? = nil
return try self.backing.signature(for: data, context: context)
}

/// Generate a signature for the given data.
///
/// - Parameters:
/// - data: The message to sign.
/// - context: The context to use for the signature.
///
/// - Returns: The signature of the message.
public func signature<D: DataProtocol, C: DataProtocol>(for data: D, context: C) throws -> Data {
try self.backing.signature(for: data, context: context)
}

/// The size of the private key in bytes.
static let byteCount = Backing.byteCount

fileprivate final class Backing {
fileprivate var key: MLDSA65_private_key
var seed: Data

/// Initialize a ML-DSA-65 private key from a random seed.
init() throws {
// We have to initialize all members before `self` is captured by the closure
self.key = .init()
self.seed = Data()

self.seed = try withUnsafeTemporaryAllocation(
of: UInt8.self,
capacity: MLDSA65.seedByteCount
) { seedPtr in
try withUnsafeTemporaryAllocation(
of: UInt8.self,
capacity: MLDSA65.PublicKey.Backing.byteCount
) { publicKeyPtr in
guard
CCryptoBoringSSL_MLDSA65_generate_key(
publicKeyPtr.baseAddress,
seedPtr.baseAddress,
&self.key
) == 1
else {
throw CryptoKitError.internalBoringSSLError()
}

return Data(bytes: seedPtr.baseAddress!, count: MLDSA65.seedByteCount)
}
}
}

/// Initialize a ML-DSA-65 private key from a seed.
///
/// - Parameter seedRepresentation: The seed to use to generate the private key.
///
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long.
init(seedRepresentation: some DataProtocol) throws {
guard seedRepresentation.count == MLDSA65.seedByteCount else {
throw CryptoKitError.incorrectKeySize
}

self.key = .init()
self.seed = Data(seedRepresentation)

guard
self.seed.withUnsafeBytes({ seedPtr in
CCryptoBoringSSL_MLDSA65_private_key_from_seed(
&self.key,
seedPtr.baseAddress,
MLDSA65.seedByteCount
)
}) == 1
else {
throw CryptoKitError.internalBoringSSLError()
}
}

/// The public key associated with this private key.
var publicKey: PublicKey {
PublicKey(privateKeyBacking: self)
}

/// Generate a signature for the given data.
///
/// - Parameters:
/// - data: The message to sign.
/// - context: The context to use for the signature.
///
/// - Returns: The signature of the message.
func signature<D: DataProtocol, C: DataProtocol>(for data: D, context: C?) throws -> Data {
var signature = Data(repeating: 0, count: MLDSA65.signatureByteCount)

let rc: CInt = signature.withUnsafeMutableBytes { signaturePtr in
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
return bytes.withUnsafeBytes { dataPtr in
context.withUnsafeBytes { contextPtr in
CCryptoBoringSSL_MLDSA65_sign(
signaturePtr.baseAddress,
&self.key,
dataPtr.baseAddress,
dataPtr.count,
contextPtr.baseAddress,
contextPtr.count
)
}
}
}

guard rc == 1 else {
throw CryptoKitError.internalBoringSSLError()
}

return signature
}

/// The size of the private key in bytes.
static let byteCount = 4032
}
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension MLDSA65 {
/// A ML-DSA-65 public key.
public struct PublicKey: Sendable {
private var backing: Backing

fileprivate init(privateKeyBacking: PrivateKey.Backing) {
self.backing = Backing(privateKeyBacking: privateKeyBacking)
}

/// Initialize a ML-DSA-65 public key from a raw representation.
///
/// - Parameter rawRepresentation: The public key bytes.
///
/// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size.
public init(rawRepresentation: some DataProtocol) throws {
self.backing = try Backing(rawRepresentation: rawRepresentation)
}

/// The raw binary representation of the public key.
public var rawRepresentation: Data {
self.backing.rawRepresentation
}

/// Verify a signature for the given data.
///
/// - Parameters:
/// - signature: The signature to verify.
/// - data: The message to verify the signature against.
///
/// - Returns: `true` if the signature is valid, `false` otherwise.
public func isValidSignature<S: DataProtocol, D: DataProtocol>(_ signature: S, for data: D) -> Bool {
let context: Data? = nil
return self.backing.isValidSignature(signature, for: data, context: context)
}

/// Verify a signature for the given data.
///
/// - Parameters:
/// - signature: The signature to verify.
/// - data: The message to verify the signature against.
/// - context: The context to use for the signature verification.
///
/// - Returns: `true` if the signature is valid, `false` otherwise.
public func isValidSignature<S: DataProtocol, D: DataProtocol, C: DataProtocol>(
_ signature: S,
for data: D,
context: C
) -> Bool {
self.backing.isValidSignature(signature, for: data, context: context)
}

/// The size of the public key in bytes.
static let byteCount = Backing.byteCount

fileprivate final class Backing {
private var key: MLDSA65_public_key

init(privateKeyBacking: PrivateKey.Backing) {
self.key = .init()
CCryptoBoringSSL_MLDSA65_public_from_private(&self.key, &privateKeyBacking.key)
}

/// Initialize a ML-DSA-65 public key from a raw representation.
///
/// - Parameter rawRepresentation: The public key bytes.
///
/// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size.
init(rawRepresentation: some DataProtocol) throws {
guard rawRepresentation.count == MLDSA65.PublicKey.Backing.byteCount else {
throw CryptoKitError.incorrectKeySize
}

self.key = .init()

let bytes: ContiguousBytes =
rawRepresentation.regions.count == 1
? rawRepresentation.regions.first!
: Array(rawRepresentation)
try bytes.withUnsafeBytes { rawBuffer in
try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in
var cbs = CBS(data: buffer.baseAddress, len: buffer.count)
guard CCryptoBoringSSL_MLDSA65_parse_public_key(&self.key, &cbs) == 1 else {
throw CryptoKitError.internalBoringSSLError()
}
}
}
}

/// The raw binary representation of the public key.
var rawRepresentation: Data {
var cbb = CBB()
// The following BoringSSL functions can only fail on allocation failure, which we define as impossible.
CCryptoBoringSSL_CBB_init(&cbb, MLDSA65.PublicKey.Backing.byteCount)
defer { CCryptoBoringSSL_CBB_cleanup(&cbb) }
CCryptoBoringSSL_MLDSA65_marshal_public_key(&cbb, &self.key)
return Data(bytes: CCryptoBoringSSL_CBB_data(&cbb), count: CCryptoBoringSSL_CBB_len(&cbb))
}

/// Verify a signature for the given data.
///
/// - Parameters:
/// - signature: The signature to verify.
/// - data: The message to verify the signature against.
/// - context: The context to use for the signature verification.
///
/// - Returns: `true` if the signature is valid, `false` otherwise.
func isValidSignature<S: DataProtocol, D: DataProtocol, C: DataProtocol>(
_ signature: S,
for data: D,
context: C?
) -> Bool {
let signatureBytes: ContiguousBytes =
signature.regions.count == 1 ? signature.regions.first! : Array(signature)
return signatureBytes.withUnsafeBytes { signaturePtr in
let dataBytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
let rc: CInt = dataBytes.withUnsafeBytes { dataPtr in
context.withUnsafeBytes { contextPtr in
CCryptoBoringSSL_MLDSA65_verify(
&self.key,
signaturePtr.baseAddress,
signaturePtr.count,
dataPtr.baseAddress,
dataPtr.count,
contextPtr.baseAddress,
contextPtr.count
)
}
}
return rc == 1
}
}

/// The size of the public key in bytes.
static let byteCount = 1952
}
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension MLDSA65 {
/// The size of the seed in bytes.
private static let seedByteCount = 32

/// The size of the signature in bytes.
private static let signatureByteCount = 3309
}
26 changes: 26 additions & 0 deletions Sources/_CryptoExtras/Util/Optional+withUnsafeBytes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

extension Optional where Wrapped: DataProtocol {
func withUnsafeBytes<ReturnValue>(_ body: (UnsafeRawBufferPointer) throws -> ReturnValue) rethrows -> ReturnValue {
if let self {
let bytes: ContiguousBytes = self.regions.count == 1 ? self.regions.first! : Array(self)
return try bytes.withUnsafeBytes { try body($0) }
} else {
return try body(UnsafeRawBufferPointer(start: nil, count: 0))
}
}
}
Loading