Skip to content

Commit

Permalink
feat: Find matching key in JWKSet, added combined JWS header
Browse files Browse the repository at this point in the history
fix: Invalid JWK thumbprint of private keys
chore: Accept JSONWebKeySet where array of keys is argument
!chore: b64 type became non-optional
  • Loading branch information
amosavian committed Jan 19, 2025
1 parent 4cd3215 commit 246a3d1
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 69 deletions.
10 changes: 0 additions & 10 deletions Sources/JWSETKit/Base/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,6 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
}
}

/// Returns value of given key.
public subscript(_ member: String) -> Bool {
get {
get(key: member, as: Bool.self) ?? false
}
set {
updateValue(key: member, value: newValue)
}
}

/// Returns value of given key decoded using base64.
public subscript(_ member: String, urlEncoded: Bool = true) -> Data? {
get {
Expand Down
7 changes: 4 additions & 3 deletions Sources/JWSETKit/Cryptography/Algorithms/Algorithms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ extension JSONWebAlgorithm {
return AnyJSONWebAlgorithm(rawValue)
}

/// Returns value with appropriate algorithm type.
func specialized() -> any JSONWebAlgorithm {
Self.specialized(rawValue)
}
Expand All @@ -90,13 +91,13 @@ extension JSONWebAlgorithm {
@frozen
public struct AnyJSONWebAlgorithm: JSONWebAlgorithm {
public let rawValue: String

public var keyType: JSONWebKeyType? {
Self.specialized(rawValue).keyType
specialized().keyType
}

public var curve: JSONWebKeyCurve? {
Self.specialized(rawValue).curve
specialized().curve
}

var keyLength: Int? {
Expand Down
4 changes: 4 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/CryptoKitAbstract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ extension CryptoECPrivateKey {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.publicKey == rhs.publicKey
}

public func thumbprint<H>(format: JSONWebKeyFormat, using hashFunction: H.Type) throws -> H.Digest where H: HashFunction {
try publicKey.thumbprint(format: format, using: hashFunction)
}
}

protocol CryptoECKeyPortable: JSONWebKeyImportable, JSONWebKeyExportable {
Expand Down
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Cryptography/EC/P256.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension P256.KeyAgreement.PrivateKey: CryptoECKeyPortable {}
#if canImport(Darwin)
extension Crypto.SecureEnclave.P256.Signing.PrivateKey: Swift.Hashable, Swift.Codable {}

extension SecureEnclave.P256.Signing.PrivateKey: CryptoECPrivateKey {
extension SecureEnclave.P256.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
public var storage: JSONWebValueStorage {
// Keys stored in SecureEnclave are not exportable.
//
Expand Down
26 changes: 0 additions & 26 deletions Sources/JWSETKit/Cryptography/KeyParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,29 +211,3 @@ extension AnyJSONWebKey {
specializers.insert(specializer, at: 0)
}
}

extension [any JSONWebKey] {
func bestMatch(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
guard let keyType = algorithm.keyType else { return nil }
let candidates = filter {
$0.keyType == keyType && $0.curve == algorithm.curve
}
if let key = candidates.first(where: { $0.keyId == id }) {
return key
} else {
return candidates.first
}
}
}

extension [any JSONWebSigningKey] {
func bestMatch(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
(self as [any JSONWebKey]).bestMatch(for: algorithm, id: id) as? Self.Element
}
}

extension [any JSONWebValidatingKey] {
func bestMatch(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
(self as [any JSONWebKey]).bestMatch(for: algorithm, id: id) as? Self.Element
}
}
76 changes: 69 additions & 7 deletions Sources/JWSETKit/Cryptography/Keys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,15 @@ extension JSONWebKey {
}
}

func jwkThumbprint<H>(using _: H.Type) throws -> H.Digest where H: HashFunction {
private func jwkThumbprint<H>(using _: H.Type) throws -> H.Digest where H: HashFunction {
// Public key required values.
let thumbprintKeys: Set<String> = [
// Algorithm-specific keys
"kty", "crv",
// RSA keys
"n", "e", "d", "p", "q", "dp", "dq", "qi",
"n", "e",
// EC/OKP keys
"x", "y", "d",
"x", "y",
// Symmetric keys
"k",
]
Expand All @@ -205,15 +206,24 @@ extension JSONWebKey {
}

public func thumbprint<H>(format: JSONWebKeyFormat, using hashFunction: H.Type) throws -> H.Digest where H: HashFunction {
let key: any JSONWebKey
switch self {
case let self as any JSONWebSigningKey:
key = self.publicKey
case let self as any JSONWebDecryptingKey:
key = self.publicKey
default:
key = self
}
switch format {
case .spki:
guard let self = self as? (any JSONWebKeyExportable) else {
guard let self = key as? (any JSONWebKeyExportable) else {
throw JSONWebKeyError.operationNotAllowed
}
let spki = try self.exportKey(format: .spki)
return H.hash(data: spki)
case .pkcs8:
guard let self = self as? (any JSONWebKeyExportable) else {
guard let self = key as? (any JSONWebKeyExportable) else {
throw JSONWebKeyError.operationNotAllowed
}
let spki = try self.exportKey(format: .pkcs8)
Expand Down Expand Up @@ -517,6 +527,20 @@ public struct JSONWebKeySet: Codable, Hashable {
self.keys = keys
}

/// Initializes JWKSet using given array of key.
///
/// - Parameter keys: An array of JWKs.
public init<T>(keys: T) where T: Sequence, T.Element == any JSONWebKey {
self.keys = .init(keys)
}

/// Initializes JWKSet using given array of key.
///
/// - Parameter keys: An array of JWKs.
public init<T>(keys: T) where T: Sequence, T.Element: JSONWebKey {
self.keys = .init(keys.map { $0 as any JSONWebKey })
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let keys = try container.decode([AnyJSONWebKey].self, forKey: .keys)
Expand All @@ -525,7 +549,8 @@ public struct JSONWebKeySet: Codable, Hashable {

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(keys.map(\.storage), forKey: .keys)
var nested = container.nestedUnkeyedContainer(forKey: .keys)
try keys.forEach { try nested.encode($0) }
}

public static func == (lhs: JSONWebKeySet, rhs: JSONWebKeySet) -> Bool {
Expand All @@ -535,6 +560,25 @@ public struct JSONWebKeySet: Codable, Hashable {
public func hash(into hasher: inout Hasher) {
keys.forEach { hasher.combine($0) }
}

public func match(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
guard let keyType = algorithm.keyType else { return nil }
let candidates = filter {
$0.keyType == keyType && $0.curve == algorithm.curve
}
if let key = candidates.first(where: { $0.keyId == id }) {
return key
} else {
return candidates.first
}
}

public func matches(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> [Self.Element] {
guard let keyType = algorithm.keyType else { return [] }
return filter {
$0.keyType == keyType && $0.curve == algorithm.curve && (id == nil || $0.keyId == id)
}
}
}

extension JSONWebKeySet: RandomAccessCollection {
Expand Down Expand Up @@ -563,6 +607,24 @@ extension JSONWebKeySet: RandomAccessCollection {
}

public subscript(bounds: Range<Int>) -> JSONWebKeySet {
.init(keys: Array(keys[bounds]))
.init(keys: keys[bounds])
}
}

extension [any JSONWebKey] {
func match(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
JSONWebKeySet(keys: self).match(for: algorithm, id: id)
}
}

extension [any JSONWebSigningKey] {
func match(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
JSONWebKeySet(keys: self).match(for: algorithm, id: id) as? Self.Element
}
}

extension [any JSONWebValidatingKey] {
func match(for algorithm: some JSONWebAlgorithm, id: String? = nil) -> Self.Element? {
JSONWebKeySet(keys: self).match(for: algorithm, id: id) as? Self.Element
}
}
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Entities/JOSE/JOSE-JWERegistered.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
#endif
import Crypto

/// Registered parameters of JOSE header in [RFC 7516](https://www.rfc-editor.org/rfc/rfc7516.html).
/// Registered parameters of JOSE header in [RFC 7516](https://www.rfc-editor.org/rfc/rfc7516.html ).
public struct JoseHeaderJWERegisteredParameters: JSONWebContainerParameters {
public typealias Container = JOSEHeader
/// The "`enc`" (encryption algorithm) Header Parameter identifies
Expand Down
19 changes: 17 additions & 2 deletions Sources/JWSETKit/Entities/JOSE/JOSE-JWSRegistered.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
import Crypto
import X509

/// Registered parameters of JOSE header in [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515.html).
/// Registered parameters of JOSE header in [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515.html ) .
public struct JoseHeaderJWSRegisteredParameters: JSONWebContainerParameters {
public typealias Container = JOSEHeader
/// The "`alg`" (algorithm) Header Parameter identifies the cryptographic algorithm used to secure the JWS.
Expand Down Expand Up @@ -157,7 +157,7 @@ public struct JoseHeaderJWSRegisteredParameters: JSONWebContainerParameters {
/// The "`b64`" header parameter specifies the payload is encoded with `Base64URL` or not.
///
/// The value is `true` if not present
public var base64: Bool?
public var base64: Bool

@_documentation(visibility: private)
public static let keys: [PartialKeyPath<Self>: String] = [
Expand All @@ -180,6 +180,21 @@ extension JOSEHeader {
}
}

@_documentation(visibility: private)
public subscript(dynamicMember keyPath: KeyPath<JoseHeaderJWSRegisteredParameters, Bool>) -> Bool {
get {
switch keyPath {
case \.base64:
storage[stringKey(keyPath)] ?? true
default:
storage[stringKey(keyPath)] ?? false
}
}
set {
storage[stringKey(keyPath)] = newValue
}
}

@_documentation(visibility: private)
public subscript(dynamicMember keyPath: KeyPath<JoseHeaderJWSRegisteredParameters, (any JSONWebKey)?>) -> (any JSONWebKey)? {
get {
Expand Down
5 changes: 3 additions & 2 deletions Sources/JWSETKit/Entities/JOSE/JOSEHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ public struct JOSEHeader: JSONWebContainer {
}

public func merging(_ other: JOSEHeader, uniquingKeysWith combine: (JSONWebValueStorage.Value, JSONWebValueStorage.Value) throws -> JSONWebValueStorage.Value) rethrows -> JOSEHeader {
guard !other.storage.storageKeys.isEmpty else { return self }
let storage = try storage.merging(other.storage, uniquingKeysWith: combine)
return .init(storage: storage)
}

private var normalizedStorage: JSONWebValueStorage {
var result = storage
result["typ"] = result["typ"].map(JSONWebContentType.init(rawValue:))?.mimeType
result["cty"] = result["cty"].map(JSONWebContentType.init(rawValue:))?.mimeType
result["typ"] = result["typ"].map(JSONWebContentType.init(rawValue:))
result["cty"] = result["cty"].map(JSONWebContentType.init(rawValue:))
return result
}

Expand Down
18 changes: 18 additions & 0 deletions Sources/JWSETKit/Entities/JWE/JWE.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ public struct JSONWebEncryption: Hashable, Sendable {
}
}

/// Decrypts encrypted data, using given private key.
///
/// - Important: For `PBES2` algorithms, provide password using
/// `SymmetricKey(data: Data(password.utf8))` to`key`.
///
/// - Parameter keys: An array of keys that used to encrypt the content encryption key.
/// - Returns: Decrypted payload.
public func decrypt(using keys: [any JSONWebKey], keyId: String? = nil) throws -> Data {
for key in keys {
guard (try? recipients.match(for: key, keyId: keyId)) != nil else {
Expand All @@ -313,6 +320,17 @@ public struct JSONWebEncryption: Hashable, Sendable {
}
throw JSONWebKeyError.keyNotFound
}

/// Decrypts encrypted data, using given private key.
///
/// - Important: For `PBES2` algorithms, provide password using
/// `SymmetricKey(data: Data(password.utf8))` to`key`.
///
/// - Parameter keySet: An array of keys that used to encrypt the content encryption key.
/// - Returns: Decrypted payload.
public func decrypt(using keySet: JSONWebKeySet, keyId: String? = nil) throws -> Data {
try decrypt(using: keySet.keys, keyId: keyId)
}
}

extension String {
Expand Down
15 changes: 13 additions & 2 deletions Sources/JWSETKit/Entities/JWS/JWS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public struct JSONWebSignature<Payload: ProtectedWebContainer>: Hashable, Sendab
/// Each object represents a signature or MAC over the JWS Payload and the JWS Protected Header.
public var signatures: [JSONWebSignatureHeader]

/// Combination of protected header and unprotected header.
///
/// - Note: If a key exists in both protected and unprotected headers, value in protected
/// will be returned.
public var header: JOSEHeader {
guard let signature = signatures.first else {
return .init()
}
return signature.protected.value.merging(signature.unprotected ?? .init(), uniquingKeysWith: { protected, _ in protected })
}

/// The "`payload`" member MUST be present and contain the value of JWS Payload.
public var payload: Payload

Expand Down Expand Up @@ -73,7 +84,7 @@ public struct JSONWebSignature<Payload: ProtectedWebContainer>: Hashable, Sendab
let signature: Data
if algorithm == .none {
signature = .init()
} else if let algorithm, let key = keys.bestMatch(for: algorithm, id: keyId) {
} else if let algorithm, let key = keys.match(for: algorithm, id: keyId) {
signature = try key.signature(message, using: algorithm)
} else {
throw JSONWebKeyError.keyNotFound
Expand Down Expand Up @@ -123,7 +134,7 @@ public struct JSONWebSignature<Payload: ProtectedWebContainer>: Hashable, Sendab
algorithm = JSONWebSignatureAlgorithm(unprotected.algorithm)
}
let keyId: String? = header.protected.keyId ?? header.unprotected?.keyId
if let algorithm, let key = keys.bestMatch(for: algorithm, id: keyId) {
if let algorithm, let key = keys.match(for: algorithm, id: keyId) {
try key.verifySignature(header.signature, for: message, using: algorithm)
return
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Entities/JWS/JWSCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ extension JSONWebSignature: Codable {
case 0:
return .compact
case 1 where signatures[0].unprotected == nil:
if signatures[0].protected.base64 == false {
if signatures[0].protected.base64 {
return .compactDetached
}
return .compact
Expand Down
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Entities/JWT/JWTOIDCStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ extension JSONWebTokenClaims {
@_documentation(visibility: private)
public subscript(dynamicMember keyPath: KeyPath<JSONWebTokenClaimsPublicOIDCStandardParameters, Bool>) -> Bool {
get {
storage[stringKey(keyPath)]
storage[stringKey(keyPath)] ?? false
}
set {
storage[stringKey(keyPath)] = newValue
Expand Down
Loading

0 comments on commit 246a3d1

Please sign in to comment.