-
Notifications
You must be signed in to change notification settings - Fork 166
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
base: main
Are you sure you want to change the base?
Conversation
…`PublicKey` extraction from `PrivateKey` non-throwing
…ror thrown by `CBB_init`
Hi @Lukasa, thank you very much for the quick feedback and directions. I think I fixed most of your requested changes, but I have some problems with the DER and PEM representations. Please let me know if the other changes I made are valid and how I could fix the PEM/DER issue. |
Update with latest `main` commits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your fixes, I've left some more notes.
Regarding PEM/DER, right now there is no PEM/DER format for these keys. The only format that is in a final standard is the "raw" format, which is defined in NIST spec FIPS 204 § 7.2. This is what I believe BoringSSL uses today, and that is roughly analogous to the "raw" representation Crypto uses for EC keys.
The most likely source of a spec for DER formatted keys is going to be draft-ietf-lamps-dilithium-certificates. This has defined a tentative private key format and profile for an SPKI public key as well as matching signature format. However, this is non-final, so I don't think we need to implement them at this time. We can stick with the raw representation until the IETF is closer to an answer, or until there's an interoperability need.
If there does become an interoperability need, we can use swift-asn1 to provide the representation, as we do for the EC keys in Crypto.
func signature(for data: some DataProtocol, context: [UInt8]? = nil) throws -> Signature { | ||
let output = try Array<UInt8>(unsafeUninitializedCapacity: Signature.bytesCount) { bufferPtr, length in | ||
let result = data.regions.first!.withUnsafeBytes { dataPtr in | ||
if let context { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to hide this branch in a helper function, something like:
extension Optional where Wrapped == ContiguousBytes {
func withUnsafeBytes<ReturnValue>(_ body: (UnsafeRawBufferPointer) throws -> ReturnValue) rethrows -> ReturnValue {
if let self {
return try self.withUnsafeBytes { try body($0) }
} else {
return try body(UnsafeRawBufferPointer(start: nil, count: 0))
}
}
}
This lets us get a single call to MLDSA65_sign
which makes this code nicer to follow and ensures we don't mix up arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be ok? (Also using your withUnsafeBytes
function)
context.map { Data($0) }.withUnsafeBytes { contextPtr in
CCryptoBoringSSL_MLDSA65_verify(
self.pointer,
signaturePtr.baseAddress,
signaturePtr.count,
dataPtr.baseAddress,
dataPtr.count,
contextPtr.baseAddress,
contextPtr.count
)
}
static let bytesCount = Backing.bytesCount | ||
|
||
fileprivate final class Backing { | ||
let pointer: UnsafeMutablePointer<MLDSA65_private_key> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be private
. It's not acceptable for us to access the raw pointer directly, we need to use a with
method to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So should I add a method to PrivateKey.Backing
like this one?
func withUnsafePointer<T>(_ body: (UnsafePointer<MLDSA65_private_key>) throws -> T) rethrows -> T {
try body(self.pointer)
}
|
||
self.pointer = UnsafeMutablePointer<MLDSA65_private_key>.allocate(capacity: 1) | ||
|
||
try rawRepresentation.regions.flatMap { $0 }.withUnsafeBufferPointer { buffer in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use the rawRepresentation.regions.count == 1
dance here too, instead of the flatMap
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I rewrite it like
let bytes: ContiguousBytes = rawRepresentation.regions.count == 1 ? rawRepresentation.regions.first! : Array(rawRepresentation)
try bytes.withUnsafeBytes { buffer in
var cbs = CBS(data: buffer.baseAddress, len: buffer.count)
guard CCryptoBoringSSL_MLDSA65_parse_private_key(self.pointer, &cbs) == 1 else {
throw CryptoKitError.internalBoringSSLError()
}
}
an error on the CBS init appears, on buffer.baseAddress
: Cannot convert value of type 'UnsafeRawPointer?' to expected argument type 'UnsafePointer<UInt8>?' because initializer 'init(data:len:)' was not imported from C header
.
Changing it to CBS(data: buffer.baseAddress?.assumingMemoryBound(to: UInt8.self), len: buffer.count)
seems to work, is it fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not, but we don't need it. We want to use a withMemoryRebound(to:)
instead. This dance should probably also be wrapped up in a helper function to avoid distracting in the flow here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, we're getting a lot closer here!
/// Initialize a ML-DSA-65 private key from a random seed. | ||
init() throws { | ||
self.pointer = UnsafeMutablePointer<MLDSA65_private_key>.allocate(capacity: 1) | ||
self.seedPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: MLDSA.seedSizeInBytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can make this a touch more efficient. We can store both of these fields in their useful formats. For the private key, we can just have a var key: MLDSA65_private_key
, and then derive pointers as needed.
For the seed, we can use a withUnsafeTemporaryAllocation
and then directly create a Data
from it. Data
is the format we need elsewhere anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means that we don't need a Backing
class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically it does, but we should keep it to avoid us abusing the power of @_implementationOnly
.
static let bytesCount = Backing.bytesCount | ||
|
||
fileprivate final class Backing { | ||
private let pointer: UnsafeMutablePointer<MLDSA65_public_key> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same note here, we can store the MLDSA64_public_key
directly inline here.
|
||
/// A lattice-based digital signature algorithm that provides security against quantum computing attacks. | ||
/// A module lattice-based digital signature algorithm that provides security against quantum computing attacks. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
self.pointer | ||
) == 1 else { | ||
throw CryptoKitError.internalBoringSSLError() | ||
(self.key, self.seed) = try withUnsafeTemporaryAllocation(of: MLDSA65_private_key.self, capacity: 1) { privateKeyPtr in |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, looking really good here. One note in the diff.
|
||
guard CCryptoBoringSSL_MLDSA65_private_key_from_seed( | ||
&self.key, | ||
Array(seed.prefix(MLDSA.seedSizeInBytes)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not create an array here, nor should we use prefix
. We know the seed size is right, and we can use self.seed.withUnsafeBytes
to get the interior pointer there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, I think this is in good shape. Let's get it merged!
Just a few CI jobs to fix up. Missing license headers, and a need to run the cmake script in the |
The license headers are there, but I think the CI wants a |
Regarding the CMake script, if I run Here's the output:
P.S. I do have the |
We expect GNU find. You can get it by running |
@Lukasa all done, now the jobs should pass |
I saw that the BoringSSL team added another parameter set to ML-KEM. Considering that they could do this for ML-DSA too, in order not to break the API should we put the current implementation inside a namespace that identifies the parameter set? If so I'm open to suggestions for the name, since the following (which is the most immediate one) won't work enum MLDSA {
enum 65 {}
// Possible future ones
enum 44 {}
enum 87 {}
} P.S. Each parameter set uses completely different C++ functions and structures |
Add support for ML-DSA post-quantum digital signatures inside
_CryptoExtras
.Checklist
If you've made changes to
gyb
files.script/generate_boilerplate_files_with_gyb
and included updated generated files in a commit of this pull requestMotivation:
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" 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, 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
MLDSA
enum inside the_CryptoExtras
module with correspondingPrivateKey
,PublicKey
andSignature
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 (extracted from a
.txt
file and encoded in JSON).Result:
ML-DSA-65 digital signatures can be created and verified with Swift Crypto.