Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ let package = Package(
targets: ["X509"]
)
],
traits: [
.trait(
name: "MLDSA",
description: "Enable ML-DSA (FIPS 204) certificate support. Requires swift-crypto 4.0.0 or later."
)
],
targets: [
.target(
name: "X509",
Expand All @@ -35,6 +41,9 @@ let package = Package(
],
exclude: [
"CMakeLists.txt"
],
swiftSettings: [
.define("SWIFT_CERTIFICATES_MLDSA", .when(traits: ["MLDSA"]))
]
),
.testTarget(
Expand All @@ -53,6 +62,9 @@ let package = Package(
.copy("PEMTestRSACertificate.pem"),
.copy("CSR Vectors/"),
.copy("ca-certificates.crt"),
],
swiftSettings: [
.define("SWIFT_CERTIFICATES_MLDSA", .when(traits: ["MLDSA"]))
]
),
.target(
Expand Down
1 change: 1 addition & 0 deletions Sources/X509/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ add_library(X509
"GeneralName.swift"
"Lock.swift"
"LockedValueBox.swift"
"MLDSA.swift"
"OCSP/BasicOCSPResponse.swift"
"OCSP/DirectoryString.swift"
"OCSP/OCSPCertID.swift"
Expand Down
24 changes: 24 additions & 0 deletions Sources/X509/CertificatePublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ extension Certificate {
case .ed25519:
let key = try Curve25519.Signing.PublicKey(rawRepresentation: spki.key.bytes)
self.backing = .ed25519(key)
case .mldsa65:
self.backing = .mldsa(try MLDSAPublicKeyBytes(spkiBytes: spki.key.bytes, variant: .mldsa65))
case .mldsa87:
self.backing = .mldsa(try MLDSAPublicKeyBytes(spkiBytes: spki.key.bytes, variant: .mldsa87))
default:
throw CertificateError.unsupportedPublicKeyAlgorithm(reason: "\(spki.algorithmIdentifier)")
}
Expand Down Expand Up @@ -154,6 +158,8 @@ extension Certificate.PublicKey {
return rsa.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm)
case .ed25519(let ed25519):
return ed25519.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm)
case .mldsa(let mldsa):
return mldsa.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm)
}
}

Expand Down Expand Up @@ -185,6 +191,8 @@ extension Certificate.PublicKey {
return rsa.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm)
case .ed25519(let ed25519):
return ed25519.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm)
case .mldsa(let mldsa):
return mldsa.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm)
}
}
}
Expand All @@ -209,6 +217,13 @@ extension Certificate.PublicKey: CustomStringConvertible {
return "RSA\(publicKey.keySizeInBits).PublicKey"
case .ed25519:
return "Ed25519.PublicKey"
case .mldsa(let backing):
switch backing.variant {
case .mldsa65:
return "MLDSA65.PublicKey"
case .mldsa87:
return "MLDSA87.PublicKey"
}
}
}
}
Expand All @@ -222,6 +237,7 @@ extension Certificate.PublicKey {
case p521(Crypto.P521.Signing.PublicKey)
case rsa(_CryptoExtras._RSA.Signing.PublicKey)
case ed25519(Curve25519.Signing.PublicKey)
case mldsa(MLDSAPublicKeyBytes)

@inlinable
static func == (lhs: BackingPublicKey, rhs: BackingPublicKey) -> Bool {
Expand All @@ -236,6 +252,8 @@ extension Certificate.PublicKey {
return l.derRepresentation == r.derRepresentation
case (.ed25519(let l), .ed25519(let r)):
return l.rawRepresentation == r.rawRepresentation
case (.mldsa(let l), .mldsa(let r)):
return l == r
default:
return false
}
Expand All @@ -259,6 +277,9 @@ extension Certificate.PublicKey {
case .ed25519(let digest):
hasher.combine(4)
hasher.combine(digest.rawRepresentation)
case .mldsa(let backing):
hasher.combine(5)
hasher.combine(backing)
}
}
}
Expand Down Expand Up @@ -287,6 +308,9 @@ extension SubjectPublicKeyInfo {
case .ed25519(let ed25519):
algorithmIdentifier = .ed25519
key = .init(bytes: ArraySlice(ed25519.rawRepresentation))
case .mldsa(let mldsa):
algorithmIdentifier = AlgorithmIdentifier(mldsaVariant: mldsa.variant)
key = .init(bytes: ArraySlice(mldsa.rawRepresentation))
}

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

@preconcurrency import Crypto
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// The ML-DSA parameter sets this library can name.
@usableFromInline
enum MLDSAVariant: Hashable, Sendable {
case mldsa65
case mldsa87
}

extension AlgorithmIdentifier {
@usableFromInline
init(mldsaVariant: MLDSAVariant) {
switch mldsaVariant {
case .mldsa65:
self = .mldsa65
case .mldsa87:
self = .mldsa87
}
}
}

