Skip to content

Commit e3c198a

Browse files
Support singing via Certificate PrivateKey (#257)
This PR introduces two key changes to improve flexibility when working with signatures in swift-certificates. Previously, it was only possible to verify signatures using `Certificate.PrivateKey`, but not to generate them. - The internal `sign(bytes:signatureAlgorithm:)` method on `Certificate.PrivateKey` is now public, allowing users to generate signatures directly. - `Certificate.Signature` gains a new `rawRepresentation` property that exposes the raw byte representation of a signature.
1 parent 8bcf6a7 commit e3c198a

3 files changed

Lines changed: 60 additions & 9 deletions

File tree

Sources/X509/CertificatePrivateKey.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,14 @@ extension Certificate {
9191
}
9292
#endif
9393

94+
/// Use the private key to sign the provided bytes with a given signature algorithm.
95+
///
96+
/// - Parameters:
97+
/// - bytes: The data to create the signature for.
98+
/// - signatureAlgorithm: The signature algorithm to use.
99+
/// - Returns: The signature.
94100
@inlinable
95-
internal func sign<Bytes: DataProtocol>(
101+
public func sign<Bytes: DataProtocol>(
96102
bytes: Bytes,
97103
signatureAlgorithm: SignatureAlgorithm
98104
) throws -> Signature {

Sources/X509/Signature.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,31 @@ extension Certificate.Signature {
129129
}
130130

131131
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
132-
extension ASN1BitString {
132+
extension Certificate.Signature {
133+
/// The raw byte representation of the signature.
133134
@inlinable
134-
init(_ signature: Certificate.Signature) {
135-
switch signature.backing {
135+
public var rawRepresentation: [UInt8] {
136+
switch self.backing {
136137
case .ecdsa(let sig):
137138
var serializer = DER.Serializer()
138139
try! serializer.serialize(sig)
139-
self = ASN1BitString(bytes: serializer.serializedBytes[...])
140-
case .rsa(let sig):
141-
self = ASN1BitString(bytes: ArraySlice(sig.rawRepresentation))
142-
case .ed25519(let sig):
143-
self = ASN1BitString(bytes: ArraySlice(sig))
140+
return serializer.serializedBytes
141+
case let .ed25519(data):
142+
return .init(data)
143+
case let .rsa(signature):
144+
return .init(signature.rawRepresentation)
144145
}
145146
}
146147
}
147148

149+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
150+
extension ASN1BitString {
151+
@inlinable
152+
init(_ signature: Certificate.Signature) {
153+
self.init(bytes: signature.rawRepresentation[...])
154+
}
155+
}
156+
148157
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
149158
extension ASN1OctetString {
150159
@inlinable

Tests/X509Tests/SignatureTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,40 @@ final class SignatureTests: XCTestCase {
5050
)
5151
#endif
5252

53+
func testRSASignatureBytes() throws {
54+
let input = Array("Hello World".utf8)
55+
let privateKey = Certificate.PrivateKey(Self.rsaKey)
56+
57+
let expected = try Self.rsaKey.signature(for: SHA256.hash(data: input), padding: .insecurePKCS1v1_5)
58+
let found = try privateKey.sign(bytes: input, signatureAlgorithm: .sha256WithRSAEncryption)
59+
60+
XCTAssertEqual(.init(expected.rawRepresentation), found.rawRepresentation)
61+
}
62+
63+
func testEd25519SignatureBytes() throws {
64+
let input = Array("Hello World".utf8)
65+
66+
let expected = try Self.ed25519Key.signature(for: input)
67+
let signature = try Certificate.Signature(
68+
signatureAlgorithm: .ed25519,
69+
signatureBytes: .init(bytes: Array(expected)[...])
70+
)
71+
72+
XCTAssertEqual(.init(expected), signature.rawRepresentation)
73+
}
74+
75+
func testECDSASignatureBytes() throws {
76+
let input = Array("Hello World".utf8)
77+
78+
let expected = try Self.p384Key.signature(for: SHA256.hash(data: input))
79+
let signature = try Certificate.Signature(
80+
signatureAlgorithm: .ecdsaWithSHA256,
81+
signatureBytes: .init(bytes: Array(expected.derRepresentation)[...])
82+
)
83+
84+
XCTAssertEqual(.init(expected.derRepresentation), signature.rawRepresentation)
85+
}
86+
5387
func testP384Signature() throws {
5488
// This is the P384 signature over LetsEncrypt Intermediate E1.
5589
let signatureBytes: [UInt8] = [
@@ -75,6 +109,8 @@ final class SignatureTests: XCTestCase {
75109
return
76110
}
77111

112+
XCTAssertEqual(signature.rawRepresentation, .init(signatureBytes))
113+
78114
// Validate that the signature is valid over the TBS certificate bytes.
79115
let issuingPublicKeyBytes: [UInt8] = [
80116
0x04, 0xCD, 0x9B, 0xD5, 0x9F, 0x80, 0x83, 0x0A, 0xEC, 0x09, 0x4A, 0xF3,

0 commit comments

Comments
 (0)