diff --git a/Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h b/Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h index 7b5a35ab2..522a06a49 100644 --- a/Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h +++ b/Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h @@ -61,6 +61,7 @@ #include "CCryptoBoringSSL_safestack.h" #include "CCryptoBoringSSL_sha.h" #include "CCryptoBoringSSL_siphash.h" +#include "CCryptoBoringSSL_slhdsa.h" #include "CCryptoBoringSSL_trust_token.h" #include "CCryptoBoringSSL_x509v3.h" #include "CCryptoBoringSSL_xwing.h" diff --git a/Sources/Crypto/CMakeLists.txt b/Sources/Crypto/CMakeLists.txt index 1b560e783..2fadd3f3c 100644 --- a/Sources/Crypto/CMakeLists.txt +++ b/Sources/Crypto/CMakeLists.txt @@ -98,7 +98,6 @@ add_library(Crypto "Signatures/MLDSA.swift" "Signatures/Signature.swift" "Util/BoringSSL/CryptoKitErrors_boring.swift" - "Util/BoringSSL/Optional+withUnsafeBytes_boring.swift" "Util/BoringSSL/RNG_boring.swift" "Util/BoringSSL/SafeCompare_boring.swift" "Util/BoringSSL/Zeroization_boring.swift" diff --git a/Sources/CryptoBoringWrapper/CMakeLists.txt b/Sources/CryptoBoringWrapper/CMakeLists.txt index 980f33c4d..9e62664fb 100644 --- a/Sources/CryptoBoringWrapper/CMakeLists.txt +++ b/Sources/CryptoBoringWrapper/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(CryptoBoringWrapper STATIC "EC/EllipticCurvePoint.swift" "Util/ArbitraryPrecisionInteger.swift" "Util/FiniteFieldArithmeticContext.swift" + "Util/Optional+withUnsafeBytes.swift" "Util/RandomBytes.swift" ) diff --git a/Sources/Crypto/Util/BoringSSL/Optional+withUnsafeBytes_boring.swift b/Sources/CryptoBoringWrapper/Util/Optional+withUnsafeBytes.swift similarity index 86% rename from Sources/Crypto/Util/BoringSSL/Optional+withUnsafeBytes_boring.swift rename to Sources/CryptoBoringWrapper/Util/Optional+withUnsafeBytes.swift index 8d8aabaca..e06f884fa 100644 --- a/Sources/Crypto/Util/BoringSSL/Optional+withUnsafeBytes_boring.swift +++ b/Sources/CryptoBoringWrapper/Util/Optional+withUnsafeBytes.swift @@ -19,7 +19,9 @@ import Foundation #endif extension Optional where Wrapped: DataProtocol { - func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ReturnValue) rethrows -> ReturnValue { + package func withUnsafeBytes( + _ 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) } diff --git a/Sources/CryptoExtras/CMakeLists.txt b/Sources/CryptoExtras/CMakeLists.txt index e08107032..b5ab9ffba 100644 --- a/Sources/CryptoExtras/CMakeLists.txt +++ b/Sources/CryptoExtras/CMakeLists.txt @@ -57,6 +57,7 @@ add_library(CryptoExtras "RSA/RSA.swift" "RSA/RSA_boring.swift" "Reexport.swift" + "SLHDSA/SLHDSA_boring.swift" "Util/BoringSSLHelpers.swift" "Util/CryptoKitErrors_boring.swift" "Util/Data+Extensions.swift" diff --git a/Sources/CryptoExtras/Docs.docc/index.md b/Sources/CryptoExtras/Docs.docc/index.md index 9bda3af77..2dcc31bab 100644 --- a/Sources/CryptoExtras/Docs.docc/index.md +++ b/Sources/CryptoExtras/Docs.docc/index.md @@ -15,6 +15,7 @@ Provides additional cryptographic APIs that are not available in CryptoKit (and ### Public key cryptography - ``_RSA`` +- ``SLHDSA`` ### Key derivation functions diff --git a/Sources/CryptoExtras/SLHDSA/SLHDSA_boring.swift b/Sources/CryptoExtras/SLHDSA/SLHDSA_boring.swift new file mode 100644 index 000000000..ec4b19638 --- /dev/null +++ b/Sources/CryptoExtras/SLHDSA/SLHDSA_boring.swift @@ -0,0 +1,314 @@ +//===----------------------------------------------------------------------===// +// +// 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 CryptoBoringWrapper + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A stateless hash-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 SLHDSA {} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension SLHDSA { + /// The SLH-DSA-SHA2-128s parameter set. + public enum SHA2_128s {} +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension SLHDSA.SHA2_128s { + /// A SLH-DSA-SHA2-128s private key. + public struct PrivateKey: @unchecked Sendable { + private var backing: Backing + + /// Initialize a SLH-DSA-SHA2-128s private key from a random seed. + public init() { + self.backing = Backing() + } + + /// Initialize a SLH-DSA-SHA2-128s private key from a raw representation. + /// + /// - Parameter rawRepresentation: The private 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 representation of the private key. + public var rawRepresentation: Data { + self.backing.rawRepresentation + } + + /// 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(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(for data: D, context: C) throws -> Data { + try self.backing.signature(for: data, context: context) + } + + fileprivate final class Backing { + private let pointer: UnsafeMutableBufferPointer + + func withUnsafePointer(_ body: (UnsafePointer) throws -> T) rethrows -> T { + try body(self.pointer.baseAddress!) + } + + /// Initialize a SLH-DSA-SHA2-128s private key from a random seed. + init() { + self.pointer = UnsafeMutableBufferPointer.allocate( + capacity: SLHDSA.SHA2_128s.PrivateKey.Backing.byteCount + ) + + withUnsafeTemporaryAllocation( + of: UInt8.self, + capacity: SLHDSA.SHA2_128s.PublicKey.Backing.byteCount + ) { publicKeyPtr in + CCryptoBoringSSL_SLHDSA_SHA2_128S_generate_key(publicKeyPtr.baseAddress, self.pointer.baseAddress) + } + } + + /// Initialize a SLH-DSA-SHA2-128s private key from a raw representation. + /// + /// - Parameter rawRepresentation: The private key bytes. + /// + /// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size. + init(rawRepresentation: some DataProtocol) throws { + guard rawRepresentation.count == SLHDSA.SHA2_128s.PrivateKey.Backing.byteCount else { + throw CryptoKitError.incorrectKeySize + } + + self.pointer = UnsafeMutableBufferPointer.allocate( + capacity: SLHDSA.SHA2_128s.PrivateKey.Backing.byteCount + ) + _ = self.pointer.initialize(fromContentsOf: rawRepresentation) + } + + /// The raw representation of the private key. + var rawRepresentation: Data { + Data( + UnsafeBufferPointer( + start: self.pointer.baseAddress, + count: SLHDSA.SHA2_128s.PrivateKey.Backing.byteCount + ) + ) + } + + /// 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(for data: D, context: C?) throws -> Data { + var signature = Data(repeating: 0, count: SLHDSA.SHA2_128s.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_SLHDSA_SHA2_128S_sign( + signaturePtr.baseAddress, + self.pointer.baseAddress, + 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 = 64 + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension SLHDSA.SHA2_128s { + /// A SLH-DSA-SHA2-128s public key. + public struct PublicKey: @unchecked Sendable { + private var backing: Backing + + fileprivate init(privateKeyBacking: PrivateKey.Backing) { + self.backing = Backing(privateKeyBacking: privateKeyBacking) + } + + /// Initialize a SLH-DSA-SHA2-128s 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 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( + _ 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( + _ signature: S, + for data: D, + context: C + ) -> Bool { + self.backing.isValidSignature(signature, for: data, context: context) + } + + fileprivate final class Backing { + private let pointer: UnsafeMutableBufferPointer + + init(privateKeyBacking: PrivateKey.Backing) { + self.pointer = UnsafeMutableBufferPointer.allocate( + capacity: SLHDSA.SHA2_128s.PublicKey.Backing.byteCount + ) + privateKeyBacking.withUnsafePointer { privateKeyPtr in + CCryptoBoringSSL_SLHDSA_SHA2_128S_public_from_private(self.pointer.baseAddress, privateKeyPtr) + } + } + + /// Initialize a SLH-DSA-SHA2-128s 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 == SLHDSA.SHA2_128s.PublicKey.Backing.byteCount else { + throw CryptoKitError.incorrectKeySize + } + + self.pointer = UnsafeMutableBufferPointer.allocate( + capacity: SLHDSA.SHA2_128s.PublicKey.Backing.byteCount + ) + _ = self.pointer.initialize(fromContentsOf: rawRepresentation) + } + + /// The raw representation of the public key. + var rawRepresentation: Data { + Data( + UnsafeBufferPointer( + start: self.pointer.baseAddress, + count: SLHDSA.SHA2_128s.PublicKey.Backing.byteCount + ) + ) + } + + /// 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( + _ 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_SLHDSA_SHA2_128S_verify( + signaturePtr.baseAddress, + signaturePtr.count, + self.pointer.baseAddress, + dataPtr.baseAddress, + dataPtr.count, + contextPtr.baseAddress, + contextPtr.count + ) + } + } + return rc == 1 + } + } + + /// The size of the public key in bytes. + static let byteCount = 32 + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension SLHDSA.SHA2_128s { + /// The size of the signature in bytes. + private static let signatureByteCount = 7856 +} diff --git a/Tests/CryptoExtrasTests/SLHDSATests.swift b/Tests/CryptoExtrasTests/SLHDSATests.swift new file mode 100644 index 000000000..6f8fb22a1 --- /dev/null +++ b/Tests/CryptoExtrasTests/SLHDSATests.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest + +@testable import _CryptoExtras + +final class SLHDSATests: XCTestCase { + func testSLHDSA_SHA2_128sSigning() throws { + let key = SLHDSA.SHA2_128s.PrivateKey() + let message = Data("Hello, World!".utf8) + let signature = try key.signature(for: message) + + XCTAssertTrue( + key.publicKey.isValidSignature( + signature, + for: message + ) + ) + + let context = Data("ctx".utf8) + + XCTAssertFalse( + key.publicKey.isValidSignature( + signature, + for: message, + context: context + ) + ) + + try XCTAssertTrue( + key.publicKey.isValidSignature( + key.signature(for: message, context: context), + for: message, + context: context + ) + ) + } + + func testEmptyMessageAndContext() throws { + let key = SLHDSA.SHA2_128s.PrivateKey() + let emptyMessage = Data("".utf8) + let emptyContext = Data("".utf8) + + try XCTAssertTrue( + key.publicKey.isValidSignature( + key.signature(for: emptyMessage, context: emptyContext), + for: emptyMessage, + context: emptyContext + ) + ) + } + + func testMaxContextLenght() throws { + let key = SLHDSA.SHA2_128s.PrivateKey() + let message = Array("Hello, World!".utf8) + + let context = [UInt8](repeating: 0, count: 255) // Maximum allowed context length + try XCTAssertTrue( + key.publicKey.isValidSignature( + key.signature(for: message, context: context), + for: message, + context: context + ) + ) + + let tooLongContext = [UInt8](repeating: 0, count: 256) + XCTAssertThrowsError( + try key.signature(for: message, context: tooLongContext) + ) + } + + func testSignatureIsRandomized() throws { + let message = "Hello, world!".data(using: .utf8)! + + let key = SLHDSA.SHA2_128s.PrivateKey() + let publicKey = key.publicKey + + let signature1 = try key.signature(for: message) + let signature2 = try key.signature(for: message) + + XCTAssertNotEqual(signature1, signature2) + + // Even though the signatures are different, they both verify. + XCTAssertTrue(publicKey.isValidSignature(signature1, for: message)) + XCTAssertTrue(publicKey.isValidSignature(signature2, for: message)) + } +} diff --git a/scripts/vendor-boringssl.sh b/scripts/vendor-boringssl.sh index 324099fe8..e51ca14c4 100755 --- a/scripts/vendor-boringssl.sh +++ b/scripts/vendor-boringssl.sh @@ -407,6 +407,7 @@ cat << EOF > "$DSTROOT/include/CCryptoBoringSSL.h" #include "CCryptoBoringSSL_safestack.h" #include "CCryptoBoringSSL_sha.h" #include "CCryptoBoringSSL_siphash.h" +#include "CCryptoBoringSSL_slhdsa.h" #include "CCryptoBoringSSL_trust_token.h" #include "CCryptoBoringSSL_x509v3.h" #include "CCryptoBoringSSL_xwing.h"