From eaed0355dc82c4212d4b7ab787a28395a4333755 Mon Sep 17 00:00:00 2001 From: Nicolas Bachschmidt Date: Mon, 27 Jan 2025 12:03:56 +0100 Subject: [PATCH] Add default value for signature algorithm (#221) Most of the time, the signature algorithm that should be used is dictated by the type of the private key. * Ed25519 keys support only one signature algorithm. * RFC 5753 section 8 recommends that "[the P-256 curve] be used with SHA-256; the P-384 curve be used with SHA-384; and the P-521 curve be used with SHA-512". * RSA keys support 4 signature algorithms. But most people use RSA with SHA-256 and nobody should use RSA with SHA-1 anymore. More over, Certificate.PrivateKey is opaque to the user, who may not know what type of private key they're using and what the appropriate signature algorithm is. For those reasons, we add convenience wrappers around methods with a signature algorithm to provide a reasonable default value. --------- Co-authored-by: Cory Benfield --- .../X509/CSR/CertificateSigningRequest.swift | 29 ++ Sources/X509/Certificate.swift | 48 +++ Sources/X509/CertificatePrivateKey.swift | 77 ++--- Sources/X509/CertificatePublicKey.swift | 52 +--- .../CMSOperations.swift | 21 ++ Sources/X509/Digests.swift | 236 -------------- Sources/X509/SecKeyWrapper.swift | 41 +-- Sources/X509/Signature.swift | 288 ++++++++++++++++++ Sources/X509/SignatureAlgorithm.swift | 4 +- Tests/X509Tests/CMSTests.swift | 128 ++++++++ Tests/X509Tests/CSRTests.swift | 41 +++ Tests/X509Tests/CertificateTests.swift | 45 +++ 12 files changed, 662 insertions(+), 348 deletions(-) diff --git a/Sources/X509/CSR/CertificateSigningRequest.swift b/Sources/X509/CSR/CertificateSigningRequest.swift index f0f2caba..50a9a4fc 100644 --- a/Sources/X509/CSR/CertificateSigningRequest.swift +++ b/Sources/X509/CSR/CertificateSigningRequest.swift @@ -145,6 +145,35 @@ 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. + /// + /// A default signature algorithm to use for the signature of this CSR is automatically chosen based on + /// the type of the private key. + /// + /// - Parameters: + /// - version: The CSR version. + /// - subject: The ``DistinguishedName`` of the subject of this CSR + /// - privateKey: The private key associated with this CSR. + /// - attributes: The attributes associated with this CSR + @inlinable + public init( + version: Version, + subject: DistinguishedName, + privateKey: Certificate.PrivateKey, + attributes: Attributes + ) throws { + try self.init( + version: version, + subject: subject, + privateKey: privateKey, + attributes: attributes, + signatureAlgorithm: privateKey.defaultSignatureAlgorithm + ) + } + @inlinable internal init( info: CertificationRequestInfo, diff --git a/Sources/X509/Certificate.swift b/Sources/X509/Certificate.swift index ef4bd459..84a531c7 100644 --- a/Sources/X509/Certificate.swift +++ b/Sources/X509/Certificate.swift @@ -209,6 +209,54 @@ 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 `issuerPrivateKey`. + /// + /// A default signature algorithm to use for the signature of this certificate is automatically chosen based + /// on the type of the issuer's private key. + /// + /// This API can be used to construct a self-signed key by passing the private key for `publicKey` as the + /// `issuerPrivateKey` 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. + /// - extensions: The extensions on this certificate. + /// - issuerPrivateKey: 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, + extensions: Extensions, + issuerPrivateKey: PrivateKey + ) throws { + try self.init( + version: version, + serialNumber: serialNumber, + publicKey: publicKey, + notValidBefore: notValidBefore, + notValidAfter: notValidAfter, + issuer: issuer, + subject: subject, + signatureAlgorithm: issuerPrivateKey.defaultSignatureAlgorithm, + extensions: extensions, + issuerPrivateKey: issuerPrivateKey + ) + } + @inlinable init( tbsCertificate: TBSCertificate, diff --git a/Sources/X509/CertificatePrivateKey.swift b/Sources/X509/CertificatePrivateKey.swift index 1dfb7c25..4123b094 100644 --- a/Sources/X509/CertificatePrivateKey.swift +++ b/Sources/X509/CertificatePrivateKey.swift @@ -91,32 +91,23 @@ extension Certificate { bytes: Bytes, signatureAlgorithm: SignatureAlgorithm ) throws -> Signature { - try self.validateAlgorithmForKey(algorithm: signatureAlgorithm) - switch self.backing { case .p256(let p256): - let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) - return try p256.signature(for: bytes, digestAlgorithm: digestAlgorithm) + return try p256.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) case .p384(let p384): - let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) - return try p384.signature(for: bytes, digestAlgorithm: digestAlgorithm) + return try p384.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) case .p521(let p521): - let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) - return try p521.signature(for: bytes, digestAlgorithm: digestAlgorithm) + return try p521.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) case .rsa(let rsa): - let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) - let padding = try _RSA.Signing.Padding(forSignatureAlgorithm: signatureAlgorithm) - return try rsa.signature(for: bytes, digestAlgorithm: digestAlgorithm, padding: padding) + return try rsa.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) #if canImport(Darwin) case .secureEnclaveP256(let secureEnclaveP256): - let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) - return try secureEnclaveP256.signature(for: bytes, digestAlgorithm: digestAlgorithm) + return try secureEnclaveP256.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) case .secKey(let secKeyWrapper): - let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) - return try secKeyWrapper.signature(for: bytes, digestAlgorithm: digestAlgorithm) + return try secKeyWrapper.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) #endif case .ed25519(let ed25519): - return try ed25519.signature(for: bytes) + return try ed25519.signature(for: bytes, signatureAlgorithm: signatureAlgorithm) } } @@ -145,51 +136,37 @@ extension Certificate { } @inlinable - func validateAlgorithmForKey(algorithm: SignatureAlgorithm) throws { - switch self.backing { - case .p256, .p384, .p521: - if !algorithm.isECDSA { - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Cannot use \(algorithm) with ECDSA key \(self)" - ) - } + var defaultSignatureAlgorithm: SignatureAlgorithm { + switch backing { + case .p256: + return .ecdsaWithSHA256 + case .p384: + return .ecdsaWithSHA384 + case .p521: + return .ecdsaWithSHA512 case .rsa: - if !algorithm.isRSA { - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Cannot use \(algorithm) with RSA key \(self)" - ) - } + return .sha256WithRSAEncryption #if canImport(Darwin) case .secureEnclaveP256: - if !algorithm.isECDSA { - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Cannot use \(algorithm) with ECDSA key \(self)" - ) - } + return .ecdsaWithSHA256 case .secKey(let key): switch key.type { - case .ECDSA: - if !algorithm.isECDSA { - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Cannot use \(algorithm) with ECDSA key \(self)" - ) - } case .RSA: - if !algorithm.isRSA { - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Cannot use \(algorithm) with RSA key \(self)" - ) + return .sha256WithRSAEncryption + case .ECDSA(let keySize): + switch keySize { + case .P256: + return .ecdsaWithSHA256 + case .P384: + return .ecdsaWithSHA384 + case .P521: + return .ecdsaWithSHA512 } } #endif case .ed25519: - if algorithm != .ed25519 { - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Cannot use \(algorithm) with Ed25519 key \(self)" - ) - } + return .ed25519 } - } } } diff --git a/Sources/X509/CertificatePublicKey.swift b/Sources/X509/CertificatePublicKey.swift index c81e57e2..0dba412c 100644 --- a/Sources/X509/CertificatePublicKey.swift +++ b/Sources/X509/CertificatePublicKey.swift @@ -137,32 +137,17 @@ extension Certificate.PublicKey { for bytes: Bytes, signatureAlgorithm: Certificate.SignatureAlgorithm ) -> Bool { - var digest: Digest? - - if let digestAlgorithm = try? AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) { - digest = try? Digest.computeDigest(for: bytes, using: digestAlgorithm) - } - - switch (self.backing, digest) { - case (.p256(let p256), .some(let digest)): - return p256.isValidSignature(signature, for: digest) - case (.p384(let p384), .some(let digest)): - return p384.isValidSignature(signature, for: digest) - case (.p521(let p521), .some(let digest)): - return p521.isValidSignature(signature, for: digest) - case (.rsa(let rsa), .some(let digest)): - // For now we don't support RSA PSS, as it's not deployed in the WebPKI. - // We could, if there are sufficient user needs. - do { - let padding = try _RSA.Signing.Padding(forSignatureAlgorithm: signatureAlgorithm) - return rsa.isValidSignature(signature, for: digest, padding: padding) - } catch { - return false - } - case (.ed25519(let ed25519), .none): - return ed25519.isValidSignature(signature, for: bytes) - default: - return false + switch self.backing { + case .p256(let p256): + return p256.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm) + case .p384(let p384): + return p384.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm) + case .p521(let p521): + return p521.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm) + case .rsa(let rsa): + return rsa.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm) + case .ed25519(let ed25519): + return ed25519.isValidSignature(signature, for: bytes, signatureAlgorithm: signatureAlgorithm) } } } @@ -277,21 +262,6 @@ extension Certificate.PublicKey { } } -extension _RSA.Signing.Padding { - @inlinable - init(forSignatureAlgorithm signatureAlgorithm: Certificate.SignatureAlgorithm) throws { - switch signatureAlgorithm { - case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: - self = .insecurePKCS1v1_5 - default: - // Either this is RSA PSS, or we hit a bug. Either way, unsupported. - throw CertificateError.unsupportedSignatureAlgorithm( - reason: "Unable to determine RSA padding mode for \(signatureAlgorithm)" - ) - } - } -} - extension P256.Signing.PublicKey { /// Create a P256 Public Key from a given ``Certificate/PublicKey-swift.struct``. /// diff --git a/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift b/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift index 34af492e..e9c59d3d 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift @@ -51,6 +51,27 @@ public enum CMS { return try self.serializeSignedData(signedData) } + @_spi(CMS) + @inlinable + public static func sign( + _ bytes: Bytes, + additionalIntermediateCertificates: [Certificate] = [], + certificate: Certificate, + privateKey: Certificate.PrivateKey, + signingTime: Date? = nil, + detached: Bool = true + ) throws -> [UInt8] { + return try self.sign( + bytes, + signatureAlgorithm: privateKey.defaultSignatureAlgorithm, + additionalIntermediateCertificates: additionalIntermediateCertificates, + certificate: certificate, + privateKey: privateKey, + signingTime: signingTime, + detached: detached + ) + } + @inlinable static func signWithSigningTime( _ bytes: Bytes, diff --git a/Sources/X509/Digests.swift b/Sources/X509/Digests.swift index 40fea288..11c0f419 100644 --- a/Sources/X509/Digests.swift +++ b/Sources/X509/Digests.swift @@ -14,7 +14,6 @@ import Foundation import Crypto -import _CryptoExtras @usableFromInline enum Digest { @@ -58,238 +57,3 @@ extension Digest: Sequence { } } } - -// MARK: Public key operations - -extension P256.Signing.PublicKey { - @inlinable - func isValidSignature(_ signature: Certificate.Signature, for digest: Digest) -> Bool { - guard case .ecdsa(let rawInnerSignature) = signature.backing, - let innerSignature = P256.Signing.ECDSASignature(rawInnerSignature) - else { - // Signature mismatch - return false - } - - switch digest { - case .insecureSHA1(let sha1): - return self.isValidSignature(innerSignature, for: sha1) - case .sha256(let sha256): - return self.isValidSignature(innerSignature, for: sha256) - case .sha384(let sha384): - return self.isValidSignature(innerSignature, for: sha384) - case .sha512(let sha512): - return self.isValidSignature(innerSignature, for: sha512) - } - } -} - -extension P384.Signing.PublicKey { - @inlinable - func isValidSignature(_ signature: Certificate.Signature, for digest: Digest) -> Bool { - guard case .ecdsa(let rawInnerSignature) = signature.backing, - let innerSignature = P384.Signing.ECDSASignature(rawInnerSignature) - else { - // Signature mismatch - return false - } - - switch digest { - case .insecureSHA1(let sha1): - return self.isValidSignature(innerSignature, for: sha1) - case .sha256(let sha256): - return self.isValidSignature(innerSignature, for: sha256) - case .sha384(let sha384): - return self.isValidSignature(innerSignature, for: sha384) - case .sha512(let sha512): - return self.isValidSignature(innerSignature, for: sha512) - } - } -} - -extension P521.Signing.PublicKey { - @inlinable - func isValidSignature(_ signature: Certificate.Signature, for digest: Digest) -> Bool { - guard case .ecdsa(let rawInnerSignature) = signature.backing, - let innerSignature = P521.Signing.ECDSASignature(rawInnerSignature) - else { - // Signature mismatch - return false - } - - switch digest { - case .insecureSHA1(let sha1): - return self.isValidSignature(innerSignature, for: sha1) - case .sha256(let sha256): - return self.isValidSignature(innerSignature, for: sha256) - case .sha384(let sha384): - return self.isValidSignature(innerSignature, for: sha384) - case .sha512(let sha512): - return self.isValidSignature(innerSignature, for: sha512) - } - } -} - -extension _RSA.Signing.PublicKey { - @inlinable - func isValidSignature(_ signature: Certificate.Signature, for digest: Digest, padding: _RSA.Signing.Padding) -> Bool - { - guard case .rsa(let innerSignature) = signature.backing else { - // Signature mismatch - return false - } - - switch digest { - case .insecureSHA1(let sha1): - return self.isValidSignature(innerSignature, for: sha1, padding: padding) - case .sha256(let sha256): - return self.isValidSignature(innerSignature, for: sha256, padding: padding) - case .sha384(let sha384): - return self.isValidSignature(innerSignature, for: sha384, padding: padding) - case .sha512(let sha512): - return self.isValidSignature(innerSignature, for: sha512, padding: padding) - } - } -} - -extension Curve25519.Signing.PublicKey { - @inlinable - func isValidSignature(_ signature: Certificate.Signature, for bytes: Bytes) -> Bool { - guard case .ed25519(let rawInnerSignature) = signature.backing else { - // Signature mismatch - return false - } - - return self.isValidSignature(rawInnerSignature, for: bytes) - } -} - -// MARK: Private key operations - -extension P256.Signing.PrivateKey { - @inlinable - func signature( - for bytes: Bytes, - digestAlgorithm: AlgorithmIdentifier - ) throws -> Certificate.Signature { - let signature: P256.Signing.ECDSASignature - - switch try Digest.computeDigest(for: bytes, using: digestAlgorithm) { - case .insecureSHA1(let sha1): - signature = try self.signature(for: sha1) - case .sha256(let sha256): - signature = try self.signature(for: sha256) - case .sha384(let sha384): - signature = try self.signature(for: sha384) - case .sha512(let sha512): - signature = try self.signature(for: sha512) - } - - return Certificate.Signature(backing: .ecdsa(.init(signature))) - } -} - -#if canImport(Darwin) -extension SecureEnclave.P256.Signing.PrivateKey { - @inlinable - func signature( - for bytes: Bytes, - digestAlgorithm: AlgorithmIdentifier - ) throws -> Certificate.Signature { - let signature: P256.Signing.ECDSASignature - - switch try Digest.computeDigest(for: bytes, using: digestAlgorithm) { - case .insecureSHA1(let sha1): - signature = try self.signature(for: sha1) - case .sha256(let sha256): - signature = try self.signature(for: sha256) - case .sha384(let sha384): - signature = try self.signature(for: sha384) - case .sha512(let sha512): - signature = try self.signature(for: sha512) - } - - return Certificate.Signature(backing: .ecdsa(.init(signature))) - } -} -#endif - -extension P384.Signing.PrivateKey { - @inlinable - func signature( - for bytes: Bytes, - digestAlgorithm: AlgorithmIdentifier - ) throws -> Certificate.Signature { - let signature: P384.Signing.ECDSASignature - - switch try Digest.computeDigest(for: bytes, using: digestAlgorithm) { - case .insecureSHA1(let sha1): - signature = try self.signature(for: sha1) - case .sha256(let sha256): - signature = try self.signature(for: sha256) - case .sha384(let sha384): - signature = try self.signature(for: sha384) - case .sha512(let sha512): - signature = try self.signature(for: sha512) - } - - return Certificate.Signature(backing: .ecdsa(.init(signature))) - } -} - -extension P521.Signing.PrivateKey { - @inlinable - func signature( - for bytes: Bytes, - digestAlgorithm: AlgorithmIdentifier - ) throws -> Certificate.Signature { - let signature: P521.Signing.ECDSASignature - - switch try Digest.computeDigest(for: bytes, using: digestAlgorithm) { - case .insecureSHA1(let sha1): - signature = try self.signature(for: sha1) - case .sha256(let sha256): - signature = try self.signature(for: sha256) - case .sha384(let sha384): - signature = try self.signature(for: sha384) - case .sha512(let sha512): - signature = try self.signature(for: sha512) - } - - return Certificate.Signature(backing: .ecdsa(.init(signature))) - } -} - -extension _RSA.Signing.PrivateKey { - @inlinable - func signature( - for bytes: Bytes, - digestAlgorithm: AlgorithmIdentifier, - padding: _RSA.Signing.Padding - ) throws -> Certificate.Signature { - let signature: _RSA.Signing.RSASignature - - switch try Digest.computeDigest(for: bytes, using: digestAlgorithm) { - case .insecureSHA1(let sha1): - signature = try self.signature(for: sha1, padding: padding) - case .sha256(let sha256): - signature = try self.signature(for: sha256, padding: padding) - case .sha384(let sha384): - signature = try self.signature(for: sha384, padding: padding) - case .sha512(let sha512): - signature = try self.signature(for: sha512, padding: padding) - } - - return Certificate.Signature(backing: .rsa(signature)) - } -} - -extension Curve25519.Signing.PrivateKey { - @inlinable - func signature( - for bytes: Bytes - ) throws -> Certificate.Signature { - let signature: Data = try self.signature(for: bytes) - return Certificate.Signature(backing: .ed25519(.init(signature))) - } -} diff --git a/Sources/X509/SecKeyWrapper.swift b/Sources/X509/SecKeyWrapper.swift index 4e99eb2e..a91c902f 100644 --- a/Sources/X509/SecKeyWrapper.swift +++ b/Sources/X509/SecKeyWrapper.swift @@ -173,11 +173,11 @@ extension Certificate.PrivateKey { static func signatureData( key: SecKey, type: KeyType, - digestAlgorithm: AlgorithmIdentifier, + signatureAlgorithm: Certificate.SignatureAlgorithm, bytes: Bytes ) throws -> Data { - let signatureAlgorithm = try Self.signatureAlgorithm(digestAlgorithm: digestAlgorithm, type: type) + let signatureAlgorithm = try Self.keyAlgorithm(signatureAlgorithm: signatureAlgorithm, type: type) var error: Unmanaged? guard @@ -200,37 +200,38 @@ extension Certificate.PrivateKey { return signatureData } - static func signatureAlgorithm(digestAlgorithm: AlgorithmIdentifier, type: KeyType) throws -> SecKeyAlgorithm { + static func keyAlgorithm( + signatureAlgorithm: Certificate.SignatureAlgorithm, + type: KeyType + ) throws -> SecKeyAlgorithm { let algorithm: SecKeyAlgorithm switch type { case .RSA: - switch digestAlgorithm { - case .sha1, .sha1UsingNil: + switch signatureAlgorithm { + case .sha1WithRSAEncryption: algorithm = .rsaSignatureMessagePKCS1v15SHA1 - case .sha256, .sha256UsingNil: + case .sha256WithRSAEncryption: algorithm = .rsaSignatureMessagePKCS1v15SHA256 - case .sha384, .sha384UsingNil: + case .sha384WithRSAEncryption: algorithm = .rsaSignatureMessagePKCS1v15SHA384 - case .sha512, .sha512UsingNil: + case .sha512WithRSAEncryption: algorithm = .rsaSignatureMessagePKCS1v15SHA512 default: - throw CertificateError.unsupportedPrivateKey( - reason: "unsupported SecKey RSA digest algorithm: \(digestAlgorithm)" + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with RSA key" ) } case .ECDSA: - switch digestAlgorithm { - case .sha1, .sha1UsingNil: - algorithm = .ecdsaSignatureMessageX962SHA1 - case .sha256, .sha256UsingNil: + switch signatureAlgorithm { + case .ecdsaWithSHA256: algorithm = .ecdsaSignatureMessageX962SHA256 - case .sha384, .sha384UsingNil: + case .ecdsaWithSHA384: algorithm = .ecdsaSignatureMessageX962SHA384 - case .sha512, .sha512UsingNil: + case .ecdsaWithSHA512: algorithm = .ecdsaSignatureMessageX962SHA512 default: - throw CertificateError.unsupportedPrivateKey( - reason: "unsupported SecKey ECDSA digest algorithm: \(digestAlgorithm)" + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with ECDSA key" ) } } @@ -241,13 +242,13 @@ extension Certificate.PrivateKey { @usableFromInline func signature( for bytes: Bytes, - digestAlgorithm: AlgorithmIdentifier + signatureAlgorithm: Certificate.SignatureAlgorithm ) throws -> Certificate.Signature { let signatureData = try Self.signatureData( key: self.privateKey, type: self.type, - digestAlgorithm: digestAlgorithm, + signatureAlgorithm: signatureAlgorithm, bytes: bytes ) diff --git a/Sources/X509/Signature.swift b/Sources/X509/Signature.swift index 6963bdbf..2a390197 100644 --- a/Sources/X509/Signature.swift +++ b/Sources/X509/Signature.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import SwiftASN1 +import Crypto import _CryptoExtras import Foundation @@ -149,3 +150,290 @@ extension ASN1OctetString { } } } + +// MARK: Public key operations + +extension P256.Signing.PublicKey { + @inlinable + internal func isValidSignature( + _ signature: Certificate.Signature, + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) -> Bool { + guard case .ecdsa(let rawInnerSignature) = signature.backing, + let innerSignature = P256.Signing.ECDSASignature(rawInnerSignature) + else { + // Signature mismatch + return false + } + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + return self.isValidSignature(innerSignature, for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + return self.isValidSignature(innerSignature, for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + return self.isValidSignature(innerSignature, for: SHA512.hash(data: bytes)) + default: + return false + } + } +} + +extension P384.Signing.PublicKey { + @inlinable + internal func isValidSignature( + _ signature: Certificate.Signature, + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) -> Bool { + guard case .ecdsa(let rawInnerSignature) = signature.backing, + let innerSignature = P384.Signing.ECDSASignature(rawInnerSignature) + else { + // Signature mismatch + return false + } + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + return self.isValidSignature(innerSignature, for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + return self.isValidSignature(innerSignature, for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + return self.isValidSignature(innerSignature, for: SHA512.hash(data: bytes)) + default: + return false + } + } +} + +extension P521.Signing.PublicKey { + @inlinable + internal func isValidSignature( + _ signature: Certificate.Signature, + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) -> Bool { + guard case .ecdsa(let rawInnerSignature) = signature.backing, + let innerSignature = P521.Signing.ECDSASignature(rawInnerSignature) + else { + // Signature mismatch + return false + } + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + return self.isValidSignature(innerSignature, for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + return self.isValidSignature(innerSignature, for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + return self.isValidSignature(innerSignature, for: SHA512.hash(data: bytes)) + default: + return false + } + } +} + +extension _RSA.Signing.PublicKey { + @inlinable + internal func isValidSignature( + _ signature: Certificate.Signature, + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) -> Bool { + guard case .rsa(let innerSignature) = signature.backing else { + // Signature mismatch + return false + } + // For now we don't support RSA PSS, as it's not deployed in the WebPKI. + let padding = _RSA.Signing.Padding.insecurePKCS1v1_5 + + switch signatureAlgorithm { + case .sha1WithRSAEncryption: + return self.isValidSignature(innerSignature, for: Insecure.SHA1.hash(data: bytes), padding: padding) + case .sha256WithRSAEncryption: + return self.isValidSignature(innerSignature, for: SHA256.hash(data: bytes), padding: padding) + case .sha384WithRSAEncryption: + return self.isValidSignature(innerSignature, for: SHA384.hash(data: bytes), padding: padding) + case .sha512WithRSAEncryption: + return self.isValidSignature(innerSignature, for: SHA512.hash(data: bytes), padding: padding) + default: + return false + } + } +} + +extension Curve25519.Signing.PublicKey { + @inlinable + internal func isValidSignature( + _ signature: Certificate.Signature, + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) -> Bool { + guard case .ed25519(let rawInnerSignature) = signature.backing else { + // Signature mismatch + return false + } + + switch signatureAlgorithm { + case .ed25519: + return self.isValidSignature(rawInnerSignature, for: bytes) + default: + return false + } + } +} + +// MARK: Private key operations + +extension P256.Signing.PrivateKey { + @inlinable + func signature( + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + let signature: P256.Signing.ECDSASignature + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + signature = try self.signature(for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + signature = try self.signature(for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + signature = try self.signature(for: SHA512.hash(data: bytes)) + default: + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with ECDSA key" + ) + } + + return Certificate.Signature(backing: .ecdsa(.init(signature))) + } +} + +#if canImport(Darwin) +extension SecureEnclave.P256.Signing.PrivateKey { + @inlinable + func signature( + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + let signature: P256.Signing.ECDSASignature + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + signature = try self.signature(for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + signature = try self.signature(for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + signature = try self.signature(for: SHA512.hash(data: bytes)) + default: + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with ECDSA key" + ) + } + + return Certificate.Signature(backing: .ecdsa(.init(signature))) + } +} +#endif + +extension P384.Signing.PrivateKey { + @inlinable + func signature( + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + let signature: P384.Signing.ECDSASignature + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + signature = try self.signature(for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + signature = try self.signature(for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + signature = try self.signature(for: SHA512.hash(data: bytes)) + default: + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with ECDSA key" + ) + } + + return Certificate.Signature(backing: .ecdsa(.init(signature))) + } +} + +extension P521.Signing.PrivateKey { + @inlinable + func signature( + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + let signature: P521.Signing.ECDSASignature + + switch signatureAlgorithm { + case .ecdsaWithSHA256: + signature = try self.signature(for: SHA256.hash(data: bytes)) + case .ecdsaWithSHA384: + signature = try self.signature(for: SHA384.hash(data: bytes)) + case .ecdsaWithSHA512: + signature = try self.signature(for: SHA512.hash(data: bytes)) + default: + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with ECDSA key" + ) + } + + return Certificate.Signature(backing: .ecdsa(.init(signature))) + } +} + +extension _RSA.Signing.PrivateKey { + @inlinable + func signature( + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + let signature: _RSA.Signing.RSASignature + // For now we don't support RSA PSS, as it's not deployed in the WebPKI. + let padding = _RSA.Signing.Padding.insecurePKCS1v1_5 + + switch signatureAlgorithm { + case .sha1WithRSAEncryption: + signature = try self.signature(for: Insecure.SHA1.hash(data: bytes), padding: padding) + case .sha256WithRSAEncryption: + signature = try self.signature(for: SHA256.hash(data: bytes), padding: padding) + case .sha384WithRSAEncryption: + signature = try self.signature(for: SHA384.hash(data: bytes), padding: padding) + case .sha512WithRSAEncryption: + signature = try self.signature(for: SHA512.hash(data: bytes), padding: padding) + default: + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with RSA key" + ) + } + + return Certificate.Signature(backing: .rsa(signature)) + } +} + +extension Curve25519.Signing.PrivateKey { + @inlinable + func signature( + for bytes: Bytes, + signatureAlgorithm: Certificate.SignatureAlgorithm + ) throws -> Certificate.Signature { + let signature: Data + + switch signatureAlgorithm { + case .ed25519: + signature = try self.signature(for: bytes) + default: + throw CertificateError.unsupportedSignatureAlgorithm( + reason: "Cannot use \(signatureAlgorithm) with Ed25519 key" + ) + } + + return Certificate.Signature(backing: .ed25519(.init(signature))) + } +} diff --git a/Sources/X509/SignatureAlgorithm.swift b/Sources/X509/SignatureAlgorithm.swift index 8becfc24..48c05827 100644 --- a/Sources/X509/SignatureAlgorithm.swift +++ b/Sources/X509/SignatureAlgorithm.swift @@ -112,6 +112,8 @@ extension Certificate.SignatureAlgorithm: CustomStringConvertible { return "SignatureAlgorithm.sha384WithRSAEncryption" case .sha512WithRSAEncryption: return "SignatureAlgorithm.sha512WithRSAEncryption" + case .ed25519: + return "SignatureAlgorithm.ed25519" default: return "SignatureAlgorithm(\(self._algorithmIdentifier))" } @@ -134,7 +136,7 @@ extension AlgorithmIdentifier { self = .sha256UsingNil case .ecdsaWithSHA384, .sha384WithRSAEncryption: self = .sha384UsingNil - case .ecdsaWithSHA512, .sha512WithRSAEncryption: + case .ecdsaWithSHA512, .sha512WithRSAEncryption, .ed25519: self = .sha512UsingNil case .sha1WithRSAEncryption: self = .sha1 diff --git a/Tests/X509Tests/CMSTests.swift b/Tests/X509Tests/CMSTests.swift index a0c8f310..02bca959 100644 --- a/Tests/X509Tests/CMSTests.swift +++ b/Tests/X509Tests/CMSTests.swift @@ -15,6 +15,7 @@ import Foundation import XCTest import Crypto +import _CryptoExtras import SwiftASN1 @testable @_spi(CMS) import X509 @@ -128,6 +129,48 @@ final class CMSTests: XCTestCase { issuerPrivateKey: intermediateKey ) + static let rsaCertKey = try! Certificate.PrivateKey(_RSA.Signing.PrivateKey(keySize: .bits2048)) + static let rsaCertName = try! DistinguishedName { + CommonName("CMS RSA") + } + static let rsaCert = try! Certificate( + version: .v3, + serialNumber: .init(), + publicKey: rsaCertKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date().advanced(by: 60 * 60 * 24 * 360), + issuer: rsaCertName, + subject: rsaCertName, + signatureAlgorithm: .sha1WithRSAEncryption, + extensions: try! Certificate.Extensions { + Critical( + BasicConstraints.isCertificateAuthority(maxPathLength: nil) + ) + }, + issuerPrivateKey: rsaCertKey + ) + + static let ed25519CertKey = Certificate.PrivateKey(Curve25519.Signing.PrivateKey()) + static let ed25519CertName = try! DistinguishedName { + CommonName("CMS ED25519") + } + static let ed25519Cert = try! Certificate( + version: .v3, + serialNumber: .init(), + publicKey: ed25519CertKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date().advanced(by: 60 * 60 * 24 * 360), + issuer: ed25519CertName, + subject: ed25519CertName, + signatureAlgorithm: .ed25519, + extensions: try! Certificate.Extensions { + Critical( + BasicConstraints.isCertificateAuthority(maxPathLength: nil) + ) + }, + issuerPrivateKey: ed25519CertKey + ) + @PolicyBuilder static var defaultPolicies: some VerifierPolicy { RFC5280Policy(validationTime: Date()) } @@ -1033,6 +1076,42 @@ final class CMSTests: XCTestCase { XCTAssertValidSignature(isValidSignature) } + func testSigningWithRSA() async throws { + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + let signature = try CMS.sign( + data, + signatureAlgorithm: .sha256WithRSAEncryption, + certificate: Self.rsaCert, + privateKey: Self.rsaCertKey + ) + let isValidSignature = await CMS.isValidSignature( + dataBytes: data, + signatureBytes: signature, + trustRoots: CertificateStore([Self.rsaCert]) + ) { + Self.defaultPolicies + } + XCTAssertValidSignature(isValidSignature) + } + + func testSigningWithEd25519() async throws { + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + let signature = try CMS.sign( + data, + signatureAlgorithm: .ed25519, + certificate: Self.ed25519Cert, + privateKey: Self.ed25519CertKey + ) + let isValidSignature = await CMS.isValidSignature( + dataBytes: data, + signatureBytes: signature, + trustRoots: CertificateStore([Self.ed25519Cert]) + ) { + Self.defaultPolicies + } + XCTAssertValidSignature(isValidSignature) + } + func testSigningWithSigningTimeSignedAttr() async throws { let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] let signature = try CMS.sign( @@ -1166,6 +1245,55 @@ final class CMSTests: XCTestCase { CMSSignerIdentifier.subjectKeyIdentifier(.init(keyIdentifier: [10, 20, 30, 40])) ) } + + func testDefaultRSASignatureAlgorithm() throws { + let privateKey = try Certificate.PrivateKey(_RSA.Signing.PrivateKey(keySize: .bits2048)) + let signerInfo = try self.signAndExtractSignerInfo(privateKey: privateKey) + XCTAssertEqual(signerInfo?.signatureAlgorithm.description, "sha256WithRSAEncryption") + } + + func testDefaultP256SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P256.Signing.PrivateKey()) + let signerInfo = try self.signAndExtractSignerInfo(privateKey: privateKey) + XCTAssertEqual(signerInfo?.signatureAlgorithm.description, "ecdsaWithSHA256") + } + + func testDefaultP384SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P384.Signing.PrivateKey()) + let signerInfo = try self.signAndExtractSignerInfo(privateKey: privateKey) + XCTAssertEqual(signerInfo?.signatureAlgorithm.description, "ecdsaWithSHA384") + } + + func testDefaultP521SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P521.Signing.PrivateKey()) + let signerInfo = try self.signAndExtractSignerInfo(privateKey: privateKey) + XCTAssertEqual(signerInfo?.signatureAlgorithm.description, "ecdsaWithSHA512") + } + + func testDefaultEd25519SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(Curve25519.Signing.PrivateKey()) + let signerInfo = try self.signAndExtractSignerInfo(privateKey: privateKey) + XCTAssertEqual(signerInfo?.signatureAlgorithm.description, "ed25519") + } + + private func signAndExtractSignerInfo(privateKey: Certificate.PrivateKey) throws -> CMSSignerInfo? { + let name = try DistinguishedName { CommonName("test") } + let certificate = try Certificate( + version: .v3, + serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), + publicKey: privateKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date() + 3600, + issuer: name, + subject: name, + extensions: Certificate.Extensions {}, + issuerPrivateKey: privateKey + ) + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + let signatureBytes = try CMS.sign(data, certificate: certificate, privateKey: privateKey) + let contentInfo = try CMSContentInfo(derEncoded: signatureBytes) + return try contentInfo.signedData?.signerInfos.first + } } extension DERSerializable { diff --git a/Tests/X509Tests/CSRTests.swift b/Tests/X509Tests/CSRTests.swift index 4b170d00..2fd5abe5 100644 --- a/Tests/X509Tests/CSRTests.swift +++ b/Tests/X509Tests/CSRTests.swift @@ -524,6 +524,47 @@ final class CSRTests: XCTestCase { CertificateSigningRequest.Attributes(attributes.prefix(2)) ) } + + func testDefaultRSASignatureAlgorithm() throws { + let privateKey = try Certificate.PrivateKey(_RSA.Signing.PrivateKey(keySize: .bits2048)) + let csr = try self.generateCertificateSigningRequest(privateKey: privateKey) + XCTAssertEqual(csr.signatureAlgorithm.description, "SignatureAlgorithm.sha256WithRSAEncryption") + } + + func testDefaultP256SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P256.Signing.PrivateKey()) + let csr = try self.generateCertificateSigningRequest(privateKey: privateKey) + XCTAssertEqual(csr.signatureAlgorithm.description, "SignatureAlgorithm.ecdsaWithSHA256") + } + + func testDefaultP384SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P384.Signing.PrivateKey()) + let csr = try self.generateCertificateSigningRequest(privateKey: privateKey) + XCTAssertEqual(csr.signatureAlgorithm.description, "SignatureAlgorithm.ecdsaWithSHA384") + } + + func testDefaultP521SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P521.Signing.PrivateKey()) + let csr = try self.generateCertificateSigningRequest(privateKey: privateKey) + XCTAssertEqual(csr.signatureAlgorithm.description, "SignatureAlgorithm.ecdsaWithSHA512") + } + + func testDefaultEd25519SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(Curve25519.Signing.PrivateKey()) + let csr = try self.generateCertificateSigningRequest(privateKey: privateKey) + XCTAssertEqual(csr.signatureAlgorithm.description, "SignatureAlgorithm.ed25519") + } + + private func generateCertificateSigningRequest( + privateKey: Certificate.PrivateKey + ) throws -> CertificateSigningRequest { + try CertificateSigningRequest( + version: .v1, + subject: DistinguishedName { CommonName("test") }, + privateKey: privateKey, + attributes: CertificateSigningRequest.Attributes() + ) + } } extension RandomAccessCollection { diff --git a/Tests/X509Tests/CertificateTests.swift b/Tests/X509Tests/CertificateTests.swift index aac4ba8b..190f0aa0 100644 --- a/Tests/X509Tests/CertificateTests.swift +++ b/Tests/X509Tests/CertificateTests.swift @@ -729,4 +729,49 @@ final class CertificateTests: XCTestCase { let reEncoded = try parsedCert.serializeAsPEM().pemString XCTAssertEqual(cert, reEncoded) } + + func testDefaultRSASignatureAlgorithm() throws { + let privateKey = try Certificate.PrivateKey(_RSA.Signing.PrivateKey(keySize: .bits2048)) + let certificate = try self.issueSelfSignedCertificate(privateKey: privateKey) + XCTAssertEqual(certificate.signatureAlgorithm.description, "SignatureAlgorithm.sha256WithRSAEncryption") + } + + func testDefaultP256SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P256.Signing.PrivateKey()) + let certificate = try self.issueSelfSignedCertificate(privateKey: privateKey) + XCTAssertEqual(certificate.signatureAlgorithm.description, "SignatureAlgorithm.ecdsaWithSHA256") + } + + func testDefaultP384SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P384.Signing.PrivateKey()) + let certificate = try self.issueSelfSignedCertificate(privateKey: privateKey) + XCTAssertEqual(certificate.signatureAlgorithm.description, "SignatureAlgorithm.ecdsaWithSHA384") + } + + func testDefaultP521SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(P521.Signing.PrivateKey()) + let certificate = try self.issueSelfSignedCertificate(privateKey: privateKey) + XCTAssertEqual(certificate.signatureAlgorithm.description, "SignatureAlgorithm.ecdsaWithSHA512") + } + + func testDefaultEd25519SignatureAlgorithm() throws { + let privateKey = Certificate.PrivateKey(Curve25519.Signing.PrivateKey()) + let certificate = try self.issueSelfSignedCertificate(privateKey: privateKey) + XCTAssertEqual(certificate.signatureAlgorithm.description, "SignatureAlgorithm.ed25519") + } + + private func issueSelfSignedCertificate(privateKey: Certificate.PrivateKey) throws -> Certificate { + let name = try DistinguishedName { CommonName("test") } + return try Certificate( + version: .v3, + serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), + publicKey: privateKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date() + 3600, + issuer: name, + subject: name, + extensions: Certificate.Extensions {}, + issuerPrivateKey: privateKey + ) + } }