Skip to content

SPHINCS+ #2

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

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ let package = Package(
"CMakeLists.txt",
],
resources: privacyManifestResource,
cSettings: [
// Enable SPHINCS+
.define("OPENSSL_UNSTABLE_EXPERIMENTAL_SPX"),
],
swiftSettings: swiftSettings
),
.target(
Expand Down
1 change: 1 addition & 0 deletions Sources/CCryptoBoringSSL/include/CCryptoBoringSSL.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
#include "CCryptoBoringSSL_safestack.h"
#include "CCryptoBoringSSL_sha.h"
#include "CCryptoBoringSSL_siphash.h"
#include "experimental/CCryptoBoringSSL_spx.h"
#include "CCryptoBoringSSL_trust_token.h"
#include "CCryptoBoringSSL_x509v3.h"

Expand Down
1 change: 1 addition & 0 deletions Sources/_CryptoExtras/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ add_library(_CryptoExtras
"RSA/RSA.swift"
"RSA/RSA_boring.swift"
"RSA/RSA_security.swift"
"SPX/SPX_boring.swift"
"Util/BoringSSLHelpers.swift"
"Util/CryptoKitErrors_boring.swift"
"Util/DigestType.swift"
Expand Down
209 changes: 209 additions & 0 deletions Sources/_CryptoExtras/SPX/SPX_boring.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//===----------------------------------------------------------------------===//
//
// 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

/// The SPHINCS+-SHA2-128s digital signature algorithm, which provides security against quantum computing attacks.
public enum SPX {}

extension SPX {
/// A SPHINCS+-SHA2-128s private key.
public struct PrivateKey: Sendable {
private let pointer: UnsafeMutablePointer<UInt8>

/// Initialize a SPHINCS+-SHA2-128s private key from a random seed.
public init() {
self.pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: SPX.PrivateKey.bytesCount)

withUnsafeTemporaryAllocation(of: UInt8.self, capacity: SPX.PublicKey.bytesCount) { publicKeyPtr in
CCryptoBoringSSL_SPX_generate_key(publicKeyPtr.baseAddress, self.pointer)
}
}

// Initialize a SPHINCS+-SHA2-128s private key from a seed.
///
/// - Parameter seed: The seed to use to generate the private key.
///
/// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 48 bytes long.
public init(seed: some DataProtocol) throws {
guard seed.count == SPX.seedSizeInBytes else {
throw CryptoKitError.incorrectKeySize
}

self.pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: SPX.PrivateKey.bytesCount)

withUnsafeTemporaryAllocation(of: UInt8.self, capacity: SPX.PublicKey.bytesCount) { publicKeyPtr in
Data(seed).withUnsafeBytes { seedPtr in
CCryptoBoringSSL_SPX_generate_key_from_seed(
publicKeyPtr.baseAddress,
self.pointer,
seedPtr.baseAddress
)
}
}
}

/// Initialize a SPHINCS+-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 {
guard rawRepresentation.count == SPX.PrivateKey.bytesCount else {
throw CryptoKitError.incorrectKeySize
}

self.pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: SPX.PrivateKey.bytesCount)

self.pointer.initialize(from: Array(rawRepresentation), count: SPX.PrivateKey.bytesCount)
}

/// The raw representation of the private key.
public var rawRepresentation: Data {
Data(UnsafeBufferPointer(start: self.pointer, count: SPX.PrivateKey.bytesCount))
}

/// The public key associated with this private key.
public var publicKey: PublicKey {
PublicKey(privateKeyBacking: self)
}

/// Generate a signature for the given data.
///
/// - Parameters:
/// - data: The message to sign.
/// - randomized: Whether to randomize the signature.
///
/// - Returns: The signature of the message.
public func signature(for data: some DataProtocol, randomized: Bool = false) -> Signature {
let output = [UInt8](unsafeUninitializedCapacity: Signature.bytesCount) { bufferPtr, length in
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
bytes.withUnsafeBytes { dataPtr in
CCryptoBoringSSL_SPX_sign(
bufferPtr.baseAddress,
self.pointer,
dataPtr.baseAddress,
dataPtr.count,
randomized ? 1 : 0
)
}
length = Signature.bytesCount
}
return Signature(signatureBytes: output)
}

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

