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/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 ... /// } 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) ])