Skip to content

Commit

Permalink
feat: Add strict parameter to verify signature when algorithm header …
Browse files Browse the repository at this point in the history
…is not protected

fix: JWS encode when protected header is empty
fix: JWS multiple signature
test: RFS7520 signature test scenarios
  • Loading branch information
amosavian committed Apr 29, 2024
1 parent 3fbf686 commit 329f300
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Cryptography/Algorithms/Signature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ extension JSONWebSignatureAlgorithm {
]

private static let keyTypes: ReadWriteLockedValue<[Self: JSONWebKeyType]> = [
.none: .symmetric,
.none: .empty,
.hmacSHA256: .symmetric,
.hmacSHA384: .symmetric,
.hmacSHA512: .symmetric,
Expand Down
6 changes: 0 additions & 6 deletions Sources/JWSETKit/Entities/JOSE/JOSEHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,6 @@ public struct JOSEHeader: JSONWebContainer {
return .init(storage: storage)
}

public func validate() throws {
guard storage.contains(key: "alg") else {
throw JSONWebValidationError.missingRequiredField(key: "alg")
}
}

private var normalizedStorage: JSONWebValueStorage {
var result = storage
result["typ"] = result["typ"].map(JSONWebContentType.init(rawValue:))?.mimeType
Expand Down
32 changes: 16 additions & 16 deletions Sources/JWSETKit/Entities/JWS/JWS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,42 +110,42 @@ public struct JSONWebSignature<Payload: ProtectedWebContainer>: Hashable, Sendab
///
/// - Parameters:
/// - keys: An array of `JSONWebValidatingKey` that would be used for validation.
public func verifySignature(using keys: [any JSONWebValidatingKey]) throws {
/// - strict: If `true` (default), the algorithm in the protected header will be used otherwise algorithm in unprotected header will be allowed.
public func verifySignature(using keys: [any JSONWebValidatingKey], strict: Bool = true) throws {
guard !signatures.isEmpty else {
throw CryptoKitError.authenticationFailure
}
for header in signatures {
let message = header.signedData(payload)
let algorithm = JSONWebSignatureAlgorithm(header.protected.value.algorithm.rawValue)
let keyId: String? = header.protected.value.keyId ?? header.unprotected?.keyId
if algorithm == .none {
// If we allow "none" algorithm in verification, a malicious user may simply
// remove the signature and change the algorithm to "none".
// As this scenario may lead to a critical security vulnaribility, "none"
// is not supported algorithm .
throw JSONWebKeyError.operationNotAllowed
} else if let key = keys.bestMatch(for: algorithm, id: keyId) {
var algorithm = JSONWebSignatureAlgorithm(header.protected.algorithm.rawValue)
if !strict, algorithm == .none, let unprotected = header.unprotected {
algorithm = JSONWebSignatureAlgorithm(unprotected.algorithm.rawValue)
}
let keyId: String? = header.protected.keyId ?? header.unprotected?.keyId
if let key = keys.bestMatch(for: algorithm, id: keyId) {
try key.verifySignature(header.signature, for: message, using: algorithm)
} else {
throw JSONWebKeyError.keyNotFound
return
}
}
throw JSONWebKeyError.keyNotFound
}

/// Verifies all signatures in protected header(s) using given key set.
///
/// - Parameters:
/// - key: A `JSONWebKeySet` object contains keys that would be used for validation.
public func verifySignature(using keySet: JSONWebKeySet) throws {
try verifySignature(using: keySet.keys.compactMap { $0 as? any JSONWebValidatingKey })
/// - strict: If `true` (default), the algorithm in the protected header will be used otherwise algorithm in unprotected header will be allowed.
public func verifySignature(using keySet: JSONWebKeySet, strict: Bool = true) throws {
try verifySignature(using: keySet.keys.compactMap { $0 as? any JSONWebValidatingKey }, strict: strict)
}

/// Verifies all signatures in protected header(s) using given key.
///
/// - Parameters:
/// - key: A `JSONWebValidatingKey` object that would be used for validation.
public func verifySignature(using key: any JSONWebValidatingKey) throws {
try verifySignature(using: [key])
/// - strict: If `true` (default), the algorithm in the protected header will be used otherwise algorithm in unprotected header will be allowed.
public func verifySignature(using key: any JSONWebValidatingKey, strict: Bool = true) throws {
try verifySignature(using: [key], strict: strict)
}

/// Validates contents and required fields if applicable.
Expand Down
5 changes: 3 additions & 2 deletions Sources/JWSETKit/Entities/JWS/JWSHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ public struct JSONWebSignatureHeader: Hashable, Codable, Sendable {

extension JSONWebSignatureHeader {
func signedData(_ payload: any ProtectedWebContainer) -> Data {
let protectedEncoded = !protected.storage.storageKeys.isEmpty ? protected.encoded.urlBase64EncodedData() : .init()
if protected.critical.contains("b64"), protected.base64 == false {
return protected.encoded.urlBase64EncodedData() + Data(".".utf8) + payload.encoded
return protectedEncoded + Data(".".utf8) + payload.encoded
} else {
return protected.encoded.urlBase64EncodedData() + Data(".".utf8) + payload.encoded.urlBase64EncodedData()
return protectedEncoded + Data(".".utf8) + payload.encoded.urlBase64EncodedData()
}
}
}
14 changes: 14 additions & 0 deletions Sources/JWSETKit/Extensions/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ extension ReadWriteLockedValue: Equatable where T: Equatable {
}
}

extension ReadWriteLockedValue: Comparable where T: Comparable {
static func < (lhs: ReadWriteLockedValue, rhs: ReadWriteLockedValue) -> Bool {
lhs.wrappedValue < rhs.wrappedValue
}
}

extension ReadWriteLockedValue: Hashable where T: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(wrappedValue)
Expand All @@ -109,6 +115,14 @@ extension ReadWriteLockedValue: Sequence where T: Sequence {
func makeIterator() -> T.Iterator {
wrappedValue.makeIterator()
}

var underestimatedCount: Int {
wrappedValue.underestimatedCount
}

func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<T.Element>) throws -> R) rethrows -> R? {
try wrappedValue.withContiguousStorageIfAvailable(body)
}
}

extension ReadWriteLockedValue: Collection where T: Collection {
Expand Down
253 changes: 253 additions & 0 deletions Tests/JWSETKitTests/Cryptography/RFC7520SignatureTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
//
// RFC7520Tests.swift
//
//
// Created by Amir Abbas Mousavian on 1/5/24.
//

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

typealias JWS = JSONWebSignature<ProtectedDataWebContainer>
final class RFC7520Tests: XCTestCase {
let payload = """
SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\
lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\
b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\
UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4
"""

func testSignatureSignRS256() throws {
let jwsString = """
eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\
hhbXBsZSJ9\
.\(payload).
"""
let signature = """
MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\
ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\
IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\
W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\
xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\
cIe8u9ipH84ogoree7vjbU5y18kDquDg
"""
var jws = JWS(jwsString)!
try jws.updateSignature(using: RFC7520ExampleKeys.rsaPrivateKey.signingKey)
XCTAssertEqual(jws.signatures[0].signature, signature.decoded)
}

func testSignatureVerifyRS256() throws {
let jwsString = """
eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\
hhbXBsZSJ9\
.\(payload).\
MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\
ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\
IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\
W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\
xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\
cIe8u9ipH84ogoree7vjbU5y18kDquDg
"""
let jws = JWS(jwsString)!
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.rsaPublicKey.validatingKey))
}

func testSignatureSignPS384() throws {
let jwsString = """
eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\
hhbXBsZSJ9\
.\(payload).
"""
var jws = JWS(jwsString)!
try jws.updateSignature(using: RFC7520ExampleKeys.rsaPrivateKey.signingKey)
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.rsaPublicKey.validatingKey))
}

func testSignatureVerifyPS384() throws {
let jwsString = """
eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\
hhbXBsZSJ9\
.\(payload).\
cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2I\
pN6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXU\
vdvWXzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRX\
e8P_ijQ7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT\
0qI0n6uiP1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a\
6GYmJUAfmWjwZ6oD4ifKo8DYM-X72Eaw
"""
let jws = JWS(jwsString)!
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.rsaPublicKey.validatingKey))
}

