Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #106 from valeriomazzeo/feature/jwk
Added JWK and JWKS support
- Loading branch information
Showing
7 changed files
with
325 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TestPayload>(from: signature, verifiedUsing: publicSigner) | ||
let privateVerified = try JWT<TestPayload>(from: signature, verifiedUsing: privateSigner) | ||
|
||
XCTAssertEqual(publicVerified.payload.name, "Foo") | ||
XCTAssertEqual(privateVerified.payload.name, "Foo") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ import XCTest | |
|
||
XCTMain([ | ||
testCase(JWTTests.allTests), | ||
testCase(JWKTests.allTests) | ||
]) |