Skip to content

Commit

Permalink
Fixup CryptoExtras RSA public key formats (#151)
Browse files Browse the repository at this point in the history
Motivation

CryptoExtras RSA public keys support being exported in DER and PEM form.
These exports work great, but it turns out they are inconsistent between
the Darwin and non-Darwin implementations. Darwin platforms would export
the public keys in PKCS1 format, while non-Darwin was exporting them as
SPKI. This isn't great!

Modifications

- Make the two consistent. `.derRepresentation` should export SPKI
    formatted public keys, because that's what it does for all the EC
    keys.
- Add `.pkcs1DERRepresentation` and `.pkcs1PEMRepresentation` for those
    users that require the PKCS1 formatted key.

Result

Key export types are now consistent on all platforms.
  • Loading branch information
Lukasa authored Mar 10, 2023
1 parent 80a6e3e commit da0fe44
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 19 deletions.
18 changes: 18 additions & 0 deletions Sources/_CryptoExtras/RSA/RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ extension _RSA.Signing {
}
}

public var pkcs1DERRepresentation: Data {
self.backing.pkcs1DERRepresentation
}

public var pkcs1PEMRepresentation: String {
self.backing.pkcs1PEMRepresentation
}

public var derRepresentation: Data {
self.backing.derRepresentation
}
Expand Down Expand Up @@ -309,3 +317,13 @@ extension _RSA.Signing {
}
}
}

extension _RSA {
static let PKCS1KeyType = "RSA PRIVATE KEY"

static let PKCS8KeyType = "PRIVATE KEY"

static let PKCS1PublicKeyType = "RSA PUBLIC KEY"

static let SPKIPublicKeyType = "PUBLIC KEY"
}
26 changes: 21 additions & 5 deletions Sources/_CryptoExtras/RSA/RSA_boring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ internal struct BoringSSLRSAPublicKey {
self.backing = try Backing(derRepresentation: derRepresentation)
}

var pkcs1DERRepresentation: Data {
self.backing.pkcs1DERRepresentation
}

var pkcs1PEMRepresentation: String {
self.backing.pkcs1PEMRepresentation
}

var derRepresentation: Data {
self.backing.derRepresentation
}
Expand Down Expand Up @@ -164,24 +172,32 @@ extension BoringSSLRSAPublicKey {
}
}

