Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading RSAPSS public keys with parameters #268

Merged
merged 7 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -141,7 +143,8 @@ let package = Package(
"CCryptoBoringSSL",
"CCryptoBoringSSLShims",
"CryptoBoringWrapper",
"Crypto"
"Crypto",
.product(name: "SwiftASN1", package: "swift-asn1")
],
exclude: privacyManifestExclude + [
"CMakeLists.txt",
Expand Down
3 changes: 2 additions & 1 deletion Sources/_CryptoExtras/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
$<TARGET_PROPERTY:CCryptoBoringSSL,INCLUDE_DIRECTORIES>
Expand Down
26 changes: 15 additions & 11 deletions Sources/_CryptoExtras/RSA/RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//
import Foundation
import Crypto
import SwiftASN1

#if CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API
fileprivate typealias BackingPublicKey = SecurityRSAPublicKey
Expand Down Expand Up @@ -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<Bytes: DataProtocol>(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
Expand All @@ -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<Bytes: DataProtocol>(unsafeDERRepresentation derRepresentation: Bytes) throws {
self.backing = try BackingPublicKey(derRepresentation: derRepresentation)
let sanitizedDer = try SubjectPublicKeyInfo.stripRsaPssParameters(derEncoded: [UInt8](derRepresentation))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a request for changes in this PR, but can you file an issue that asks us to move all our RSA key parsing into Swift ASN1? This is a pragmatic change for now, but as we're already parsing the key we should probably just stick with that now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


self.backing = try BackingPublicKey(derRepresentation: sanitizedDer)

guard self.keySizeInBits >= 1024 else {
throw CryptoKitError.incorrectParameterSize
Expand Down
124 changes: 124 additions & 0 deletions Sources/_CryptoExtras/Util/SubjectPublicKeyInfo.swift
Original file line number Diff line number Diff line change
@@ -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
//
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the license header that is present in other files and remove this header?

// 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
}
}
50 changes: 50 additions & 0 deletions Tests/_CryptoExtrasTests/TestRSASigning.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down