func testSignatureSignES521() throws {
let jwsString = """
eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\
hhbXBsZSJ9\
.\(payload).
"""
var jws = JWS(jwsString)!
try jws.updateSignature(using: RFC7520ExampleKeys.ecPrivateKey.signingKey)
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.ecPublicKey.validatingKey))
}

func testSignatureVerifyES521() throws {
let jwsString = """
eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\
hhbXBsZSJ9\
.\(payload).\
AE_R_YZCChjn4791jSQCrdPZCNYqHXCTZH0-JZGYNlaAjP2kqaluUIIUnC9qvb\
u9Plon7KRTzoNEuT4Va2cmL1eJAQy3mtPBu_u_sDDyYjnAMDxXPn7XrT0lw-kv\
AD890jl8e2puQens_IEKBpHABlsbEPX6sFY8OcGDqoRuBomu9xQ2
"""
let jws = JWS(jwsString)!
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.ecPublicKey.validatingKey))
}

func testSignatureSignHS256() throws {
let jwsString = """
eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW\
VlZjMxNGJjNzAzNyJ9\
.\(payload).
"""
let signature = """
s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0
"""
var jws = JWS(jwsString)!
try jws.updateSignature(using: RFC7520ExampleKeys.macSymmetricKey.signingKey)
XCTAssertEqual(jws.signatures[0].signature, signature.decoded)
}

