Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added JWK and JWKS support #106

Merged
merged 3 commits into from Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions 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
}
}
42 changes: 42 additions & 0 deletions 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)
}
}
}
191 changes: 191 additions & 0 deletions 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.")
}
}
}
32 changes: 32 additions & 0 deletions 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)
}
}
}
2 changes: 1 addition & 1 deletion Sources/JWT/JWTSigner.swift
Expand Up @@ -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 ...
/// }
Expand Down
36 changes: 36 additions & 0 deletions 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<TestPayload>(from: signature, verifiedUsing: publicSigner)
let privateVerified = try JWT<TestPayload>(from: signature, verifiedUsing: privateSigner)

XCTAssertEqual(publicVerified.payload.name, "Foo")
XCTAssertEqual(privateVerified.payload.name, "Foo")
}
}
1 change: 1 addition & 0 deletions Tests/LinuxMain.swift
Expand Up @@ -3,4 +3,5 @@ import XCTest

XCTMain([
testCase(JWTTests.allTests),
testCase(JWKTests.allTests)
])