Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7ada70d
Certificate.Signer
Juice805 Oct 8, 2025
fc55fb1
AsyncSigner
Juice805 Oct 9, 2025
418294d
use some instead of any
Juice805 Oct 9, 2025
32ccb78
public signature init
Juice805 Oct 9, 2025
e8c95ad
Add CSR initializers
Juice805 Oct 9, 2025
5054744
Signer → SignatureProvider
Juice805 Oct 9, 2025
b344fd4
Certificate.SignatureProvider → Certificate.PrivateKeyProtocol
Juice805 Oct 10, 2025
2623976
Fix CSR inits
Juice805 Oct 10, 2025
f4eea46
formatting
Juice805 Oct 13, 2025
d4db0e6
rename file to match protocol name
Juice805 Oct 13, 2025
3797b50
adopt Hashable
Juice805 Oct 13, 2025
7265cfc
Move protocols to root level
Juice805 Oct 24, 2025
5a1fec4
Drop PrivateKey protocol conformance
Juice805 Oct 24, 2025
0807f83
Rename file to match
Juice805 Oct 24, 2025
3dc6e3b
Merge CustomPrivateKey protocols
Juice805 Oct 24, 2025
939f1f9
CustomPrivateKey backs Certificate.PrivateKey
Juice805 Oct 24, 2025
0df7335
signSynchronously
Juice805 Oct 29, 2025
2f2bb60
require PEMSerializable adoption
Juice805 Oct 29, 2025
bdaa274
Update Sources/X509/CustomPrivateKey.swift
Juice805 Nov 10, 2025
d5aee23
explicit self
Juice805 Oct 30, 2025
327fd88
always import import SwiftASN1 for CustomPrivateKey
Juice805 Oct 30, 2025
6269c6d
differentiate async and sync intializers
Juice805 Oct 30, 2025
0bb57aa
Update docs to match new arg names
Juice805 Oct 30, 2025
b9e729a
Make signAsynchronously default implementation
Juice805 Nov 10, 2025
84da6ba
Async method bytes must be sendable
Juice805 Nov 10, 2025
ee0f956
an initializer would be a good idea
Juice805 Nov 10, 2025
64c9352
CustomPrivateKeyTests
Juice805 Nov 10, 2025
ec20d98
verify defaultSignatureAlgorithm is forwarded properly
Juice805 Nov 10, 2025
8a254cd
switch to swift-testing for new tests
Juice805 Nov 10, 2025
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
39 changes: 39 additions & 0 deletions Sources/X509/CSR/CertificateSigningRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions Sources/X509/Certificate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions Sources/X509/CertificatePrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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: DataProtocol & Sendable>(
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)
}
}

Expand All @@ -143,6 +171,8 @@ extension Certificate {
#endif
case .ed25519(let ed25519):
return PublicKey(ed25519.publicKey)
case .custom(let custom):
return custom.publicKey
}
}

Expand Down Expand Up @@ -177,6 +207,8 @@ extension Certificate {
#endif
case .ed25519:
return .ed25519
case .custom(let custom):
return custom.defaultSignatureAlgorithm
}
}
}
Expand Down Expand Up @@ -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"
}
}
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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()
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions Sources/X509/CustomPrivateKey.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
2 changes: 1 addition & 1 deletion Sources/X509/Signature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading