Skip to content
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

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

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
43 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
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"
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
#include "CCryptoBoringSSL_obj_mac.h"
#include "CCryptoBoringSSL_objects.h"
#include "CCryptoBoringSSL_opensslv.h"
Expand Down
315 changes: 315 additions & 0 deletions Sources/_CryptoExtras/MLDSA/MLDSA_boring.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
//===----------------------------------------------------------------------===//
//
// 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.md for the list of SwiftCrypto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Crypto
import Foundation

@_implementationOnly import CCryptoBoringSSL

/// A module lattice-based digital signature algorithm that provides security against quantum computing attacks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want module here?

Copy link
Contributor Author

@fpseverino fpseverino Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it just to complete the ML-DSA acronym, maybe I could add an hyphen between it and lattice to make it clearer, as in the FIPS title

public enum MLDSA {}

extension MLDSA {
/// A ML-DSA-65 private key.
public struct PrivateKey: Sendable {
Lukasa marked this conversation as resolved.
Show resolved Hide resolved
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.
///
/// The seed must be at least 32 bytes long.
/// Any additional bytes in the seed are ignored.
///
/// - Parameter seed: The seed to use to generate the private key.
///
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not at least 32 bytes long.
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
public init(seed: some DataProtocol) throws {
self.backing = try Backing(seed: seed)
}

/// The seed from which this private key was generated.
public var seed: 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.
///
/// - 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>(for data: D, context: D? = nil) throws -> Signature {
try self.backing.signature(for: data, context: context)
}

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

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

/// Initialize a ML-DSA-65 private key from a random seed.
init() throws {
(self.key, self.seed) = try withUnsafeTemporaryAllocation(of: MLDSA65_private_key.self, capacity: 1) { privateKeyPtr in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need an unsafeTemporaryAllocation. We can just zero-initialize the value self.key = .init() and then take a pointer to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I also tried to remove the seed's unsafeTemporaryAllocation by passing to the BoringSSL function an empty [UInt8] var and then initializing the Data object from it, but I encountered some seg faults in testing

try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: MLDSA.seedSizeInBytes) { seedPtr in
try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: MLDSA.PublicKey.Backing.bytesCount) { publicKeyPtr in
guard CCryptoBoringSSL_MLDSA65_generate_key(
publicKeyPtr.baseAddress,
seedPtr.baseAddress,
privateKeyPtr.baseAddress
) == 1 else {
throw CryptoKitError.internalBoringSSLError()
}

let key = privateKeyPtr.baseAddress!.pointee
let seed = Data(bytes: seedPtr.baseAddress!, count: MLDSA.seedSizeInBytes)
return (key, seed)
}
}
}
}

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

self.key = try withUnsafeTemporaryAllocation(of: MLDSA65_private_key.self, capacity: 1) { privateKeyPtr in
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
guard CCryptoBoringSSL_MLDSA65_private_key_from_seed(
privateKeyPtr.baseAddress,
Array(seed.prefix(MLDSA.seedSizeInBytes)),
MLDSA.seedSizeInBytes
) == 1 else {
throw CryptoKitError.internalBoringSSLError()
}

return privateKeyPtr.baseAddress!.pointee
}
self.seed = Data(seed)
}

/// 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>(for data: D, context: D? = nil) throws -> Signature {
let output = try Array<UInt8>(unsafeUninitializedCapacity: Signature.bytesCount) { bufferPtr, length in
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
let result = bytes.withUnsafeBytes { dataPtr in
context.withUnsafeBytes { contextPtr in
CCryptoBoringSSL_MLDSA65_sign(
bufferPtr.baseAddress,
&self.key,
dataPtr.baseAddress,
dataPtr.count,
contextPtr.baseAddress,
contextPtr.count
)
}
}

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

length = Signature.bytesCount
}
return Signature(signatureBytes: output)
}

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

extension MLDSA {
/// 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.
/// - context: The context to use for the signature verification.
///
/// - Returns: `true` if the signature is valid, `false` otherwise.
public func isValidSignature<D: DataProtocol>(_ signature: Signature, for data: D, context: D? = nil) -> Bool {
self.backing.isValidSignature(signature, for: data, context: context)
}

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

fileprivate final class Backing {
var key: MLDSA65_public_key

init(privateKeyBacking: PrivateKey.Backing) {
self.key = withUnsafeTemporaryAllocation(of: MLDSA65_public_key.self, capacity: 1) { publicKeyPtr in
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
CCryptoBoringSSL_MLDSA65_public_from_private(publicKeyPtr.baseAddress, &privateKeyBacking.key)
return publicKeyPtr.baseAddress!.pointee
}
}

/// 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 == MLDSA.PublicKey.Backing.bytesCount else {
throw CryptoKitError.incorrectKeySize
}

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

return publicKeyPtr.baseAddress!.pointee
}
}
}
}

/// 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, MLDSA.PublicKey.Backing.bytesCount)
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
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<D: DataProtocol>(_ signature: Signature, for data: D, context: D? = nil) -> Bool {
signature.withUnsafeBytes { signaturePtr in
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
let rc: CInt = bytes.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 bytesCount = 1952
}
}
}

extension MLDSA {
/// A ML-DSA-65 signature.
public struct Signature: Sendable, ContiguousBytes {
/// The raw binary representation of the signature.
public var rawRepresentation: Data

/// Initialize a ML-DSA-65 signature from a raw representation.
///
/// - Parameter rawRepresentation: The signature bytes.
public init(rawRepresentation: some DataProtocol) {
self.rawRepresentation = Data(rawRepresentation)
}

/// Initialize a ML-DSA-65 signature from a raw representation.
///
/// - Parameter signatureBytes: The signature bytes.
init(signatureBytes: [UInt8]) {
self.rawRepresentation = Data(signatureBytes)
}

/// Access the signature bytes.
///
/// - Parameter body: The closure to execute with the signature bytes.
///
/// - Returns: The result of the closure.
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
try self.rawRepresentation.withUnsafeBytes(body)
}

/// The size of the signature in bytes.
fileprivate static let bytesCount = 3309
}
}

extension MLDSA {
/// The size of the seed in bytes.
private static let seedSizeInBytes = 32
}
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.md 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