-
Notifications
You must be signed in to change notification settings - Fork 174
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
Changes from all commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
b42638c
Initial commit
fpseverino ef1c321
Complete implementation
fpseverino e7015bc
Use `some` instead of generics
fpseverino dad4590
Add NIST test vectors
fpseverino f595a7d
Remove unnecessary `PublicKey` init, make `bytesCount` private, make …
fpseverino 58351d1
Use `copyBytes(to:)`, move `cbsPointer` inside the closure, remove er…
fpseverino dee0c3d
Move `CBB` from heap to stack
fpseverino 9d417d4
Move `CBS` pointer inside closure for `PublicKey` too
fpseverino d5650a1
Don't escape context pointers
fpseverino a5075ff
Create `Backing`s for the keys
fpseverino 891b5a1
Remove `CryptoMLDSAError`
fpseverino b856f48
Merge pull request #4 from apple/main
fpseverino 8569b02
Small improvements
fpseverino e85aa8e
Fix access control
fpseverino 114339f
First set of requested changes
fpseverino d39ef72
Update Sources/_CryptoExtras/MLDSA/MLDSA_boring.swift
fpseverino b3c8328
Update from seed init
fpseverino 4e29fb1
Make pointer private and change DER to raw representation
fpseverino 0334c64
Add `withUnsafeBytes` function for `ContiguousBytes?`
fpseverino 36b37cc
Move `Optional.withUnsafeBytes` to a separate file
fpseverino 530d87f
Add Wycheproof tests
fpseverino a7049c3
Simplify tests
fpseverino acd486a
Add license header to `Optional+withUnsafeBytes.swift`
fpseverino 5950937
Stack-allocate `CBS` and make `var rawRepresentation` non-throwing
fpseverino e34c7e3
Adapt custom `withUnsafeBytes` to `DataProtocol`
fpseverino b0dc44b
Refactor MLDSA private key initialization and seed handling
fpseverino e3776c2
CBB cleanup and remove superfluous seed check
fpseverino b6494ff
Check if seed is exactly 32 bytes long
fpseverino 545f5e7
Store keys and seed in their own format
fpseverino aff3763
Remove some `withUnsafeTemporaryAllocation`
fpseverino 06154e4
Fix DocC
fpseverino 0ac196b
Don't use Array for seed initialization
fpseverino 654953e
Merge branch 'main' into ml-dsa
fpseverino 4834aa5
Update CMake and license headers
fpseverino ffe2688
Merge branch 'main' into ml-dsa
fpseverino b0839bc
Merge branch 'main' into ml-dsa
fpseverino aae10bf
Swift Format
fpseverino c820d7a
Change `MLDSA` to `MLDSA65`
fpseverino 0213029
Small renaming in tests
fpseverino 7f4a578
Merge branch 'main' into ml-dsa
fpseverino 9b1b317
Merge branch 'main' into ml-dsa
fpseverino e61fe0b
Merge branch 'main' into ml-dsa
fpseverino c879a71
Merge branch 'main' into ml-dsa
fpseverino 1efae6c
Add `MLDSA65` to CryptoExtras DocC landing page
fpseverino 7e3966e
Remove `Signature` struct
fpseverino 653f094
Rename constants with the style of the rest of the project
fpseverino 7742c64
Merge branch 'main' into ml-dsa
fpseverino 39b7677
Merge branch 'main' into ml-dsa
fpseverino 6479b62
Add `@available` attribute due to `CryptoKitError`
fpseverino d94f10c
Make`key` in `Backing` private/fileprivate
fpseverino a6ce825
Rename `seed` to `seedRepresentation`
fpseverino 88c5246
Split `isValidSignature`
fpseverino 06128fe
Split `isValidSignature` only in the public struct
fpseverino ba04f1b
Split `signature` function
fpseverino 547d497
Update DocC
fpseverino f2c3601
Make `context` non-optional in public API
fpseverino 451158b
Try fixing test on Windows
fpseverino File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.