From b639b5bd1faed3ddc5ec3b883920f0452a0d0b03 Mon Sep 17 00:00:00 2001 From: Gautier Delorme Date: Thu, 3 Oct 2024 19:33:02 +0200 Subject: [PATCH] Support loading RSAPSS public keys with parameters (#268) * Support loading RSAPSS public keys with parameters Parameters are stripped and the key is treated as a regular public key. * address review + reset algorithm as well * run update_cmakelists.sh * attempt to fix the cmake build * include FetchContent * early return for non-PSS keys * simplify code --- CMakeLists.txt | 10 ++ Package.swift | 7 +- Sources/_CryptoExtras/CMakeLists.txt | 3 +- Sources/_CryptoExtras/RSA/RSA.swift | 26 ++-- .../Util/SubjectPublicKeyInfo.swift | 124 ++++++++++++++++++ Tests/_CryptoExtrasTests/TestRSASigning.swift | 50 +++++++ 6 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 Sources/_CryptoExtras/Util/SubjectPublicKeyInfo.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index 9831cee6..061466ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,5 +48,15 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) find_package(Foundation CONFIG) endif() +include(FetchContent) +find_package(SwiftASN1 CONFIG) +if(NOT SwiftASN1_FOUND) + message("-- Vending swift-asn1") + FetchContent_Declare(ASN1 + GIT_REPOSITORY https://github.com/apple/swift-asn1 + GIT_TAG 1.1.0) + FetchContent_MakeAvailable(ASN1) +endif() + add_subdirectory(Sources) add_subdirectory(cmake/modules) diff --git a/Package.swift b/Package.swift index 689e6c83..3d8b3ef7 100644 --- a/Package.swift +++ b/Package.swift @@ -84,7 +84,9 @@ let package = Package( .library(name: "CCryptoBoringSSL", type: .static, targets: ["CCryptoBoringSSL"]), MANGLE_END */ ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/apple/swift-asn1.git", from: "1.2.0") + ], targets: [ .target( name: "CCryptoBoringSSL", @@ -141,7 +143,8 @@ let package = Package( "CCryptoBoringSSL", "CCryptoBoringSSLShims", "CryptoBoringWrapper", - "Crypto" + "Crypto", + .product(name: "SwiftASN1", package: "swift-asn1") ], exclude: privacyManifestExclude + [ "CMakeLists.txt", diff --git a/Sources/_CryptoExtras/CMakeLists.txt b/Sources/_CryptoExtras/CMakeLists.txt index 3ea4defd..1f91c0a3 100644 --- a/Sources/_CryptoExtras/CMakeLists.txt +++ b/Sources/_CryptoExtras/CMakeLists.txt @@ -26,7 +26,8 @@ add_library(_CryptoExtras "Util/PEMDocument.swift" "Util/RandomBytes.swift" "Util/Shared/ArbitraryPrecisionInteger_boring.swift" - "Util/Shared/FiniteFieldArithmeticContext_boring.swift") + "Util/Shared/FiniteFieldArithmeticContext_boring.swift" + "Util/SubjectPublicKeyInfo.swift") target_include_directories(_CryptoExtras PRIVATE $ diff --git a/Sources/_CryptoExtras/RSA/RSA.swift b/Sources/_CryptoExtras/RSA/RSA.swift index 00b98e86..03b1006d 100644 --- a/Sources/_CryptoExtras/RSA/RSA.swift +++ b/Sources/_CryptoExtras/RSA/RSA.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation import Crypto +import SwiftASN1 #if CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API fileprivate typealias BackingPublicKey = SecurityRSAPublicKey @@ -57,34 +58,34 @@ extension _RSA.Signing { /// /// This constructor supports key sizes of 2048 bits or more. Users should validate that key sizes are appropriate /// for their use-case. + /// Parameters from RSA PSS keys will be stripped. public init(pemRepresentation: String) throws { - self.backing = try BackingPublicKey(pemRepresentation: pemRepresentation) + let derBytes = try PEMDocument(pemString: pemRepresentation).derBytes - guard self.keySizeInBits >= 2048 else { - throw CryptoKitError.incorrectParameterSize - } + try self.init(derRepresentation: derBytes) } /// Construct an RSA public key from a PEM representation. /// /// This constructor supports key sizes of 1024 bits or more. Users should validate that key sizes are appropriate /// for their use-case. + /// Parameters from RSA PSS keys will be stripped. /// - Warning: Key sizes less than 2048 are not recommended and should only be used for compatibility reasons. public init(unsafePEMRepresentation pemRepresentation: String) throws { - self.backing = try BackingPublicKey(pemRepresentation: pemRepresentation) - - guard self.keySizeInBits >= 1024 else { - throw CryptoKitError.incorrectParameterSize - } + let derBytes = try PEMDocument(pemString: pemRepresentation).derBytes + try self.init(unsafeDERRepresentation: derBytes) } /// Construct an RSA public key from a DER representation. /// /// This constructor supports key sizes of 2048 bits or more. Users should validate that key sizes are appropriate /// for their use-case. + /// Parameters from RSA PSS keys will be stripped. public init(derRepresentation: Bytes) throws { - self.backing = try BackingPublicKey(derRepresentation: derRepresentation) + let sanitizedDer = try SubjectPublicKeyInfo.stripRsaPssParameters(derEncoded: [UInt8](derRepresentation)) + + self.backing = try BackingPublicKey(derRepresentation: sanitizedDer) guard self.keySizeInBits >= 2048 else { throw CryptoKitError.incorrectParameterSize @@ -95,9 +96,12 @@ extension _RSA.Signing { /// /// This constructor supports key sizes of 1024 bits or more. Users should validate that key sizes are appropriate /// for their use-case. + /// Parameters from RSA PSS keys will be stripped. /// - Warning: Key sizes less than 2048 are not recommended and should only be used for compatibility reasons. public init(unsafeDERRepresentation derRepresentation: Bytes) throws { - self.backing = try BackingPublicKey(derRepresentation: derRepresentation) + let sanitizedDer = try SubjectPublicKeyInfo.stripRsaPssParameters(derEncoded: [UInt8](derRepresentation)) + + self.backing = try BackingPublicKey(derRepresentation: sanitizedDer) guard self.keySizeInBits >= 1024 else { throw CryptoKitError.incorrectParameterSize diff --git a/Sources/_CryptoExtras/Util/SubjectPublicKeyInfo.swift b/Sources/_CryptoExtras/Util/SubjectPublicKeyInfo.swift new file mode 100644 index 00000000..6d9ff61c --- /dev/null +++ b/Sources/_CryptoExtras/Util/SubjectPublicKeyInfo.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftASN1 + +struct SubjectPublicKeyInfo: DERImplicitlyTaggable, Hashable { + static var defaultIdentifier: ASN1Identifier { + .sequence + } + + var algorithmIdentifier: RFC5480AlgorithmIdentifier + + var key: ASN1BitString + + init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + // The SPKI block looks like this: + // + // SubjectPublicKeyInfo ::= SEQUENCE { + // algorithm AlgorithmIdentifier, + // subjectPublicKey BIT STRING + // } + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let algorithmIdentifier = try RFC5480AlgorithmIdentifier(derEncoded: &nodes) + let key = try ASN1BitString(derEncoded: &nodes) + + return SubjectPublicKeyInfo(algorithmIdentifier: algorithmIdentifier, key: key) + } + } + + private init(algorithmIdentifier: RFC5480AlgorithmIdentifier, key: ASN1BitString) { + self.algorithmIdentifier = algorithmIdentifier + self.key = key + } + + internal init(algorithmIdentifier: RFC5480AlgorithmIdentifier, key: [UInt8]) { + self.algorithmIdentifier = algorithmIdentifier + self.key = ASN1BitString(bytes: key[...]) + } + + func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.algorithmIdentifier) + try coder.serialize(self.key) + } + } +} + +struct RFC5480AlgorithmIdentifier: DERImplicitlyTaggable, Hashable { + static var defaultIdentifier: ASN1Identifier { + .sequence + } + + var algorithm: ASN1ObjectIdentifier + + var parameters: ASN1Any? + + init(algorithm: ASN1ObjectIdentifier, parameters: ASN1Any?) { + self.algorithm = algorithm + self.parameters = parameters + } + + init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + // The AlgorithmIdentifier block looks like this. + // + // AlgorithmIdentifier ::= SEQUENCE { + // algorithm OBJECT IDENTIFIER, + // parameters ANY DEFINED BY algorithm OPTIONAL + // } + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // + // We don't bother with helpers: we just try to decode it directly. + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let algorithmOID = try ASN1ObjectIdentifier(derEncoded: &nodes) + + let parameters = nodes.next().map { ASN1Any(derEncoded: $0) } + + return .init(algorithm: algorithmOID, parameters: parameters) + } + } + + func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.algorithm) + if let parameters = self.parameters { + try coder.serialize(parameters) + } + } + } +} + +extension SubjectPublicKeyInfo { + static func stripRsaPssParameters(derEncoded: [UInt8]) throws -> [UInt8] { + guard var spki = try? SubjectPublicKeyInfo(derEncoded: derEncoded), + spki.algorithmIdentifier.algorithm == .AlgorithmIdentifier.rsaPSS + else { + // If it's neither a SPKI nor a PSS key, we don't have to modify it. + return derEncoded + } + + spki.algorithmIdentifier.algorithm = .AlgorithmIdentifier.rsaEncryption + spki.algorithmIdentifier.parameters = try ASN1Any(erasing: ASN1Null()) + + var serializer = DER.Serializer() + try serializer.serialize(spki) + + return serializer.serializedBytes + } +} diff --git a/Tests/_CryptoExtrasTests/TestRSASigning.swift b/Tests/_CryptoExtrasTests/TestRSASigning.swift index 505e55ac..8ca810ee 100644 --- a/Tests/_CryptoExtrasTests/TestRSASigning.swift +++ b/Tests/_CryptoExtrasTests/TestRSASigning.swift @@ -17,6 +17,56 @@ import Crypto @testable import _CryptoExtras final class TestRSASigning: XCTestCase { + + func test_rsaPssParameters() throws { + let rsaPssPublicKeyPEM = """ + -----BEGIN PUBLIC KEY----- + MIIBUjA9BgkqhkiG9w0BAQowMKANMAsGCWCGSAFlAwQCAaEaMBgGCSqGSIb3DQEB + CDALBglghkgBZQMEAgGiAwIBIAOCAQ8AMIIBCgKCAQEAvcOaxSJoSiiXIQme6HEF + d0/QHjtk5+U1RbeejxeUR80Q1f8E5v7+uIBEFVbwZpIJZtmSB3bxbS31rOBGVcrI + IAfCnUlq6DK1fEL1fgn61XMiSSyKr75L5ZXv9Rib95h3lrNbhW0DUaXzf61kw3+Z + 4KV1btD7C+fdiLzPm18UQv8jJSbCE6hv3MWdkG3NcwgZC+iXwz3DFcsclyYg/+Om + 0hx8UJ/34vNpeE+0MHwyl0j/eO7izrzTZnfsm4ZRaU3mw0ORDQmo8MyIDFa55R/v + 30otk9y3LFkaeEyl1+7VFjJzoOEtze6VkTEzV8e/BTu4eXlKQ6CEYvHhUkNmHGC+ + mwIDAQAB + -----END PUBLIC KEY----- + """ + + let rsaPssPublicKeyDER = Data(base64Encoded: + "MIIBUjA9BgkqhkiG9w0BAQowMKANMAsGCWCGSAFlAwQCAaEaMBgGCSqGSIb3DQEB" + + "CDALBglghkgBZQMEAgGiAwIBIAOCAQ8AMIIBCgKCAQEAxPJvJDGPzb2rBWfE5JCB" + + "p2OAmR46zIbaVjIR1lUabKCdb5CxdnHvQBymp3AlvOGTNzSLxTXOaYn7MzeFvAVI" + + "mpRRzXzalG0ZfM4AkPBtjPz93pPLWEfgk+/i+JLWlWUStUGgGKNbJn4yJ8cJ8n+E" + + "/5+ry+tUYHEJm9A4/HwH4Agg78kPtnEvIvdC/aIw4TEpjZDewVNAEW2rBuQNd01r" + + "fAo2CSzbH76gL02mnLuvh1xyrKz+v9gyo9Taw273KU+83HPs91obgX4WpEfWOnd6" + + "LMJHRZo92FXnW6IHkCdz12khyS1TVIq4ONwjvmS6q3V9UwQg/uuyoSNnRfWXvZXQ" + + "aQIDAQAB" + )! + + let rsaPssPublicKey1024PEM = """ + -----BEGIN PUBLIC KEY----- + MIHPMD0GCSqGSIb3DQEBCjAwoA0wCwYJYIZIAWUDBAIBoRowGAYJKoZIhvcNAQEI + MAsGCWCGSAFlAwQCAaIDAgEgA4GNADCBiQKBgQDGv67JltnwgkFxQOI8YUldC1LG + rCLOpyAN/Vq4WyLQ6TKcPevcYA8XmuXL8tC85rMQQG1GMwMWKcf/kf0NDKblUFjZ + BevUPmQF3Jadsn9ST+RMn8D+kq31Hdc0UG/WjZSpMHTkc8SWIjr2E6DIILn/OA/w + G3jVOeTsEfUeGExhVwIDAQAB + -----END PUBLIC KEY----- + """ + + let rsaPssPublicKey1024DER = Data(base64Encoded: + "MIHPMD0GCSqGSIb3DQEBCjAwoA0wCwYJYIZIAWUDBAIBoRowGAYJKoZIhvcNAQEI" + + "MAsGCWCGSAFlAwQCAaIDAgEgA4GNADCBiQKBgQC7LZLbFhzOCoTmXEABRsyOkRiB" + + "18XkkJBwTkn2JES1jVZogXtcq5ZV+KmPulOrzLuaC45IliS5OZ1hJuC7m8/devXk" + + "HaNId+y2cZxRYnfNCsEzvTryxt+01VMQJA4VHsdmhJO6TEIUzDIfj3BlahZuoU11" + + "VZ4wgVIpYymQidJigQIDAQAB" + )! + + XCTAssertEqual(try _RSA.Signing.PublicKey(pemRepresentation: rsaPssPublicKeyPEM).keySizeInBits, 2048) + XCTAssertEqual(try _RSA.Signing.PublicKey(derRepresentation: rsaPssPublicKeyDER).keySizeInBits, 2048) + XCTAssertEqual(try _RSA.Signing.PublicKey(unsafePEMRepresentation: rsaPssPublicKey1024PEM).keySizeInBits, 1024) + XCTAssertEqual(try _RSA.Signing.PublicKey(unsafeDERRepresentation: rsaPssPublicKey1024DER).keySizeInBits, 1024) + } + func test_wycheproofPKCS1Vectors() throws { try wycheproofTest( jsonName: "rsa_signature_test",