fileprivate var derRepresentation: Data {
fileprivate var pkcs1DERRepresentation: Data {
return BIOHelper.withWritableMemoryBIO { bio in
let rc = CCryptoBoringSSL_i2d_RSA_PUBKEY_bio(bio, self.pointer)
let rc = CCryptoBoringSSL_i2d_RSAPublicKey_bio(bio, self.pointer)
precondition(rc == 1)

return try! Data(copyingMemoryBIO: bio)
}
}

fileprivate var pemRepresentation: String {
fileprivate var pkcs1PEMRepresentation: String {
return ASN1.PEMDocument(type: _RSA.PKCS1PublicKeyType, derBytes: self.pkcs1DERRepresentation).pemString
}

fileprivate var derRepresentation: Data {
return BIOHelper.withWritableMemoryBIO { bio in
let rc = CCryptoBoringSSL_PEM_write_bio_RSA_PUBKEY(bio, self.pointer)
let rc = CCryptoBoringSSL_i2d_RSA_PUBKEY_bio(bio, self.pointer)
precondition(rc == 1)

return try! String(copyingUTF8MemoryBIO: bio)
return try! Data(copyingMemoryBIO: bio)
}
}

fileprivate var pemRepresentation: String {
return ASN1.PEMDocument(type: _RSA.SPKIPublicKeyType, derBytes: self.derRepresentation).pemString
}

fileprivate var keySizeInBits: Int {
return Int(CCryptoBoringSSL_RSA_size(self.pointer)) * 8
}
Expand Down
97 changes: 88 additions & 9 deletions Sources/_CryptoExtras/RSA/RSA_security.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,22 @@ internal struct SecurityRSAPublicKey {
self.backing = unwrappedKey
}

var derRepresentation: Data {
var pkcs1DERRepresentation: Data {
var error: Unmanaged<CFError>? = nil
let representation = SecKeyCopyExternalRepresentation(self.backing, &error)
return representation! as Data
}

var pkcs1PEMRepresentation: String {
return ASN1.PEMDocument(type: _RSA.PKCS1PublicKeyType, derBytes: self.pkcs1DERRepresentation).pemString
}

var derRepresentation: Data {
return Data(spkiBytesForPKCS1Bytes: self.pkcs1DERRepresentation)
}

var pemRepresentation: String {
return ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation).pemString
return ASN1.PEMDocument(type: _RSA.SPKIPublicKeyType, derBytes: self.derRepresentation).pemString
}

var keySizeInBits: Int {
Expand All @@ -66,18 +74,14 @@ internal struct SecurityRSAPublicKey {
internal struct SecurityRSAPrivateKey {
private var backing: SecKey

static let PKCS1KeyType = "RSA PRIVATE KEY"

static let PKCS8KeyType = "PRIVATE KEY"

init(pemRepresentation: String) throws {
let document = try ASN1.PEMDocument(pemString: pemRepresentation)

switch document.type {
case SecurityRSAPrivateKey.PKCS1KeyType:
case _RSA.PKCS1KeyType:
// This is what is expected by Security.framework
self = try .init(derRepresentation: document.derBytes)
case SecurityRSAPrivateKey.PKCS8KeyType:
case _RSA.PKCS8KeyType:
guard let pkcs8Bytes = document.derBytes.pkcs8RSAKeyBytes else {
throw _CryptoRSAError.invalidPEMDocument
}
Expand Down Expand Up @@ -138,7 +142,7 @@ internal struct SecurityRSAPrivateKey {
}

var pemRepresentation: String {
return ASN1.PEMDocument(type: SecurityRSAPrivateKey.PKCS1KeyType, derBytes: self.derRepresentation).pemString
return ASN1.PEMDocument(type: _RSA.PKCS1KeyType, derBytes: self.derRepresentation).pemString
}

var keySizeInBits: Int {
Expand Down Expand Up @@ -301,6 +305,81 @@ extension Data {

return self.dropFirst(4 + Data.partialPKCS8Prefix.count + 4)
}

// Corresponds to the ASN.1 encoding of the RSA AlgorithmIdentifier:
//
// SEQUENCE of OID (:rsaEncryption) and NULL.
static let rsaAlgorithmIdentifierBytes = Data([
0x30, 0x0D, // SEQUENCE, Length 13
0x06, 0x09, // OID, length 9
0x2A, 0x86 , 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, // 1.2.840.113549.1.1.1 :rsaEncryption
0x05, 0x00, // NULL, length 0

])

fileprivate init(spkiBytesForPKCS1Bytes pkcs1Bytes: Data) {
// This does an ad-hoc SPKI encode. Ideally we'd bring over the entire ASN.1 stack, but it's not worth doing
// for just this one use-case.
let keyLength = (pkcs1Bytes.count + 1)
let bitStringOverhead = 1 + keyLength._bytesNeededToEncodeASN1Length // 1 byte for tag.
let totalLengthOfSequencePayload = Self.rsaAlgorithmIdentifierBytes.count + bitStringOverhead + keyLength

var bytes = Data()
bytes.reserveCapacity(1 + totalLengthOfSequencePayload._bytesNeededToEncodeASN1Length + totalLengthOfSequencePayload)

bytes.append(0x30) // SEQUENCE marker.
bytes.appendAsASN1NodeLength(totalLengthOfSequencePayload)
bytes.append(Self.rsaAlgorithmIdentifierBytes)

bytes.append(0x03) // BITSTRING marker
bytes.appendAsASN1NodeLength(keyLength)
bytes.append(UInt8(0)) // No padding bits
bytes.append(contentsOf: pkcs1Bytes)

self = bytes
}

fileprivate mutating func appendAsASN1NodeLength(_ length: Int) {
let bytesNeeded = length._bytesNeededToEncodeASN1Length

if bytesNeeded == 1 {
self.append(UInt8(length))
} else {
// We first write the number of length bytes
// we need, setting the high bit. Then we write the bytes of the length.
self.append(0x80 | UInt8(bytesNeeded - 1))

for shift in (0..<(bytesNeeded - 1)).reversed() {
// Shift and mask the integer.
self.append(UInt8(truncatingIfNeeded: (length >> (shift * 8))))
}
}
}
}

extension Int {
fileprivate var _bytesNeededToEncodeASN1Length: Int {
// ASN.1 lengths are in two forms. If we can store the length in 7 bits, we should:
// that requires only one byte. Otherwise, we need multiple bytes: work out how many,
// plus one for the length of the length bytes.
if self <= 0x7F {
return 1
} else {
// We need to work out how many bytes we need. There are many fancy bit-twiddling
// ways of doing this, but honestly we don't do this enough to need them, so we'll
// do it the easy way. This math is done on UInt because it makes the shift semantics clean.
// We save a branch here because we can never overflow this addition.
return UInt(self).neededBytes &+ 1
}
}
}

extension UInt {
// Bytes needed to store a given integer in 7 bit bytes.
fileprivate var neededBytes: Int {
let neededBits = self.bitWidth - self.leadingZeroBitCount
return (neededBits + 7) / 8
}
}

#endif
42 changes: 37 additions & 5 deletions Tests/_CryptoExtrasTests/TestRSASigning.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//
import Foundation
import XCTest
import Crypto
@testable import Crypto
import _CryptoExtras

final class TestRSASigning: XCTestCase {
Expand Down Expand Up @@ -569,7 +569,7 @@ final class TestRSASigning: XCTestCase {
XCTAssertThrowsError(try _RSA.Signing.PrivateKey(keySize: .init(bitCount: 1016)))
}

func testParsingPKCS1PublicKeyKeyDER() throws {
func testParsingPKCS1PublicKeyDER() throws {
let pkcs1Key = Data(base64Encoded:
"MIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNwAHG8U9/E+ioSj0t" +
"/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR62RRr55yzha" +
Expand All @@ -584,10 +584,11 @@ final class TestRSASigning: XCTestCase {
"aG4Nj/QN370EKIf6MzOi5cHkERgWPOGHFrK+ymircxXDpqR+DDeVnWIBqv8" +
"mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/s1Hap0flhFMCAwEAAQ=="
)!
XCTAssertNoThrow(try _RSA.Signing.PublicKey(derRepresentation: pkcs1Key))
let key = try _RSA.Signing.PublicKey(derRepresentation: pkcs1Key)
XCTAssertEqual(pkcs1Key, key.pkcs1DERRepresentation)
}

func testParsingPKCS1PublicKeyKeyPEM() throws {
func testParsingPKCS1PublicKeyPEM() throws {
let pemKey = """
-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNwAHG8U9/E+ioSj0t/EFa9
Expand All @@ -603,7 +604,38 @@ final class TestRSASigning: XCTestCase {
eVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/s1Hap0flhFMCAwEAAQ==
-----END RSA PUBLIC KEY-----
"""
XCTAssertNoThrow(try _RSA.Signing.PublicKey(pemRepresentation: pemKey))
let key = try _RSA.Signing.PublicKey(pemRepresentation: pemKey)
XCTAssertEqual(pemKey, key.pkcs1PEMRepresentation)
}

func testParsingSPKIPublicKeyDER() throws {
let derKey = Data(base64Encoded:
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA509zjqylvktpuN3zMpdw" +
"YwsZ2dp9/cJZ2Krp2EqK+UvMJcp4T3O9rWPMZk1RocQWLpfSwF8jtfyy1OHDQEZh" +
"7UkpnlHmCwlNzzCj+/eaC+JP2Dy6p62nCMonjebPCZ5lhramaO4csrL4bmKdCw5i" +
"XEEaQdwaA8k7Pvv2pkT+X50ZJKBQAaiHo2yRILI5n15UZ4y0fB+HCvA5qebZtkM0" +
"gFqLPxNy1f8oYXuG9KE6sRn/pRwuYuBYD3eAqP6GquO0DkJKmq8RXeewx8ijUBd7" +
"2xiZlbnBZxwvu5eEH5XD9iqf+liS+yA1wORQtQhSwuWApk9acaIP9IjyW2zojAtS" +
"hwIDAQAB"
)!
let key = try _RSA.Signing.PublicKey(derRepresentation: derKey)
XCTAssertEqual(derKey, key.derRepresentation)
}

func testParsingSPKIPublicKeyPEM() throws {
let pemKey = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA509zjqylvktpuN3zMpdw
YwsZ2dp9/cJZ2Krp2EqK+UvMJcp4T3O9rWPMZk1RocQWLpfSwF8jtfyy1OHDQEZh
7UkpnlHmCwlNzzCj+/eaC+JP2Dy6p62nCMonjebPCZ5lhramaO4csrL4bmKdCw5i
XEEaQdwaA8k7Pvv2pkT+X50ZJKBQAaiHo2yRILI5n15UZ4y0fB+HCvA5qebZtkM0
gFqLPxNy1f8oYXuG9KE6sRn/pRwuYuBYD3eAqP6GquO0DkJKmq8RXeewx8ijUBd7
2xiZlbnBZxwvu5eEH5XD9iqf+liS+yA1wORQtQhSwuWApk9acaIP9IjyW2zojAtS
hwIDAQAB
-----END PUBLIC KEY-----
"""
let key = try _RSA.Signing.PublicKey(pemRepresentation: pemKey)
XCTAssertEqual(pemKey, key.pemRepresentation)
}

private func testPKCS1Group(_ group: RSAPKCS1TestGroup) throws {
Expand Down

0 comments on commit da0fe44

Please sign in to comment.