Skip to content

Commit e8f2ddd

Browse files
authored
extras: Add API to create RSA keys with random primes (#250)
This adds a _createFromNumbers API which creates an RSA key generating p and q primes based on n, e and d Motivation: There might be cases where private RSA keys have to be generated based on their raw elements (modulus n, exponent e and private exponent d) without p and q. These are random primes which are integral parts of the key but can be generated based on the former numbers, and are not always included when representing RSA keys, for example these parameters are not required in JWKs Modifications: This adds a static _createFromNumbers method which creates a new RSA key given its raw modulus, exponent and private exponent, generating the needed p and q primes without having to provide them as parameters. The algorithm to generate primes is based on a hybrid approach of the Miller Rabin primality test and the proof of Fact 1 in Dan Boneh's Twenty Years of Attacks on the RSA Cryptosystem paper. To write this algorithm, some additions had to be made to the ArbitraryPrecisionInteger Result: Private RSA keys can now be generated without providing p and q
1 parent 55201a8 commit e8f2ddd

File tree

6 files changed

+281
-2
lines changed

6 files changed

+281
-2
lines changed

Sources/Crypto/Util/BoringSSL/Shared/ArbitraryPrecisionInteger_boring.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,13 @@ extension ArbitraryPrecisionInteger: SignedNumeric {
405405
// MARK: - Other arithmetic operations
406406

407407
extension ArbitraryPrecisionInteger {
408+
@usableFromInline
409+
var trailingZeroBitCount: Int32 {
410+
self.withUnsafeBignumPointer {
411+
CCryptoBoringSSL_BN_count_low_zero_bits($0)
412+
}
413+
}
414+
408415
@usableFromInline
409416
static func gcd(_ a: ArbitraryPrecisionInteger, _ b: ArbitraryPrecisionInteger) throws -> ArbitraryPrecisionInteger {
410417
var result = ArbitraryPrecisionInteger()
@@ -443,6 +450,46 @@ extension ArbitraryPrecisionInteger {
443450

444451
return result
445452
}
453+
454+
@usableFromInline
455+
static func >> (lhs: ArbitraryPrecisionInteger, rhs: Int32) -> ArbitraryPrecisionInteger {
456+
var result = ArbitraryPrecisionInteger()
457+
458+
let rc = result.withUnsafeMutableBignumPointer { resultPtr in
459+
lhs.withUnsafeBignumPointer { lhsPtr in
460+
CCryptoBoringSSL_BN_rshift(resultPtr, lhsPtr, rhs)
461+
}
462+
}
463+
464+
precondition(rc == 1, "Unable to allocate memory for new ArbitraryPrecisionInteger")
465+
466+
return result
467+
}
468+
469+
@usableFromInline
470+
static func / (lhs: ArbitraryPrecisionInteger, rhs: ArbitraryPrecisionInteger) -> ArbitraryPrecisionInteger {
471+
var result = ArbitraryPrecisionInteger()
472+
473+
let rc = result.withUnsafeMutableBignumPointer { resultPtr in
474+
lhs.withUnsafeBignumPointer { lhsPtr in
475+
rhs.withUnsafeBignumPointer { rhsPtr in
476+
ArbitraryPrecisionInteger.withUnsafeBN_CTX { bnCtx in
477+
CCryptoBoringSSL_BN_div(resultPtr, nil, lhsPtr, rhsPtr, bnCtx)
478+
}
479+
}
480+
}
481+
}
482+
precondition(rc == 1, "Unable to allocate memory for new ArbitraryPrecisionInteger")
483+
484+
return result
485+
}
486+
487+
@usableFromInline
488+
var isEven: Bool {
489+
self.withUnsafeBignumPointer {
490+
CCryptoBoringSSL_BN_is_odd($0) == 0
491+
}
492+
}
446493
}
447494

448495
// MARK: - Serializing

Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,34 @@ extension _RSA.BlindSigning {
242242
public var publicKey: _RSA.BlindSigning.PublicKey<H> {
243243
_RSA.BlindSigning.PublicKey(self.backing.publicKey, self.parameters)
244244
}
245+
246+
/// Construct a private key with the specified parameters.
247+
///
248+
/// The use of this API is strongly discouraged for performance reasons,
249+
/// as it requires the factorization of the modulus, which is resource-intensive.
250+
/// It is recommended to use the other initializers to construct a private key,
251+
/// unless you have only the modulus, public exponent, and private exponent
252+
/// to construct the key.
253+
///
254+
/// - Parameters:
255+
/// - n: modulus of the key
256+
/// - e: public exponent of the key
257+
/// - d: private exponent of the key
258+
/// - parameters: parameters used in the blind signing protocol
259+
public static func _createFromNumbers(n: some ContiguousBytes, e: some ContiguousBytes, d: some ContiguousBytes, parameters: Parameters) throws -> Self {
260+
let (p, q) = try _RSA.extractPrimeFactors(
261+
n: try ArbitraryPrecisionInteger(bytes: n),
262+
e: try ArbitraryPrecisionInteger(bytes: e),
263+
d: try ArbitraryPrecisionInteger(bytes: d)
264+
)
265+
266+
return try Self.init(
267+
n: n, e: e, d: d,
268+
p: try Data(bytesOf: p, paddedToSize: p.byteCount),
269+
q: try Data(bytesOf: q, paddedToSize: q.byteCount),
270+
parameters: parameters
271+
)
272+
}
245273
}
246274
}
247275

Sources/_CryptoExtras/RSA/RSA.swift

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,32 @@ extension _RSA.Signing {
257257
public var publicKey: _RSA.Signing.PublicKey {
258258
_RSA.Signing.PublicKey(self.backing.publicKey)
259259
}
260+
261+
/// Construct a private key with the specified parameters.
262+
///
263+
/// The use of this API is strongly discouraged for performance reasons,
264+
/// as it requires the factorization of the modulus, which is resource-intensive.
265+
/// It is recommended to use the other initializers to construct a private key,
266+
/// unless you have only the modulus, public exponent, and private exponent
267+
/// to construct the key.
268+
///
269+
/// - Parameters:
270+
/// - n: modulus of the key
271+
/// - e: public exponent of the key
272+
/// - d: private exponent of the key
273+
public static func _createFromNumbers(n: some ContiguousBytes, e: some ContiguousBytes, d: some ContiguousBytes) throws -> Self {
274+
let (p, q) = try _RSA.extractPrimeFactors(
275+
n: try ArbitraryPrecisionInteger(bytes: n),
276+
e: try ArbitraryPrecisionInteger(bytes: e),
277+
d: try ArbitraryPrecisionInteger(bytes: d)
278+
)
279+
280+
return try Self.init(
281+
n: n, e: e, d: d,
282+
p: try Data(bytesOf: p, paddedToSize: p.byteCount),
283+
q: try Data(bytesOf: q, paddedToSize: q.byteCount)
284+
)
285+
}
260286
}
261287
}
262288

@@ -593,6 +619,32 @@ extension _RSA.Encryption {
593619
public var pkcs8PEMRepresentation: String { self.backing.pkcs8PEMRepresentation }
594620
public var keySizeInBits: Int { self.backing.keySizeInBits }
595621
public var publicKey: _RSA.Encryption.PublicKey { .init(self.backing.publicKey) }
622+
623+
/// Construct a private key with the specified parameters.
624+
///
625+
/// The use of this API is strongly discouraged for performance reasons,
626+
/// as it requires the factorization of the modulus, which is resource-intensive.
627+
/// It is recommended to use the other initializers to construct a private key,
628+
/// unless you have only the modulus, public exponent, and private exponent
629+
/// to construct the key.
630+
///
631+
/// - Parameters:
632+
/// - n: modulus of the key
633+
/// - e: public exponent of the key
634+
/// - d: private exponent of the key
635+
public static func _createFromNumbers(n: some ContiguousBytes, e: some ContiguousBytes, d: some ContiguousBytes) throws -> Self {
636+
let (p, q) = try _RSA.extractPrimeFactors(
637+
n: try ArbitraryPrecisionInteger(bytes: n),
638+
e: try ArbitraryPrecisionInteger(bytes: e),
639+
d: try ArbitraryPrecisionInteger(bytes: d)
640+
)
641+
642+
return try Self.init(
643+
n: n, e: e, d: d,
644+
p: try Data(bytesOf: p, paddedToSize: p.byteCount),
645+
q: try Data(bytesOf: q, paddedToSize: q.byteCount)
646+
)
647+
}
596648
}
597649
}
598650

