From 50a46ba65fa4e2c1bb3783ec7464b42232a23ee2 Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 20 Aug 2020 14:34:23 -0400 Subject: [PATCH] Add RSAKey support for x509 certificate (#42) * add rsa support for x509 certificate * add firebase test --- Sources/JWTKit/RSA/RSAKey.swift | 104 +++++++++++++++++++ Sources/JWTKit/Utilities/OpenSSLSigner.swift | 11 +- Tests/JWTKitTests/JWTKitTests.swift | 102 ++++++++++++++++++ 3 files changed, 209 insertions(+), 8 deletions(-) diff --git a/Sources/JWTKit/RSA/RSAKey.swift b/Sources/JWTKit/RSA/RSAKey.swift index 9894cd4f..b83d31ab 100644 --- a/Sources/JWTKit/RSA/RSAKey.swift +++ b/Sources/JWTKit/RSA/RSAKey.swift @@ -2,10 +2,38 @@ import CJWTKitBoringSSL import struct Foundation.Data public final class RSAKey: OpenSSLKey { + /// Creates RSAKey from public key pem file. + /// + /// Public key pem files look like: + /// + /// -----BEGIN PUBLIC KEY----- + /// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx + /// ... + /// aX4rbSL49Z3dAQn8vQIDAQAB + /// -----END PUBLIC KEY----- + /// + /// This key can only be used to verify JWTs. + /// + /// - parameters: + /// - pem: Contents of pem file. public static func `public`(pem string: String) throws -> RSAKey { try .public(pem: [UInt8](string.utf8)) } + /// Creates RSAKey from public key pem file. + /// + /// Public key pem files look like: + /// + /// -----BEGIN PUBLIC KEY----- + /// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx + /// ... + /// aX4rbSL49Z3dAQn8vQIDAQAB + /// -----END PUBLIC KEY----- + /// + /// This key can only be used to verify JWTs. + /// + /// - parameters: + /// - pem: Contents of pem file. public static func `public`(pem data: Data) throws -> RSAKey where Data: DataProtocol { @@ -20,10 +48,86 @@ public final class RSAKey: OpenSSLKey { return self.init(c, .public) } + /// Creates RSAKey from public certificate pem file. + /// + /// Certificate pem files look like: + /// + /// -----BEGIN CERTIFICATE----- + /// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx + /// ... + /// aX4rbSL49Z3dAQn8vQIDAQAB + /// -----END CERTIFICATE----- + /// + /// This key can only be used to verify JWTs. + /// + /// - parameters: + /// - pem: Contents of pem file. + public static func certificate(pem string: String) throws -> RSAKey { + try self.certificate(pem: [UInt8](string.utf8)) + } + + /// Creates RSAKey from public certificate pem file. + /// + /// Certificate pem files look like: + /// + /// -----BEGIN CERTIFICATE----- + /// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx + /// ... + /// aX4rbSL49Z3dAQn8vQIDAQAB + /// -----END CERTIFICATE----- + /// + /// This key can only be used to verify JWTs. + /// + /// - parameters: + /// - pem: Contents of pem file. + public static func certificate(pem data: Data) throws -> RSAKey + where Data: DataProtocol + { + let x509 = try self.load(pem: data) { bio in + CJWTKitBoringSSL_PEM_read_bio_X509(bio, nil, nil, nil) + } + defer { CJWTKitBoringSSL_X509_free(x509) } + let pkey = CJWTKitBoringSSL_X509_get_pubkey(x509) + defer { CJWTKitBoringSSL_EVP_PKEY_free(pkey) } + + guard let c = CJWTKitBoringSSL_EVP_PKEY_get1_RSA(pkey) else { + throw JWTError.signingAlgorithmFailure(RSAError.keyInitializationFailure) + } + return self.init(c, .public) + } + + /// Creates RSAKey from private key pem file. + /// + /// Private key pem files look like: + /// + /// -----BEGIN PRIVATE KEY----- + /// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx + /// ... + /// aX4rbSL49Z3dAQn8vQIDAQAB + /// -----END PRIVATE KEY----- + /// + /// This key can be used to verify and sign JWTs. + /// + /// - parameters: + /// - pem: Contents of pem file. public static func `private`(pem string: String) throws -> RSAKey { try .private(pem: [UInt8](string.utf8)) } + /// Creates RSAKey from private key pem file. + /// + /// Private key pem files look like: + /// + /// -----BEGIN PRIVATE KEY----- + /// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx + /// ... + /// aX4rbSL49Z3dAQn8vQIDAQAB + /// -----END PRIVATE KEY----- + /// + /// This key can be used to verify and sign JWTs. + /// + /// - parameters: + /// - pem: Contents of pem file. public static func `private`(pem data: Data) throws -> RSAKey where Data: DataProtocol { diff --git a/Sources/JWTKit/Utilities/OpenSSLSigner.swift b/Sources/JWTKit/Utilities/OpenSSLSigner.swift index aa2012d2..b1aa21b1 100644 --- a/Sources/JWTKit/Utilities/OpenSSLSigner.swift +++ b/Sources/JWTKit/Utilities/OpenSSLSigner.swift @@ -42,15 +42,10 @@ extension OpenSSLKey { static func load(pem data: Data, _ closure: (UnsafeMutablePointer) -> (T?)) throws -> T where Data: DataProtocol { - let bio = CJWTKitBoringSSL_BIO_new(CJWTKitBoringSSL_BIO_s_mem()) + let bytes = data.copyBytes() + let bio = CJWTKitBoringSSL_BIO_new_mem_buf(bytes, numericCast(bytes.count)) defer { CJWTKitBoringSSL_BIO_free(bio) } - - guard (data.copyBytes() + [0]).withUnsafeBytes({ pointer in - CJWTKitBoringSSL_BIO_puts(bio, pointer.baseAddress?.assumingMemoryBound(to: Int8.self)) - }) >= 0 else { - throw JWTError.signingAlgorithmFailure(OpenSSLError.bioPutsFailure) - } - + guard let bioPtr = bio, let c = closure(bioPtr) else { throw JWTError.signingAlgorithmFailure(OpenSSLError.bioConversionFailure) } diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index d7348fce..538c9b9c 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -474,6 +474,29 @@ class JWTKitTests: XCTestCase { let signers = JWTSigners() try signers.use(jwksJSON: microsoftJWKS) } + + func testRSACertificate() throws { + let test = TestPayload( + sub: "vapor", + name: "foo", + admin: true, + exp: .init(value: .distantFuture) + ) + let jwt = try JWTSigner.rs256( + key: .private(pem: rsa2PrivateKey) + ).sign(test) + + let payload = try JWTSigner.rs256( + key: .certificate(pem: rsa2Cert) + ).verify(jwt, as: TestPayload.self) + XCTAssertEqual(payload, test) + } + + func testFirebaseJWTAndCertificate() throws { + let payload = try JWTSigner.rs256(key: .certificate(pem: firebaseCert)) + .verify(firebaseJWT, as: FirebasePayload.self) + XCTAssertEqual(payload.userID, "y8wiKThXGKM88xxrQWDZzKnBuqv2") + } } struct AudiencePayload: Codable { @@ -611,6 +634,85 @@ aX4rbSL49Z3dAQn8vQIDAQAB -----END PUBLIC KEY----- """ +let rsa2PrivateKey = """ +-----BEGIN PRIVATE KEY----- +MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAtgeOpWeiRIq0Blbc +qq4P7sKnyDmj1mpQq7OyRKZM0qbwyyMM5Nisf5Y+RSDM7JDwqMeLspGo5znLBzN5 +L14JIQIDAQABAkBlMWRSfX9O3VDhKU65L9S5pcsCW1DCdQ3tthMHaO/SNn4jhmbf +MamrK4TWctjuau+CwUtQz/kS/fjveYBSVklVAiEA2r1fExLdTwo1pRzCqvUhq7MO +4wu1dPvv8mJZZvGxQGMCIQDVCVsmeiN+s9erwd95wUZKb4zBkT6MQC0r1fGQBnEN +qwIgBBT6nDmC5cG0BJPH0jbm3PRnd7c1OKym6qgJMRGblC8CICh9Zr2haS2jsNIM +PxU9DscG/JGtsV2mtO8n8omVL9eRAiEA1ccs/gJCMAwJ/jeA8tZwOF3GEb/9tGow +RR8+JsDsJY8= +-----END PRIVATE KEY----- +""" + +let rsa2Cert = """ +-----BEGIN CERTIFICATE----- +MIIBzjCCAXgCCQDnzO/FvcHZbjANBgkqhkiG9w0BAQsFADBuMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDDAKBgNVBAcMA05ZQzEOMAwGA1UECgwFVmFwb3IxFDAS +BgNVBAMMC3ZhcG9yLmNvZGVzMR4wHAYJKoZIhvcNAQkBFg9qd3RAdmFwb3IuY29k +ZXMwHhcNMjAwNzMxMjMyOTQ5WhcNMjEwNzMxMjMyOTQ5WjBuMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDDAKBgNVBAcMA05ZQzEOMAwGA1UECgwFVmFwb3IxFDAS +BgNVBAMMC3ZhcG9yLmNvZGVzMR4wHAYJKoZIhvcNAQkBFg9qd3RAdmFwb3IuY29k +ZXMwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAtgeOpWeiRIq0Blbcqq4P7sKnyDmj +1mpQq7OyRKZM0qbwyyMM5Nisf5Y+RSDM7JDwqMeLspGo5znLBzN5L14JIQIDAQAB +MA0GCSqGSIb3DQEBCwUAA0EAQyBP1X40S4joTg1ov4eK0aKNlRLbWftEorGh5jCc +F3IAwlztc7uFj589k/M+xO4TGdrEVlMyiVdC5/B0MLa8LQ== +-----END CERTIFICATE----- +""" + +struct FirebasePayload: JWTPayload, Equatable { + enum CodingKeys: String, CodingKey { + case providerID = "provider_id" + case issuer = "iss" + case audience = "aud" + case authTime = "auth_time" + case userID = "user_id" + case subject = "sub" + case issuedAt = "iat" + case expiration = "exp" + } + let providerID: String + let issuer: IssuerClaim + let audience: AudienceClaim + let authTime: Int + let userID: String + let subject: SubjectClaim + let issuedAt: IssuedAtClaim + let expiration: ExpirationClaim + + func verify(using signer: JWTSigner) throws { + try self.expiration.verifyNotExpired(currentDate: .distantPast) + } +} + +let firebaseJWT = """ +eyJhbGciOiJSUzI1NiIsImtpZCI6IjU1NGE3NTQ3Nzg1ODdjOTRjMTY3M2U4ZWEyNDQ2MTZjMGMwNDNjYmMiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS92enNnLXNjaGVkdWxlLXRlc3QiLCJhdWQiOiJ2enNnLXNjaGVkdWxlLXRlc3QiLCJhdXRoX3RpbWUiOjE1OTYyMzg5ODIsInVzZXJfaWQiOiJ5OHdpS1RoWEdLTTg4eHhyUVdEWnpLbkJ1cXYyIiwic3ViIjoieTh3aUtUaFhHS004OHh4clFXRFp6S25CdXF2MiIsImlhdCI6MTU5NjIzODk4MiwiZXhwIjoxNTk2MjQyNTgyLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.vW5N3RqN8ba_P56GgjyMY-RE3hr_ciEw-E_oBtVjMJw3pgIO7MDHj0eRqTDTbjapN0BhkxTjkOA-L5pGO-9uA7afO-45vmiyaFDaN_oIYHNCewDgVaphDy_CYQ1PJugZHVjumk-qgzdS9nen_6oXmWZ1CYMop-g8UEyVHUaU-yjnvYSvvRWcas--HaErcsPY6uDx9DR8R2_mC-_VHBD58zN1svjTELkeVIZtkvA2Pxy1WO1NKxc0hWiz7w6RTu6P56_DJ1OqyMwxQavblaufdjccuC3bnv_MGKM8xhtsYLFWPnwFD762A50cHyS6SondruP7UnFQc1owlB6gaxEihw +""" + +let firebaseCert = """ +-----BEGIN CERTIFICATE----- +MIIDHDCCAgSgAwIBAgIIOvZ+ZDrIgmQwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE +AxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMjAw +NzI0MDkyMDAxWhcNMjAwODA5MjEzNTAxWjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl +bi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANSBPQydBvIITxwMsm0adXL5ToKR6Aihi3fCepGZj1Oq2pdq +r9ObfFcDX4GKHF7w6pm8WXxoZnjO37waSJc1ECmZt11tR0Ei/f0huLqDqNItGWRc +ApogR3Af8C12IwFbxvp5tPj4s8H7Ldnrr97zzXogrTKvQCVJQJE43SfqcOO0T1br +gfskj+G863Uy5JN7S8OijDLFK3YGIIvQDv6jp0tVrRwUUedJ4qET3IVWLkW5jAcd +WAy7/RmIVVZFXuqjyunU6xNd6gLw5uZPZdLjSW9CccFmZQfinuNKyFGLhdF00TMq +Torq8EOjFanRbxRi3mb9g01hVKY8WcsK1CE4RCMCAwEAAaM4MDYwDAYDVR0TAQH/ +BAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ +KoZIhvcNAQEFBQADggEBAGMRck+Afw3zQF3SqgJ80bCgFJy4CidQuoNuElA673Y+ +H4ulR5n/UV3feelR2+q0PvbZIVNf3Y5Yt+AWK9uK3LPprouFnx4U2X+mxsLHlHUC +Kl+wKoLuDvAmiDHu5JIjoYO0el6JJYNVnG3wCrSLLc6ehA32hfngdtJmkDN0/OoM +xmbj7X3JWctiJw0NxmH8wrKbeZLVIsaCwfc8iKjwcqRyA6hUxTobcsNs3IZsYv2W +g/5ZupoI8k2foTq4OdXJH/hkq4N5AyLp9S/RSodW6X+gexxohtgJxGx0gojotMzX +sb7NLsl7DkvjjxTz7I98xaGbfhofgYympeKT6UO+tmc= +-----END CERTIFICATE----- +""" + extension String { var bytes: [UInt8] { return .init(self.utf8)