From 39ed414a41f57918081aac7f52126c8a55896a12 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 23 Jun 2025 13:55:08 +0200 Subject: [PATCH 1/5] Support singing via Certificate PrivateKey --- Sources/X509/CertificatePrivateKey.swift | 8 +++++++- Sources/X509/Signature.swift | 19 +++++++++++++++++++ Tests/X509Tests/SignatureTests.swift | 21 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Sources/X509/CertificatePrivateKey.swift b/Sources/X509/CertificatePrivateKey.swift index a875653f..33b46d68 100644 --- a/Sources/X509/CertificatePrivateKey.swift +++ b/Sources/X509/CertificatePrivateKey.swift @@ -91,8 +91,14 @@ extension Certificate { } #endif + /// Use the private key to sign the provided bytes with a given signature algorithm. + /// + /// - Parameters: + /// - bytes: The data to create the signature for. + /// - signatureAlgorithm: The signature algorithm to use. + /// - Returns: The signature. @inlinable - internal func sign( + public func sign( bytes: Bytes, signatureAlgorithm: SignatureAlgorithm ) throws -> Signature { diff --git a/Sources/X509/Signature.swift b/Sources/X509/Signature.swift index 9fec295e..760f0bc5 100644 --- a/Sources/X509/Signature.swift +++ b/Sources/X509/Signature.swift @@ -128,6 +128,25 @@ extension Certificate.Signature { } } +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension Certificate.Signature { + /// The raw byte representation of the signature. + @inlinable + public var rawRepresentation: [UInt8] { + switch backing { + case let .ecdsa(signature): + var serializer = DER.Serializer() + // we are serializing from raw bytes, force try is safe here + try! signature.serialize(into: &serializer, withIdentifier: ECDSASignature.defaultIdentifier) + return serializer.serializedBytes + case let .ed25519(data): + return .init(data) + case let .rsa(signature): + return .init(signature.rawRepresentation) + } + } +} + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) extension ASN1BitString { @inlinable diff --git a/Tests/X509Tests/SignatureTests.swift b/Tests/X509Tests/SignatureTests.swift index efc9d0fe..ccb5cba5 100644 --- a/Tests/X509Tests/SignatureTests.swift +++ b/Tests/X509Tests/SignatureTests.swift @@ -50,6 +50,25 @@ final class SignatureTests: XCTestCase { ) #endif + func testRSASignatureBytes() throws { + let input = Array("Hello World".utf8) + let privateKey = Certificate.PrivateKey(Self.rsaKey) + + let expected = try Self.rsaKey.signature(for: SHA256.hash(data: input), padding: .insecurePKCS1v1_5) + let found = try privateKey.sign(bytes: input, signatureAlgorithm: .sha256WithRSAEncryption) + + XCTAssertEqual(.init(expected.rawRepresentation), found.rawRepresentation) + } + + func testEd25519SignatureBytes() throws { + let input = Array("Hello World".utf8) + + let expected = try Self.ed25519Key.signature(for: input) + let signature = try Certificate.Signature(signatureAlgorithm: .ed25519, signatureBytes: .init(bytes: Array(expected)[...])) + + XCTAssertEqual(.init(expected), signature.rawRepresentation) + } + func testP384Signature() throws { // This is the P384 signature over LetsEncrypt Intermediate E1. let signatureBytes: [UInt8] = [ @@ -75,6 +94,8 @@ final class SignatureTests: XCTestCase { return } + XCTAssertEqual(signature.rawRepresentation, signatureBytes) + // Validate that the signature is valid over the TBS certificate bytes. let issuingPublicKeyBytes: [UInt8] = [ 0x04, 0xCD, 0x9B, 0xD5, 0x9F, 0x80, 0x83, 0x0A, 0xEC, 0x09, 0x4A, 0xF3, From b09985dc7483a78b388bb5dad04e950b69ce13ad Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 23 Jun 2025 14:10:39 +0200 Subject: [PATCH 2/5] Format --- Tests/X509Tests/SignatureTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/X509Tests/SignatureTests.swift b/Tests/X509Tests/SignatureTests.swift index ccb5cba5..0da344a9 100644 --- a/Tests/X509Tests/SignatureTests.swift +++ b/Tests/X509Tests/SignatureTests.swift @@ -64,7 +64,10 @@ final class SignatureTests: XCTestCase { let input = Array("Hello World".utf8) let expected = try Self.ed25519Key.signature(for: input) - let signature = try Certificate.Signature(signatureAlgorithm: .ed25519, signatureBytes: .init(bytes: Array(expected)[...])) + let signature = try Certificate.Signature( + signatureAlgorithm: .ed25519, + signatureBytes: .init(bytes: Array(expected)[...]) + ) XCTAssertEqual(.init(expected), signature.rawRepresentation) } From f54776e497ba6a5e836584c8473ad9fa72a8d640 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 30 Jun 2025 17:21:22 +0200 Subject: [PATCH 3/5] Add test for ECDSA --- Tests/X509Tests/SignatureTests.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Tests/X509Tests/SignatureTests.swift b/Tests/X509Tests/SignatureTests.swift index 0da344a9..3660dc91 100644 --- a/Tests/X509Tests/SignatureTests.swift +++ b/Tests/X509Tests/SignatureTests.swift @@ -72,6 +72,18 @@ final class SignatureTests: XCTestCase { XCTAssertEqual(.init(expected), signature.rawRepresentation) } + func testECDSASignatureBytes() throws { + let input = Array("Hello World".utf8) + + let expected = try Self.p384Key.signature(for: SHA256.hash(data: input)) + let signature = try Certificate.Signature( + signatureAlgorithm: .ecdsaWithSHA256, + signatureBytes: .init(bytes: Array(expected.derRepresentation)[...]) + ) + + XCTAssertEqual(.init(expected.derRepresentation), signature.rawRepresentation) + } + func testP384Signature() throws { // This is the P384 signature over LetsEncrypt Intermediate E1. let signatureBytes: [UInt8] = [ @@ -97,7 +109,7 @@ final class SignatureTests: XCTestCase { return } - XCTAssertEqual(signature.rawRepresentation, signatureBytes) + XCTAssertEqual(signature.rawRepresentation, .init(signatureBytes)) // Validate that the signature is valid over the TBS certificate bytes. let issuingPublicKeyBytes: [UInt8] = [ From c530747bf5740567b3805ae0aae886554496d7d5 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 30 Jun 2025 17:21:30 +0200 Subject: [PATCH 4/5] Reuse serialization from ASN1BitString init --- Sources/X509/Signature.swift | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Sources/X509/Signature.swift b/Sources/X509/Signature.swift index 760f0bc5..1fe06bb4 100644 --- a/Sources/X509/Signature.swift +++ b/Sources/X509/Signature.swift @@ -132,18 +132,9 @@ extension Certificate.Signature { extension Certificate.Signature { /// The raw byte representation of the signature. @inlinable - public var rawRepresentation: [UInt8] { - switch backing { - case let .ecdsa(signature): - var serializer = DER.Serializer() - // we are serializing from raw bytes, force try is safe here - try! signature.serialize(into: &serializer, withIdentifier: ECDSASignature.defaultIdentifier) - return serializer.serializedBytes - case let .ed25519(data): - return .init(data) - case let .rsa(signature): - return .init(signature.rawRepresentation) - } + public var rawRepresentation: ArraySlice { + let bitString = ASN1BitString(self) + return bitString.bytes } } From cfa132b269e4118bf6c42c79e50372a7bf0755b5 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 30 Jun 2025 17:29:00 +0200 Subject: [PATCH 5/5] Flip the way we reuse --- Sources/X509/Signature.swift | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Sources/X509/Signature.swift b/Sources/X509/Signature.swift index 1fe06bb4..1787597c 100644 --- a/Sources/X509/Signature.swift +++ b/Sources/X509/Signature.swift @@ -132,9 +132,17 @@ extension Certificate.Signature { extension Certificate.Signature { /// The raw byte representation of the signature. @inlinable - public var rawRepresentation: ArraySlice { - let bitString = ASN1BitString(self) - return bitString.bytes + public var rawRepresentation: [UInt8] { + switch self.backing { + case .ecdsa(let sig): + var serializer = DER.Serializer() + try! serializer.serialize(sig) + return serializer.serializedBytes + case let .ed25519(data): + return .init(data) + case let .rsa(signature): + return .init(signature.rawRepresentation) + } } } @@ -142,16 +150,7 @@ extension Certificate.Signature { extension ASN1BitString { @inlinable init(_ signature: Certificate.Signature) { - switch signature.backing { - case .ecdsa(let sig): - var serializer = DER.Serializer() - try! serializer.serialize(sig) - self = ASN1BitString(bytes: serializer.serializedBytes[...]) - case .rsa(let sig): - self = ASN1BitString(bytes: ArraySlice(sig.rawRepresentation)) - case .ed25519(let sig): - self = ASN1BitString(bytes: ArraySlice(sig)) - } + self.init(bytes: signature.rawRepresentation[...]) } }