diff --git a/Sources/X509/CSR/CertificateSigningRequest.swift b/Sources/X509/CSR/CertificateSigningRequest.swift index 1825a80..a134bb6 100644 --- a/Sources/X509/CSR/CertificateSigningRequest.swift +++ b/Sources/X509/CSR/CertificateSigningRequest.swift @@ -150,6 +150,45 @@ public struct CertificateSigningRequest { self.signatureBytes = try DER.Serializer.serialized(element: ASN1BitString(self.signature))[...] } + /// Construct a CSR for a specific private key. + /// + /// This API can be used to construct a certificate signing request that can be passed to a certificate + /// authority. It will correctly generate a signature over the request. + /// + /// - Parameters: + /// - version: The CSR version. + /// - subject: The ``DistinguishedName`` of the subject of this CSR + /// - asyncPrivateKey: The private key associated with this CSR. + /// - attributes: The attributes associated with this CSR + /// - signatureAlgorithm: The signature algorithm to use for the signature on this CSR. + @inlinable + public init( + version: Version, + subject: DistinguishedName, + asyncPrivateKey: Certificate.PrivateKey, + attributes: Attributes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) async throws { + self.info = CertificationRequestInfo( + version: version, + subject: subject, + publicKey: asyncPrivateKey.publicKey, + attributes: attributes + ) + self.signatureAlgorithm = signatureAlgorithm + + let infoBytes = try DER.Serializer.serialized(element: self.info) + self.signature = try await asyncPrivateKey.signAsynchronously( + bytes: infoBytes, + signatureAlgorithm: signatureAlgorithm + ) + self.infoBytes = infoBytes[...] + self.signatureAlgorithmBytes = try DER.Serializer.serialized( + element: AlgorithmIdentifier(self.signatureAlgorithm) + )[...] + self.signatureBytes = try DER.Serializer.serialized(element: ASN1BitString(self.signature))[...] + } + /// Construct a CSR for a specific private key. /// /// This API can be used to construct a certificate signing request that can be passed to a certificate diff --git a/Sources/X509/Certificate.swift b/Sources/X509/Certificate.swift index 4ea02e2..b5dad20 100644 --- a/Sources/X509/Certificate.swift +++ b/Sources/X509/Certificate.swift @@ -214,6 +214,66 @@ public struct Certificate { self.signatureBytes = try DER.Serializer.serialized(element: ASN1BitString(self.signature))[...] } + /// Construct a certificate from constituent parts, signed by an issuer key. + /// + /// This API can be used to construct a ``Certificate`` directly, without an intermediary + /// Certificate Signing Request. The ``signature-swift.property`` for this certificate will be produced + /// automatically, using `issuerAsyncPrivateKey`. + /// + /// This API can be used to construct a self-signed key by passing the private key for `publicKey` as the + /// `issuerAsyncPrivateKey` argument. + /// + /// - Parameters: + /// - version: The X.509 specification version for this certificate. + /// - serialNumber: The serial number of this certificate. + /// - publicKey: The public key associated with this certificate. + /// - notValidBefore: The date before which this certificate is not valid. + /// - notValidAfter: The date after which this certificate is not valid. + /// - issuer: The ``DistinguishedName`` of the issuer of this certificate. + /// - subject: The ``DistinguishedName`` of the subject of this certificate. + /// - signatureAlgorithm: The signature algorithm that will be used to produce `signature`. Must be compatible with the private key type. + /// - extensions: The extensions on this certificate. + /// - issuerAsyncPrivateKey: The private key to use to sign this certificate. + @inlinable + public init( + version: Version, + serialNumber: SerialNumber, + publicKey: PublicKey, + notValidBefore: Date, + notValidAfter: Date, + issuer: DistinguishedName, + subject: DistinguishedName, + signatureAlgorithm: SignatureAlgorithm, + extensions: Extensions, + issuerAsyncPrivateKey: PrivateKey + ) async throws { + self.tbsCertificate = TBSCertificate( + version: version, + serialNumber: serialNumber, + signature: signatureAlgorithm, + issuer: issuer, + validity: try Validity( + notBefore: .makeTime(from: notValidBefore), + notAfter: .makeTime(from: notValidAfter) + ), + subject: subject, + publicKey: publicKey, + extensions: extensions + ) + self.signatureAlgorithm = signatureAlgorithm + + let tbsCertificateBytes = try DER.Serializer.serialized(element: self.tbsCertificate)[...] + self.signature = try await issuerAsyncPrivateKey.signAsynchronously( + bytes: tbsCertificateBytes, + signatureAlgorithm: signatureAlgorithm + ) + self.tbsCertificateBytes = tbsCertificateBytes + self.signatureAlgorithmBytes = try DER.Serializer.serialized( + element: AlgorithmIdentifier(self.signatureAlgorithm) + )[...] + self.signatureBytes = try DER.Serializer.serialized(element: ASN1BitString(self.signature))[...] + } + /// Construct a certificate from constituent parts, signed by an issuer key. /// /// This API can be used to construct a ``Certificate`` directly, without an intermediary diff --git a/Sources/X509/CertificatePrivateKey.swift b/Sources/X509/CertificatePrivateKey.swift index 33b46d6..680d0c1 100644 --- a/Sources/X509/CertificatePrivateKey.swift +++ b/Sources/X509/CertificatePrivateKey.swift @@ -91,6 +91,13 @@ extension Certificate { } #endif + /// Construct a private key wrapping a custom private key. + /// - Parameter custom: The custom private key to wrap. + @inlinable + public init(_ custom: some CustomPrivateKey) { + self.backing = .custom(custom) + } + /// Use the private key to sign the provided bytes with a given signature algorithm. /// /// - Parameters: @@ -119,6 +126,27 @@ extension Certificate { #endif case .ed25519(let ed25519): return try ed25519.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) + case .custom(let custom): + return try custom.signSynchronously(bytes: bytes, signatureAlgorithm: signatureAlgorithm) + } + } + + /// Use the private key to sign the provided bytes asynchronously with a given signature algorithm. + /// + /// - Parameters: + /// - bytes: The data to create the signature for. + /// - signatureAlgorithm: The signature algorithm to use. + /// - Returns: The signature. + @inlinable + public func signAsynchronously( + bytes: Bytes, + signatureAlgorithm: SignatureAlgorithm + ) async throws -> Signature { + switch self.backing { + case .custom(let custom): + return try await custom.signAsynchronously(bytes: bytes, signatureAlgorithm: signatureAlgorithm) + default: + return try self.sign(bytes: bytes, signatureAlgorithm: signatureAlgorithm) } } @@ -143,6 +171,8 @@ extension Certificate { #endif case .ed25519(let ed25519): return PublicKey(ed25519.publicKey) + case .custom(let custom): + return custom.publicKey } } @@ -177,6 +207,8 @@ extension Certificate { #endif case .ed25519: return .ed25519 + case .custom(let custom): + return custom.defaultSignatureAlgorithm } } } @@ -208,6 +240,11 @@ extension Certificate.PrivateKey: CustomStringConvertible { #endif case .ed25519: return "Ed25519.PrivateKey" + case .custom(let custom): + if let custom = custom as? CustomStringConvertible { + return custom.description + } + return "CustomPrivateKey" } } } @@ -225,6 +262,7 @@ extension Certificate.PrivateKey { case secKey(SecKeyWrapper) #endif case ed25519(Crypto.Curve25519.Signing.PrivateKey) + case custom(any CustomPrivateKey) @inlinable static func == (lhs: BackingPrivateKey, rhs: BackingPrivateKey) -> Bool { @@ -245,6 +283,8 @@ extension Certificate.PrivateKey { #endif case (.ed25519(let l), .ed25519(let r)): return l.rawRepresentation == r.rawRepresentation + case (.custom(let l), .custom(let r)): + return l.publicKey == r.publicKey default: return false } @@ -277,6 +317,8 @@ extension Certificate.PrivateKey { case .ed25519(let digest): hasher.combine(6) hasher.combine(digest.rawRepresentation) + case .custom(let key): + hasher.combine(key) } } } @@ -350,6 +392,7 @@ extension Certificate.PrivateKey { case .secKey(let key): return try key.pemDocument() #endif case .ed25519(let key): return key.pemRepresentation + case .custom(let key): return try key.serializeAsPEM() } } } diff --git a/Sources/X509/CustomPrivateKey.swift b/Sources/X509/CustomPrivateKey.swift new file mode 100644 index 0000000..7a8e707 --- /dev/null +++ b/Sources/X509/CustomPrivateKey.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import SwiftASN1 + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +public protocol CustomPrivateKey: Sendable, Hashable, PEMSerializable { + + /// Obtain the ``Certificate/PublicKey-swift.struct`` corresponding to + /// this private key. + var publicKey: Certificate.PublicKey { get } + + var defaultSignatureAlgorithm: Certificate.SignatureAlgorithm { get } + + /// 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 + func signSynchronously( + bytes: some DataProtocol, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature + + /// Use the private key to sign the provided bytes asynchronously with a given signature algorithm. + /// + /// - Parameters: + /// - bytes: The data to create the signature for. + /// - signatureAlgorithm: The signature algorithm to use. + /// - Returns: The signature. + @inlinable + func signAsynchronously( + bytes: some DataProtocol & Sendable, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) async throws -> Certificate.Signature + +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension CustomPrivateKey { + + public func signAsynchronously( + bytes: some DataProtocol & Sendable, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) async throws -> Certificate.Signature { + try self.signSynchronously(bytes: bytes, signatureAlgorithm: signatureAlgorithm) + } + +} diff --git a/Sources/X509/Signature.swift b/Sources/X509/Signature.swift index 1787597..4a8caa2 100644 --- a/Sources/X509/Signature.swift +++ b/Sources/X509/Signature.swift @@ -46,7 +46,7 @@ extension Certificate { } @inlinable - internal init(signatureAlgorithm: SignatureAlgorithm, signatureBytes: ASN1BitString) throws { + public init(signatureAlgorithm: SignatureAlgorithm, signatureBytes: ASN1BitString) throws { switch signatureAlgorithm { case .ecdsaWithSHA256, .ecdsaWithSHA384, .ecdsaWithSHA512: let signature = try ECDSASignature(derEncoded: signatureBytes.bytes) diff --git a/Tests/X509Tests/CustomPrivateKeyTests.swift b/Tests/X509Tests/CustomPrivateKeyTests.swift new file mode 100644 index 0000000..f297fbc --- /dev/null +++ b/Tests/X509Tests/CustomPrivateKeyTests.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +import CryptoKit +import Foundation +import SwiftASN1 +import Testing +@testable import X509 + +@Suite +final class CustomPrivateKeyTests { + + @Test("CustomPrivateKey Backing Properties") + func testCustomPrivateKeyBackingProperties() { + let keyBacking = TestAsyncKey() + let privateKey = Certificate.PrivateKey(keyBacking) + #expect(privateKey.publicKey == keyBacking.publicKey) + #expect(privateKey.description == "CustomPrivateKey") + #expect(keyBacking.hashValue == privateKey.hashValue) + #expect(keyBacking.defaultSignatureAlgorithm == privateKey.defaultSignatureAlgorithm) + } + + func testCustomPrivateKeySigning() async throws { + let privateKey = Certificate.PrivateKey(TestAsyncKey()) + + _ = try await privateKey.signAsynchronously( + bytes: Data(), + signatureAlgorithm: .ecdsaWithSHA256 + ) + #expect(throws: TestAsyncKey.MyError.self) { + try privateKey.sign( + bytes: Data(), + signatureAlgorithm: .ecdsaWithSHA256 + ) + } + } + + func testCustomPrivateKeyBackingEquality() { + let keyBacking = TestAsyncKey() + let leftKey = Certificate.PrivateKey(keyBacking) + let rightKey = Certificate.PrivateKey(keyBacking) + #expect(leftKey == rightKey) + } + + func testCustomPrivateKeySerialization() { + let privateKey = Certificate.PrivateKey(TestAsyncKey()) + #expect(throws: TestAsyncKey.MyError.self) { + try privateKey.serializeAsPEM() + } + } + +} + +/// A theoretical private key which only supports asynchronous signing. +private struct TestAsyncKey: CustomPrivateKey { + + var publicKey: Certificate.PublicKey { privateKey.publicKey } + + // Not required for CustomPrivateKey protocol. + private let privateKey = Certificate.PrivateKey(P256.Signing.PrivateKey()) + + let defaultSignatureAlgorithm: Certificate.SignatureAlgorithm = .sha256WithRSAEncryption + + func signSynchronously( + bytes: some DataProtocol, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + throw MyError() + } + + func signAsynchronously( + bytes: some DataProtocol & Sendable, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) async throws -> Certificate.Signature { + try await Task { + try privateKey.sign(bytes: bytes, signatureAlgorithm: signatureAlgorithm) + } + .value + } + + static let defaultPEMDiscriminator: String = "TestKey" + + func serializeAsPEM(discriminator: String) throws -> PEMDocument { + throw MyError() + } + + func serialize(into coder: inout DER.Serializer) throws { + throw MyError() + } + + struct MyError: Error {} + +}