func testSignatureVerifyHS256() throws {
let jwsString = """
eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW\
VlZjMxNGJjNzAzNyJ9\
.\(payload).\
s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0
"""
let jws = JWS(jwsString)!
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.macSymmetricKey.validatingKey))
}

func testSignatureDeteched() throws {
let jwsString = """
eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW\
VlZjMxNGJjNzAzNyJ9\
..\
s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0
"""
let jws = JWS(jwsString)!
XCTAssertEqual(jws.payload.encoded, .init())

let encoder = JSONEncoder()
encoder.userInfo[.jwsEncodedRepresentation] = JSONWebSignatureRepresentation.jsonFlattened
let jwsFlattened = try encoder.encode(jws)
let jwsFlattenedValue = try JSONDecoder().decode(JSONWebValueStorage.self, from: jwsFlattened)
XCTAssertFalse(jwsFlattenedValue.contains(key: "payload"))

encoder.userInfo[.jwsEncodedRepresentation] = JSONWebSignatureRepresentation.jsonGeneral
let jwsJSON = try encoder.encode(jws)
let jwsJSONValue = try JSONDecoder().decode(JSONWebValueStorage.self, from: jwsJSON)
XCTAssertFalse(jwsJSONValue.contains(key: "payload"))
}

func testSignatureUnprotectedHeader() throws {
let jws = try JWS(signatures: [
.init(protected: "eyJhbGciOiJIUzI1NiJ9".decoded,
unprotected: .init({ $0.keyId = "018c0ae5-4d9b-471b-bfd6-eef314bc7037" }),
signature: "bWUSVaxorn7bEF1djytBd0kHv70Ly5pvbomzMWSOr20".decoded)
], payload: .init(encoded: payload.decoded))

XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.macSymmetricKey.validatingKey))

let encoder = JSONEncoder()
encoder.userInfo[.jwsEncodedRepresentation] = JSONWebSignatureRepresentation.jsonFlattened
let jwsFlattened = try encoder.encode(jws)
let jwsFlattenedValue = try JSONDecoder().decode(JSONWebValueStorage.self, from: jwsFlattened)
XCTAssertTrue(jwsFlattenedValue.contains(key: "header"))
XCTAssertEqual((jwsFlattenedValue.header as JOSEHeader?)?.keyId, "018c0ae5-4d9b-471b-bfd6-eef314bc7037")

encoder.userInfo[.jwsEncodedRepresentation] = JSONWebSignatureRepresentation.jsonGeneral
let jwsJSON = try encoder.encode(jws)
let jwsJSONValue = try JSONDecoder().decode(JSONWebValueStorage.self, from: jwsJSON)
XCTAssertTrue(jwsJSONValue.contains(key: "signatures"))
XCTAssertEqual((jwsJSONValue.signatures as [JSONWebSignatureHeader]?)?.first?.unprotected?.keyId, "018c0ae5-4d9b-471b-bfd6-eef314bc7037")
}