extension SPX {
/// A SPHINCS+-SHA2-128s public key.
public struct PublicKey: Sendable {
private let pointer: UnsafeMutablePointer<UInt8>

fileprivate init(privateKeyBacking: PrivateKey) {
self.pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: SPX.PublicKey.bytesCount)
self.pointer.initialize(
from: privateKeyBacking.rawRepresentation.suffix(SPX.PublicKey.bytesCount),
count: SPX.PublicKey.bytesCount
)
}

/// Initialize a SPHINCS+-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 {
guard rawRepresentation.count == SPX.PublicKey.bytesCount else {
throw CryptoKitError.incorrectKeySize
}

self.pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: SPX.PublicKey.bytesCount)

self.pointer.initialize(from: Array(rawRepresentation), count: SPX.PublicKey.bytesCount)
}

/// The raw representation of the public key.
public var rawRepresentation: Data {
Data(UnsafeBufferPointer(start: self.pointer, count: SPX.PublicKey.bytesCount))
}

/// 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<D: DataProtocol>(_ signature: Signature, for data: D) -> Bool {
signature.withUnsafeBytes { signaturePtr in
let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data)
let rc: CInt = bytes.withUnsafeBytes { dataPtr in
CCryptoBoringSSL_SPX_verify(
signaturePtr.baseAddress,
self.pointer,
dataPtr.baseAddress,
dataPtr.count
)
}
return rc == 1
}
}

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

extension SPX {
/// A SPHINCS+-SHA2-128s signature.
public struct Signature: Sendable, ContiguousBytes {
/// The raw binary representation of the signature.
public var rawRepresentation: Data

/// Initialize a SPHINCS+-SHA2-128s signature from a raw representation.
///
/// - Parameter rawRepresentation: The signature bytes.
public init(rawRepresentation: some DataProtocol) {
self.rawRepresentation = Data(rawRepresentation)
}

/// Initialize a SPHINCS+-SHA2-128s signature from a raw representation.
///
/// - Parameter rawRepresentation: 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.
static let bytesCount = 7856
}
}

extension SPX {
static let seedSizeInBytes = 3 * 16
}
161 changes: 161 additions & 0 deletions Tests/_CryptoExtrasTests/SPXTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//===----------------------------------------------------------------------===//
//
// 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 SPXTests: XCTestCase {
func testSPXSigning() throws {
testSPXSigning(SPX.PrivateKey())
let seed: [UInt8] = (0..<48).map { _ in UInt8.random(in: 0...255) }
testSPXSigning(try SPX.PrivateKey(seed: seed))
}

private func testSPXSigning(_ key: SPX.PrivateKey) {
let test = Data("Hello, World!".utf8)

XCTAssertTrue(
key.publicKey.isValidSignature(
key.signature(for: test),
for: test
)
)

// Test randomized signature
XCTAssertTrue(
key.publicKey.isValidSignature(
key.signature(for: test, randomized: true),
for: test
)
)
}

func testSignatureSerialization() {
let data = Array("Hello, World!".utf8)
let key = SPX.PrivateKey()
let signature = key.signature(for: data)
let roundTripped = SPX.Signature(rawRepresentation: signature.rawRepresentation)
XCTAssertEqual(signature.rawRepresentation, roundTripped.rawRepresentation)
XCTAssertTrue(key.publicKey.isValidSignature(roundTripped, for: data))
}

func testSPXKeyGeneration() throws {
let seed: [UInt8] = Array(repeating: 0, count: SPX.seedSizeInBytes)

let expectedPublicKey: [UInt8] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xbe, 0x6b, 0xd7, 0xe8, 0xe1, 0x98,
0xea, 0xf6, 0x2d, 0x57, 0x2f, 0x13, 0xfc, 0x79, 0xf2, 0x6f,
]

let expectedSecretKey: [UInt8] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xbe, 0x6b, 0xd7, 0xe8, 0xe1, 0x98, 0xea,
0xf6, 0x2d, 0x57, 0x2f, 0x13, 0xfc, 0x79, 0xf2, 0x6f,
]

let key = try SPX.PrivateKey(seed: seed)
XCTAssertEqual(Array(key.publicKey.rawRepresentation), expectedPublicKey)
XCTAssertEqual(Array(key.rawRepresentation), expectedSecretKey)
}

