Skip to content

Commit

Permalink
feat: JWK/SPKI thumbprint digest calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
amosavian committed Apr 18, 2024
1 parent 5c8fe54 commit 71d9d05
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 19 deletions.
7 changes: 7 additions & 0 deletions Sources/JWSETKit/Base/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
return result
}

public func filter(_ isIncluded: (String) throws -> Bool) rethrows -> JSONWebValueStorage {
let storage = try self.storage.filter { try isIncluded($0.key) }
var result = JSONWebValueStorage()
result.storage = storage
return result
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

import Foundation
import X509
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

/// JSON Web Key (JWK) container for X509 Certificate chain.
///
Expand Down Expand Up @@ -38,6 +43,10 @@ public struct JSONWebCertificateChain: MutableJSONWebKey, JSONWebValidatingKey,
public func verifySignature<S, D>(_ signature: S, for data: D, using algorithm: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol {
try leaf.verifySignature(signature, for: data, using: algorithm)
}

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

extension JSONWebCertificateChain: Expirable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ extension SecCertificate: JSONWebValidatingKey {
return key
}
}

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

extension SecCertificate: Expirable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ extension Certificate.PublicKey: JSONWebValidatingKey {
#endif
throw JSONWebKeyError.unknownKeyType
}

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

extension DERImplicitlyTaggable {
Expand Down
84 changes: 69 additions & 15 deletions Sources/JWSETKit/Cryptography/Keys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ public protocol JSONWebKey: Codable, Hashable {

/// Validates contents and required fields if applicable.
func validate() throws

/// Creates a thumbprint of current key.
///
/// Valid formats for public keys are `spki` and `jwk`. SPKI thumbprints are used in SSL-pinning.
///
/// While it is possible to create a thumbprint for private keys, it is typically not useful to do so,
/// as the thumbprint is a cryptographic hash of the key, and the private key contains all the information
/// needed to compute the thumbprint. It is possible by passing `pkcs8` or `jwk` as format.
///
/// - Important: A hash of a symmetric key has the potential to leak information about
/// the key value. Thus, the JWK Thumbprint of a symmetric key should
/// typically be concealed from parties not in possession of the
/// symmetric key, unless in the application context, the cryptographic
/// hash used, such as SHA-256, is known to provide sufficient protection
/// against disclosure of the key value.
///
/// - Parameter format: Format of key that thumbprint will be calculated from.
/// - Parameter hashFunction: Algorithm of thumbprint hashing.
///
/// - Returns: A new instance of thumbprint digest.
func thumbprint<H>(format: JSONWebKeyFormat, using hashFunction: H.Type) throws -> H.Digest where H: HashFunction

}

@_documentation(visibility: private)
Expand Down Expand Up @@ -126,6 +148,53 @@ extension JSONWebKey {
}
}
}

func checkRequiredFields<T>(_ fields: [KeyPath<Self, T?>]) throws {
for field in fields {
if self[keyPath: field] == nil {
throw JSONWebKeyError.keyNotFound
}
}
}

func jwkThumbprint<H>(using hashFunction: H.Type) throws -> H.Digest where H : HashFunction {
let thumbprintKeys: Set<String> = [
// Algorithm-specific keys
"kty", "crv",
// RSA keys
"n", "e", "d", "p", "q", "dp", "dq", "qi",
// EC/OKP keys
"x", "y", "d",
// Symmetric keys
"k"
]
let thumbprintStorage = storage.filter(thumbprintKeys.contains)
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
let data = try encoder.encode(thumbprintStorage)
return H.hash(data: data)
}

public func thumbprint<H>(format: JSONWebKeyFormat, using hashFunction: H.Type) throws -> H.Digest where H : HashFunction {
switch format {
case .spki:
guard let self = self 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 {
throw JSONWebKeyError.operationNotAllowed
}
let spki = try self.exportKey(format: .pkcs8)
return H.hash(data: spki)
case .jwk:
return try jwkThumbprint(using: hashFunction)
case .raw:
throw JSONWebKeyError.operationNotAllowed
}
}
}

/// A JSON Web Key (JWK) able to encrypt plain-texts.
Expand Down Expand Up @@ -331,21 +400,6 @@ public struct AnyJSONWebKey: MutableJSONWebKey {
}

extension AnyJSONWebKey: JSONWebKeyImportable, JSONWebKeyExportable {
private init(importing key: Data, format: JSONWebKeyFormat, keyType: JSONWebKeyType) throws {
switch (keyType, format) {
case (.ellipticCurve, .spki):
self.storage = try JSONWebECPublicKey(importing: key, format: format).storage
case (.rsa, .spki):
self.storage = try JSONWebRSAPublicKey(importing: key, format: format).storage
case (.ellipticCurve, .pkcs8):
self.storage = try JSONWebECPrivateKey(importing: key, format: format).storage
case (.rsa, .pkcs8):
self.storage = try JSONWebRSAPrivateKey(importing: key, format: format).storage
default:
throw JSONWebKeyError.invalidKeyFormat
}
}

public init(importing key: Data, format: JSONWebKeyFormat) throws {
if format == .jwk {
let key = try JSONDecoder().decode(AnyJSONWebKey.self, from: key).specialized()
Expand Down
4 changes: 2 additions & 2 deletions Sources/JWSETKit/Cryptography/Symmetric/CommonCrypto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ extension CCPseudoRandomAlgorithm {
}
}

private extension Int32 {
var cryptoKitError: CryptoKitError? {
extension Int32 {
fileprivate var cryptoKitError: CryptoKitError? {
switch Int(self) {
case kCCSuccess:
return nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/JWSETKit/Extensions/KeyLookup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension JSONWebContainer {
public func stringKey<P: JSONWebContainerParameters<Self>, T>(_ keyPath: KeyPath<P, T>, force: Bool = false, locale: Locale? = nil) -> String {
let key = P.keys[keyPath] ?? keyPath.name.jsonWebKey
guard P.localizableKeys.contains(keyPath), let locale else { return key }
if force == true {
if force {
return "\(key)#\(locale.bcp47)"
} else {
let locales = storage.storageKeys
Expand All @@ -37,7 +37,7 @@ extension JSONWebContainer {
}
}

extension KeyPath {
extension AnyKeyPath {
var name: String {
#if canImport(Darwin)
// `components` never returns empty array.
Expand Down
43 changes: 43 additions & 0 deletions Tests/JWSETKitTests/Cryptography/ThumbprintTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// File.swift
//
//
// Created by Amir Abbas Mousavian on 4/18/24.
//

import XCTest
@testable import JWSETKit
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

final class ThumbprintTests: XCTestCase {
let keyData: Data = .init("""
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt\
VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6\
4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD\
W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9\
1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH\
aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB",
"alg": "RS256",
"kid": "2011-04-29"
}
""".utf8)

func testJWKThumbprint() throws {
let key = try JSONWebRSAPublicKey(importing: keyData, format: .jwk)
let thumbprint = try key.thumbprint(format: .jwk, using: SHA256.self)
XCTAssertEqual(thumbprint.data, Data(urlBase64Encoded: "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"))
}

func testSPKIThumbprint() throws {
let key = try JSONWebRSAPublicKey(importing: keyData, format: .jwk)
let thumbprint = try key.thumbprint(format: .spki, using: SHA256.self)
XCTAssertEqual(thumbprint.data, Data(urlBase64Encoded: "HDoH_pBCw1_TM0QPO5q74tZfDFsYFTyw4pknhCU2HP8"))
}
}

0 comments on commit 71d9d05

Please sign in to comment.