Skip to content

Commit

Permalink
Merge pull request #106 from valeriomazzeo/feature/jwk
Browse files Browse the repository at this point in the history
Added JWK and JWKS support
  • Loading branch information
tanner0101 committed Oct 23, 2019
2 parents 2e225c7 + c4aaa00 commit 0debb7f
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 1 deletion.
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)
])

0 comments on commit 0debb7f

Please sign in to comment.