@@ -669,3 +721,60 @@ extension _RSA {
669721

670722
static let SPKIPublicKeyType = "PUBLIC KEY"
671723
}
724+
725+
extension _RSA {
726+
static func extractPrimeFactors(
727+
n: ArbitraryPrecisionInteger,
728+
e: ArbitraryPrecisionInteger,
729+
d: ArbitraryPrecisionInteger
730+
) throws -> (p: ArbitraryPrecisionInteger, q: ArbitraryPrecisionInteger) {
731+
// This is based on the proof of fact 1 in https://www.ams.org/notices/199902/boneh.pdf
732+
let k = (d * e) - 1
733+
let t = k.trailingZeroBitCount
734+
let r = k >> t
735+
736+
guard k.isEven else {
737+
throw CryptoKitError.incorrectParameterSize
738+
}
739+
740+
var y: ArbitraryPrecisionInteger = 0
741+
var i = 1
742+
743+
let context = try FiniteFieldArithmeticContext(fieldSize: n)
744+
745+
while i <= 100 {
746+
let g = try ArbitraryPrecisionInteger.random(inclusiveMin: 2, exclusiveMax: n)
747+
y = try context.pow(g, r)
748+
749+
guard y != 1, y != n - 1 else {
750+
continue
751+
}
752+
753+
var j = 1
754+
var x: ArbitraryPrecisionInteger
755+
756+
while j <= t &- 1 {
757+
x = try context.pow(y, 2)
758+
759+
guard x != 1, x != n - 1 else {
760+
break
761+
}
762+
763+
y = x
764+
j &+= 1
765+
}
766+
767+
x = try context.pow(y, 2)
768+
if x == 1 {
769+
let p = try ArbitraryPrecisionInteger.gcd(y - 1, n)
770+
let q = n / p
771+
772+
return (p, q)
773+
}
774+
775+
i &+= 1
776+
}
777+
778+
throw CryptoKitError.incorrectParameterSize
779+
}
780+
}