func testSignatureProtectedContentOnly() throws {
let jws = try JWS(signatures: [
.init(protected: JOSEHeader(),
unprotected: .init({
$0.algorithm = JSONWebSignatureAlgorithm.hmacSHA256
$0.keyId = "018c0ae5-4d9b-471b-bfd6-eef314bc7037"
}),
signature: "xuLifqLGiblpv9zBpuZczWhNj1gARaLV3UxvxhJxZuk".decoded)
], payload: .init(encoded: payload.decoded))

XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.macSymmetricKey.validatingKey, strict: false))

let encoder = JSONEncoder()
encoder.userInfo[.jwsEncodedRepresentation] = JSONWebSignatureRepresentation.jsonFlattened
let jwsFlattened = try encoder.encode(jws)
let jwsFlattenedValue = try JSONDecoder().decode(JSONWebValueStorage.self, from: jwsFlattened)
XCTAssertTrue(jwsFlattenedValue.contains(key: "header"))
let jwsFlattenedHeader = jwsFlattenedValue.header as JOSEHeader?
XCTAssert(jwsFlattenedHeader?.algorithm == .hmacSHA256)
XCTAssertEqual(jwsFlattenedHeader?.keyId, "018c0ae5-4d9b-471b-bfd6-eef314bc7037")

encoder.userInfo[.jwsEncodedRepresentation] = JSONWebSignatureRepresentation.jsonGeneral
let jwsJSON = try encoder.encode(jws)
let jwsJSONValue = try JSONDecoder().decode(JSONWebValueStorage.self, from: jwsJSON)
XCTAssertTrue(jwsJSONValue.contains(key: "signatures"))
XCTAssertEqual((jwsJSONValue.signatures as [JSONWebSignatureHeader]?)?.first?.unprotected?.keyId, "018c0ae5-4d9b-471b-bfd6-eef314bc7037")
}

func testMultipleSignatures() throws {
let jwsString = """
{
"payload": "\(payload)",
"signatures": [
{
"protected": "eyJhbGciOiJSUzI1NiJ9",
"header": {
"kid": "bilbo.baggins@hobbiton.example"
},
"signature": "MIsjqtVlOpa71KE-Mss8_Nq2YH4FGhiocsqrgi5Nvy\
G53uoimic1tcMdSg-qptrzZc7CG6Svw2Y13TDIqHzTUrL_lR2ZFc\
ryNFiHkSw129EghGpwkpxaTn_THJTCglNbADko1MZBCdwzJxwqZc\
-1RlpO2HibUYyXSwO97BSe0_evZKdjvvKSgsIqjytKSeAMbhMBdM\
ma622_BG5t4sdbuCHtFjp9iJmkio47AIwqkZV1aIZsv33uPUqBBC\
XbYoQJwt7mxPftHmNlGoOSMxR_3thmXTCm4US-xiNOyhbm8afKK6\
4jU6_TPtQHiJeQJxz9G3Tx-083B745_AfYOnlC9w"
},
{
"header": {
"alg": "ES512",
"kid": "bilbo.baggins@hobbiton.example"
},
"signature": "ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHjwDO1muhc\
dCoFZFFjfISu0Cdkn9Ybdlmi54ho0x924DUz8sK7ZXkhc7AFM8Ob\
LfTvNCrqcI3Jkl2U5IX3utNhODH6v7xgy1Qahsn0fyb4zSAkje8b\
AWz4vIfj5pCMYxxm4fgV3q7ZYhm5eD"
},
{
"protected": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LT\
RkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9",
"signature": "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
}
]
}
"""

let jws = try JSONDecoder().decode(JWS.self, from: jwsString.data)
XCTAssertEqual(jws.signatures.count, 3)

XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.rsaPublicKey.validatingKey))
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.ecPublicKey.validatingKey, strict: false))
XCTAssertNoThrow(try jws.verifySignature(using: RFC7520ExampleKeys.macSymmetricKey.validatingKey))
}
}

0 comments on commit 329f300

Please sign in to comment.