From 05466214b580d6c5e74860722bd2b7d11f811e12 Mon Sep 17 00:00:00 2001 From: "Martin J. Lasek" Date: Wed, 29 Aug 2018 11:40:24 +0200 Subject: [PATCH 01/12] Typo in Doc-Block Comment (I suppose) --- Sources/JWT/JWTSigner.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JWT/JWTSigner.swift b/Sources/JWT/JWTSigner.swift index fa9f87a..722eb03 100644 --- a/Sources/JWT/JWTSigner.swift +++ b/Sources/JWT/JWTSigner.swift @@ -14,7 +14,7 @@ public final class JWTSigner { /// /// Can be transformed into a String like so: /// - /// let signature = try jws.sign() + /// let signature = try jwt.sign() /// guard let string = String(bytes: signed, encoding: .utf8) else { /// throw ... /// } From c4aaa00b972f4bd353c2dbdf4ae196525f5a77cb Mon Sep 17 00:00:00 2001 From: Valerio Mazzeo Date: Fri, 23 Nov 2018 17:00:29 +0000 Subject: [PATCH 02/12] added JWK and JWKS support --- Sources/JWT/JWK+KeyOperation.swift | 22 ++++ Sources/JWT/JWK+PublicKeyUse.swift | 42 +++++++ Sources/JWT/JWK.swift | 191 +++++++++++++++++++++++++++++ Sources/JWT/JWKS.swift | 32 +++++ Tests/JWTTests/JWKTests.swift | 36 ++++++ Tests/LinuxMain.swift | 1 + 6 files changed, 324 insertions(+) create mode 100644 Sources/JWT/JWK+KeyOperation.swift create mode 100644 Sources/JWT/JWK+PublicKeyUse.swift create mode 100644 Sources/JWT/JWK.swift create mode 100644 Sources/JWT/JWKS.swift create mode 100644 Tests/JWTTests/JWKTests.swift diff --git a/Sources/JWT/JWK+KeyOperation.swift b/Sources/JWT/JWK+KeyOperation.swift new file mode 100644 index 0000000..ac689cb --- /dev/null +++ b/Sources/JWT/JWK+KeyOperation.swift @@ -0,0 +1,22 @@ +import Foundation + +public extension JWK { + public enum KeyOperation: String, Codable { + /// Compute digital signature or MAC. + case sign + /// Verify digital signature or MAC. + case verify + /// Encrypt content. + case encrypt + /// Decrypt content and validate decryption, if applicable. + case decrypt + /// Encrypt key. + case wrapKey + /// Decrypt key and validate decryption, if applicable. + case unwrapKey + /// Derive key. + case deriveKey + /// Derive bits not to be used as a key. + case deriveBits + } +} diff --git a/Sources/JWT/JWK+PublicKeyUse.swift b/Sources/JWT/JWK+PublicKeyUse.swift new file mode 100644 index 0000000..05f94a9 --- /dev/null +++ b/Sources/JWT/JWK+PublicKeyUse.swift @@ -0,0 +1,42 @@ +import Foundation + +public extension JWK { + public enum PublicKeyUse: RawRepresentable, Codable { + case signature + case encryption + case other(String) + + public var rawValue: String { + switch self { + case .signature: + return "sig" + case .encryption: + return "enc" + case .other(let value): + return value + } + } + + public init(rawValue: String) { + switch rawValue { + case "sig": + self = .signature + case "enc": + self = .encryption + default: + self = .other(rawValue) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + self.init(rawValue: rawValue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } + } +} diff --git a/Sources/JWT/JWK.swift b/Sources/JWT/JWK.swift new file mode 100644 index 0000000..2f6d1c4 --- /dev/null +++ b/Sources/JWT/JWK.swift @@ -0,0 +1,191 @@ +import Foundation +import Crypto + +/// A JSON Web Key. +/// +/// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517. +public struct JWK: Codable { + /// The `kty` (key type) parameter identifies the cryptographic algorithm family used with the key, such as `RSA` or `EC`. The `kty` value is a case-sensitive string. + public var kty: String + + /// The `use` (public key use) parameter identifies the intended use of the public key. The `use` parameter is employed to indicate whether a public key is used for encrypting data or verifying the signature on data. + public var use: PublicKeyUse? + + /// The `key_ops` (key operations) parameter identifies the operation(s) for which the key is intended to be used. The `key_ops` parameter is intended for use cases in which public, private, or symmetric keys may be present. + public var keyOps: [KeyOperation]? + + /// The `alg` (algorithm) parameter identifies the algorithm intended for use with the key. The `alg` value is a case-sensitive ASCII string. + public var alg: String? + + /** + The `kid` (key ID) parameter is used to match a specific key. This is used, for instance, to choose among a set of keys within a JWK Set during key rollover. + + The structure of the `kid` value is unspecified. When `kid` values are used within a JWK Set, different keys within the JWK Set SHOULD use distinct `kid` values. + + (One example in which different keys might use the same `kid` value is if they have different `kty` (key type) values but are considered to be equivalent alternatives by the application using them.) + + The `kid` value is a case-sensitive string. + */ + public var kid: String? + + /// The `x5u` (X.509 URL) parameter is a URI [RFC3986] that refers to a resource for an X.509 public key certificate or certificate chain [RFC5280]. + public var x5u: String? + + /// The `x5c` (X.509 certificate chain) parameter contains a chain of one or more PKIX certificates [RFC5280]. + public var x5c: [String]? + + /// The `x5t` (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate [RFC5280]. Note that certificate thumbprints are also sometimes known as certificate fingerprints. + public var x5t: String? + + /// The `x5t#S256` (X.509 certificate SHA-256 thumbprint) parameter is a base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate [RFC5280]. + public var x5tS256: String? + + // RSA keys + // Represented as the base64url encoding of the valueโ€™s unsigned big endian representation as an octet sequence. + + /// Modulus. + public var n: String? + + /// Exponent. + public var e: String? + + /// Private exponent. + public var d: String? + + /// First prime factor. + public var p: String? + + /// Second prime factor. + public var q: String? + + /// First factor CRT exponent. + public var dp: String? + + /// Second factor CRT exponent. + public var dq: String? + + /// First CRT coefficient. + public var qi: String? + + /// Other primes info. + public var oth: OthType? + + // EC DSS keys + + public var crv: String? + + public var x: String? + + public var y: String? + + public enum OthType: String, Codable { + case r + case d + case t + } + + private enum CodingKeys: String, CodingKey { + case kty + case use + case keyOps = "key_ops" + case alg + case kid + case x5u + case x5c + case x5t + case x5tS256 = "x5t#S256" + case n + case e + case d + case p + case q + case dp + case dq + case qi + case oth + case crv + case x + case y + } + + public init( + kty: String, + use: PublicKeyUse? = nil, + keyOps: [KeyOperation]? = nil, + alg: String? = nil, + kid: String? = nil, + x5u: String? = nil, + x5c: [String]? = nil, + x5t: String? = nil, + x5tS256: String? = nil, + n: String? = nil, + e: String? = nil, + d: String? = nil, + p: String? = nil, + q: String? = nil, + dp: String? = nil, + dq: String? = nil, + qi: String? = nil, + oth: OthType? = nil, + crv: String? = nil, + x: String? = nil, + y: String? = nil + ) { + self.kty = kty + self.use = use + self.keyOps = keyOps + self.alg = alg + self.kid = kid + self.x5u = x5u + self.x5c = x5c + self.x5t = x5t + self.x5tS256 = x5tS256 + self.n = n + self.e = e + self.d = d + self.p = p + self.q = q + self.dp = dp + self.dq = dq + self.qi = qi + self.oth = oth + self.crv = crv + self.x = x + self.y = y + } +} + +public extension JWTSigner { + + /// Creates a JWT signer with the supplied JWK + public static func jwk(key: JWK) throws -> JWTSigner { + switch key.kty.lowercased() { + case "rsa": + guard let n = key.n else { + throw JWTError(identifier: "missingModulus", reason: "Modulus not specified for JWK RSA key.") + } + guard let e = key.e else { + throw JWTError(identifier: "missingExponent", reason: "Exponent not specified for JWK RSA key.") + } + + guard let algorithm = key.alg?.lowercased() else { + throw JWTError(identifier: "missingAlgorithm", reason: "Algorithm missing for JWK RSA key.") + } + + let rsaKey = try RSAKey.components(n: n, e: e, d: key.d) + + switch algorithm { + case "rs256": + return JWTSigner.rs256(key: rsaKey) + case "rs384": + return JWTSigner.rs384(key: rsaKey) + case "rs512": + return JWTSigner.rs512(key: rsaKey) + default: + throw JWTError(identifier: "invalidAlgorithm", reason: "Algorithm \(String(describing: key.alg)) not supported for JWK RSA key.") + } + default: + throw JWTError(identifier: "invalidKeyType", reason: "Key type \(String(describing: key.kty)) not supported.") + } + } +} diff --git a/Sources/JWT/JWKS.swift b/Sources/JWT/JWKS.swift new file mode 100644 index 0000000..d02ad40 --- /dev/null +++ b/Sources/JWT/JWKS.swift @@ -0,0 +1,32 @@ +import Foundation + +/// A JSON Web Key Set. +/// +/// A JSON object that represents a set of JWKs. +/// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517. +public struct JWKS: Codable { + + public var keys: [JWK] + + public init(keys: [JWK]) { + self.keys = keys + } +} + +public extension JWTSigners { + + public convenience init(jwks: JWKS, skipAnonymousKeys: Bool = true) throws { + self.init() + for jwk in jwks.keys { + guard let kid = jwk.kid else { + if skipAnonymousKeys { + continue + } else { + throw JWTError(identifier: "missingKID", reason: "At least a JSON Web Key in the JSON Web Key Set is missing a `kid`.") + } + } + + try self.use(JWTSigner.jwk(key: jwk), kid: kid) + } + } +} diff --git a/Tests/JWTTests/JWKTests.swift b/Tests/JWTTests/JWKTests.swift new file mode 100644 index 0000000..4520069 --- /dev/null +++ b/Tests/JWTTests/JWKTests.swift @@ -0,0 +1,36 @@ +import XCTest +@testable import JWT + +class JWKTests: XCTestCase { + + static let allTests = [ + ("testJWKSigner", testJWKSigner) + ] + + func testJWKSigner() throws { + + let jsonDecoder = JSONDecoder() + + let privateJWK = try jsonDecoder.decode(JWK.self, from: "{\"kty\":\"RSA\",\"d\":\"L4z0tz7QWE0aGuOA32YqCSnrSYKdBTPFDILCdfHonzfP7WMPibz4jWxu_FzNk9s4Dh-uN2lV3NGW10pAsnqffD89LtYanRjaIdHnLW_PFo5fEL2yltK7qMB9hO1JegppKCfoc79W4-dr-4qy1Op0B3npOP-DaUYlNamfDmIbQW32UKeJzdGIn-_ryrBT7hQW6_uHLS2VFPPk0rNkPPKZYoNaqGnJ0eaFFF-dFwiThXIpPz--dxTAL8xYf275rjG8C9lh6awOfJSIdXMVuQITWf62E0mSQPR2-219bShMKriDYcYLbT3BJEgOkRBBHGuHo9R5TN298anxZqV1u5jtUQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"1234\",\"alg\":\"RS256\",\"n\":\"gWu7yhI35FScdKARYboJoAm-T7yJfJ9JTvAok_RKOJYcL8oLIRSeLqQX83PPZiWdKTdXaiGWntpDu6vW7VAb-HWPF6tNYSLKDSmR3sEu2488ibWijZtNTCKOSb_1iAKAI5BJ80LTqyQtqaKzT0XUBtMsde8vX1nKI05UxujfTX3kqUtkZgLv1Yk1ZDpUoLOWUTtCm68zpjtBrPiN8bU2jqCGFyMyyXys31xFRzz4MyJ5tREHkQCzx0g7AvW0ge_sBTPQ2U6NSkcZvQyDbfDv27cMUHij1Sjx16SY9a2naTuOgamjtUzyClPLVpchX-McNyS0tjdxWY_yRL9MYuw4AQ\"}".convertToData()) + + let publicJWK = try jsonDecoder.decode(JWK.self, from: "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"1234\",\"alg\":\"RS256\",\"n\":\"gWu7yhI35FScdKARYboJoAm-T7yJfJ9JTvAok_RKOJYcL8oLIRSeLqQX83PPZiWdKTdXaiGWntpDu6vW7VAb-HWPF6tNYSLKDSmR3sEu2488ibWijZtNTCKOSb_1iAKAI5BJ80LTqyQtqaKzT0XUBtMsde8vX1nKI05UxujfTX3kqUtkZgLv1Yk1ZDpUoLOWUTtCm68zpjtBrPiN8bU2jqCGFyMyyXys31xFRzz4MyJ5tREHkQCzx0g7AvW0ge_sBTPQ2U6NSkcZvQyDbfDv27cMUHij1Sjx16SY9a2naTuOgamjtUzyClPLVpchX-McNyS0tjdxWY_yRL9MYuw4AQ\"}".convertToData()) + + let privateSigner = try JWTSigner.jwk(key: privateJWK) + let publicSigner = try JWTSigner.jwk(key: publicJWK) + + let jwt = JWT(payload: TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + )) + + let signature = try privateSigner.sign(jwt) + + let publicVerified = try JWT(from: signature, verifiedUsing: publicSigner) + let privateVerified = try JWT(from: signature, verifiedUsing: privateSigner) + + XCTAssertEqual(publicVerified.payload.name, "Foo") + XCTAssertEqual(privateVerified.payload.name, "Foo") + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 7cbedb4..903b7ef 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -3,4 +3,5 @@ import XCTest XCTMain([ testCase(JWTTests.allTests), + testCase(JWKTests.allTests) ]) From 5ce0d72cd06c91b216c5df84b799374b81110732 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 12 Jun 2019 19:54:31 -0400 Subject: [PATCH 03/12] jwt kit --- .gitignore | 1 + Package.swift | 26 ++-- Sources/CJWTKitOpenSSL/module.modulemap | 5 + Sources/CJWTKitOpenSSL/shim.c | 1 + Sources/CJWTKitOpenSSL/shim.h | 17 +++ Sources/JWT/Deprecated.swift | 15 -- Sources/JWT/Exports.swift | 1 - Sources/JWT/JWT.swift | 128 ------------------ Sources/JWT/JWTAlgorithm.swift | 90 ------------ Sources/JWTKit/Base64URL.swift | 64 +++++++++ Sources/JWTKit/Exports.swift | 2 + Sources/JWTKit/JWT.swift | 48 +++++++ Sources/JWTKit/JWTAlgorithm.swift | 59 ++++++++ Sources/{JWT => JWTKit}/JWTClaim.swift | 0 Sources/{JWT => JWTKit}/JWTClaims.swift | 0 Sources/{JWT => JWTKit}/JWTError.swift | 4 +- Sources/{JWT => JWTKit}/JWTHeader.swift | 0 Sources/JWTKit/JWTMessage.swift | 7 + Sources/{JWT => JWTKit}/JWTPayload.swift | 0 Sources/{JWT => JWTKit}/JWTSigner.swift | 57 +++++--- Sources/{JWT => JWTKit}/JWTSigners.swift | 0 Sources/JWTKit/Utilities.swift | 22 +++ .../{JWTTests => JWTKitTests}/JWTTests.swift | 0 23 files changed, 277 insertions(+), 270 deletions(-) create mode 100644 Sources/CJWTKitOpenSSL/module.modulemap create mode 100644 Sources/CJWTKitOpenSSL/shim.c create mode 100644 Sources/CJWTKitOpenSSL/shim.h delete mode 100644 Sources/JWT/Deprecated.swift delete mode 100644 Sources/JWT/Exports.swift delete mode 100644 Sources/JWT/JWT.swift delete mode 100644 Sources/JWT/JWTAlgorithm.swift create mode 100644 Sources/JWTKit/Base64URL.swift create mode 100644 Sources/JWTKit/Exports.swift create mode 100644 Sources/JWTKit/JWT.swift create mode 100644 Sources/JWTKit/JWTAlgorithm.swift rename Sources/{JWT => JWTKit}/JWTClaim.swift (100%) rename Sources/{JWT => JWTKit}/JWTClaims.swift (100%) rename Sources/{JWT => JWTKit}/JWTError.swift (86%) rename Sources/{JWT => JWTKit}/JWTHeader.swift (100%) create mode 100644 Sources/JWTKit/JWTMessage.swift rename Sources/{JWT => JWTKit}/JWTPayload.swift (100%) rename Sources/{JWT => JWTKit}/JWTSigner.swift (64%) rename Sources/{JWT => JWTKit}/JWTSigners.swift (100%) create mode 100644 Sources/JWTKit/Utilities.swift rename Tests/{JWTTests => JWTKitTests}/JWTTests.swift (100%) diff --git a/.gitignore b/.gitignore index 0ec8f3e..638dec5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ Packages Package.pins Package.resolved DerivedData +.swiftpm diff --git a/Package.swift b/Package.swift index 34012ef..8ec0273 100644 --- a/Package.swift +++ b/Package.swift @@ -1,20 +1,22 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.0 import PackageDescription let package = Package( - name: "JWT", + name: "jwt-kit", products: [ - .library(name: "JWT", targets: ["JWT"]), - ], - dependencies: [ - // ๐ŸŒŽ Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging. - .package(url: "https://github.com/vapor/core.git", from: "3.0.0"), - - // ๐Ÿ”‘ Hashing (BCrypt, SHA, HMAC, etc), encryption, and randomness. - .package(url: "https://github.com/vapor/crypto.git", from: "3.0.0"), + .library(name: "JWTKit", targets: ["JWTKit"]), ], + dependencies: [ ], targets: [ - .target(name: "JWT", dependencies: ["Core", "Crypto"]), - .testTarget(name: "JWTTests", dependencies: ["JWT"]), + .systemLibrary( + name: "CJWTKitOpenSSL", + pkgConfig: "openssl", + providers: [ + .apt(["openssl libssl-dev"]), + .brew(["openssl@1.1"]) + ] + ), + .target(name: "JWTKit", dependencies: ["CJWTKitOpenSSL"]), + .testTarget(name: "JWTKitTests", dependencies: ["JWTKit"]), ] ) diff --git a/Sources/CJWTKitOpenSSL/module.modulemap b/Sources/CJWTKitOpenSSL/module.modulemap new file mode 100644 index 0000000..81cc080 --- /dev/null +++ b/Sources/CJWTKitOpenSSL/module.modulemap @@ -0,0 +1,5 @@ +module CJWTKitOpenSSL [system] { + header "shim.h" + link "ssl" + link "crypto" +} diff --git a/Sources/CJWTKitOpenSSL/shim.c b/Sources/CJWTKitOpenSSL/shim.c new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sources/CJWTKitOpenSSL/shim.c @@ -0,0 +1 @@ + diff --git a/Sources/CJWTKitOpenSSL/shim.h b/Sources/CJWTKitOpenSSL/shim.h new file mode 100644 index 0000000..48d9c22 --- /dev/null +++ b/Sources/CJWTKitOpenSSL/shim.h @@ -0,0 +1,17 @@ +#ifndef C_JWT_CRYTPO_H +#define C_JWT_CRYTPO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/Sources/JWT/Deprecated.swift b/Sources/JWT/Deprecated.swift deleted file mode 100644 index 2dab0b0..0000000 --- a/Sources/JWT/Deprecated.swift +++ /dev/null @@ -1,15 +0,0 @@ -extension ExpirationClaim { - /// Deprecated. - @available(*, deprecated, renamed: "verifyNotExpired") - public func verify() throws { - try verifyNotExpired() - } -} - -extension NotBeforeClaim { - /// Deprecated. - @available(*, deprecated, renamed: "verifyNotBefore") - public func verify() throws { - try verifyNotBefore() - } -} diff --git a/Sources/JWT/Exports.swift b/Sources/JWT/Exports.swift deleted file mode 100644 index fc16f3e..0000000 --- a/Sources/JWT/Exports.swift +++ /dev/null @@ -1 +0,0 @@ -@_exported import Core diff --git a/Sources/JWT/JWT.swift b/Sources/JWT/JWT.swift deleted file mode 100644 index 458c1c3..0000000 --- a/Sources/JWT/JWT.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Crypto - -/// A JSON Web Token with a generic, codable payload. -/// -/// let jwt = JWT(payload: ...) -/// let data = try jwt.sign(using: ...) -/// -/// Learn more at https://jwt.io. -/// Read specification (RFC 7519) https://tools.ietf.org/html/rfc7519. -public struct JWT where Payload: JWTPayload { - /// The headers linked to this message - public var header: JWTHeader - - /// The JSON payload within this message - public var payload: Payload - - /// Creates a new JSON Web Signature from predefined data - public init(header: JWTHeader = .init(), payload: Payload) { - self.header = header - self.payload = payload - } - - /// Parses a JWT string into a JSON Web Signature - public init(from data: LosslessDataConvertible, verifiedUsing signer: JWTSigner) throws { - let parts = data.convertToData().split(separator: .period) - guard parts.count == 3 else { - throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") - } - - let headerData = Data(parts[0]) - let payloadData = Data(parts[1]) - let signatureData = Data(parts[2]) - - guard try signer.verify(signatureData, header: headerData, payload: payloadData) else { - throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") - } - - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .secondsSince1970 - - guard let decodedHeader = Data(base64URLEncoded: headerData) else { - throw JWTError(identifier: "base64", reason: "JWT header is not valid base64-url") - } - guard let decodedPayload = Data(base64URLEncoded: payloadData) else { - throw JWTError(identifier: "base64", reason: "JWT payload is not valid base64-url") - } - - self.header = try jsonDecoder.decode(JWTHeader.self, from: decodedHeader) - self.payload = try jsonDecoder.decode(Payload.self, from: decodedPayload) - try payload.verify(using: signer) - } - - /// Parses a JWT string into a JSON Web Signature - public init(from data: LosslessDataConvertible, verifiedUsing signers: JWTSigners) throws { - let parts = data.convertToData().split(separator: .period) - guard parts.count == 3 else { - throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") - } - - let headerData = Data(parts[0]) - let payloadData = Data(parts[1]) - let signatureData = Data(parts[2]) - - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .secondsSince1970 - - guard let decodedHeader = Data(base64URLEncoded: headerData) else { - throw JWTError(identifier: "base64", reason: "JWT header is not valid base64-url") - } - - let header = try jsonDecoder.decode(JWTHeader.self, from: decodedHeader) - guard let kid = header.kid else { - throw JWTError(identifier: "missingKID", reason: "`kid` header property required to identify signer") - } - - let signer = try signers.requireSigner(kid: kid) - guard try signer.verify(signatureData, header: headerData, payload: payloadData) else { - throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") - } - - guard let decodedPayload = Data(base64URLEncoded: payloadData) else { - throw JWTError(identifier: "base64", reason: "JWT payload is not valid base64-url") - } - - self.header = header - self.payload = try jsonDecoder.decode(Payload.self, from: decodedPayload) - try payload.verify(using: signer) - } - - /// Parses a JWT string into a JSON Web Signature - public init(unverifiedFrom data: Data) throws { - let parts = data.split(separator: .period) - guard parts.count == 3 else { - throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") - } - - let headerData = Data(parts[0]) - let payloadData = Data(parts[1]) - - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .secondsSince1970 - - guard let decodedHeader = Data(base64URLEncoded: headerData) else { - throw JWTError(identifier: "base64", reason: "JWT header is not valid base64-url") - } - guard let decodedPayload = Data(base64URLEncoded: payloadData) else { - throw JWTError(identifier: "base64", reason: "JWT payload is not valid base64-url") - } - - self.header = try jsonDecoder.decode(JWTHeader.self, from: decodedHeader) - self.payload = try jsonDecoder.decode(Payload.self, from: decodedPayload) - } - - /// Signs the message and returns the serialized JSON web token - public func sign(using signers: JWTSigners) throws -> Data { - guard let kid = header.kid else { - throw JWTError(identifier: "missingKID", reason: "`kid` header property required to identify signer") - } - - let signer = try signers.requireSigner(kid: kid) - return try signer.sign(self) - } - - /// Signs the message and returns the serialized JSON web token - public func sign(using signer: JWTSigner) throws -> Data { - return try signer.sign(self) - } -} diff --git a/Sources/JWT/JWTAlgorithm.swift b/Sources/JWT/JWTAlgorithm.swift deleted file mode 100644 index b11d93a..0000000 --- a/Sources/JWT/JWTAlgorithm.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Crypto - -/// Algorithm powering a `JWTSigner`. -public protocol JWTAlgorithm { - /// Unique JWT-standard name for this algorithm. - var jwtAlgorithmName: String { get } - - /// Creates a signature from the supplied plaintext. - /// - /// let sig = try alg.sign("hello") - /// - /// - parameters: - /// - plaintext: Plaintext data to sign. - /// - returns: Signature unique to the supplied data. - func sign(_ plaintext: LosslessDataConvertible) throws -> Data - - /// Returns `true` if the signature was creating by signing the plaintext. - /// - /// let sig = try alg.sign("hello") - /// - /// if alg.verify(sig, signs: "hello") { - /// print("signature is valid") - /// } else { - /// print("signature is invalid") - /// } - /// - /// The above snippet should print `"signature is valid"`. - /// - /// - parameters: - /// - signature: Signature data resulting from a previous call to `sign(:_)`. - /// - plaintext: Plaintext data to check signature against. - /// - returns: Returns `true` if the signature was created by the supplied plaintext data. - func verify(_ signature: LosslessDataConvertible, signs plaintext: LosslessDataConvertible) throws -> Bool -} - -extension JWTAlgorithm { - /// See `JWTAlgorithm`. - public func verify(_ signature: LosslessDataConvertible, signs plaintext: LosslessDataConvertible) throws -> Bool { - let chk = try sign(plaintext) - let sig = signature.convertToData() - - // byte-by-byte comparison to avoid timing attacks - var match = true - for i in 0.. Data - - /// See `JWTAlgorithm`. - private let verifyClosure: (LosslessDataConvertible, LosslessDataConvertible) throws -> Bool - - /// Create a new `CustomJWTAlgorithm`. - public init( - name: String, - sign: @escaping (LosslessDataConvertible) throws -> Data, - verify: @escaping (LosslessDataConvertible, LosslessDataConvertible) throws -> Bool - ) { - self.jwtAlgorithmName = name - self.signClosure = sign - self.verifyClosure = verify - } - - /// See `JWTAlgorithm`. - public func sign(_ plaintext: LosslessDataConvertible) throws -> Data { - return try signClosure(plaintext) - } - - /// See `JWTAlgorithm`. - public func verify(_ signature: LosslessDataConvertible, signs plaintext: LosslessDataConvertible) throws -> Bool { - return try verifyClosure(signature, plaintext) - } -} diff --git a/Sources/JWTKit/Base64URL.swift b/Sources/JWTKit/Base64URL.swift new file mode 100644 index 0000000..5134027 --- /dev/null +++ b/Sources/JWTKit/Base64URL.swift @@ -0,0 +1,64 @@ +import struct Foundation.Data + +extension DataProtocol { + func base64URLDecodedBytes() -> [UInt8] { + return Data(base64Encoded: Data(self.copyBytes()).base64URLUnescaped())?.copyBytes() ?? [] + } + + func base64URLEncodedBytes() -> [UInt8] { + return Data(self.copyBytes()).base64EncodedData().base64URLEscaped().copyBytes() + } +} + +/// MARK: Data Escape +private extension Data { + /// Converts base64-url encoded data to a base64 encoded data. + /// + /// https://tools.ietf.org/html/rfc4648#page-7 + mutating func base64URLUnescape() { + for (i, byte) in self.enumerated() { + switch byte { + case 0x2D: self[self.index(self.startIndex, offsetBy: i)] = 0x2B + case 0x5F: self[self.index(self.startIndex, offsetBy: i)] = 0x2F + default: break + } + } + /// https://stackoverflow.com/questions/43499651/decode-base64url-to-base64-swift + let padding = count % 4 + if padding > 0 { + self += Data(repeating: 0x3D, count: 4 - count % 4) + } + } + + /// Converts base64 encoded data to a base64-url encoded data. + /// + /// https://tools.ietf.org/html/rfc4648#page-7 + mutating func base64URLEscape() { + for (i, byte) in enumerated() { + switch byte { + case 0x2B: self[self.index(self.startIndex, offsetBy: i)] = 0x2D + case 0x2F: self[self.index(self.startIndex, offsetBy: i)] = 0x5F + default: break + } + } + self = split(separator: 0x3D).first ?? .init() + } + + /// Converts base64-url encoded data to a base64 encoded data. + /// + /// https://tools.ietf.org/html/rfc4648#page-7 + func base64URLUnescaped() -> Self { + var data = self + data.base64URLUnescape() + return data + } + + /// Converts base64 encoded data to a base64-url encoded data. + /// + /// https://tools.ietf.org/html/rfc4648#page-7 + func base64URLEscaped() -> Self { + var data = self + data.base64URLEscape() + return data + } +} diff --git a/Sources/JWTKit/Exports.swift b/Sources/JWTKit/Exports.swift new file mode 100644 index 0000000..b98ecb0 --- /dev/null +++ b/Sources/JWTKit/Exports.swift @@ -0,0 +1,2 @@ +@_exported import struct Foundation.Date +@_exported import protocol Foundation.DataProtocol diff --git a/Sources/JWTKit/JWT.swift b/Sources/JWTKit/JWT.swift new file mode 100644 index 0000000..51e9963 --- /dev/null +++ b/Sources/JWTKit/JWT.swift @@ -0,0 +1,48 @@ +import class Foundation.JSONDecoder + +/// A JSON Web Token with a generic, codable payload. +/// +/// let jwt = JWT(payload: ...) +/// let data = try jwt.sign(using: ...) +/// +/// Learn more at https://jwt.io. +/// Read specification (RFC 7519) https://tools.ietf.org/html/rfc7519. +public struct JWT where Payload: JWTPayload { + /// The headers linked to this message + public var header: JWTHeader + + /// The JSON payload within this message + public var payload: Payload + + /// Creates a new JSON Web Signature from predefined data + public init(header: JWTHeader = .init(), payload: Payload) { + self.header = header + self.payload = payload + } + + /// Parses a JWT string into a JSON Web Signature + public init(header: Header, payload: Payload) throws + where Header: DataProtocol, Payload: DataProtocol + { + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .secondsSince1970 + + self.header = try jsonDecoder.decode(JWTHeader.self, from: .init(header.copyBytes())) + self.payload = try jsonDecoder.decode(Payload.self, from: .init(payload.copyBytes())) + } + + /// Signs the message and returns the serialized JSON web token + public func sign(using signers: JWTSigners) throws -> Data { + guard let kid = self.header.kid else { + throw JWTError(identifier: "missingKID", reason: "`kid` header property required to identify signer") + } + + let signer = try signers.requireSigner(kid: kid) + return try signer.sign(self) + } + + /// Signs the message and returns the serialized JSON web token + public func sign(using signer: JWTSigner) throws -> Data { + return try signer.sign(self) + } +} diff --git a/Sources/JWTKit/JWTAlgorithm.swift b/Sources/JWTKit/JWTAlgorithm.swift new file mode 100644 index 0000000..eb80d41 --- /dev/null +++ b/Sources/JWTKit/JWTAlgorithm.swift @@ -0,0 +1,59 @@ +/// Algorithm powering a `JWTSigner`. +public protocol JWTAlgorithm { + /// Unique JWT-standard name for this algorithm. + var jwtAlgorithmName: String { get } + + /// Creates a signature from the supplied plaintext. + /// + /// let sig = try alg.sign("hello") + /// + /// - parameters: + /// - plaintext: Plaintext data to sign. + /// - returns: Signature unique to the supplied data. + func sign(_ plaintext: Plaintext) throws -> [UInt8] + where Plaintext: DataProtocol + + /// Returns `true` if the signature was creating by signing the plaintext. + /// + /// let sig = try alg.sign("hello") + /// + /// if alg.verify(sig, signs: "hello") { + /// print("signature is valid") + /// } else { + /// print("signature is invalid") + /// } + /// + /// The above snippet should print `"signature is valid"`. + /// + /// - parameters: + /// - signature: Signature data resulting from a previous call to `sign(:_)`. + /// - plaintext: Plaintext data to check signature against. + /// - returns: Returns `true` if the signature was created by the supplied plaintext data. + func verify<Signature, Plaintext>(_ signature: Signature, signs plaintext: Plaintext) throws -> Bool + where Signature: DataProtocol, Plaintext: DataProtocol +} + +extension JWTAlgorithm { + /// See `JWTAlgorithm`. + func verify<Signature, Plaintext>(_ signature: Signature, signs plaintext: Plaintext) throws -> Bool + where Signature: DataProtocol, Plaintext: DataProtocol + { + // create test signature + let check = try self.sign(plaintext) + + // byte-by-byte comparison to avoid timing attacks + var match = true + for (a, b) in zip(check, signature) { + if a != b { + match = false + } + } + + // finally, if the counts match then we can accept the result + if check.count == signature.count { + return match + } else { + return false + } + } +} diff --git a/Sources/JWT/JWTClaim.swift b/Sources/JWTKit/JWTClaim.swift similarity index 100% rename from Sources/JWT/JWTClaim.swift rename to Sources/JWTKit/JWTClaim.swift diff --git a/Sources/JWT/JWTClaims.swift b/Sources/JWTKit/JWTClaims.swift similarity index 100% rename from Sources/JWT/JWTClaims.swift rename to Sources/JWTKit/JWTClaims.swift diff --git a/Sources/JWT/JWTError.swift b/Sources/JWTKit/JWTError.swift similarity index 86% rename from Sources/JWT/JWTError.swift rename to Sources/JWTKit/JWTError.swift index f034f05..0a78afb 100644 --- a/Sources/JWT/JWTError.swift +++ b/Sources/JWTKit/JWTError.swift @@ -1,7 +1,5 @@ -import Debugging - /// Errors that can be thrown while working with JWT. -public struct JWTError: Debuggable, Error { +public struct JWTError: Error { /// See `Debuggable`. public static var readableName = "JWT Error" diff --git a/Sources/JWT/JWTHeader.swift b/Sources/JWTKit/JWTHeader.swift similarity index 100% rename from Sources/JWT/JWTHeader.swift rename to Sources/JWTKit/JWTHeader.swift diff --git a/Sources/JWTKit/JWTMessage.swift b/Sources/JWTKit/JWTMessage.swift new file mode 100644 index 0000000..c6245f0 --- /dev/null +++ b/Sources/JWTKit/JWTMessage.swift @@ -0,0 +1,7 @@ +public struct JWTMessage { + let bytes: [UInt8] + + init(bytes: [UInt8]) { + self.bytes = bytes + } +} diff --git a/Sources/JWT/JWTPayload.swift b/Sources/JWTKit/JWTPayload.swift similarity index 100% rename from Sources/JWT/JWTPayload.swift rename to Sources/JWTKit/JWTPayload.swift diff --git a/Sources/JWT/JWTSigner.swift b/Sources/JWTKit/JWTSigner.swift similarity index 64% rename from Sources/JWT/JWTSigner.swift rename to Sources/JWTKit/JWTSigner.swift index 722eb03..2e939a0 100644 --- a/Sources/JWT/JWTSigner.swift +++ b/Sources/JWTKit/JWTSigner.swift @@ -1,4 +1,4 @@ -import Crypto +import class Foundation.JSONEncoder /// A JWT signer. public final class JWTSigner { @@ -23,7 +23,7 @@ public final class JWTSigner { /// - parameters: /// - jwt: JWT to sign. /// - returns: Signed JWT data. - public func sign<Payload>(_ jwt: JWT<Payload>) throws -> Data { + public func sign<Payload>(_ jwt: JWT<Payload>) throws -> JWTMessage { let jsonEncoder = JSONEncoder() jsonEncoder.dateEncodingStrategy = .secondsSince1970 @@ -31,33 +31,48 @@ public final class JWTSigner { var header = jwt.header header.alg = algorithm.jwtAlgorithmName let headerData = try jsonEncoder.encode(header) - let encodedHeader = headerData.base64URLEncodedData() + let encodedHeader = headerData.base64URLEncodedBytes() // encode payload let payloadData = try jsonEncoder.encode(jwt.payload) - let encodedPayload = payloadData.base64URLEncodedData() + let encodedPayload = payloadData.base64URLEncodedBytes() // combine header and payload to create signature - let encodedSignature = try signature(header: encodedHeader, payload: encodedPayload) + let signatureData = try self.algorithm.sign(encodedHeader + [.period] + encodedPayload) // yield complete jwt - return encodedHeader + Data([.period]) + encodedPayload + Data([.period]) + encodedSignature + return JWTMessage( + bytes: encodedHeader + + [.period] + + encodedPayload + + [.period] + + signatureData.base64URLEncodedBytes() + ) } /// Generates a signature for the supplied payload and header. - public func signature(header: LosslessDataConvertible, payload: LosslessDataConvertible) throws -> Data { - let message: Data = header.convertToData() + Data([.period]) + payload.convertToData() - let signature = try algorithm.sign(message) - return signature.base64URLEncodedData() - } + public func verify<Payload>(_ message: JWTMessage) throws -> JWT<Payload> { + let message = message.bytes.split(separator: .period) + guard message.count == 3 else { + throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") + } - /// Generates a signature for the supplied payload and header. - public func verify(_ signature: LosslessDataConvertible, header: LosslessDataConvertible, payload: LosslessDataConvertible) throws -> Bool { - let message: Data = header.convertToData() + Data([.period]) + payload.convertToData() - guard let signature = Data(base64URLEncoded: signature.convertToData()) else { - throw JWTError(identifier: "base64", reason: "JWT signature is not valid base64-url") + let encodedHeader = message[0] + let encodedPayload = message[1] + let encodedSignature = message[2] + guard try self.algorithm.verify( + encodedSignature.base64URLDecodedBytes(), + signs: encodedHeader + [.period] + encodedPayload + ) else { + throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") } - return try algorithm.verify(signature, signs: message) + + let jwt = try JWT<Payload>( + header: encodedHeader.base64URLDecodedBytes(), + payload: encodedPayload.base64URLDecodedBytes() + ) + try jwt.payload.verify(using: self) + return jwt } } @@ -65,22 +80,22 @@ public final class JWTSigner { extension JWTSigner { /// Creates an HS256 JWT signer with the supplied key - public static func hs256(key: LosslessDataConvertible) -> JWTSigner { + public static func hs256(key: CryptoData) -> JWTSigner { return hmac(HMAC.SHA256, name: "HS256", key: key) } /// Creates an HS384 JWT signer with the supplied key - public static func hs384(key: LosslessDataConvertible) -> JWTSigner { + public static func hs384(key: CryptoData) -> JWTSigner { return hmac(HMAC.SHA384, name: "HS384", key: key) } /// Creates an HS512 JWT signer with the supplied key - public static func hs512(key: LosslessDataConvertible) -> JWTSigner { + public static func hs512(key: CryptoData) -> JWTSigner { return hmac(HMAC.SHA512, name: "HS512", key: key) } /// Creates an HMAC-based `CustomJWTAlgorithm` and `JWTSigner`. - private static func hmac(_ hmac: HMAC, name: String, key: LosslessDataConvertible) -> JWTSigner { + private static func hmac(_ hmac: HMAC, name: String, key: CryptoData) -> JWTSigner { let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in return try hmac.authenticate(plaintext, key: key) }, verify: { signature, plaintext in diff --git a/Sources/JWT/JWTSigners.swift b/Sources/JWTKit/JWTSigners.swift similarity index 100% rename from Sources/JWT/JWTSigners.swift rename to Sources/JWTKit/JWTSigners.swift diff --git a/Sources/JWTKit/Utilities.swift b/Sources/JWTKit/Utilities.swift new file mode 100644 index 0000000..9a9c4d1 --- /dev/null +++ b/Sources/JWTKit/Utilities.swift @@ -0,0 +1,22 @@ +extension DataProtocol { + func copyBytes() -> [UInt8] { + if let array = self.withContiguousStorageIfAvailable({ buffer in + return [UInt8](buffer) + }) { + return array + } else { + var buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: self.count) + self.copyBytes(to: buffer) + defer { buffer.deallocate() } + return [UInt8](buffer) + } + } +} + + + +extension UInt8 { + static var period: UInt8 { + return Character(".").asciiValue! + } +} diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTKitTests/JWTTests.swift similarity index 100% rename from Tests/JWTTests/JWTTests.swift rename to Tests/JWTKitTests/JWTTests.swift From 2d0b50b6901b5a62a4e4d426ff1d03344375556f Mon Sep 17 00:00:00 2001 From: tanner0101 <me@tanner.xyz> Date: Wed, 12 Jun 2019 20:57:57 -0400 Subject: [PATCH 04/12] add hmac --- Sources/JWTKit/Exports.swift | 1 + Sources/JWTKit/JWT.swift | 65 ++++++++--- Sources/JWTKit/JWTError.swift | 8 +- Sources/JWTKit/JWTMessage.swift | 40 ++++++- Sources/JWTKit/JWTSigner+HMAC.swift | 67 +++++++++++ Sources/JWTKit/JWTSigner.swift | 169 ++++++++++------------------ Sources/JWTKit/JWTSigners.swift | 7 ++ Tests/JWTKitTests/JWTTests.swift | 90 ++++++++------- 8 files changed, 272 insertions(+), 175 deletions(-) create mode 100644 Sources/JWTKit/JWTSigner+HMAC.swift diff --git a/Sources/JWTKit/Exports.swift b/Sources/JWTKit/Exports.swift index b98ecb0..cc82669 100644 --- a/Sources/JWTKit/Exports.swift +++ b/Sources/JWTKit/Exports.swift @@ -1,2 +1,3 @@ @_exported import struct Foundation.Date @_exported import protocol Foundation.DataProtocol +@_exported import protocol Foundation.ContiguousBytes diff --git a/Sources/JWTKit/JWT.swift b/Sources/JWTKit/JWT.swift index 51e9963..df1ce38 100644 --- a/Sources/JWTKit/JWT.swift +++ b/Sources/JWTKit/JWT.swift @@ -1,4 +1,6 @@ +import class Foundation.JSONEncoder import class Foundation.JSONDecoder +import struct Foundation.Data /// A JSON Web Token with a generic, codable payload. /// @@ -21,28 +23,61 @@ public struct JWT<Payload> where Payload: JWTPayload { } /// Parses a JWT string into a JSON Web Signature - public init<Header, Payload>(header: Header, payload: Payload) throws - where Header: DataProtocol, Payload: DataProtocol + public init<Message>(message: Message, using signer: JWTSigner) throws + where Message: DataProtocol { + let message = message.copyBytes().split(separator: .period) + guard message.count == 3 else { + throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") + } + + let encodedHeader = message[0] + let encodedPayload = message[1] + let encodedSignature = message[2] + + let jsonDecoder = JSONDecoder() jsonDecoder.dateDecodingStrategy = .secondsSince1970 + let header = try jsonDecoder.decode(JWTHeader.self, from: Data(encodedHeader.base64URLDecodedBytes())) + let payload = try jsonDecoder.decode(Payload.self, from: Data(encodedPayload.base64URLDecodedBytes())) - self.header = try jsonDecoder.decode(JWTHeader.self, from: .init(header.copyBytes())) - self.payload = try jsonDecoder.decode(Payload.self, from: .init(payload.copyBytes())) - } - - /// Signs the message and returns the serialized JSON web token - public func sign(using signers: JWTSigners) throws -> Data { - guard let kid = self.header.kid else { - throw JWTError(identifier: "missingKID", reason: "`kid` header property required to identify signer") + guard try signer.verify( + encodedSignature.base64URLDecodedBytes(), + signs: encodedHeader + [.period] + encodedPayload, + header: header + ) else { + throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") } - - let signer = try signers.requireSigner(kid: kid) - return try signer.sign(self) + + self.header = header + self.payload = payload + + try self.payload.verify(using: signer) } /// Signs the message and returns the serialized JSON web token - public func sign(using signer: JWTSigner) throws -> Data { - return try signer.sign(self) + public func sign(using signer: JWTSigner) throws -> [UInt8] { + let jsonEncoder = JSONEncoder() + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + + // encode header, copying header struct to mutate alg + var header = self.header + header.alg = signer.algorithm.jwtAlgorithmName + let headerData = try jsonEncoder.encode(header) + let encodedHeader = headerData.base64URLEncodedBytes() + + // encode payload + let payloadData = try jsonEncoder.encode(self.payload) + let encodedPayload = payloadData.base64URLEncodedBytes() + + // combine header and payload to create signature + let signatureData = try signer.algorithm.sign(encodedHeader + [.period] + encodedPayload) + + // yield complete jwt + return encodedHeader + + [.period] + + encodedPayload + + [.period] + + signatureData.base64URLEncodedBytes() } } diff --git a/Sources/JWTKit/JWTError.swift b/Sources/JWTKit/JWTError.swift index 0a78afb..868ceb1 100644 --- a/Sources/JWTKit/JWTError.swift +++ b/Sources/JWTKit/JWTError.swift @@ -1,5 +1,7 @@ +import Foundation + /// Errors that can be thrown while working with JWT. -public struct JWTError: Error { +public struct JWTError: Error, LocalizedError { /// See `Debuggable`. public static var readableName = "JWT Error" @@ -8,6 +10,10 @@ public struct JWTError: Error { /// See `Debuggable`. public var identifier: String + + public var errorDescription: String? { + return self.reason + } /// Create a new `JWTError`. public init(identifier: String, reason: String) { diff --git a/Sources/JWTKit/JWTMessage.swift b/Sources/JWTKit/JWTMessage.swift index c6245f0..c2f49bc 100644 --- a/Sources/JWTKit/JWTMessage.swift +++ b/Sources/JWTKit/JWTMessage.swift @@ -1,7 +1,39 @@ -public struct JWTMessage { - let bytes: [UInt8] +public struct JWTMessage: ContiguousBytes, CustomStringConvertible, Hashable, Sequence { + var header: [UInt8] + var payload: [UInt8] + var signature: [UInt8] - init(bytes: [UInt8]) { - self.bytes = bytes + public var description: String { + return String(decoding: self.bytes, as: UTF8.self) + } + + public var bytes: [UInt8] { + return self.header + [.period] + self.payload + [.period] + self.signature + } + + init(bytes: [UInt8]) throws { + let parts = bytes.split(separator: .period) + guard parts.count == 3 else { + throw JWTError(identifier: "format", reason: "Invalid JWT format") + } + self.init(header: parts[0], payload: parts[1], signature: parts[2]) + } + + init<Header, Payload, Signature>( + header: Header, payload: Payload, signature: Signature + ) + where Header: DataProtocol, Payload: DataProtocol, Signature: DataProtocol + { + self.header = header.copyBytes() + self.payload = payload.copyBytes() + self.signature = signature.copyBytes() + } + + public func makeIterator() -> Array<UInt8>.Iterator { + return self.bytes.makeIterator() + } + + public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) } } diff --git a/Sources/JWTKit/JWTSigner+HMAC.swift b/Sources/JWTKit/JWTSigner+HMAC.swift new file mode 100644 index 0000000..cbe25d9 --- /dev/null +++ b/Sources/JWTKit/JWTSigner+HMAC.swift @@ -0,0 +1,67 @@ +import CJWTKitOpenSSL + +extension JWTSigner { + public static func hs256<Key>(key: Key) -> JWTSigner + where Key: DataProtocol + { + return .init(algorithm: HMACAlgorithm( + key: key.copyBytes(), + algorithm: EVP_sha256(), + jwtAlgorithmName: "HS256" + )) + } + + public static func hs384<Key>(key: Key) -> JWTSigner + where Key: DataProtocol + { + return .init(algorithm: HMACAlgorithm( + key: key.copyBytes(), + algorithm: EVP_sha384(), + jwtAlgorithmName: "HS384" + )) + } + + public static func hs512<Key>(key: Key) -> JWTSigner + where Key: DataProtocol + { + return .init(algorithm: HMACAlgorithm( + key: key.copyBytes(), + algorithm: EVP_sha512(), + jwtAlgorithmName: "HS512" + )) + } +} + +private struct HMACAlgorithm: JWTAlgorithm { + let key: [UInt8] + let algorithm: OpaquePointer + let jwtAlgorithmName: String + + func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] + where Plaintext: DataProtocol + { + let context = HMAC_CTX_new() + defer { HMAC_CTX_free(context) } + + guard self.key.withUnsafeBytes({ + return HMAC_Init_ex(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32($0.count), self.algorithm, nil) + }) == 1 else { + fatalError("Failed initializing HMAC context") + } + + guard plaintext.copyBytes().withUnsafeBytes({ + return HMAC_Update(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) + }) == 1 else { + fatalError("Failed updating HMAC digest") + } + var hash = [UInt8](repeating: 0, count: Int(EVP_MAX_MD_SIZE)) + var count: UInt32 = 0 + + guard hash.withUnsafeMutableBytes({ + return HMAC_Final(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), &count) + }) == 1 else { + fatalError("Failed finalizing HMAC digest") + } + return .init(hash[0..<Int(count)]) + } +} diff --git a/Sources/JWTKit/JWTSigner.swift b/Sources/JWTKit/JWTSigner.swift index 2e939a0..68c0928 100644 --- a/Sources/JWTKit/JWTSigner.swift +++ b/Sources/JWTKit/JWTSigner.swift @@ -1,15 +1,7 @@ import class Foundation.JSONEncoder /// A JWT signer. -public final class JWTSigner { - /// Algorithm - public var algorithm: JWTAlgorithm - - /// Create a new JWT signer. - public init(algorithm: JWTAlgorithm) { - self.algorithm = algorithm - } - +public protocol JWTSigner { /// Signs the message and returns the UTF8 of this message /// /// Can be transformed into a String like so: @@ -23,113 +15,72 @@ public final class JWTSigner { /// - parameters: /// - jwt: JWT to sign. /// - returns: Signed JWT data. - public func sign<Payload>(_ jwt: JWT<Payload>) throws -> JWTMessage { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = .secondsSince1970 - - // encode header, copying header struct to mutate alg - var header = jwt.header - header.alg = algorithm.jwtAlgorithmName - let headerData = try jsonEncoder.encode(header) - let encodedHeader = headerData.base64URLEncodedBytes() - - // encode payload - let payloadData = try jsonEncoder.encode(jwt.payload) - let encodedPayload = payloadData.base64URLEncodedBytes() - - // combine header and payload to create signature - let signatureData = try self.algorithm.sign(encodedHeader + [.period] + encodedPayload) - - // yield complete jwt - return JWTMessage( - bytes: encodedHeader - + [.period] - + encodedPayload - + [.period] - + signatureData.base64URLEncodedBytes() - ) + public func sign<Payload>(_ jwt: JWT<Payload>) throws -> [UInt8] { + return [] } /// Generates a signature for the supplied payload and header. - public func verify<Payload>(_ message: JWTMessage) throws -> JWT<Payload> { - let message = message.bytes.split(separator: .period) - guard message.count == 3 else { - throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") - } - - let encodedHeader = message[0] - let encodedPayload = message[1] - let encodedSignature = message[2] - guard try self.algorithm.verify( - encodedSignature.base64URLDecodedBytes(), - signs: encodedHeader + [.period] + encodedPayload - ) else { - throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") - } - - let jwt = try JWT<Payload>( - header: encodedHeader.base64URLDecodedBytes(), - payload: encodedPayload.base64URLDecodedBytes() - ) - try jwt.payload.verify(using: self) - return jwt + public func verify<Message, Payload>(_ message: Message, as payload: Payload.Type) throws -> JWT<Payload> + where Message: DataProtocol + { + fatalError() } } /// MARK: HMAC -extension JWTSigner { - /// Creates an HS256 JWT signer with the supplied key - public static func hs256(key: CryptoData) -> JWTSigner { - return hmac(HMAC.SHA256, name: "HS256", key: key) - } - - /// Creates an HS384 JWT signer with the supplied key - public static func hs384(key: CryptoData) -> JWTSigner { - return hmac(HMAC.SHA384, name: "HS384", key: key) - } - - /// Creates an HS512 JWT signer with the supplied key - public static func hs512(key: CryptoData) -> JWTSigner { - return hmac(HMAC.SHA512, name: "HS512", key: key) - } - - /// Creates an HMAC-based `CustomJWTAlgorithm` and `JWTSigner`. - private static func hmac(_ hmac: HMAC, name: String, key: CryptoData) -> JWTSigner { - let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in - return try hmac.authenticate(plaintext, key: key) - }, verify: { signature, plaintext in - return try hmac.authenticate(plaintext, key: key) == signature.convertToData() - }) - return .init(algorithm: alg) - } -} +//extension JWTSigner { +// /// Creates an HS256 JWT signer with the supplied key +// public static func hs256<Key>(key: CryptoData) -> JWTSigner { +// return hmac(HMAC.SHA256, name: "HS256", key: key) +// } +// +// /// Creates an HS384 JWT signer with the supplied key +// public static func hs384(key: CryptoData) -> JWTSigner { +// return hmac(HMAC.SHA384, name: "HS384", key: key) +// } +// +// /// Creates an HS512 JWT signer with the supplied key +// public static func hs512(key: CryptoData) -> JWTSigner { +// return hmac(HMAC.SHA512, name: "HS512", key: key) +// } +// +// /// Creates an HMAC-based `CustomJWTAlgorithm` and `JWTSigner`. +// private static func hmac(_ hmac: HMAC, name: String, key: CryptoData) -> JWTSigner { +// let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in +// return try hmac.authenticate(plaintext, key: key) +// }, verify: { signature, plaintext in +// return try hmac.authenticate(plaintext, key: key) == signature.convertToData() +// }) +// return .init(algorithm: alg) +// } +//} /// MARK: RSA - -extension JWTSigner { - /// Creates an RS256 JWT signer with the supplied key - public static func rs256(key: RSAKey) -> JWTSigner { - return rsa(RSA.SHA256, name: "RS256", key: key) - } - - /// Creates an RS384 JWT signer with the supplied key - public static func rs384(key: RSAKey) -> JWTSigner { - return rsa(RSA.SHA384, name: "RS384", key: key) - } - - /// Creates an RS512 JWT signer with the supplied key - public static func rs512(key: RSAKey) -> JWTSigner { - return rsa(RSA.SHA512, name: "RS512", key: key) - } - - /// Creates an RSA-based `CustomJWTAlgorithm` and `JWTSigner`. - private static func rsa(_ rsa: RSA, name: String, key: RSAKey) -> JWTSigner { - let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in - return try rsa.sign(plaintext, key: key) - }, verify: { signature, plaintext in - return try rsa.verify(signature, signs: plaintext, key: key) - }) - return .init(algorithm: alg) - } -} +// +//extension JWTSigner { +// /// Creates an RS256 JWT signer with the supplied key +// public static func rs256(key: RSAKey) -> JWTSigner { +// return rsa(RSA.SHA256, name: "RS256", key: key) +// } +// +// /// Creates an RS384 JWT signer with the supplied key +// public static func rs384(key: RSAKey) -> JWTSigner { +// return rsa(RSA.SHA384, name: "RS384", key: key) +// } +// +// /// Creates an RS512 JWT signer with the supplied key +// public static func rs512(key: RSAKey) -> JWTSigner { +// return rsa(RSA.SHA512, name: "RS512", key: key) +// } +// +// /// Creates an RSA-based `CustomJWTAlgorithm` and `JWTSigner`. +// private static func rsa(_ rsa: RSA, name: String, key: RSAKey) -> JWTSigner { +// let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in +// return try rsa.sign(plaintext, key: key) +// }, verify: { signature, plaintext in +// return try rsa.verify(signature, signs: plaintext, key: key) +// }) +// return .init(algorithm: alg) +// } +//} diff --git a/Sources/JWTKit/JWTSigners.swift b/Sources/JWTKit/JWTSigners.swift index 110fea3..aa4718d 100644 --- a/Sources/JWTKit/JWTSigners.swift +++ b/Sources/JWTKit/JWTSigners.swift @@ -25,4 +25,11 @@ public final class JWTSigners { } return signer } + + + public func verify<Message, Payload>(_ message: Message, as payload: Payload.Type) throws -> JWT<Payload> + where Message: DataProtocol + { + fatalError() + } } diff --git a/Tests/JWTKitTests/JWTTests.swift b/Tests/JWTKitTests/JWTTests.swift index 996cb95..0eaa3e9 100644 --- a/Tests/JWTKitTests/JWTTests.swift +++ b/Tests/JWTKitTests/JWTTests.swift @@ -1,14 +1,12 @@ import XCTest -@testable import JWT -import Bits -import Crypto +import JWTKit class JWTTests: XCTestCase { func testParse() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6OTk5OTk5OTk5OTk5fQ.Ks7KcdjrlUTYaSNeAO5SzBla_sFCHkUh4vvJYn6q29U" - let signer = JWTSigner.hs256(key: Data("secret".utf8)) - let jwt = try JWT<TestPayload>(from: data, verifiedUsing: signer) + let signer = JWTSigner.hs256(key: "secret".bytes) + let jwt = try signer.verify(data.bytes, as: TestPayload.self) XCTAssertEqual(jwt.payload.name, "John Doe") XCTAssertEqual(jwt.payload.sub.value, "1234567890") XCTAssertEqual(jwt.payload.admin, true) @@ -17,9 +15,9 @@ class JWTTests: XCTestCase { func testExpired() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MX0.-x_DAYIg4R4R9oZssqgWyJP_oWO1ESj8DgKrGCk7i5o" - let signer = JWTSigner.hs256(key: Data("secret".utf8)) + let signer = JWTSigner.hs256(key: "secret".bytes) do { - _ = try JWT<TestPayload>(from: data, verifiedUsing: signer) + let _ = try signer.verify(data.bytes, as: TestPayload.self) } catch let error as JWTError { XCTAssertEqual(error.identifier, "exp") } @@ -28,8 +26,8 @@ class JWTTests: XCTestCase { func testExpirationDecoding() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwMDAwMDAwMDB9.JgCO_GqUQnbS0z2hCxJLE9Tpt5SMoZObHBxzGBWuTYQ" - let signer = JWTSigner.hs256(key: Data("secret".utf8)) - let jwt = try JWT<ExpirationPayload>(from: data, verifiedUsing: signer) + let signer = JWTSigner.hs256(key: "secret".bytes) + let jwt = try signer.verify(data.bytes, as: ExpirationPayload.self) XCTAssertEqual(jwt.payload.exp.value, Date(timeIntervalSince1970: 2_000_000_000)) } @@ -38,56 +36,50 @@ class JWTTests: XCTestCase { let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000)) var jwt = JWT(payload: ExpirationPayload(exp: exp)) - let signer = JWTSigner.hs256(key: Data("secret".utf8)) + let signer = JWTSigner.hs256(key: "secret".bytes) jwt.header.typ = nil // set to nil to avoid dictionary re-ordering causing probs let data = try signer.sign(jwt) - XCTAssertEqual(String(data: data, encoding: .utf8), "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjIwMDAwMDAwMDB9.4W6egHvMSp9bBiGUnE7WhVfXazOfg-ADcjvIYILgyPU") + XCTAssertEqual( + String(decoding: data, as: UTF8.self), + "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjIwMDAwMDAwMDB9.4W6egHvMSp9bBiGUnE7WhVfXazOfg-ADcjvIYILgyPU" + ) } func testSigners() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6OTk5OTk5OTk5OTk5OTl9.Gf7leJ8i30LmMI7GBTpWDMXV60y1wkTOCOBudP9v9ms" - let signer = JWTSigner.hs256(key: Data("bar".utf8)) + let signer = JWTSigner.hs256(key: "bar".bytes) let signers = JWTSigners() signers.use(signer, kid: "foo") - let jwt = try JWT<TestPayload>(from: data, verifiedUsing: signers) + let jwt = try signers.verify(data.bytes, as: TestPayload.self) XCTAssertEqual(jwt.payload.name, "John Doe") } - - func testRSA() throws { - let privateSigner = try JWTSigner.rs256(key: .private(pem: privateKeyString)) - let publicSigner = try JWTSigner.rs256(key: .public(pem: publicKeyString)) - - let payload = TestPayload( - sub: "vapor", - name: "Foo", - admin: false, - exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) - ) - let jwt = JWT(payload: payload) - do { - _ = try jwt.sign(using: publicSigner) - XCTFail("cannot sign with public signer") - } catch { - // pass - } - let privateSigned = try jwt.sign(using: privateSigner) - let publicVerified = try JWT<TestPayload>(from: privateSigned, verifiedUsing: publicSigner) - let privateVerified = try JWT<TestPayload>(from: privateSigned, verifiedUsing: privateSigner) - XCTAssertEqual(publicVerified.payload.name, "Foo") - XCTAssertEqual(privateVerified.payload.name, "Foo") - } - - static var allTests = [ - ("testParse", testParse), - ("testExpired", testExpired), - ("testExpirationDecoding", testExpirationDecoding), - ("testExpirationEncoding", testExpirationEncoding), - ("testSigners", testSigners), - ("testRSA", testRSA), - ] +// +// func testRSA() throws { +// let privateSigner = try JWTSigner.rs256(key: .private(pem: privateKeyString)) +// let publicSigner = try JWTSigner.rs256(key: .public(pem: publicKeyString)) +// +// let payload = TestPayload( +// sub: "vapor", +// name: "Foo", +// admin: false, +// exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) +// ) +// let jwt = JWT(payload: payload) +// do { +// _ = try jwt.sign(using: publicSigner) +// XCTFail("cannot sign with public signer") +// } catch { +// // pass +// } +// let privateSigned = try jwt.sign(using: privateSigner) +// let publicVerified = try JWT<TestPayload>(from: privateSigned, verifiedUsing: publicSigner) +// let privateVerified = try JWT<TestPayload>(from: privateSigned, verifiedUsing: privateSigner) +// XCTAssertEqual(publicVerified.payload.name, "Foo") +// XCTAssertEqual(privateVerified.payload.name, "Foo") +// } } struct TestPayload: JWTPayload { @@ -137,3 +129,9 @@ PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv aX4rbSL49Z3dAQn8vQIDAQAB -----END PUBLIC KEY----- """ + +extension String { + var bytes: [UInt8] { + return .init(self.utf8) + } +} From 87eceeb6c1a6c7e3e3df580a15e1496369326284 Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 14:30:31 -0400 Subject: [PATCH 05/12] updates + circle.ci --- Sources/JWTKit/JWT.swift | 76 +++++--- Sources/JWTKit/JWTMessage.swift | 39 ----- Sources/JWTKit/JWTSigner+HMAC.swift | 2 + Sources/JWTKit/JWTSigner+RSA.swift | 260 ++++++++++++++++++++++++++++ Sources/JWTKit/JWTSigner.swift | 84 +-------- Sources/JWTKit/JWTSigners.swift | 15 -- Tests/JWTKitTests/JWTTests.swift | 70 ++++---- circle.yml | 71 ++++---- 8 files changed, 396 insertions(+), 221 deletions(-) delete mode 100644 Sources/JWTKit/JWTMessage.swift create mode 100644 Sources/JWTKit/JWTSigner+RSA.swift diff --git a/Sources/JWTKit/JWT.swift b/Sources/JWTKit/JWT.swift index df1ce38..9cfacb9 100644 --- a/Sources/JWTKit/JWT.swift +++ b/Sources/JWTKit/JWT.swift @@ -16,42 +16,78 @@ public struct JWT<Payload> where Payload: JWTPayload { /// The JSON payload within this message public var payload: Payload - /// Creates a new JSON Web Signature from predefined data - public init(header: JWTHeader = .init(), payload: Payload) { - self.header = header - self.payload = payload + private struct JWTComponents { + var header: JWTHeader + var payload: Payload + var signature: [UInt8] + var message: [UInt8] } - - /// Parses a JWT string into a JSON Web Signature - public init<Message>(message: Message, using signer: JWTSigner) throws + + private static func parse<Message>(message: Message) throws -> JWTComponents where Message: DataProtocol { let message = message.copyBytes().split(separator: .period) guard message.count == 3 else { throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") } - + let encodedHeader = message[0] let encodedPayload = message[1] let encodedSignature = message[2] - - + let jsonDecoder = JSONDecoder() jsonDecoder.dateDecodingStrategy = .secondsSince1970 let header = try jsonDecoder.decode(JWTHeader.self, from: Data(encodedHeader.base64URLDecodedBytes())) let payload = try jsonDecoder.decode(Payload.self, from: Data(encodedPayload.base64URLDecodedBytes())) - - guard try signer.verify( - encodedSignature.base64URLDecodedBytes(), - signs: encodedHeader + [.period] + encodedPayload, - header: header - ) else { - throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") - } - + + return .init( + header: header, + payload: payload, + signature: encodedSignature.base64URLDecodedBytes(), + message: .init(encodedHeader) + [.period] + .init(encodedPayload) + ) + } + + public init(header: JWTHeader = .init(), payload: Payload) { self.header = header self.payload = payload - + } + + public init<Message>(fromUnverified message: Message) throws + where Message: DataProtocol + { + let components = try JWT<Payload>.parse(message: message) + self.header = components.header + self.payload = components.payload + } + + public init<Message>(from message: Message, verifiedBy signer: JWTSigner) throws + where Message: DataProtocol + { + let components = try JWT<Payload>.parse(message: message) + guard try signer.algorithm.verify(components.signature, signs: components.message) else { + throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") + } + self.header = components.header + self.payload = components.payload + try self.payload.verify(using: signer) + } + + public init<Message>(from message: Message, verifiedBy signers: JWTSigners) throws + where Message: DataProtocol + { + let components = try JWT<Payload>.parse(message: message) + guard let kid = components.header.kid else { + throw JWTError(identifier: "missingKID", reason: "JWT is missing kid field in header") + } + guard let signer = signers.signer(kid: kid) else { + throw JWTError(identifier: "unknownKID", reason: "No signers are available for the supplied kid") + } + guard try signer.algorithm.verify(components.signature, signs: components.message) else { + throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") + } + self.header = components.header + self.payload = components.payload try self.payload.verify(using: signer) } diff --git a/Sources/JWTKit/JWTMessage.swift b/Sources/JWTKit/JWTMessage.swift deleted file mode 100644 index c2f49bc..0000000 --- a/Sources/JWTKit/JWTMessage.swift +++ /dev/null @@ -1,39 +0,0 @@ -public struct JWTMessage: ContiguousBytes, CustomStringConvertible, Hashable, Sequence { - var header: [UInt8] - var payload: [UInt8] - var signature: [UInt8] - - public var description: String { - return String(decoding: self.bytes, as: UTF8.self) - } - - public var bytes: [UInt8] { - return self.header + [.period] + self.payload + [.period] + self.signature - } - - init(bytes: [UInt8]) throws { - let parts = bytes.split(separator: .period) - guard parts.count == 3 else { - throw JWTError(identifier: "format", reason: "Invalid JWT format") - } - self.init(header: parts[0], payload: parts[1], signature: parts[2]) - } - - init<Header, Payload, Signature>( - header: Header, payload: Payload, signature: Signature - ) - where Header: DataProtocol, Payload: DataProtocol, Signature: DataProtocol - { - self.header = header.copyBytes() - self.payload = payload.copyBytes() - self.signature = signature.copyBytes() - } - - public func makeIterator() -> Array<UInt8>.Iterator { - return self.bytes.makeIterator() - } - - public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - return try self.bytes.withUnsafeBytes(body) - } -} diff --git a/Sources/JWTKit/JWTSigner+HMAC.swift b/Sources/JWTKit/JWTSigner+HMAC.swift index cbe25d9..4f00be6 100644 --- a/Sources/JWTKit/JWTSigner+HMAC.swift +++ b/Sources/JWTKit/JWTSigner+HMAC.swift @@ -1,6 +1,8 @@ import CJWTKitOpenSSL extension JWTSigner { + // MARK: HMAC + public static func hs256<Key>(key: Key) -> JWTSigner where Key: DataProtocol { diff --git a/Sources/JWTKit/JWTSigner+RSA.swift b/Sources/JWTKit/JWTSigner+RSA.swift new file mode 100644 index 0000000..6a5b8aa --- /dev/null +++ b/Sources/JWTKit/JWTSigner+RSA.swift @@ -0,0 +1,260 @@ +import CJWTKitOpenSSL +import struct Foundation.Data + +extension JWTSigner { + // MARK: RSA + + public static func rs256(key: RSAKey) -> JWTSigner { + return .init(algorithm: RSAAlgorithm( + key: key, + algorithm: EVP_sha256(), + jwtAlgorithmName: "RS256" + )) + } + + public static func rs384(key: RSAKey) -> JWTSigner { + return .init(algorithm: RSAAlgorithm( + key: key, + algorithm: EVP_sha384(), + jwtAlgorithmName: "RS384" + )) + } + + public static func rs512(key: RSAKey) -> JWTSigner { + return .init(algorithm: RSAAlgorithm( + key: key, + algorithm: EVP_sha512(), + jwtAlgorithmName: "RS512" + )) + } +} + +/// Represents an in-memory RSA key. +public struct RSAKey { + /// Supported RSA key types. + public enum Kind { + /// A public RSA key. Used for verifying signatures. + case `public` + /// A private RSA key. Used for creating and verifying signatures. + case `private` + } + + // MARK: Static + + /// Creates a new `RSAKey` from a private key pem file. + public static func `private`<Data>(pem: Data) -> RSAKey + where Data: DataProtocol + { + return .init(type: .private, key: .make(type: .private, from: pem)) + } + + /// Creates a new `RSAKey` from a public key pem file. + public static func `public`<Data>(pem: Data) -> RSAKey + where Data: DataProtocol + { + return .init(type: .public, key: .make(type: .public, from: pem)) + } + + /// Creates a new `RSAKey` from a public key certificate file. + public static func `public`<Data>(certificate: Data) -> RSAKey + where Data: DataProtocol + { + return .init(type: .public, key: .make(type: .public, from: certificate, x509: true)) + } + + // MARK: Properties + /// The specific RSA key type. Either public or private. + /// + /// Note: public keys can only verify signatures. A private key + /// is required to create new signatures. + public var type: Kind + + /// The C OpenSSL key ref. + fileprivate let c: CRSAKey + + // MARK: Init + + /// Creates a new `RSAKey` from a public or private key. + fileprivate init(type: Kind, key: CRSAKey) { + self.type = type + self.c = key + } + + /// Creates a new `RSAKey` from components. + /// + /// For example, if you want to use Google's [public OAuth2 keys](https://www.googleapis.com/oauth2/v3/certs), + /// you could parse the request using: + /// + /// struct CertKeysResponse: APIResponse { + /// let keys: [Key] + /// + /// struct Key: Codable { + /// let kty: String + /// let alg: String + /// let kid: String + /// + /// let n: String + /// let e: String + /// let d: String? + /// } + /// } + /// + /// And then instantiate the key as: + /// + /// try RSAKey.components(n: key.n, e: key.e, d: key.d) + /// + /// - throws: `CryptoError` if key generation fails. + public static func components(n: String, e: String, d: String? = nil) -> RSAKey { + guard let rsa = RSA_new() else { + fatalError("RSA key creation failed") + } + + let n = parseBignum(n) + let e = parseBignum(e) + let d = d.flatMap { parseBignum($0) } + RSA_set0_key(rsa, n, e, d) + return .init(type: d == nil ? .public : .private, key: CRSAKey(rsa)) + } +} + +// MARK: Private + +private final class CRSAKey { + let pointer: OpaquePointer + + internal init(_ pointer: OpaquePointer) { + self.pointer = pointer + } + + static func make<Data>(type: RSAKey.Kind, from data: Data, x509: Bool = false) -> CRSAKey + where Data: DataProtocol + { + let bio = BIO_new(BIO_s_mem()) + defer { BIO_free(bio) } + + let bytes = data.copyBytes() + let nullTerminatedData = bytes + [0] + _ = nullTerminatedData.withUnsafeBytes { (p: UnsafeRawBufferPointer) -> Int32 in + return BIO_puts(bio, p.baseAddress?.assumingMemoryBound(to: Int8.self)) + } + + let maybePkey: OpaquePointer? + + if x509 { + guard let x509 = PEM_read_bio_X509(bio, nil, nil, nil) else { + fatalError("Key creation from certificate failed") + } + + defer { X509_free(x509) } + maybePkey = X509_get_pubkey(x509) + } else { + switch type { + case .public: maybePkey = PEM_read_bio_PUBKEY(bio, nil, nil, nil) + case .private: maybePkey = PEM_read_bio_PrivateKey(bio, nil, nil, nil) + } + } + + guard let pkey = maybePkey else { + fatalError("RSA key creation failed") + } + defer { EVP_PKEY_free(pkey) } + + guard let rsa = EVP_PKEY_get1_RSA(pkey) else { + fatalError("RSA key creation failed") + } + return .init(rsa) + } + + deinit { RSA_free(self.pointer) } +} + +private func parseBignum(_ s: String) -> OpaquePointer { + return Data(s.utf8).base64URLDecodedBytes().withUnsafeBytes { (p: UnsafeRawBufferPointer) -> OpaquePointer in + return BN_bin2bn(p.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(p.count), nil) + } +} + +private struct RSAAlgorithm: JWTAlgorithm { + let key: RSAKey + let algorithm: OpaquePointer + let jwtAlgorithmName: String + + func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] + where Plaintext: DataProtocol + { + switch key.type { + case .public: + throw JWTError(identifier: "rsa", reason: "Cannot create RSA signature with a public key. A private key is required.") + case .private: + break + } + + var siglen: UInt32 = 0 + var signature = [UInt8]( + repeating: 0, + count: Int(RSA_size(key.c.pointer)) + ) + + guard self.hash(plaintext).withUnsafeBytes ({ inputBuffer in + signature.withUnsafeMutableBytes({ signatureBuffer in + RSA_sign( + EVP_MD_type(self.algorithm), + inputBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + UInt32(inputBuffer.count), + signatureBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + &siglen, + key.c.pointer + ) + }) + }) == 1 else { + throw JWTError(identifier: "rsaSign", reason: "RSA signature creation failed") + } + + return signature + } + + func verify<Signature, Plaintext>( + _ signature: Signature, + signs plaintext: Plaintext + ) throws -> Bool + where Signature: DataProtocol, Plaintext: DataProtocol + { + return self.hash(plaintext).withUnsafeBytes({ inputBuffer in + signature.copyBytes().withUnsafeBytes({ signatureBuffer in + RSA_verify( + EVP_MD_type(self.algorithm), + inputBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self), + UInt32(inputBuffer.count), + signatureBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self), + UInt32(signatureBuffer.count), + key.c.pointer + ) + }) + }) == 1 + } + + private func hash<Plaintext>(_ plaintext: Plaintext) -> [UInt8] + where Plaintext: DataProtocol + { + let context = EVP_MD_CTX_new() + defer { EVP_MD_CTX_free(context) } + + guard EVP_DigestInit_ex(context, self.algorithm, nil) == 1 else { + fatalError("Failed initializing digest context") + } + guard plaintext.copyBytes().withUnsafeBytes({ + EVP_DigestUpdate(context, $0.baseAddress, $0.count) + }) == 1 else { + fatalError("Failed updating digest") + } + var hash: [UInt8] = .init(repeating: 0, count: Int(EVP_MAX_MD_SIZE)) + var count: UInt32 = 0 + + guard hash.withUnsafeMutableBytes({ + EVP_DigestFinal_ex(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), &count) + }) == 1 else { + fatalError("Failed finalizing digest") + } + return .init(hash[0..<Int(count)]) + } +} diff --git a/Sources/JWTKit/JWTSigner.swift b/Sources/JWTKit/JWTSigner.swift index 68c0928..9da1772 100644 --- a/Sources/JWTKit/JWTSigner.swift +++ b/Sources/JWTKit/JWTSigner.swift @@ -1,86 +1,10 @@ import class Foundation.JSONEncoder /// A JWT signer. -public protocol JWTSigner { - /// Signs the message and returns the UTF8 of this message - /// - /// Can be transformed into a String like so: - /// - /// let signature = try jwt.sign() - /// guard let string = String(bytes: signed, encoding: .utf8) else { - /// throw ... - /// } - /// print(string) - /// - /// - parameters: - /// - jwt: JWT to sign. - /// - returns: Signed JWT data. - public func sign<Payload>(_ jwt: JWT<Payload>) throws -> [UInt8] { - return [] - } +public final class JWTSigner { + public let algorithm: JWTAlgorithm - /// Generates a signature for the supplied payload and header. - public func verify<Message, Payload>(_ message: Message, as payload: Payload.Type) throws -> JWT<Payload> - where Message: DataProtocol - { - fatalError() + public init(algorithm: JWTAlgorithm) { + self.algorithm = algorithm } } - -/// MARK: HMAC - -//extension JWTSigner { -// /// Creates an HS256 JWT signer with the supplied key -// public static func hs256<Key>(key: CryptoData) -> JWTSigner { -// return hmac(HMAC.SHA256, name: "HS256", key: key) -// } -// -// /// Creates an HS384 JWT signer with the supplied key -// public static func hs384(key: CryptoData) -> JWTSigner { -// return hmac(HMAC.SHA384, name: "HS384", key: key) -// } -// -// /// Creates an HS512 JWT signer with the supplied key -// public static func hs512(key: CryptoData) -> JWTSigner { -// return hmac(HMAC.SHA512, name: "HS512", key: key) -// } -// -// /// Creates an HMAC-based `CustomJWTAlgorithm` and `JWTSigner`. -// private static func hmac(_ hmac: HMAC, name: String, key: CryptoData) -> JWTSigner { -// let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in -// return try hmac.authenticate(plaintext, key: key) -// }, verify: { signature, plaintext in -// return try hmac.authenticate(plaintext, key: key) == signature.convertToData() -// }) -// return .init(algorithm: alg) -// } -//} - -/// MARK: RSA -// -//extension JWTSigner { -// /// Creates an RS256 JWT signer with the supplied key -// public static func rs256(key: RSAKey) -> JWTSigner { -// return rsa(RSA.SHA256, name: "RS256", key: key) -// } -// -// /// Creates an RS384 JWT signer with the supplied key -// public static func rs384(key: RSAKey) -> JWTSigner { -// return rsa(RSA.SHA384, name: "RS384", key: key) -// } -// -// /// Creates an RS512 JWT signer with the supplied key -// public static func rs512(key: RSAKey) -> JWTSigner { -// return rsa(RSA.SHA512, name: "RS512", key: key) -// } -// -// /// Creates an RSA-based `CustomJWTAlgorithm` and `JWTSigner`. -// private static func rsa(_ rsa: RSA, name: String, key: RSAKey) -> JWTSigner { -// let alg = CustomJWTAlgorithm(name: name, sign: { plaintext in -// return try rsa.sign(plaintext, key: key) -// }, verify: { signature, plaintext in -// return try rsa.verify(signature, signs: plaintext, key: key) -// }) -// return .init(algorithm: alg) -// } -//} diff --git a/Sources/JWTKit/JWTSigners.swift b/Sources/JWTKit/JWTSigners.swift index aa4718d..e4c6851 100644 --- a/Sources/JWTKit/JWTSigners.swift +++ b/Sources/JWTKit/JWTSigners.swift @@ -17,19 +17,4 @@ public final class JWTSigners { public func signer(kid: String) -> JWTSigner? { return storage[kid] } - - /// Returns a signer for the `kid` or throws an error. - public func requireSigner(kid: String) throws -> JWTSigner { - guard let signer = self.signer(kid: kid) else { - throw JWTError(identifier: "unknownKID", reason: "No signers are available for the supplied `kid`") - } - return signer - } - - - public func verify<Message, Payload>(_ message: Message, as payload: Payload.Type) throws -> JWT<Payload> - where Message: DataProtocol - { - fatalError() - } } diff --git a/Tests/JWTKitTests/JWTTests.swift b/Tests/JWTKitTests/JWTTests.swift index 0eaa3e9..11ff58d 100644 --- a/Tests/JWTKitTests/JWTTests.swift +++ b/Tests/JWTKitTests/JWTTests.swift @@ -5,8 +5,7 @@ class JWTTests: XCTestCase { func testParse() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6OTk5OTk5OTk5OTk5fQ.Ks7KcdjrlUTYaSNeAO5SzBla_sFCHkUh4vvJYn6q29U" - let signer = JWTSigner.hs256(key: "secret".bytes) - let jwt = try signer.verify(data.bytes, as: TestPayload.self) + let jwt = try JWT<TestPayload>(from: data.bytes, verifiedBy: .hs256(key: "secret".bytes)) XCTAssertEqual(jwt.payload.name, "John Doe") XCTAssertEqual(jwt.payload.sub.value, "1234567890") XCTAssertEqual(jwt.payload.admin, true) @@ -15,9 +14,8 @@ class JWTTests: XCTestCase { func testExpired() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MX0.-x_DAYIg4R4R9oZssqgWyJP_oWO1ESj8DgKrGCk7i5o" - let signer = JWTSigner.hs256(key: "secret".bytes) do { - let _ = try signer.verify(data.bytes, as: TestPayload.self) + let _ = try JWT<TestPayload>(from: data.bytes, verifiedBy: .hs256(key: "secret".bytes)) } catch let error as JWTError { XCTAssertEqual(error.identifier, "exp") } @@ -26,9 +24,7 @@ class JWTTests: XCTestCase { func testExpirationDecoding() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwMDAwMDAwMDB9.JgCO_GqUQnbS0z2hCxJLE9Tpt5SMoZObHBxzGBWuTYQ" - let signer = JWTSigner.hs256(key: "secret".bytes) - let jwt = try signer.verify(data.bytes, as: ExpirationPayload.self) - + let jwt = try JWT<ExpirationPayload>(from: data.bytes, verifiedBy: .hs256(key: "secret".bytes)) XCTAssertEqual(jwt.payload.exp.value, Date(timeIntervalSince1970: 2_000_000_000)) } @@ -36,9 +32,8 @@ class JWTTests: XCTestCase { let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000)) var jwt = JWT(payload: ExpirationPayload(exp: exp)) - let signer = JWTSigner.hs256(key: "secret".bytes) jwt.header.typ = nil // set to nil to avoid dictionary re-ordering causing probs - let data = try signer.sign(jwt) + let data = try jwt.sign(using: .hs256(key: "secret".bytes)) XCTAssertEqual( String(decoding: data, as: UTF8.self), @@ -49,37 +44,36 @@ class JWTTests: XCTestCase { func testSigners() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6OTk5OTk5OTk5OTk5OTl9.Gf7leJ8i30LmMI7GBTpWDMXV60y1wkTOCOBudP9v9ms" - let signer = JWTSigner.hs256(key: "bar".bytes) let signers = JWTSigners() - signers.use(signer, kid: "foo") + signers.use(.hs256(key: "bar".bytes), kid: "foo") - let jwt = try signers.verify(data.bytes, as: TestPayload.self) + let jwt = try JWT<TestPayload>(from: data.bytes, verifiedBy: signers) XCTAssertEqual(jwt.payload.name, "John Doe") } -// -// func testRSA() throws { -// let privateSigner = try JWTSigner.rs256(key: .private(pem: privateKeyString)) -// let publicSigner = try JWTSigner.rs256(key: .public(pem: publicKeyString)) -// -// let payload = TestPayload( -// sub: "vapor", -// name: "Foo", -// admin: false, -// exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) -// ) -// let jwt = JWT(payload: payload) -// do { -// _ = try jwt.sign(using: publicSigner) -// XCTFail("cannot sign with public signer") -// } catch { -// // pass -// } -// let privateSigned = try jwt.sign(using: privateSigner) -// let publicVerified = try JWT<TestPayload>(from: privateSigned, verifiedUsing: publicSigner) -// let privateVerified = try JWT<TestPayload>(from: privateSigned, verifiedUsing: privateSigner) -// XCTAssertEqual(publicVerified.payload.name, "Foo") -// XCTAssertEqual(privateVerified.payload.name, "Foo") -// } + + func testRSA() throws { + let privateSigner = JWTSigner.rs256(key: .private(pem: privateKeyString.bytes)) + let publicSigner = JWTSigner.rs256(key: .public(pem: publicKeyString.bytes)) + + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + let jwt = JWT(payload: payload) + do { + _ = try jwt.sign(using: publicSigner) + XCTFail("cannot sign with public signer") + } catch { + // pass + } + let privateSigned = try jwt.sign(using: privateSigner) + let publicVerified = try JWT<TestPayload>(from: privateSigned, verifiedBy: publicSigner) + let privateVerified = try JWT<TestPayload>(from: privateSigned, verifiedBy: privateSigner) + XCTAssertEqual(publicVerified.payload.name, "Foo") + XCTAssertEqual(privateVerified.payload.name, "Foo") + } } struct TestPayload: JWTPayload { @@ -89,7 +83,7 @@ struct TestPayload: JWTPayload { var exp: ExpirationClaim func verify(using signer: JWTSigner) throws { - try exp.verifyNotExpired() + try self.exp.verifyNotExpired() } } @@ -97,7 +91,7 @@ struct ExpirationPayload: JWTPayload { var exp: ExpirationClaim func verify(using signer: JWTSigner) throws { - try exp.verifyNotExpired() + try self.exp.verifyNotExpired() } } diff --git a/circle.yml b/circle.yml index ce738e9..f9dd07b 100644 --- a/circle.yml +++ b/circle.yml @@ -3,44 +3,57 @@ version: 2 jobs: macos: macos: - xcode: "9.2" + xcode: "10.2.0" steps: - checkout + - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install openssl@1.1 - run: swift build - run: swift test - - linux: + macos-release: + macos: + xcode: "10.2.0" + steps: + - checkout + - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install openssl@1.1 + - run: swift build -c release + bionic: docker: - - image: codevapor/swift:4.1 + - image: swift:5.0-bionic steps: - checkout - - run: - name: Compile code - command: swift build - - run: - name: Run unit tests - command: swift test - - run: - name: Compile code with optimizations - command: swift build -c release - + - run: apt-get -y install openssl libssl-dev + - run: swift build --disable-index-store + - run: swift test --disable-index-store + bionic-release: + docker: + - image: swift:5.0-bionic + steps: + - checkout + - run: apt-get -y install openssl libssl-dev + - run: swift build -c release + # xenial: + # docker: + # - image: swift:5.0-xenial + # steps: + # - checkout + # - run: apt-get -y install openssl libssl-dev + # - run: swift build --disable-index-store + # - run: swift test --disable-index-store + # xenial-release: + # docker: + # - image: swift:5.0-xenial + # steps: + # - checkout + # - run: apt-get -y install openssl libssl-dev + # - run: swift build -c release workflows: version: 2 tests: jobs: - # - macos - - linux - - nightly: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - jobs: - - linux - # - macos - \ No newline at end of file + - macos + - macos-release + - bionic + - bionic-release + # - xenial + # - xenial-release From 0f5cfdf8d4fef61dd4a2909ddc56cca811c08b8c Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 14:31:35 -0400 Subject: [PATCH 06/12] fix Self usage --- Sources/JWTKit/Base64URL.swift | 4 ++-- Tests/JWTKitTests/XCTestManifests.swift | 23 +++++++++++++++++++++++ Tests/LinuxMain.swift | 10 ++++++---- 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 Tests/JWTKitTests/XCTestManifests.swift diff --git a/Sources/JWTKit/Base64URL.swift b/Sources/JWTKit/Base64URL.swift index 5134027..ac273c4 100644 --- a/Sources/JWTKit/Base64URL.swift +++ b/Sources/JWTKit/Base64URL.swift @@ -47,7 +47,7 @@ private extension Data { /// Converts base64-url encoded data to a base64 encoded data. /// /// https://tools.ietf.org/html/rfc4648#page-7 - func base64URLUnescaped() -> Self { + func base64URLUnescaped() -> Data { var data = self data.base64URLUnescape() return data @@ -56,7 +56,7 @@ private extension Data { /// Converts base64 encoded data to a base64-url encoded data. /// /// https://tools.ietf.org/html/rfc4648#page-7 - func base64URLEscaped() -> Self { + func base64URLEscaped() -> Data { var data = self data.base64URLEscape() return data diff --git a/Tests/JWTKitTests/XCTestManifests.swift b/Tests/JWTKitTests/XCTestManifests.swift new file mode 100644 index 0000000..0b2d6d6 --- /dev/null +++ b/Tests/JWTKitTests/XCTestManifests.swift @@ -0,0 +1,23 @@ +#if !canImport(ObjectiveC) +import XCTest + +extension JWTTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__JWTTests = [ + ("testExpirationDecoding", testExpirationDecoding), + ("testExpirationEncoding", testExpirationEncoding), + ("testExpired", testExpired), + ("testParse", testParse), + ("testRSA", testRSA), + ("testSigners", testSigners), + ] +} + +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(JWTTests.__allTests__JWTTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 7cbedb4..04c80ea 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,6 +1,8 @@ import XCTest -@testable import JWTTests -XCTMain([ - testCase(JWTTests.allTests), -]) +import JWTKitTests + +var tests = [XCTestCaseEntry]() +tests += JWTKitTests.__allTests() + +XCTMain(tests) From 96d47f409e5151058db8d776cac2a4b8e833a770 Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 15:43:56 -0400 Subject: [PATCH 07/12] add openssl 1.0 support --- Package.swift | 5 +- Sources/CJWTKitCrypto/c_jwtkit_crypto.c | 30 ++++++++++ .../CJWTKitCrypto/include/c_jwtkit_crypto.h | 25 ++++++++ Sources/CJWTKitOpenSSL/module.modulemap | 1 - Sources/CJWTKitOpenSSL/shim.c | 1 - Sources/CJWTKitOpenSSL/shim.h | 17 ------ Sources/JWTKit/JWT.swift | 2 +- Sources/JWTKit/JWTAlgorithm.swift | 2 +- Sources/JWTKit/JWTSigner+HMAC.swift | 18 +++--- Sources/JWTKit/JWTSigner+RSA.swift | 57 +++++++++---------- Sources/JWTKit/Utilities.swift | 22 +++++++ .../{JWTTests.swift => JWTKitTests.swift} | 2 +- Tests/JWTKitTests/XCTestManifests.swift | 6 +- circle.yml | 34 +++++------ 14 files changed, 139 insertions(+), 83 deletions(-) create mode 100644 Sources/CJWTKitCrypto/c_jwtkit_crypto.c create mode 100644 Sources/CJWTKitCrypto/include/c_jwtkit_crypto.h delete mode 100644 Sources/CJWTKitOpenSSL/shim.c delete mode 100644 Sources/CJWTKitOpenSSL/shim.h rename Tests/JWTKitTests/{JWTTests.swift => JWTKitTests.swift} (99%) diff --git a/Package.swift b/Package.swift index 8ec0273..2e527ae 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,11 @@ let package = Package( pkgConfig: "openssl", providers: [ .apt(["openssl libssl-dev"]), - .brew(["openssl@1.1"]) + .brew(["openssl"]) ] ), - .target(name: "JWTKit", dependencies: ["CJWTKitOpenSSL"]), + .target(name: "CJWTKitCrypto", dependencies: ["CJWTKitOpenSSL"]), + .target(name: "JWTKit", dependencies: ["CJWTKitCrypto"]), .testTarget(name: "JWTKitTests", dependencies: ["JWTKit"]), ] ) diff --git a/Sources/CJWTKitCrypto/c_jwtkit_crypto.c b/Sources/CJWTKitCrypto/c_jwtkit_crypto.c new file mode 100644 index 0000000..f704d3b --- /dev/null +++ b/Sources/CJWTKitCrypto/c_jwtkit_crypto.c @@ -0,0 +1,30 @@ +#include "include/c_jwtkit_crypto.h" + +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || defined(LIBRESSL_VERSION_NUMBER) +EVP_MD_CTX *EVP_MD_CTX_new(void) { + return EVP_MD_CTX_create(); +}; + +void EVP_MD_CTX_free(EVP_MD_CTX *ctx) { + EVP_MD_CTX_cleanup(ctx); + free(ctx); +}; + +int RSA_set0_key(RSA *r, BIGNUM *n, BIGNUM *e, BIGNUM *d) { + r->n = n; + r->e = e; + r->d = d; + return 0; +}; + +HMAC_CTX *HMAC_CTX_new(void) { + HMAC_CTX *ptr = malloc(sizeof(HMAC_CTX)); + HMAC_CTX_init(ptr); + return ptr; +}; + +void HMAC_CTX_free(HMAC_CTX *ctx) { + HMAC_CTX_cleanup(ctx); + free(ctx); +}; +#endif diff --git a/Sources/CJWTKitCrypto/include/c_jwtkit_crypto.h b/Sources/CJWTKitCrypto/include/c_jwtkit_crypto.h new file mode 100644 index 0000000..574fd25 --- /dev/null +++ b/Sources/CJWTKitCrypto/include/c_jwtkit_crypto.h @@ -0,0 +1,25 @@ +#ifndef C_JWTKIT_OPENSSL_H +#define C_JWTKIT_OPENSSL_H + +#include <openssl/conf.h> +#include <openssl/evp.h> +#include <openssl/err.h> +#include <openssl/bio.h> +#include <openssl/ssl.h> +#include <openssl/sha.h> +#include <openssl/md5.h> +#include <openssl/hmac.h> +#include <openssl/rand.h> +#include <openssl/rsa.h> +#include <openssl/pkcs12.h> +#include <openssl/x509v3.h> + +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || defined(LIBRESSL_VERSION_NUMBER) +EVP_MD_CTX *EVP_MD_CTX_new(void); +void EVP_MD_CTX_free(EVP_MD_CTX *ctx); +int RSA_set0_key(RSA *r, BIGNUM *n, BIGNUM *e, BIGNUM *d); +HMAC_CTX *HMAC_CTX_new(void); +void HMAC_CTX_free(HMAC_CTX *ctx); +#endif + +#endif diff --git a/Sources/CJWTKitOpenSSL/module.modulemap b/Sources/CJWTKitOpenSSL/module.modulemap index 81cc080..1fa3136 100644 --- a/Sources/CJWTKitOpenSSL/module.modulemap +++ b/Sources/CJWTKitOpenSSL/module.modulemap @@ -1,5 +1,4 @@ module CJWTKitOpenSSL [system] { - header "shim.h" link "ssl" link "crypto" } diff --git a/Sources/CJWTKitOpenSSL/shim.c b/Sources/CJWTKitOpenSSL/shim.c deleted file mode 100644 index 8b13789..0000000 --- a/Sources/CJWTKitOpenSSL/shim.c +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Sources/CJWTKitOpenSSL/shim.h b/Sources/CJWTKitOpenSSL/shim.h deleted file mode 100644 index 48d9c22..0000000 --- a/Sources/CJWTKitOpenSSL/shim.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef C_JWT_CRYTPO_H -#define C_JWT_CRYTPO_H - -#include <openssl/conf.h> -#include <openssl/evp.h> -#include <openssl/err.h> -#include <openssl/bio.h> -#include <openssl/ssl.h> -#include <openssl/sha.h> -#include <openssl/md5.h> -#include <openssl/hmac.h> -#include <openssl/rand.h> -#include <openssl/rsa.h> -#include <openssl/pkcs12.h> -#include <openssl/x509v3.h> - -#endif diff --git a/Sources/JWTKit/JWT.swift b/Sources/JWTKit/JWT.swift index 9cfacb9..0816c9a 100644 --- a/Sources/JWTKit/JWT.swift +++ b/Sources/JWTKit/JWT.swift @@ -98,7 +98,7 @@ public struct JWT<Payload> where Payload: JWTPayload { // encode header, copying header struct to mutate alg var header = self.header - header.alg = signer.algorithm.jwtAlgorithmName + header.alg = signer.algorithm.name let headerData = try jsonEncoder.encode(header) let encodedHeader = headerData.base64URLEncodedBytes() diff --git a/Sources/JWTKit/JWTAlgorithm.swift b/Sources/JWTKit/JWTAlgorithm.swift index eb80d41..2b128f8 100644 --- a/Sources/JWTKit/JWTAlgorithm.swift +++ b/Sources/JWTKit/JWTAlgorithm.swift @@ -1,7 +1,7 @@ /// Algorithm powering a `JWTSigner`. public protocol JWTAlgorithm { /// Unique JWT-standard name for this algorithm. - var jwtAlgorithmName: String { get } + var name: String { get } /// Creates a signature from the supplied plaintext. /// diff --git a/Sources/JWTKit/JWTSigner+HMAC.swift b/Sources/JWTKit/JWTSigner+HMAC.swift index 4f00be6..a8f3588 100644 --- a/Sources/JWTKit/JWTSigner+HMAC.swift +++ b/Sources/JWTKit/JWTSigner+HMAC.swift @@ -1,4 +1,4 @@ -import CJWTKitOpenSSL +import CJWTKitCrypto extension JWTSigner { // MARK: HMAC @@ -8,8 +8,8 @@ extension JWTSigner { { return .init(algorithm: HMACAlgorithm( key: key.copyBytes(), - algorithm: EVP_sha256(), - jwtAlgorithmName: "HS256" + algorithm: convert(EVP_sha256()), + name: "HS256" )) } @@ -18,8 +18,8 @@ extension JWTSigner { { return .init(algorithm: HMACAlgorithm( key: key.copyBytes(), - algorithm: EVP_sha384(), - jwtAlgorithmName: "HS384" + algorithm: convert(EVP_sha384()), + name: "HS384" )) } @@ -28,8 +28,8 @@ extension JWTSigner { { return .init(algorithm: HMACAlgorithm( key: key.copyBytes(), - algorithm: EVP_sha512(), - jwtAlgorithmName: "HS512" + algorithm: convert(EVP_sha512()), + name: "HS512" )) } } @@ -37,7 +37,7 @@ extension JWTSigner { private struct HMACAlgorithm: JWTAlgorithm { let key: [UInt8] let algorithm: OpaquePointer - let jwtAlgorithmName: String + let name: String func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] where Plaintext: DataProtocol @@ -46,7 +46,7 @@ private struct HMACAlgorithm: JWTAlgorithm { defer { HMAC_CTX_free(context) } guard self.key.withUnsafeBytes({ - return HMAC_Init_ex(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32($0.count), self.algorithm, nil) + return HMAC_Init_ex(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32($0.count), convert(self.algorithm), nil) }) == 1 else { fatalError("Failed initializing HMAC context") } diff --git a/Sources/JWTKit/JWTSigner+RSA.swift b/Sources/JWTKit/JWTSigner+RSA.swift index 6a5b8aa..8854044 100644 --- a/Sources/JWTKit/JWTSigner+RSA.swift +++ b/Sources/JWTKit/JWTSigner+RSA.swift @@ -1,4 +1,4 @@ -import CJWTKitOpenSSL +import CJWTKitCrypto import struct Foundation.Data extension JWTSigner { @@ -7,24 +7,24 @@ extension JWTSigner { public static func rs256(key: RSAKey) -> JWTSigner { return .init(algorithm: RSAAlgorithm( key: key, - algorithm: EVP_sha256(), - jwtAlgorithmName: "RS256" + algorithm: convert(EVP_sha256()), + name: "RS256" )) } public static func rs384(key: RSAKey) -> JWTSigner { return .init(algorithm: RSAAlgorithm( key: key, - algorithm: EVP_sha384(), - jwtAlgorithmName: "RS384" + algorithm: convert(EVP_sha384()), + name: "RS384" )) } public static func rs512(key: RSAKey) -> JWTSigner { return .init(algorithm: RSAAlgorithm( key: key, - algorithm: EVP_sha512(), - jwtAlgorithmName: "RS512" + algorithm: convert(EVP_sha512()), + name: "RS512" )) } } @@ -39,8 +39,6 @@ public struct RSAKey { case `private` } - // MARK: Static - /// Creates a new `RSAKey` from a private key pem file. public static func `private`<Data>(pem: Data) -> RSAKey where Data: DataProtocol @@ -62,7 +60,6 @@ public struct RSAKey { return .init(type: .public, key: .make(type: .public, from: certificate, x509: true)) } - // MARK: Properties /// The specific RSA key type. Either public or private. /// /// Note: public keys can only verify signatures. A private key @@ -72,8 +69,6 @@ public struct RSAKey { /// The C OpenSSL key ref. fileprivate let c: CRSAKey - // MARK: Init - /// Creates a new `RSAKey` from a public or private key. fileprivate init(type: Kind, key: CRSAKey) { self.type = type @@ -112,8 +107,8 @@ public struct RSAKey { let n = parseBignum(n) let e = parseBignum(e) let d = d.flatMap { parseBignum($0) } - RSA_set0_key(rsa, n, e, d) - return .init(type: d == nil ? .public : .private, key: CRSAKey(rsa)) + RSA_set0_key(rsa, convert(n), convert(e), d.flatMap { convert($0) }) + return .init(type: d == nil ? .public : .private, key: CRSAKey(convert(rsa))) } } @@ -146,38 +141,40 @@ private final class CRSAKey { } defer { X509_free(x509) } - maybePkey = X509_get_pubkey(x509) + maybePkey = convert(X509_get_pubkey(x509)) } else { switch type { - case .public: maybePkey = PEM_read_bio_PUBKEY(bio, nil, nil, nil) - case .private: maybePkey = PEM_read_bio_PrivateKey(bio, nil, nil, nil) + case .public: + maybePkey = convert(PEM_read_bio_PUBKEY(bio, nil, nil, nil)) + case .private: + maybePkey = convert(PEM_read_bio_PrivateKey(bio, nil, nil, nil)) } } guard let pkey = maybePkey else { fatalError("RSA key creation failed") } - defer { EVP_PKEY_free(pkey) } + defer { EVP_PKEY_free(convert(pkey)) } - guard let rsa = EVP_PKEY_get1_RSA(pkey) else { + guard let rsa = EVP_PKEY_get1_RSA(convert(pkey)) else { fatalError("RSA key creation failed") } - return .init(rsa) + return .init(convert(rsa)) } - deinit { RSA_free(self.pointer) } + deinit { RSA_free(convert(self.pointer)) } } private func parseBignum(_ s: String) -> OpaquePointer { - return Data(s.utf8).base64URLDecodedBytes().withUnsafeBytes { (p: UnsafeRawBufferPointer) -> OpaquePointer in - return BN_bin2bn(p.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(p.count), nil) + return Data(s.utf8).base64URLDecodedBytes().withUnsafeBytes { pointer in + convert(BN_bin2bn(pointer.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(pointer.count), nil)) } } private struct RSAAlgorithm: JWTAlgorithm { let key: RSAKey let algorithm: OpaquePointer - let jwtAlgorithmName: String + let name: String func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] where Plaintext: DataProtocol @@ -192,18 +189,18 @@ private struct RSAAlgorithm: JWTAlgorithm { var siglen: UInt32 = 0 var signature = [UInt8]( repeating: 0, - count: Int(RSA_size(key.c.pointer)) + count: Int(RSA_size(convert(key.c.pointer))) ) guard self.hash(plaintext).withUnsafeBytes ({ inputBuffer in signature.withUnsafeMutableBytes({ signatureBuffer in RSA_sign( - EVP_MD_type(self.algorithm), + EVP_MD_type(convert(self.algorithm)), inputBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), UInt32(inputBuffer.count), signatureBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), &siglen, - key.c.pointer + convert(key.c.pointer) ) }) }) == 1 else { @@ -222,12 +219,12 @@ private struct RSAAlgorithm: JWTAlgorithm { return self.hash(plaintext).withUnsafeBytes({ inputBuffer in signature.copyBytes().withUnsafeBytes({ signatureBuffer in RSA_verify( - EVP_MD_type(self.algorithm), + EVP_MD_type(convert(self.algorithm)), inputBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self), UInt32(inputBuffer.count), signatureBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self), UInt32(signatureBuffer.count), - key.c.pointer + convert(key.c.pointer) ) }) }) == 1 @@ -239,7 +236,7 @@ private struct RSAAlgorithm: JWTAlgorithm { let context = EVP_MD_CTX_new() defer { EVP_MD_CTX_free(context) } - guard EVP_DigestInit_ex(context, self.algorithm, nil) == 1 else { + guard EVP_DigestInit_ex(context, convert(self.algorithm), nil) == 1 else { fatalError("Failed initializing digest context") } guard plaintext.copyBytes().withUnsafeBytes({ diff --git a/Sources/JWTKit/Utilities.swift b/Sources/JWTKit/Utilities.swift index 9a9c4d1..091285d 100644 --- a/Sources/JWTKit/Utilities.swift +++ b/Sources/JWTKit/Utilities.swift @@ -20,3 +20,25 @@ extension UInt8 { return Character(".").asciiValue! } } + +// pointer hacks + +func convert(_ pointer: OpaquePointer) -> OpaquePointer { + return pointer +} + +func convert<T>(_ pointer: UnsafePointer<T>) -> OpaquePointer { + return .init(pointer) +} + +func convert<T>(_ pointer: UnsafeMutablePointer<T>) -> OpaquePointer { + return .init(pointer) +} + +func convert<T>(_ pointer: OpaquePointer) -> UnsafePointer<T> { + return .init(pointer) +} + +func convert<T>(_ pointer: OpaquePointer) -> UnsafeMutablePointer<T> { + return .init(pointer) +} diff --git a/Tests/JWTKitTests/JWTTests.swift b/Tests/JWTKitTests/JWTKitTests.swift similarity index 99% rename from Tests/JWTKitTests/JWTTests.swift rename to Tests/JWTKitTests/JWTKitTests.swift index 11ff58d..d508095 100644 --- a/Tests/JWTKitTests/JWTTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -1,7 +1,7 @@ import XCTest import JWTKit -class JWTTests: XCTestCase { +class JWTKitTests: XCTestCase { func testParse() throws { let data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6OTk5OTk5OTk5OTk5fQ.Ks7KcdjrlUTYaSNeAO5SzBla_sFCHkUh4vvJYn6q29U" diff --git a/Tests/JWTKitTests/XCTestManifests.swift b/Tests/JWTKitTests/XCTestManifests.swift index 0b2d6d6..3108965 100644 --- a/Tests/JWTKitTests/XCTestManifests.swift +++ b/Tests/JWTKitTests/XCTestManifests.swift @@ -1,11 +1,11 @@ #if !canImport(ObjectiveC) import XCTest -extension JWTTests { +extension JWTKitTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` // to regenerate. - static let __allTests__JWTTests = [ + static let __allTests__JWTKitTests = [ ("testExpirationDecoding", testExpirationDecoding), ("testExpirationEncoding", testExpirationEncoding), ("testExpired", testExpired), @@ -17,7 +17,7 @@ extension JWTTests { public func __allTests() -> [XCTestCaseEntry] { return [ - testCase(JWTTests.__allTests__JWTTests), + testCase(JWTKitTests.__allTests__JWTKitTests), ] } #endif diff --git a/circle.yml b/circle.yml index f9dd07b..a2ed147 100644 --- a/circle.yml +++ b/circle.yml @@ -31,21 +31,21 @@ jobs: - checkout - run: apt-get -y install openssl libssl-dev - run: swift build -c release - # xenial: - # docker: - # - image: swift:5.0-xenial - # steps: - # - checkout - # - run: apt-get -y install openssl libssl-dev - # - run: swift build --disable-index-store - # - run: swift test --disable-index-store - # xenial-release: - # docker: - # - image: swift:5.0-xenial - # steps: - # - checkout - # - run: apt-get -y install openssl libssl-dev - # - run: swift build -c release + xenial: + docker: + - image: swift:5.0-xenial + steps: + - checkout + - run: apt-get -y install openssl libssl-dev + - run: swift build --disable-index-store + - run: swift test --disable-index-store + xenial-release: + docker: + - image: swift:5.0-xenial + steps: + - checkout + - run: apt-get -y install openssl libssl-dev + - run: swift build -c release workflows: version: 2 @@ -55,5 +55,5 @@ workflows: - macos-release - bionic - bionic-release - # - xenial - # - xenial-release + - xenial + - xenial-release From 91cdcba94fe22397ed7104419220767455f7e1b1 Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 15:44:19 -0400 Subject: [PATCH 08/12] don't install openssl@1.1 on macos --- circle.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/circle.yml b/circle.yml index a2ed147..7a871b0 100644 --- a/circle.yml +++ b/circle.yml @@ -6,7 +6,6 @@ jobs: xcode: "10.2.0" steps: - checkout - - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install openssl@1.1 - run: swift build - run: swift test macos-release: @@ -14,7 +13,6 @@ jobs: xcode: "10.2.0" steps: - checkout - - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install openssl@1.1 - run: swift build -c release bionic: docker: From 75806f6feb501908ea93b396bb739bc69e4d864b Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 15:45:21 -0400 Subject: [PATCH 09/12] disable index store fix --- circle.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 7a871b0..7469b02 100644 --- a/circle.yml +++ b/circle.yml @@ -20,8 +20,8 @@ jobs: steps: - checkout - run: apt-get -y install openssl libssl-dev - - run: swift build --disable-index-store - - run: swift test --disable-index-store + - run: swift build + - run: swift test bionic-release: docker: - image: swift:5.0-bionic @@ -35,8 +35,8 @@ jobs: steps: - checkout - run: apt-get -y install openssl libssl-dev - - run: swift build --disable-index-store - - run: swift test --disable-index-store + - run: swift build + - run: swift test xenial-release: docker: - image: swift:5.0-xenial From 5bd63dcb97039bc653e4df353421a23815fd7d7e Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 17:45:26 -0400 Subject: [PATCH 10/12] add ECDSA support --- Sources/JWTKit/JWTClaims.swift | 14 +- Sources/JWTKit/JWTSigner+ECDSA.swift | 119 ++++++++++++ Sources/JWTKit/JWTSigner+HMAC.swift | 10 +- Sources/JWTKit/JWTSigner+RSA.swift | 240 ++++++------------------ Sources/JWTKit/OpenSSLSigner.swift | 51 +++++ Sources/JWTKit/Utilities.swift | 2 - Tests/JWTKitTests/JWTKitTests.swift | 75 ++++++-- Tests/JWTKitTests/XCTestManifests.swift | 3 + 8 files changed, 306 insertions(+), 208 deletions(-) create mode 100644 Sources/JWTKit/JWTSigner+ECDSA.swift create mode 100644 Sources/JWTKit/OpenSSLSigner.swift diff --git a/Sources/JWTKit/JWTClaims.swift b/Sources/JWTKit/JWTClaims.swift index 46d858a..a5e7fb0 100644 --- a/Sources/JWTKit/JWTClaims.swift +++ b/Sources/JWTKit/JWTClaims.swift @@ -2,7 +2,7 @@ /// JWT. The processing of this claim is generally application specific. /// The "iss" value is a case-sensitive string containing a StringOrURI /// value. Use of this claim is OPTIONAL. -public struct IssuerClaim: JWTClaim, ExpressibleByStringLiteral { +public struct IssuerClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { /// See `JWTClaim`. public var value: String @@ -19,7 +19,7 @@ public struct IssuerClaim: JWTClaim, ExpressibleByStringLiteral { /// The processing of this claim is generally application specific. The /// "sub" value is a case-sensitive string containing a StringOrURI /// value. Use of this claim is OPTIONAL. -public struct SubjectClaim: JWTClaim, ExpressibleByStringLiteral { +public struct SubjectClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { /// See `JWTClaim`. public var value: String @@ -40,7 +40,7 @@ public struct SubjectClaim: JWTClaim, ExpressibleByStringLiteral { /// single case-sensitive string containing a StringOrURI value. The /// interpretation of audience values is generally application specific. /// Use of this claim is OPTIONAL. -public struct AudienceClaim: JWTClaim, ExpressibleByStringLiteral { +public struct AudienceClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { /// See `JWTClaim`. public var value: String @@ -58,7 +58,7 @@ public struct AudienceClaim: JWTClaim, ExpressibleByStringLiteral { /// produced by different issuers as well. The "jti" claim can be used /// to prevent the JWT from being replayed. The "jti" value is a case- /// sensitive string. Use of this claim is OPTIONAL. -public struct IDClaim: JWTClaim, ExpressibleByStringLiteral { +public struct IDClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { /// See `JWTClaim`. public var value: String @@ -72,7 +72,7 @@ public struct IDClaim: JWTClaim, ExpressibleByStringLiteral { /// issued. This claim can be used to determine the age of the JWT. Its /// value MUST be a number containing a NumericDate value. Use of this /// claim is OPTIONAL. -public struct IssuedAtClaim: JWTUnixEpochClaim { +public struct IssuedAtClaim: JWTUnixEpochClaim, Equatable { /// See `JWTClaim`. public var value: Date @@ -89,7 +89,7 @@ public struct IssuedAtClaim: JWTUnixEpochClaim { /// Implementers MAY provide for some small leeway, usually no more than /// a few minutes, to account for clock skew. Its value MUST be a number /// containing a NumericDate value. Use of this claim is OPTIONAL. -public struct ExpirationClaim: JWTUnixEpochClaim { +public struct ExpirationClaim: JWTUnixEpochClaim, Equatable { /// See `JWTClaim`. public var value: Date @@ -114,7 +114,7 @@ public struct ExpirationClaim: JWTUnixEpochClaim { /// provide for some small leeway, usually no more than a few minutes, to /// account for clock skew. Its value MUST be a number containing a /// NumericDate value. Use of this claim is OPTIONAL. -public struct NotBeforeClaim: JWTUnixEpochClaim { +public struct NotBeforeClaim: JWTUnixEpochClaim, Equatable { /// See `JWTClaim`. public var value: Date diff --git a/Sources/JWTKit/JWTSigner+ECDSA.swift b/Sources/JWTKit/JWTSigner+ECDSA.swift new file mode 100644 index 0000000..d923140 --- /dev/null +++ b/Sources/JWTKit/JWTSigner+ECDSA.swift @@ -0,0 +1,119 @@ +import CJWTKitCrypto + +extension JWTSigner { + // MARK: ECDSA + + public static func es256(key: ECDSAKey) -> JWTSigner { + return .init(algorithm: ECDSASigner( + key: key, + algorithm: convert(EVP_sha256()), + name: "ES256" + )) + } + + public static func es384(key: ECDSAKey) -> JWTSigner { + return .init(algorithm: ECDSASigner( + key: key, + algorithm: convert(EVP_sha384()), + name: "ES384" + )) + } + + public static func es512(key: ECDSAKey) -> JWTSigner { + return .init(algorithm: ECDSASigner( + key: key, + algorithm: convert(EVP_sha512()), + name: "ES512" + )) + } +} + +public final class ECDSAKey: OpenSSLKey { + public static func generate() -> ECDSAKey { + guard let c = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1) else { + fatalError("EC_KEY_new_by_curve_name failed") + } + guard EC_KEY_generate_key(c) != 0 else { + fatalError("EC_KEY_generate_key failed") + } + return .init(c) + } + + public static func `public`<Data>(pem data: Data) -> ECDSAKey + where Data: DataProtocol + { + let c = self.load(pem: data) { bio in + PEM_read_bio_EC_PUBKEY(convert(bio), nil, nil, nil) + } + return self.init(c) + } + + public static func `private`<Data>(pem data: Data) -> ECDSAKey + where Data: DataProtocol + { + let c = self.load(pem: data) { bio in + PEM_read_bio_ECPrivateKey(convert(bio), nil, nil, nil) + } + return self.init(c) + } + + let c: OpaquePointer + + init(_ c: OpaquePointer) { + self.c = c + } + + deinit { + EC_KEY_free(self.c) + } +} + +// MARK: Private + +private struct ECDSASigner: JWTAlgorithm, OpenSSLSigner { + let key: ECDSAKey + let algorithm: OpaquePointer + let name: String + + func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] + where Plaintext: DataProtocol + { + var signatureLength: UInt32 = 0 + var signature = [UInt8]( + repeating: 0, + count: Int(ECDSA_size(self.key.c)) + ) + + let digest = self.digest(plaintext) + guard ECDSA_sign( + 0, + digest, + numericCast(digest.count), + &signature, + &signatureLength, + self.key.c + ) == 1 else { + fatalError("ECDSA sign failed") + } + + return .init(signature[0..<numericCast(signatureLength)]) + } + + func verify<Signature, Plaintext>( + _ signature: Signature, + signs plaintext: Plaintext + ) throws -> Bool + where Signature: DataProtocol, Plaintext: DataProtocol + { + let digest = self.digest(plaintext) + let signature = signature.copyBytes() + return ECDSA_verify( + 0, + digest, + numericCast(digest.count), + signature, + numericCast(signature.count), + self.key.c + ) == 1 + } +} diff --git a/Sources/JWTKit/JWTSigner+HMAC.swift b/Sources/JWTKit/JWTSigner+HMAC.swift index a8f3588..7bd9714 100644 --- a/Sources/JWTKit/JWTSigner+HMAC.swift +++ b/Sources/JWTKit/JWTSigner+HMAC.swift @@ -6,7 +6,7 @@ extension JWTSigner { public static func hs256<Key>(key: Key) -> JWTSigner where Key: DataProtocol { - return .init(algorithm: HMACAlgorithm( + return .init(algorithm: HMACSigner( key: key.copyBytes(), algorithm: convert(EVP_sha256()), name: "HS256" @@ -16,7 +16,7 @@ extension JWTSigner { public static func hs384<Key>(key: Key) -> JWTSigner where Key: DataProtocol { - return .init(algorithm: HMACAlgorithm( + return .init(algorithm: HMACSigner( key: key.copyBytes(), algorithm: convert(EVP_sha384()), name: "HS384" @@ -26,7 +26,7 @@ extension JWTSigner { public static func hs512<Key>(key: Key) -> JWTSigner where Key: DataProtocol { - return .init(algorithm: HMACAlgorithm( + return .init(algorithm: HMACSigner( key: key.copyBytes(), algorithm: convert(EVP_sha512()), name: "HS512" @@ -34,7 +34,9 @@ extension JWTSigner { } } -private struct HMACAlgorithm: JWTAlgorithm { +// MARK: Private + +private struct HMACSigner: JWTAlgorithm { let key: [UInt8] let algorithm: OpaquePointer let name: String diff --git a/Sources/JWTKit/JWTSigner+RSA.swift b/Sources/JWTKit/JWTSigner+RSA.swift index 8854044..217c6bd 100644 --- a/Sources/JWTKit/JWTSigner+RSA.swift +++ b/Sources/JWTKit/JWTSigner+RSA.swift @@ -5,7 +5,7 @@ extension JWTSigner { // MARK: RSA public static func rs256(key: RSAKey) -> JWTSigner { - return .init(algorithm: RSAAlgorithm( + return .init(algorithm: RSASigner( key: key, algorithm: convert(EVP_sha256()), name: "RS256" @@ -13,7 +13,7 @@ extension JWTSigner { } public static func rs384(key: RSAKey) -> JWTSigner { - return .init(algorithm: RSAAlgorithm( + return .init(algorithm: RSASigner( key: key, algorithm: convert(EVP_sha384()), name: "RS384" @@ -21,7 +21,7 @@ extension JWTSigner { } public static func rs512(key: RSAKey) -> JWTSigner { - return .init(algorithm: RSAAlgorithm( + return .init(algorithm: RSASigner( key: key, algorithm: convert(EVP_sha512()), name: "RS512" @@ -29,149 +29,55 @@ extension JWTSigner { } } -/// Represents an in-memory RSA key. -public struct RSAKey { - /// Supported RSA key types. - public enum Kind { - /// A public RSA key. Used for verifying signatures. - case `public` - /// A private RSA key. Used for creating and verifying signatures. - case `private` - } - - /// Creates a new `RSAKey` from a private key pem file. - public static func `private`<Data>(pem: Data) -> RSAKey - where Data: DataProtocol - { - return .init(type: .private, key: .make(type: .private, from: pem)) - } - - /// Creates a new `RSAKey` from a public key pem file. - public static func `public`<Data>(pem: Data) -> RSAKey +public final class RSAKey: OpenSSLKey { + public static func `public`<Data>(pem data: Data) -> RSAKey where Data: DataProtocol { - return .init(type: .public, key: .make(type: .public, from: pem)) - } - - /// Creates a new `RSAKey` from a public key certificate file. - public static func `public`<Data>(certificate: Data) -> RSAKey - where Data: DataProtocol - { - return .init(type: .public, key: .make(type: .public, from: certificate, x509: true)) - } - - /// The specific RSA key type. Either public or private. - /// - /// Note: public keys can only verify signatures. A private key - /// is required to create new signatures. - public var type: Kind - - /// The C OpenSSL key ref. - fileprivate let c: CRSAKey - - /// Creates a new `RSAKey` from a public or private key. - fileprivate init(type: Kind, key: CRSAKey) { - self.type = type - self.c = key - } + let pkey = self.load(pem: data) { bio in + PEM_read_bio_PUBKEY(convert(bio), nil, nil, nil) + } + defer { EVP_PKEY_free(pkey) } - /// Creates a new `RSAKey` from components. - /// - /// For example, if you want to use Google's [public OAuth2 keys](https://www.googleapis.com/oauth2/v3/certs), - /// you could parse the request using: - /// - /// struct CertKeysResponse: APIResponse { - /// let keys: [Key] - /// - /// struct Key: Codable { - /// let kty: String - /// let alg: String - /// let kid: String - /// - /// let n: String - /// let e: String - /// let d: String? - /// } - /// } - /// - /// And then instantiate the key as: - /// - /// try RSAKey.components(n: key.n, e: key.e, d: key.d) - /// - /// - throws: `CryptoError` if key generation fails. - public static func components(n: String, e: String, d: String? = nil) -> RSAKey { - guard let rsa = RSA_new() else { + guard let c = EVP_PKEY_get1_RSA(pkey) else { fatalError("RSA key creation failed") } - - let n = parseBignum(n) - let e = parseBignum(e) - let d = d.flatMap { parseBignum($0) } - RSA_set0_key(rsa, convert(n), convert(e), d.flatMap { convert($0) }) - return .init(type: d == nil ? .public : .private, key: CRSAKey(convert(rsa))) - } -} - -// MARK: Private - -private final class CRSAKey { - let pointer: OpaquePointer - - internal init(_ pointer: OpaquePointer) { - self.pointer = pointer + return self.init(convert(c), .public) } - static func make<Data>(type: RSAKey.Kind, from data: Data, x509: Bool = false) -> CRSAKey + public static func `private`<Data>(pem data: Data) -> RSAKey where Data: DataProtocol { - let bio = BIO_new(BIO_s_mem()) - defer { BIO_free(bio) } - - let bytes = data.copyBytes() - let nullTerminatedData = bytes + [0] - _ = nullTerminatedData.withUnsafeBytes { (p: UnsafeRawBufferPointer) -> Int32 in - return BIO_puts(bio, p.baseAddress?.assumingMemoryBound(to: Int8.self)) - } - - let maybePkey: OpaquePointer? - - if x509 { - guard let x509 = PEM_read_bio_X509(bio, nil, nil, nil) else { - fatalError("Key creation from certificate failed") - } - - defer { X509_free(x509) } - maybePkey = convert(X509_get_pubkey(x509)) - } else { - switch type { - case .public: - maybePkey = convert(PEM_read_bio_PUBKEY(bio, nil, nil, nil)) - case .private: - maybePkey = convert(PEM_read_bio_PrivateKey(bio, nil, nil, nil)) - } + let pkey = self.load(pem: data) { bio in + PEM_read_bio_PrivateKey(convert(bio), nil, nil, nil) } + defer { EVP_PKEY_free(pkey) } - guard let pkey = maybePkey else { + guard let c = EVP_PKEY_get1_RSA(pkey) else { fatalError("RSA key creation failed") } - defer { EVP_PKEY_free(convert(pkey)) } + return self.init(convert(c), .private) + } - guard let rsa = EVP_PKEY_get1_RSA(convert(pkey)) else { - fatalError("RSA key creation failed") - } - return .init(convert(rsa)) + enum KeyType { + case `public`, `private` } - deinit { RSA_free(convert(self.pointer)) } -} + let type: KeyType + let c: OpaquePointer + + init(_ c: OpaquePointer, _ type: KeyType) { + self.type = type + self.c = c + } -private func parseBignum(_ s: String) -> OpaquePointer { - return Data(s.utf8).base64URLDecodedBytes().withUnsafeBytes { pointer in - convert(BN_bin2bn(pointer.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(pointer.count), nil)) + deinit { + RSA_free(convert(self.c)) } } -private struct RSAAlgorithm: JWTAlgorithm { +// MARK: Private + +private struct RSASigner: JWTAlgorithm, OpenSSLSigner { let key: RSAKey let algorithm: OpaquePointer let name: String @@ -179,35 +85,28 @@ private struct RSAAlgorithm: JWTAlgorithm { func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] where Plaintext: DataProtocol { - switch key.type { - case .public: - throw JWTError(identifier: "rsa", reason: "Cannot create RSA signature with a public key. A private key is required.") - case .private: - break + guard case .private = self.key.type else { + throw JWTError(identifier: "rsa", reason: "Private key required to sign") } - - var siglen: UInt32 = 0 + var signatureLength: UInt32 = 0 var signature = [UInt8]( repeating: 0, - count: Int(RSA_size(convert(key.c.pointer))) + count: Int(RSA_size(convert(key.c))) ) - guard self.hash(plaintext).withUnsafeBytes ({ inputBuffer in - signature.withUnsafeMutableBytes({ signatureBuffer in - RSA_sign( - EVP_MD_type(convert(self.algorithm)), - inputBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), - UInt32(inputBuffer.count), - signatureBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), - &siglen, - convert(key.c.pointer) - ) - }) - }) == 1 else { + let digest = self.digest(plaintext) + guard RSA_sign( + EVP_MD_type(convert(self.algorithm)), + digest, + numericCast(digest.count), + &signature, + &signatureLength, + convert(self.key.c) + ) == 1 else { throw JWTError(identifier: "rsaSign", reason: "RSA signature creation failed") } - return signature + return .init(signature[0..<numericCast(signatureLength)]) } func verify<Signature, Plaintext>( @@ -216,42 +115,15 @@ private struct RSAAlgorithm: JWTAlgorithm { ) throws -> Bool where Signature: DataProtocol, Plaintext: DataProtocol { - return self.hash(plaintext).withUnsafeBytes({ inputBuffer in - signature.copyBytes().withUnsafeBytes({ signatureBuffer in - RSA_verify( - EVP_MD_type(convert(self.algorithm)), - inputBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self), - UInt32(inputBuffer.count), - signatureBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self), - UInt32(signatureBuffer.count), - convert(key.c.pointer) - ) - }) - }) == 1 - } - - private func hash<Plaintext>(_ plaintext: Plaintext) -> [UInt8] - where Plaintext: DataProtocol - { - let context = EVP_MD_CTX_new() - defer { EVP_MD_CTX_free(context) } - - guard EVP_DigestInit_ex(context, convert(self.algorithm), nil) == 1 else { - fatalError("Failed initializing digest context") - } - guard plaintext.copyBytes().withUnsafeBytes({ - EVP_DigestUpdate(context, $0.baseAddress, $0.count) - }) == 1 else { - fatalError("Failed updating digest") - } - var hash: [UInt8] = .init(repeating: 0, count: Int(EVP_MAX_MD_SIZE)) - var count: UInt32 = 0 - - guard hash.withUnsafeMutableBytes({ - EVP_DigestFinal_ex(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), &count) - }) == 1 else { - fatalError("Failed finalizing digest") - } - return .init(hash[0..<Int(count)]) + let digest = self.digest(plaintext) + let signature = signature.copyBytes() + return RSA_verify( + EVP_MD_type(convert(self.algorithm)), + digest, + numericCast(digest.count), + signature, + numericCast(signature.count), + convert(self.key.c) + ) == 1 } } diff --git a/Sources/JWTKit/OpenSSLSigner.swift b/Sources/JWTKit/OpenSSLSigner.swift new file mode 100644 index 0000000..b57446f --- /dev/null +++ b/Sources/JWTKit/OpenSSLSigner.swift @@ -0,0 +1,51 @@ +import CJWTKitCrypto + +protocol OpenSSLSigner { + var algorithm: OpaquePointer { get } +} + +extension OpenSSLSigner { + func digest<Plaintext>(_ plaintext: Plaintext) -> [UInt8] + where Plaintext: DataProtocol + { + let context = EVP_MD_CTX_new() + defer { EVP_MD_CTX_free(context) } + + guard EVP_DigestInit_ex(context, convert(self.algorithm), nil) == 1 else { + fatalError("Failed initializing digest context") + } + let plaintext = plaintext.copyBytes() + guard EVP_DigestUpdate(context, plaintext, plaintext.count) == 1 else { + fatalError("Failed updating digest") + } + var digest: [UInt8] = .init(repeating: 0, count: Int(EVP_MAX_MD_SIZE)) + var digestLength: UInt32 = 0 + + guard EVP_DigestFinal_ex(context, &digest, &digestLength) == 1 else { + fatalError("Failed finalizing digest") + } + return .init(digest[0..<Int(digestLength)]) + } +} + +protocol OpenSSLKey { } + +extension OpenSSLKey { + static func load<Data, T>(pem data: Data, _ closure: (OpaquePointer) -> (T?)) -> T + where Data: DataProtocol + { + let bio = BIO_new(BIO_s_mem()) + defer { BIO_free(bio) } + + guard (data.copyBytes() + [0]).withUnsafeBytes({ pointer in + BIO_puts(bio, pointer.baseAddress?.assumingMemoryBound(to: Int8.self)) + }) >= 0 else { + fatalError("BIO puts failed") + } + + guard let c = closure(convert(bio!)) else { + fatalError("PEM_read_bio_EC_PUBKEY failed") + } + return c + } +} diff --git a/Sources/JWTKit/Utilities.swift b/Sources/JWTKit/Utilities.swift index 091285d..bb424ed 100644 --- a/Sources/JWTKit/Utilities.swift +++ b/Sources/JWTKit/Utilities.swift @@ -13,8 +13,6 @@ extension DataProtocol { } } - - extension UInt8 { static var period: UInt8 { return Character(".").asciiValue! diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index d508095..d3a3e1c 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -52,9 +52,23 @@ class JWTKitTests: XCTestCase { } func testRSA() throws { - let privateSigner = JWTSigner.rs256(key: .private(pem: privateKeyString.bytes)) - let publicSigner = JWTSigner.rs256(key: .public(pem: publicKeyString.bytes)) + let privateSigner = JWTSigner.rs256(key: .private(pem: rsaPrivateKey.bytes)) + let publicSigner = JWTSigner.rs256(key: .public(pem: rsaPublicKey.bytes)) + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + let jwt = JWT(payload: payload) + let privateSigned = try jwt.sign(using: privateSigner) + try XCTAssertEqual(JWT<TestPayload>(from: privateSigned, verifiedBy: publicSigner).payload, payload) + try XCTAssertEqual(JWT<TestPayload>(from: privateSigned, verifiedBy: privateSigner).payload, payload) + } + + func testRSASignWithPublic() throws { + let publicSigner = JWTSigner.rs256(key: .public(pem: rsaPublicKey.bytes)) let payload = TestPayload( sub: "vapor", name: "Foo", @@ -68,15 +82,41 @@ class JWTKitTests: XCTestCase { } catch { // pass } - let privateSigned = try jwt.sign(using: privateSigner) - let publicVerified = try JWT<TestPayload>(from: privateSigned, verifiedBy: publicSigner) - let privateVerified = try JWT<TestPayload>(from: privateSigned, verifiedBy: privateSigner) - XCTAssertEqual(publicVerified.payload.name, "Foo") - XCTAssertEqual(privateVerified.payload.name, "Foo") + } + + func testECDSAGenerate() throws { + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + let jwt = JWT(payload: payload) + let signer = JWTSigner.es256(key: .generate()) + let data = try jwt.sign(using: signer) + try XCTAssertEqual(JWT<TestPayload>(from: data, verifiedBy: signer).payload, payload) + } + + func testECDSAPublicPrivate() throws { + let publicSigner = JWTSigner.es256(key: .public(pem: ecdsaPublicKey.bytes)) + let privateSigner = JWTSigner.es256(key: .private(pem: ecdsaPrivateKey.bytes)) + + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + let jwt = JWT(payload: payload) + let data = try jwt.sign(using: privateSigner) + // test private signer decoding + try XCTAssertEqual(JWT<TestPayload>(from: data, verifiedBy: privateSigner).payload, payload) + // test public signer decoding + try XCTAssertEqual(JWT<TestPayload>(from: data, verifiedBy: publicSigner).payload, payload) } } -struct TestPayload: JWTPayload { +struct TestPayload: JWTPayload, Equatable { var sub: SubjectClaim var name: String var admin: Bool @@ -95,9 +135,22 @@ struct ExpirationPayload: JWTPayload { } } -/// MARK: RSA +let ecdsaPrivateKey = """ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm +jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf +ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve +y+77Vzsd +-----END PRIVATE KEY----- +""" +let ecdsaPublicKey = """ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx +C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ== +-----END PUBLIC KEY----- +""" -let privateKeyString = """ +let rsaPrivateKey = """ -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQC0cOtPjzABybjzm3fCg1aCYwnxPmjXpbCkecAWLj/CcDWEcuTZ kYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv7FPo5Cq8FkvrdDzeacwRSxYu @@ -115,7 +168,7 @@ bxEd3Ax0uhHVqKRWNioL7UBvd4lxoReY8RmmfghZHEA= -----END RSA PRIVATE KEY----- """ -let publicKeyString = """ +let rsaPublicKey = """ -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv diff --git a/Tests/JWTKitTests/XCTestManifests.swift b/Tests/JWTKitTests/XCTestManifests.swift index 3108965..873cff6 100644 --- a/Tests/JWTKitTests/XCTestManifests.swift +++ b/Tests/JWTKitTests/XCTestManifests.swift @@ -6,11 +6,14 @@ extension JWTKitTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__JWTKitTests = [ + ("testECDSAGenerate", testECDSAGenerate), + ("testECDSAPublicPrivate", testECDSAPublicPrivate), ("testExpirationDecoding", testExpirationDecoding), ("testExpirationEncoding", testExpirationEncoding), ("testExpired", testExpired), ("testParse", testParse), ("testRSA", testRSA), + ("testRSASignWithPublic", testRSASignWithPublic), ("testSigners", testSigners), ] } From 3b93f5e565d46923d89f43cb9eb2d85f0aff5985 Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 18:01:58 -0400 Subject: [PATCH 11/12] remove fatalerror --- Sources/JWTKit/JWT.swift | 10 +++---- Sources/JWTKit/JWTClaims.swift | 14 +++++---- Sources/JWTKit/JWTError.swift | 43 +++++++++++++++++----------- Sources/JWTKit/JWTSigner+ECDSA.swift | 26 ++++++++++------- Sources/JWTKit/JWTSigner+HMAC.swift | 12 ++++++-- Sources/JWTKit/JWTSigner+RSA.swift | 26 ++++++++++------- Sources/JWTKit/OpenSSLSigner.swift | 22 +++++++++----- Tests/JWTKitTests/JWTKitTests.swift | 19 +++++++----- 8 files changed, 109 insertions(+), 63 deletions(-) diff --git a/Sources/JWTKit/JWT.swift b/Sources/JWTKit/JWT.swift index 0816c9a..78879cd 100644 --- a/Sources/JWTKit/JWT.swift +++ b/Sources/JWTKit/JWT.swift @@ -28,7 +28,7 @@ public struct JWT<Payload> where Payload: JWTPayload { { let message = message.copyBytes().split(separator: .period) guard message.count == 3 else { - throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") + throw JWTError.malformedToken } let encodedHeader = message[0] @@ -66,7 +66,7 @@ public struct JWT<Payload> where Payload: JWTPayload { { let components = try JWT<Payload>.parse(message: message) guard try signer.algorithm.verify(components.signature, signs: components.message) else { - throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") + throw JWTError.signatureVerifictionFailed } self.header = components.header self.payload = components.payload @@ -78,13 +78,13 @@ public struct JWT<Payload> where Payload: JWTPayload { { let components = try JWT<Payload>.parse(message: message) guard let kid = components.header.kid else { - throw JWTError(identifier: "missingKID", reason: "JWT is missing kid field in header") + throw JWTError.missingKIDHeader } guard let signer = signers.signer(kid: kid) else { - throw JWTError(identifier: "unknownKID", reason: "No signers are available for the supplied kid") + throw JWTError.unknownKID(kid) } guard try signer.algorithm.verify(components.signature, signs: components.message) else { - throw JWTError(identifier: "invalidSignature", reason: "Invalid JWT signature") + throw JWTError.signatureVerifictionFailed } self.header = components.header self.payload = components.payload diff --git a/Sources/JWTKit/JWTClaims.swift b/Sources/JWTKit/JWTClaims.swift index a5e7fb0..1c08243 100644 --- a/Sources/JWTKit/JWTClaims.swift +++ b/Sources/JWTKit/JWTClaims.swift @@ -100,9 +100,11 @@ public struct ExpirationClaim: JWTUnixEpochClaim, Equatable { /// Throws an error if the claim's date is later than current date. public func verifyNotExpired(currentDate: Date = .init()) throws { - switch value.compare(currentDate) { - case .orderedAscending, .orderedSame: throw JWTError(identifier: "exp", reason: "Expiration claim failed") - case .orderedDescending: break + switch self.value.compare(currentDate) { + case .orderedAscending, .orderedSame: + throw JWTError.claimVerificationFailure(name: "exp", reason: "expired") + case .orderedDescending: + break } } } @@ -126,8 +128,10 @@ public struct NotBeforeClaim: JWTUnixEpochClaim, Equatable { /// Throws an error if the claim's date is earlier than current date. public func verifyNotBefore(currentDate: Date = .init()) throws { switch value.compare(currentDate) { - case .orderedDescending: throw JWTError(identifier: "nbf", reason: "Not before claim failed") - case .orderedAscending, .orderedSame: break + case .orderedDescending: + throw JWTError.claimVerificationFailure(name: "nbf", reason: "too soon") + case .orderedAscending, .orderedSame: + break } } } diff --git a/Sources/JWTKit/JWTError.swift b/Sources/JWTKit/JWTError.swift index 868ceb1..65a7187 100644 --- a/Sources/JWTKit/JWTError.swift +++ b/Sources/JWTKit/JWTError.swift @@ -1,23 +1,34 @@ import Foundation +public enum JWTError: Error, CustomStringConvertible, LocalizedError { + case claimVerificationFailure(name: String, reason: String) + case signingAlgorithmFailure(Error) + case malformedToken + case signatureVerifictionFailed + case missingKIDHeader + case unknownKID(String) -/// Errors that can be thrown while working with JWT. -public struct JWTError: Error, LocalizedError { - /// See `Debuggable`. - public static var readableName = "JWT Error" - - /// See `Debuggable`. - public var reason: String + public var reason: String { + switch self { + case .claimVerificationFailure(let name, let reason): + return "\(name) claim verification failed: \(reason)" + case .signingAlgorithmFailure(let error): + return "signing algorithm error: \(error)" + case .malformedToken: + return "malformed JWT" + case .signatureVerifictionFailed: + return "signature verification failed" + case .missingKIDHeader: + return "missing kid field in header" + case .unknownKID(let kid): + return "unknown kid: \(kid)" + } + } - /// See `Debuggable`. - public var identifier: String + public var description: String { + return "JWTKit error: \(self.reason)" + } public var errorDescription: String? { - return self.reason - } - - /// Create a new `JWTError`. - public init(identifier: String, reason: String) { - self.identifier = identifier - self.reason = reason + return self.description } } diff --git a/Sources/JWTKit/JWTSigner+ECDSA.swift b/Sources/JWTKit/JWTSigner+ECDSA.swift index d923140..85a7074 100644 --- a/Sources/JWTKit/JWTSigner+ECDSA.swift +++ b/Sources/JWTKit/JWTSigner+ECDSA.swift @@ -29,29 +29,29 @@ extension JWTSigner { } public final class ECDSAKey: OpenSSLKey { - public static func generate() -> ECDSAKey { + public static func generate() throws -> ECDSAKey { guard let c = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1) else { - fatalError("EC_KEY_new_by_curve_name failed") + throw JWTError.signingAlgorithmFailure(ECDSAError.newKeyByCurveFailure) } guard EC_KEY_generate_key(c) != 0 else { - fatalError("EC_KEY_generate_key failed") + throw JWTError.signingAlgorithmFailure(ECDSAError.generateKeyFailure) } return .init(c) } - public static func `public`<Data>(pem data: Data) -> ECDSAKey + public static func `public`<Data>(pem data: Data) throws -> ECDSAKey where Data: DataProtocol { - let c = self.load(pem: data) { bio in + let c = try self.load(pem: data) { bio in PEM_read_bio_EC_PUBKEY(convert(bio), nil, nil, nil) } return self.init(c) } - public static func `private`<Data>(pem data: Data) -> ECDSAKey + public static func `private`<Data>(pem data: Data) throws -> ECDSAKey where Data: DataProtocol { - let c = self.load(pem: data) { bio in + let c = try self.load(pem: data) { bio in PEM_read_bio_ECPrivateKey(convert(bio), nil, nil, nil) } return self.init(c) @@ -70,6 +70,12 @@ public final class ECDSAKey: OpenSSLKey { // MARK: Private +private enum ECDSAError: Error { + case newKeyByCurveFailure + case generateKeyFailure + case signFailure +} + private struct ECDSASigner: JWTAlgorithm, OpenSSLSigner { let key: ECDSAKey let algorithm: OpaquePointer @@ -84,7 +90,7 @@ private struct ECDSASigner: JWTAlgorithm, OpenSSLSigner { count: Int(ECDSA_size(self.key.c)) ) - let digest = self.digest(plaintext) + let digest = try self.digest(plaintext) guard ECDSA_sign( 0, digest, @@ -93,7 +99,7 @@ private struct ECDSASigner: JWTAlgorithm, OpenSSLSigner { &signatureLength, self.key.c ) == 1 else { - fatalError("ECDSA sign failed") + throw JWTError.signingAlgorithmFailure(ECDSAError.signFailure) } return .init(signature[0..<numericCast(signatureLength)]) @@ -105,7 +111,7 @@ private struct ECDSASigner: JWTAlgorithm, OpenSSLSigner { ) throws -> Bool where Signature: DataProtocol, Plaintext: DataProtocol { - let digest = self.digest(plaintext) + let digest = try self.digest(plaintext) let signature = signature.copyBytes() return ECDSA_verify( 0, diff --git a/Sources/JWTKit/JWTSigner+HMAC.swift b/Sources/JWTKit/JWTSigner+HMAC.swift index 7bd9714..ab34a90 100644 --- a/Sources/JWTKit/JWTSigner+HMAC.swift +++ b/Sources/JWTKit/JWTSigner+HMAC.swift @@ -36,6 +36,12 @@ extension JWTSigner { // MARK: Private +private enum HMACError: Error { + case initializationFailure + case updateFailure + case finalizationFailure +} + private struct HMACSigner: JWTAlgorithm { let key: [UInt8] let algorithm: OpaquePointer @@ -50,13 +56,13 @@ private struct HMACSigner: JWTAlgorithm { guard self.key.withUnsafeBytes({ return HMAC_Init_ex(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32($0.count), convert(self.algorithm), nil) }) == 1 else { - fatalError("Failed initializing HMAC context") + throw JWTError.signingAlgorithmFailure(HMACError.initializationFailure) } guard plaintext.copyBytes().withUnsafeBytes({ return HMAC_Update(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) }) == 1 else { - fatalError("Failed updating HMAC digest") + throw JWTError.signingAlgorithmFailure(HMACError.updateFailure) } var hash = [UInt8](repeating: 0, count: Int(EVP_MAX_MD_SIZE)) var count: UInt32 = 0 @@ -64,7 +70,7 @@ private struct HMACSigner: JWTAlgorithm { guard hash.withUnsafeMutableBytes({ return HMAC_Final(context, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), &count) }) == 1 else { - fatalError("Failed finalizing HMAC digest") + throw JWTError.signingAlgorithmFailure(HMACError.finalizationFailure) } return .init(hash[0..<Int(count)]) } diff --git a/Sources/JWTKit/JWTSigner+RSA.swift b/Sources/JWTKit/JWTSigner+RSA.swift index 217c6bd..4e2db3c 100644 --- a/Sources/JWTKit/JWTSigner+RSA.swift +++ b/Sources/JWTKit/JWTSigner+RSA.swift @@ -30,30 +30,30 @@ extension JWTSigner { } public final class RSAKey: OpenSSLKey { - public static func `public`<Data>(pem data: Data) -> RSAKey + public static func `public`<Data>(pem data: Data) throws -> RSAKey where Data: DataProtocol { - let pkey = self.load(pem: data) { bio in + let pkey = try self.load(pem: data) { bio in PEM_read_bio_PUBKEY(convert(bio), nil, nil, nil) } defer { EVP_PKEY_free(pkey) } guard let c = EVP_PKEY_get1_RSA(pkey) else { - fatalError("RSA key creation failed") + throw JWTError.signingAlgorithmFailure(RSAError.keyInitializationFailure) } return self.init(convert(c), .public) } - public static func `private`<Data>(pem data: Data) -> RSAKey + public static func `private`<Data>(pem data: Data) throws -> RSAKey where Data: DataProtocol { - let pkey = self.load(pem: data) { bio in + let pkey = try self.load(pem: data) { bio in PEM_read_bio_PrivateKey(convert(bio), nil, nil, nil) } defer { EVP_PKEY_free(pkey) } guard let c = EVP_PKEY_get1_RSA(pkey) else { - fatalError("RSA key creation failed") + throw JWTError.signingAlgorithmFailure(RSAError.keyInitializationFailure) } return self.init(convert(c), .private) } @@ -77,6 +77,12 @@ public final class RSAKey: OpenSSLKey { // MARK: Private +private enum RSAError: Error { + case privateKeyRequired + case signFailure + case keyInitializationFailure +} + private struct RSASigner: JWTAlgorithm, OpenSSLSigner { let key: RSAKey let algorithm: OpaquePointer @@ -86,7 +92,7 @@ private struct RSASigner: JWTAlgorithm, OpenSSLSigner { where Plaintext: DataProtocol { guard case .private = self.key.type else { - throw JWTError(identifier: "rsa", reason: "Private key required to sign") + throw JWTError.signingAlgorithmFailure(RSAError.privateKeyRequired) } var signatureLength: UInt32 = 0 var signature = [UInt8]( @@ -94,7 +100,7 @@ private struct RSASigner: JWTAlgorithm, OpenSSLSigner { count: Int(RSA_size(convert(key.c))) ) - let digest = self.digest(plaintext) + let digest = try self.digest(plaintext) guard RSA_sign( EVP_MD_type(convert(self.algorithm)), digest, @@ -103,7 +109,7 @@ private struct RSASigner: JWTAlgorithm, OpenSSLSigner { &signatureLength, convert(self.key.c) ) == 1 else { - throw JWTError(identifier: "rsaSign", reason: "RSA signature creation failed") + throw JWTError.signingAlgorithmFailure(RSAError.signFailure) } return .init(signature[0..<numericCast(signatureLength)]) @@ -115,7 +121,7 @@ private struct RSASigner: JWTAlgorithm, OpenSSLSigner { ) throws -> Bool where Signature: DataProtocol, Plaintext: DataProtocol { - let digest = self.digest(plaintext) + let digest = try self.digest(plaintext) let signature = signature.copyBytes() return RSA_verify( EVP_MD_type(convert(self.algorithm)), diff --git a/Sources/JWTKit/OpenSSLSigner.swift b/Sources/JWTKit/OpenSSLSigner.swift index b57446f..453d563 100644 --- a/Sources/JWTKit/OpenSSLSigner.swift +++ b/Sources/JWTKit/OpenSSLSigner.swift @@ -4,25 +4,33 @@ protocol OpenSSLSigner { var algorithm: OpaquePointer { get } } +private enum OpenSSLError: Error { + case digestInitializationFailure + case digestUpdateFailure + case digestFinalizationFailure + case bioPutsFailure + case bioConversionFailure +} + extension OpenSSLSigner { - func digest<Plaintext>(_ plaintext: Plaintext) -> [UInt8] + func digest<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] where Plaintext: DataProtocol { let context = EVP_MD_CTX_new() defer { EVP_MD_CTX_free(context) } guard EVP_DigestInit_ex(context, convert(self.algorithm), nil) == 1 else { - fatalError("Failed initializing digest context") + throw JWTError.signingAlgorithmFailure(OpenSSLError.digestInitializationFailure) } let plaintext = plaintext.copyBytes() guard EVP_DigestUpdate(context, plaintext, plaintext.count) == 1 else { - fatalError("Failed updating digest") + throw JWTError.signingAlgorithmFailure(OpenSSLError.digestUpdateFailure) } var digest: [UInt8] = .init(repeating: 0, count: Int(EVP_MAX_MD_SIZE)) var digestLength: UInt32 = 0 guard EVP_DigestFinal_ex(context, &digest, &digestLength) == 1 else { - fatalError("Failed finalizing digest") + throw JWTError.signingAlgorithmFailure(OpenSSLError.digestFinalizationFailure) } return .init(digest[0..<Int(digestLength)]) } @@ -31,7 +39,7 @@ extension OpenSSLSigner { protocol OpenSSLKey { } extension OpenSSLKey { - static func load<Data, T>(pem data: Data, _ closure: (OpaquePointer) -> (T?)) -> T + static func load<Data, T>(pem data: Data, _ closure: (OpaquePointer) -> (T?)) throws -> T where Data: DataProtocol { let bio = BIO_new(BIO_s_mem()) @@ -40,11 +48,11 @@ extension OpenSSLKey { guard (data.copyBytes() + [0]).withUnsafeBytes({ pointer in BIO_puts(bio, pointer.baseAddress?.assumingMemoryBound(to: Int8.self)) }) >= 0 else { - fatalError("BIO puts failed") + throw JWTError.signingAlgorithmFailure(OpenSSLError.bioPutsFailure) } guard let c = closure(convert(bio!)) else { - fatalError("PEM_read_bio_EC_PUBKEY failed") + throw JWTError.signingAlgorithmFailure(OpenSSLError.bioConversionFailure) } return c } diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index d3a3e1c..599a851 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -17,7 +17,12 @@ class JWTKitTests: XCTestCase { do { let _ = try JWT<TestPayload>(from: data.bytes, verifiedBy: .hs256(key: "secret".bytes)) } catch let error as JWTError { - XCTAssertEqual(error.identifier, "exp") + switch error { + case .claimVerificationFailure(let name, _): + XCTAssertEqual(name, "exp") + default: + XCTFail("wrong error") + } } } @@ -52,8 +57,8 @@ class JWTKitTests: XCTestCase { } func testRSA() throws { - let privateSigner = JWTSigner.rs256(key: .private(pem: rsaPrivateKey.bytes)) - let publicSigner = JWTSigner.rs256(key: .public(pem: rsaPublicKey.bytes)) + let privateSigner = try JWTSigner.rs256(key: .private(pem: rsaPrivateKey.bytes)) + let publicSigner = try JWTSigner.rs256(key: .public(pem: rsaPublicKey.bytes)) let payload = TestPayload( sub: "vapor", @@ -68,7 +73,7 @@ class JWTKitTests: XCTestCase { } func testRSASignWithPublic() throws { - let publicSigner = JWTSigner.rs256(key: .public(pem: rsaPublicKey.bytes)) + let publicSigner = try JWTSigner.rs256(key: .public(pem: rsaPublicKey.bytes)) let payload = TestPayload( sub: "vapor", name: "Foo", @@ -92,14 +97,14 @@ class JWTKitTests: XCTestCase { exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) ) let jwt = JWT(payload: payload) - let signer = JWTSigner.es256(key: .generate()) + let signer = try JWTSigner.es256(key: .generate()) let data = try jwt.sign(using: signer) try XCTAssertEqual(JWT<TestPayload>(from: data, verifiedBy: signer).payload, payload) } func testECDSAPublicPrivate() throws { - let publicSigner = JWTSigner.es256(key: .public(pem: ecdsaPublicKey.bytes)) - let privateSigner = JWTSigner.es256(key: .private(pem: ecdsaPrivateKey.bytes)) + let publicSigner = try JWTSigner.es256(key: .public(pem: ecdsaPublicKey.bytes)) + let privateSigner = try JWTSigner.es256(key: .private(pem: ecdsaPrivateKey.bytes)) let payload = TestPayload( sub: "vapor", From 1744b94cf9f26fa9787da8d602ae83053932363b Mon Sep 17 00:00:00 2001 From: Tanner Nelson <me@tanner.xyz> Date: Thu, 13 Jun 2019 18:22:08 -0400 Subject: [PATCH 12/12] update readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ac520c6..af4a9e5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ <p align="center"> - <img src="https://user-images.githubusercontent.com/1342803/36768561-e10bba90-1c0d-11e8-85b5-f9d1e605cc8a.png" height="64" alt="JWT"> + <img src="https://user-images.githubusercontent.com/1342803/59471117-1c77b300-8e08-11e9-838e-441b280855b3.png" alt="JWTKit"> <br> <br> - <a href="https://docs.vapor.codes/3.0/jwt/getting-started/"> - <img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation"> + <a href="https://api.vapor.codes/jwt-kit/master/JWTKit/index.html"> + <img src="http://img.shields.io/badge/api-docs-2196f3.svg" alt="Documentation"> </a> <a href="https://discord.gg/vapor"> <img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat"> @@ -11,11 +11,11 @@ <a href="LICENSE"> <img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License"> </a> - <a href="https://circleci.com/gh/vapor/jwt"> - <img src="https://circleci.com/gh/vapor/jwt.svg?style=shield" alt="Continuous Integration"> + <a href="https://circleci.com/gh/vapor/jwt-kit"> + <img src="https://circleci.com/gh/vapor/jwt-kit.svg?style=shield" alt="Continuous Integration"> </a> <a href="https://swift.org"> - <img src="http://img.shields.io/badge/swift-4.1-brightgreen.svg" alt="Swift 4.1"> + <img src="http://img.shields.io/badge/swift-5.0-brightgreen.svg" alt="Swift 5.0"> </a> </p>