func testSPXKeyGeneration2() throws {
let seed: [UInt8] = [
0x3f, 0x00, 0xff, 0x1c, 0x9c, 0x5e, 0xaa, 0xfe, 0x09, 0xc3, 0x08, 0x0d,
0xac, 0xc1, 0x83, 0x2b, 0x35, 0x8a, 0x40, 0xd5, 0xf3, 0x8c, 0xcb, 0x97,
0xe3, 0xa6, 0xc1, 0xb3, 0xb7, 0x5f, 0x42, 0xab, 0x17, 0x34, 0xe6, 0x41,
0x89, 0xe1, 0x57, 0x93, 0x12, 0x74, 0xdb, 0xbd, 0xb4, 0x28, 0xd0, 0xfb,
]

let expectedPublicKey: [UInt8] = [
0x17, 0x34, 0xe6, 0x41, 0x89, 0xe1, 0x57, 0x93, 0x12, 0x74, 0xdb,
0xbd, 0xb4, 0x28, 0xd0, 0xfb, 0x59, 0xc8, 0x64, 0xd2, 0x52, 0x96,
0xa9, 0x22, 0xdc, 0x61, 0xb8, 0xc1, 0x92, 0x15, 0xac, 0x74,
]

let expectedSecretKey: [UInt8] = [
0x3f, 0x00, 0xff, 0x1c, 0x9c, 0x5e, 0xaa, 0xfe, 0x09, 0xc3, 0x08,
0x0d, 0xac, 0xc1, 0x83, 0x2b, 0x35, 0x8a, 0x40, 0xd5, 0xf3, 0x8c,
0xcb, 0x97, 0xe3, 0xa6, 0xc1, 0xb3, 0xb7, 0x5f, 0x42, 0xab, 0x17,
0x34, 0xe6, 0x41, 0x89, 0xe1, 0x57, 0x93, 0x12, 0x74, 0xdb, 0xbd,
0xb4, 0x28, 0xd0, 0xfb, 0x59, 0xc8, 0x64, 0xd2, 0x52, 0x96, 0xa9,
0x22, 0xdc, 0x61, 0xb8, 0xc1, 0x92, 0x15, 0xac, 0x74,
]

let key = try SPX.PrivateKey(seed: seed)
XCTAssertEqual(Array(key.publicKey.rawRepresentation), expectedPublicKey)
XCTAssertEqual(Array(key.rawRepresentation), expectedSecretKey)
}

func testSPXSigningFile() throws {
try spxTest(jsonName: "spx_tests") { testVector in
var message = try Data(hexString: testVector.msg)
let publicKey = try SPX.PublicKey(rawRepresentation: Data(hexString: testVector.pk))
let signature = try SPX.Signature(rawRepresentation: Data(hexString: testVector.sm))
XCTAssertTrue(publicKey.isValidSignature(signature, for: message))
message[0] ^= 1
XCTAssertFalse(publicKey.isValidSignature(signature, for: message))
}
}

func testSPXSigningDeterministicFile() throws {
try spxTest(jsonName: "spx_tests_deterministic") { testVector in
let message = try Data(hexString: testVector.msg)
let secretKey = try SPX.PrivateKey(rawRepresentation: Data(hexString: testVector.sk))
let expectedSignature = try SPX.Signature(
rawRepresentation: Data(hexString: testVector.sm).prefix(SPX.Signature.bytesCount)
)
let signature = secretKey.signature(for: message)
XCTAssertEqual(signature.rawRepresentation, expectedSignature.rawRepresentation)
}
}
}

struct SPXTestVector: Decodable {
let count: Int
let seed: String
let mlen: Int
let msg: String
let pk: String
let sk: String
let smlen: Int
let sm: String
}

struct SPXTestVectorFile: Decodable {
let testVectors: [SPXTestVector]
}

func spxTest(
jsonName: String,
file: StaticString = #file,
line: UInt = #line,
testFunction: (SPXTestVector) throws -> Void
) throws {
let testsDirectory: String = URL(fileURLWithPath: "\(#file)").pathComponents.dropLast(2).joined(separator: "/")
let fileURL: URL? = URL(fileURLWithPath: "\(testsDirectory)/_CryptoExtrasVectors/\(jsonName).json")

let data = try Data(contentsOf: fileURL!)

let decoder = JSONDecoder()
let testFile = try decoder.decode(SPXTestVectorFile.self, from: data)

for vector in testFile.testVectors {
try testFunction(vector)
}
}
Loading
Loading