/// The raw bytes of an ML-DSA public key, plus its parameter set.
///
/// This type cannot store swift-crypto's `MLDSA65.PublicKey` / `MLDSA87.PublicKey` while
/// remaining available on all platforms, because swift-crypto's ML-DSA APIs have 26.0
/// platform requirements. Only the key's bytes are stored; methods that need swift-crypto's
/// ML-DSA APIs create an `MLDSA65.PublicKey` / `MLDSA87.PublicKey` from the raw bytes.
@usableFromInline
struct MLDSAPublicKeyBytes: Hashable, Sendable {
@usableFromInline
var variant: MLDSAVariant

@usableFromInline
var rawRepresentation: Data

/// Validates and stores SPKI subjectPublicKey bytes for the given parameter set.
///
/// Throws ``CertificateError/unsupportedPublicKeyAlgorithm(reason:file:line:)`` when the
/// library was built without the `MLDSA` package trait, or at runtime on Darwin platforms
/// older than macOS 26 (where CryptoKit has no ML-DSA).
@usableFromInline
init(spkiBytes: ArraySlice<UInt8>, variant: MLDSAVariant) throws {
#if SWIFT_CERTIFICATES_MLDSA
guard #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
else {
throw CertificateError.unsupportedPublicKeyAlgorithm(
reason: "ML-DSA requires macOS 26, iOS 26, watchOS 26, tvOS 26, or visionOS 26"
)
}
// Let swift-crypto validate the encoding; we store the validated raw bytes.
switch variant {
case .mldsa65:
self.rawRepresentation = try MLDSA65.PublicKey(rawRepresentation: spkiBytes).rawRepresentation
case .mldsa87:
self.rawRepresentation = try MLDSA87.PublicKey(rawRepresentation: spkiBytes).rawRepresentation
}
self.variant = variant
#else
throw CertificateError.unsupportedPublicKeyAlgorithm(
reason:
"ML-DSA support requires the 'MLDSA' package trait on swift-certificates and swift-crypto 4.0.0 or later"
)
#endif
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension MLDSAPublicKeyBytes {
@usableFromInline
func isValidSignature<Bytes: DataProtocol>(
_ signature: Certificate.Signature,
for bytes: Bytes,
signatureAlgorithm: Certificate.SignatureAlgorithm
) -> Bool {
guard case .mldsa(let signatureVariant, let rawSignature) = signature.backing,
signatureVariant == self.variant
else {
// Signature mismatch, including an ML-DSA signature under the other parameter set.
return false
}
return self.isValidSignature(rawSignature, for: bytes, signatureAlgorithm: signatureAlgorithm)
}

@usableFromInline
func isValidSignature<SignatureBytes: DataProtocol, Bytes: DataProtocol>(
_ signature: SignatureBytes,
for bytes: Bytes,
signatureAlgorithm: Certificate.SignatureAlgorithm
) -> Bool {
switch (self.variant, signatureAlgorithm) {
case (.mldsa65, .mldsa65), (.mldsa87, .mldsa87):
break
default:
return false
}
#if SWIFT_CERTIFICATES_MLDSA
guard #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
else {
// The initializer validates the platform requirement, so this `fatalError` is unreachable.
fatalError("Unreachable: MLDSAPublicKeyBytes cannot be constructed without ML-DSA availability")
}
switch self.variant {
case .mldsa65:
guard let key = try? MLDSA65.PublicKey(rawRepresentation: self.rawRepresentation) else {
return false
}
return key.isValidSignature(signature, for: bytes)
case .mldsa87:
guard let key = try? MLDSA87.PublicKey(rawRepresentation: self.rawRepresentation) else {
return false
}
return key.isValidSignature(signature, for: bytes)
}
#else
// The initializer requires the MLDSA package trait, so this `fatalError` is unreachable.
fatalError("Unreachable: MLDSAPublicKeyBytes cannot be constructed without the MLDSA package trait")
#endif
}
}

#if SWIFT_CERTIFICATES_MLDSA
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
extension MLDSAPublicKeyBytes {
@usableFromInline
init(_ mldsa65: MLDSA65.PublicKey) {
self.variant = .mldsa65
self.rawRepresentation = mldsa65.rawRepresentation
}

@usableFromInline
init(_ mldsa87: MLDSA87.PublicKey) {
self.variant = .mldsa87
self.rawRepresentation = mldsa87.rawRepresentation
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
extension Certificate.PublicKey {
/// Construct a public key wrapping an ML-DSA-65 public key.
/// - Parameter mldsa65: The ML-DSA-65 public key to wrap.
@inlinable
public init(_ mldsa65: MLDSA65.PublicKey) {
self.init(backing: .mldsa(MLDSAPublicKeyBytes(mldsa65)))
}

/// Construct a public key wrapping an ML-DSA-87 public key.
/// - Parameter mldsa87: The ML-DSA-87 public key to wrap.
@inlinable
public init(_ mldsa87: MLDSA87.PublicKey) {
self.init(backing: .mldsa(MLDSAPublicKeyBytes(mldsa87)))
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
extension MLDSA65.PublicKey {
/// Create an ML-DSA-65 public key from a given ``Certificate/PublicKey-swift.struct``.
///
/// Fails if the key is not an ML-DSA-65 key.
///
/// - Parameters:
/// - key: The key to unwrap.
public init?(_ key: Certificate.PublicKey) {
guard case .mldsa(let backing) = key.backing, backing.variant == .mldsa65,
let key = try? MLDSA65.PublicKey(rawRepresentation: backing.rawRepresentation)
else {
return nil
}
self = key
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
extension MLDSA87.PublicKey {
/// Create an ML-DSA-87 public key from a given ``Certificate/PublicKey-swift.struct``.
///
/// Fails if the key is not an ML-DSA-87 key.
///
/// - Parameters:
/// - key: The key to unwrap.
public init?(_ key: Certificate.PublicKey) {
guard case .mldsa(let backing) = key.backing, backing.variant == .mldsa87,
let key = try? MLDSA87.PublicKey(rawRepresentation: backing.rawRepresentation)
else {
return nil
}
self = key
}
}
#endif
Loading
Loading