Tests/_CryptoExtrasTests/TestRSABlindSigning.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,25 @@ final class TestRSABlindSigning: XCTestCase {
203203
)
204204
}
205205

206+
func testConstructAndUseKeyFromRSANumbersWhileRecoveringPrimes() throws {
207+
let data = Array("hello, world!".utf8)
208+
209+
for testVector in RFC9474TestVector.allValues {
210+
let key = try _RSA.BlindSigning.PrivateKey._createFromNumbers(
211+
n: Data(hexString: testVector.n),
212+
e: Data(hexString: testVector.e),
213+
d: Data(hexString: testVector.d),
214+
parameters: testVector.parameters
215+
)
216+
217+
let preparedMessage = key.publicKey.prepare(data)
218+
let blindedMessage = try key.publicKey.blind(preparedMessage)
219+
let blindSignature = try key.blindSignature(for: blindedMessage.blindedMessage)
220+
let signature = try key.publicKey.finalize(blindSignature, for: preparedMessage, blindingInverse: blindedMessage.inverse)
221+
XCTAssert(key.publicKey.isValidSignature(signature, for: preparedMessage))
222+
}
223+
}
224+
206225
func testGetKeyPrimitives() throws {
207226
for testVector in RFC9474TestVector.allValues {
208227
let n = try Data(hexString: testVector.n)

Tests/_CryptoExtrasTests/TestRSAEncryption.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,23 @@ final class TestRSAEncryption: XCTestCase {
109109
)
110110
}
111111

112+
func testConstructAndUseKeyFromRSANumbersWhileRecoveringPrimes() throws {
113+
let data = Array("hello, world!".utf8)
114+
115+
for testVector in RFC9474TestVector.allValues {
116+
let key = try _RSA.Encryption.PrivateKey._createFromNumbers(
117+
n: Data(hexString: testVector.n),
118+
e: Data(hexString: testVector.e),
119+
d: Data(hexString: testVector.d)
120+
)
121+
122+
let encrypted = try key.publicKey.encrypt(data, padding: .PKCS1_OAEP_SHA256)
123+
let decrypted = try key.decrypt(encrypted, padding: .PKCS1_OAEP_SHA256)
124+
125+
XCTAssertEqual(Data(data), decrypted)
126+
}
127+
}
128+
112129
func testGetKeyPrimitives() throws {
113130
for testVector in RFC9474TestVector.allValues {
114131
let n = try Data(hexString: testVector.n)

Tests/_CryptoExtrasTests/TestRSASigning.swift

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
//===----------------------------------------------------------------------===//
1414
import Foundation
1515
import XCTest
16-
@testable import Crypto
17-
import _CryptoExtras
16+
import Crypto
17+
@testable import _CryptoExtras
1818

1919
final class TestRSASigning: XCTestCase {
2020
func test_wycheproofPKCS1Vectors() throws {
@@ -73,6 +73,15 @@ final class TestRSASigning: XCTestCase {
7373
testFunction: self.testPSSGroup)
7474
}
7575

76+
func test_wycheproofPrimitives() throws {
77+
try wycheproofTest(
78+
jsonName: "rsa_oaep_2048_sha1_mgf1sha1_test",
79+
testFunction: self.testPrimeFactors)
80+
try wycheproofTest(
81+
jsonName: "rsa_oaep_2048_sha256_mgf1sha256_test",
82+
testFunction: self.testPrimeFactors)
83+
}
84+
7685
func testPKCS1Signing() throws {
7786
try self.testPKCS1Signing(_RSA.Signing.PrivateKey(keySize: .bits2048))
7887
try self.testPKCS1Signing(_RSA.Signing.PrivateKey(keySize: .bits3072))
@@ -708,6 +717,23 @@ final class TestRSASigning: XCTestCase {
708717
)
709718
}
710719

720+
func testConstructAndUseKeyFromRSANumbersWhileRecoveringPrimes() throws {
721+
let data = Array("hello, world!".utf8)
722+
723+
for testVector in RFC9474TestVector.allValues {
724+
let key = try _RSA.Signing.PrivateKey._createFromNumbers(
725+
n: Data(hexString: testVector.n),
726+
e: Data(hexString: testVector.e),
727+
d: Data(hexString: testVector.d)
728+
)
729+
730+
let signature = try key.signature(for: data)
731+
let roundTripped = _RSA.Signing.RSASignature(rawRepresentation: signature.rawRepresentation)
732+
XCTAssertEqual(signature.rawRepresentation, roundTripped.rawRepresentation)
733+
XCTAssertTrue(key.publicKey.isValidSignature(roundTripped, for: data))
734+
}
735+
}
736+
711737
func testGetKeyPrimitives() throws {
712738
for testVector in RFC9474TestVector.allValues {
713739
let n = try Data(hexString: testVector.n)
@@ -801,6 +827,15 @@ final class TestRSASigning: XCTestCase {
801827
}
802828
}
803829

830+
private func testPrimeFactors(_ group: RSAPrimitivesTestGroup) throws {
831+
let n = try ArbitraryPrecisionInteger(hexString: group.n)
832+
let e = try ArbitraryPrecisionInteger(hexString: group.e)
833+
let d = try ArbitraryPrecisionInteger(hexString: group.d)
834+
835+
let (p, q) = try _RSA.extractPrimeFactors(n: n, e: e, d: d)
836+
XCTAssertEqual(p * q, n, "The product of p and q should equal n; got \(p) * \(q) != \(n)")
837+
}
838+
804839
private func pemForDERBytes(discriminator: String, derBytes: Data) -> String {
805840
let lineLength = 64
806841
var encoded = derBytes.base64EncodedString()[...]
@@ -882,3 +917,27 @@ struct RSATest: Codable {
882917
}
883918
}
884919
}
920+
921+
struct RSAPrimitivesTestGroup: Codable {
922+
let n: String
923+
let e: String
924+
let d: String
925+
let privateKeyJwk: PrivateKeyJWK?
926+
927+
struct PrivateKeyJWK: Codable {
928+
let kty: String
929+
let n: String
930+
let e: String
931+
let d: String
932+
let p: String
933+
let q: String
934+
let dp: String
935+
let dq: String
936+
let qi: String
937+
}
938+
}
939+
940+
struct RSAPrimitivesTestVectors: Codable {
941+
let testGroups: [RSAPrimitivesTestGroup]
942+
}
943+

0 commit comments

Comments
 (0)