From 5d61aa022d7d0c209f945506ad63d520f9c5696a Mon Sep 17 00:00:00 2001 From: Visa Varjus Date: Thu, 7 Apr 2022 16:13:16 +0300 Subject: [PATCH 1/5] Add support for JWS unencoded payload option https://datatracker.ietf.org/doc/html/rfc7797 --- JOSESwift.xcodeproj/project.pbxproj | 4 +++ JOSESwift/Sources/JWS.swift | 1 + JOSESwift/Sources/JWSHeader.swift | 17 ++++++++++ JOSESwift/Sources/JWSSigningInput.swift | 42 +++++++++++++++++++++++++ JOSESwift/Sources/Signer.swift | 14 +-------- JOSESwift/Sources/Verifier.swift | 4 +-- Tests/ECVerifierTests.swift | 2 +- Tests/JWSSigningInputTest.swift | 4 +-- Tests/RSAVerifierTests.swift | 2 +- 9 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 JOSESwift/Sources/JWSSigningInput.swift diff --git a/JOSESwift.xcodeproj/project.pbxproj b/JOSESwift.xcodeproj/project.pbxproj index 73edfbb1..f689aa80 100644 --- a/JOSESwift.xcodeproj/project.pbxproj +++ b/JOSESwift.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 5C0F564327FF18C8006328D1 /* JWSSigningInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0F564227FF18C8006328D1 /* JWSSigningInput.swift */; }; 5FB760497BB7711EFB470B5A /* ECKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB76A41AECF99F3673C1C40 /* ECKeys.swift */; }; 5FB76093CE81BF1F8E7C254A /* JWKECDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7625CF5EF5D8F2135967F /* JWKECDecodingTests.swift */; }; 5FB7628EC6EA2C4263853DE9 /* DataECPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */; }; @@ -139,6 +140,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 5C0F564227FF18C8006328D1 /* JWSSigningInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWSSigningInput.swift; sourceTree = ""; }; 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataECPrivateKey.swift; sourceTree = ""; }; 5FB760DB390F90F91102DB74 /* EC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EC.swift; sourceTree = ""; }; 5FB7625CF5EF5D8F2135967F /* JWKECDecodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWKECDecodingTests.swift; sourceTree = ""; }; @@ -533,6 +535,7 @@ children = ( C85B1EF1204D82640026BDCB /* JWS.swift */, 6571F6221F7BF786004D53C5 /* JWSHeader.swift */, + 5C0F564227FF18C8006328D1 /* JWSSigningInput.swift */, C85B1EF3204D82860026BDCB /* Signer.swift */, 65D868101F7CEBA200769BBF /* Verifier.swift */, ); @@ -859,6 +862,7 @@ 5FB7641E9D67F245F52F3A7E /* EC.swift in Sources */, 5FB762CE28963B4780B33973 /* SecKeyECPublicKey.swift in Sources */, 5FB76E6F2080BEC6B63939D6 /* ECSigner.swift in Sources */, + 5C0F564327FF18C8006328D1 /* JWSSigningInput.swift in Sources */, 5FB76B8D7F1214FF01FA1A8B /* ECVerifier.swift in Sources */, 5FB763AFF6BE04E8A6AE4DFC /* DataECPublicKey.swift in Sources */, 5FB7628EC6EA2C4263853DE9 /* DataECPrivateKey.swift in Sources */, diff --git a/JOSESwift/Sources/JWS.swift b/JOSESwift/Sources/JWS.swift index 5be2dd1b..fc04e7d5 100644 --- a/JOSESwift/Sources/JWS.swift +++ b/JOSESwift/Sources/JWS.swift @@ -26,6 +26,7 @@ import Foundation internal enum JWSError: Error { case algorithmMismatch case cannotComputeSigningInput + case unencodedPayloadOptionMustNotBeUsedWithJWT } /// A JWS object consisting of a header, payload and signature. The three components of a JWS object diff --git a/JOSESwift/Sources/JWSHeader.swift b/JOSESwift/Sources/JWSHeader.swift index ef1adf33..d497ddd9 100644 --- a/JOSESwift/Sources/JWSHeader.swift +++ b/JOSESwift/Sources/JWSHeader.swift @@ -223,6 +223,23 @@ extension JWSHeader: CommonHeaderParameterSpace { } } + /// The unencoded payload option. + /// + /// Reference: [](https://datatracker.ietf.org/doc/html/rfc7797#section-3). + /// + /// When set to `false` and `crit` contains `b64`, payload data is not encoded when computing the JWS signature. + /// + /// **NOTE**: Due to [Unencoded Payload Content Restrictions](https://datatracker.ietf.org/doc/html/rfc7797#section-5), + /// [detached payload](https://www.rfc-editor.org/rfc/rfc7515.html#appendix-F) is always used when serializing `JWS`. + public var b64: Bool? { + get { + return parameters["b64"] as? Bool + } + set { + parameters["b64"] = newValue + } + } + /// The critical header parameter indicates the header parameter extensions. public var crit: [String]? { get { diff --git a/JOSESwift/Sources/JWSSigningInput.swift b/JOSESwift/Sources/JWSSigningInput.swift new file mode 100644 index 00000000..f30477a1 --- /dev/null +++ b/JOSESwift/Sources/JWSSigningInput.swift @@ -0,0 +1,42 @@ +import Foundation + +struct JWSSigningInput { + + let header: JWSHeader + + let payload: Payload + + func signingInput() throws -> Data { + let headerData = try computeHeaderData() + let payloadData = try computePayloadData() + + // Force unwrapping is ok, since `".".data(using: .ascii)` should always work. + // swiftlint:disable:next force_unwrapping + return headerData + ".".data(using: .ascii)! + payloadData + } + + private func computeHeaderData() throws -> Data { + guard let headerData = header.data().base64URLEncodedString().data(using: .ascii) else { + throw JWSError.cannotComputeSigningInput + } + return headerData + } + + private func computePayloadData() throws -> Data { + let encodePayload = (header.crit?.contains("b64") == true) + ? (header.b64 ?? true) + : true + + if encodePayload { + guard let encodedPayload = payload.data().base64URLEncodedString().data(using: .ascii) else { + throw JWSError.cannotComputeSigningInput + } + return encodedPayload + } else if let typ = header.typ, typ.caseInsensitiveCompare("jwt") == .orderedSame { + throw JWSError.unencodedPayloadOptionMustNotBeUsedWithJWT + } else { + return payload.data() + } + } + +} diff --git a/JOSESwift/Sources/Signer.swift b/JOSESwift/Sources/Signer.swift index 41519719..4bcc4187 100644 --- a/JOSESwift/Sources/Signer.swift +++ b/JOSESwift/Sources/Signer.swift @@ -77,24 +77,12 @@ public struct Signer { throw JWSError.algorithmMismatch } - guard let signingInput = [header, payload].asJOSESigningInput() else { - throw JWSError.cannotComputeSigningInput - } + let signingInput = try JWSSigningInput(header: header, payload: payload).signingInput() return try signer.sign(signingInput) } } -extension Array where Element == DataConvertible { - func asJOSESigningInput() -> Data? { - let encoded = self.map { component in - return component.data().base64URLEncodedString() - } - - return encoded.joined(separator: ".").data(using: .ascii) - } -} - // MARK: - Deprecated API extension Signer { diff --git a/JOSESwift/Sources/Verifier.swift b/JOSESwift/Sources/Verifier.swift index d125154f..0dd97cc5 100644 --- a/JOSESwift/Sources/Verifier.swift +++ b/JOSESwift/Sources/Verifier.swift @@ -78,9 +78,7 @@ public struct Verifier { throw JWSError.algorithmMismatch } - guard let signingInput = [header, payload].asJOSESigningInput() else { - throw JWSError.cannotComputeSigningInput - } + let signingInput = try JWSSigningInput(header: header, payload: payload).signingInput() return try verifier.verify(signingInput, against: signature) } diff --git a/Tests/ECVerifierTests.swift b/Tests/ECVerifierTests.swift index 30280a4d..bf37fc45 100644 --- a/Tests/ECVerifierTests.swift +++ b/Tests/ECVerifierTests.swift @@ -41,7 +41,7 @@ class ECVerifierTests: ECCryptoTestCase { let jws = try! JWS(compactSerialization: serializedJWS) let verifier = ECVerifier(algorithm: algorithm, publicKey: keyData.publicKey) - guard let signingInput = [jws.header, jws.payload].asJOSESigningInput() else { + guard let signingInput = try? JWSSigningInput(header: jws.header, payload: jws.payload).signingInput() else { XCTFail() return false } diff --git a/Tests/JWSSigningInputTest.swift b/Tests/JWSSigningInputTest.swift index 4cbfcab8..7fa17c81 100644 --- a/Tests/JWSSigningInputTest.swift +++ b/Tests/JWSSigningInputTest.swift @@ -42,8 +42,8 @@ class JWSSigningInputTest: XCTestCase { 106, 112, 48, 99, 110, 86, 108, 102, 81 ] - func testSigningInputComputation() { - let signingInput: [UInt8] = Array([header, payload].asJOSESigningInput()!) + func testSigningInputComputation() throws { + let signingInput: [UInt8] = Array(try JWSSigningInput(header: header, payload: payload).signingInput()) XCTAssertEqual(signingInput, expectedSigningInput) } diff --git a/Tests/RSAVerifierTests.swift b/Tests/RSAVerifierTests.swift index a839fdcf..e8f7ebb8 100644 --- a/Tests/RSAVerifierTests.swift +++ b/Tests/RSAVerifierTests.swift @@ -44,7 +44,7 @@ class RSAVerifierTests: RSACryptoTestCase { let jws = try! JWS(compactSerialization: compactSerializedJWSRS512Const) let verifier = RSAVerifier(algorithm: .RS512, publicKey: publicKeyAlice2048!) - guard let signingInput = [jws.header, jws.payload].asJOSESigningInput() else { + guard let signingInput = try? JWSSigningInput(header: jws.header, payload: jws.payload).signingInput() else { XCTFail() return } From 3ef0503e24ce75cc642853a3d52b1a262fd2eb51 Mon Sep 17 00:00:00 2001 From: Visa Varjus Date: Thu, 21 Apr 2022 11:20:54 +0300 Subject: [PATCH 2/5] Add tests for JWS unencoded payload option --- Tests/JWSHeaderTests.swift | 7 ++- Tests/JWSSigningInputTest.swift | 107 ++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/Tests/JWSHeaderTests.swift b/Tests/JWSHeaderTests.swift index 179b2aed..cb9423fa 100644 --- a/Tests/JWSHeaderTests.swift +++ b/Tests/JWSHeaderTests.swift @@ -111,7 +111,8 @@ class JWSHeaderTests: XCTestCase { let x5tS256 = "x5tS256" let typ = "typ" let cty = "cty" - let crit = ["crit1", "crit2"] + let b64 = false + let crit = ["crit1", "crit2", "b64"] var header = JWSHeader(algorithm: .RS512) header.jku = jku @@ -123,6 +124,7 @@ class JWSHeaderTests: XCTestCase { header.x5tS256 = x5tS256 header.typ = typ header.cty = cty + header.b64 = b64 header.crit = crit XCTAssertEqual(header.data().count, try! JSONSerialization.data(withJSONObject: header.parameters, options: []).count) @@ -154,6 +156,9 @@ class JWSHeaderTests: XCTestCase { XCTAssertEqual(header.parameters["cty"] as? String, cty) XCTAssertEqual(header.cty, cty) + XCTAssertEqual(header.parameters["b64"] as? Bool, b64) + XCTAssertEqual(header.b64, b64) + XCTAssertEqual(header.parameters["crit"] as? [String], crit) XCTAssertEqual(header.crit, crit) } diff --git a/Tests/JWSSigningInputTest.swift b/Tests/JWSSigningInputTest.swift index 7fa17c81..cd698e12 100644 --- a/Tests/JWSSigningInputTest.swift +++ b/Tests/JWSSigningInputTest.swift @@ -27,24 +27,105 @@ import XCTest class JWSSigningInputTest: XCTestCase { - let header: JWSHeader = JWSHeader("{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}".data(using: .utf8)!)! - let payload: Payload = Payload("{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}".data(using: .utf8)!) - - let expectedSigningInput: [UInt8] = [ - 101, 121, 74, 48, 101, 88, 65, 105, 79, 105, 74, 75, 86, 49, 81, - 105, 76, 65, 48, 75, 73, 67, 74, 104, 98, 71, 99, 105, 79, 105, 74, - 73, 85, 122, 73, 49, 78, 105, 74, 57, 46, 101, 121, 74, 112, 99, 51, - 77, 105, 79, 105, 74, 113, 98, 50, 85, 105, 76, 65, 48, 75, 73, 67, - 74, 108, 101, 72, 65, 105, 79, 106, 69, 122, 77, 68, 65, 52, 77, 84, - 107, 122, 79, 68, 65, 115, 68, 81, 111, 103, 73, 109, 104, 48, 100, - 72, 65, 54, 76, 121, 57, 108, 101, 71, 70, 116, 99, 71, 120, 108, 76, - 109, 78, 118, 98, 83, 57, 112, 99, 49, 57, 121, 98, 50, 57, 48, 73, - 106, 112, 48, 99, 110, 86, 108, 102, 81 + let payload = Payload("{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}".data(using: .utf8)!) + + let expectedSeparatorBytes: [UInt8] = [46] // "." + + let expectedEncodedPayloadBytes: [UInt8] = [ + 101, 121, 74, 112, 99, 51, 77, 105, 79, 105, 74, 113, 98, 50, 85, 105, + 76, 65, 48, 75, 73, 67, 74, 108, 101, 72, 65, 105, 79, 106, 69, 122, + 77, 68, 65, 52, 77, 84, 107, 122, 79, 68, 65, 115, 68, 81, 111, 103, + 73, 109, 104, 48, 100, 72, 65, 54, 76, 121, 57, 108, 101, 71, 70, 116, + 99, 71, 120, 108, 76, 109, 78, 118, 98, 83, 57, 112, 99, 49, 57, 121, + 98, 50, 57, 48, 73, 106, 112, 48, 99, 110, 86, 108, 102, 81 ] func testSigningInputComputation() throws { + let header: JWSHeader = JWSHeader("{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}".data(using: .utf8)!)! + + let expectedHeaderBytes: [UInt8] = [ + 101, 121, 74, 48, 101, 88, 65, 105, 79, 105, 74, 75, 86, 49, 81, + 105, 76, 65, 48, 75, 73, 67, 74, 104, 98, 71, 99, 105, 79, 105, 74, + 73, 85, 122, 73, 49, 78, 105, 74, 57 + ] + let expectedSigningInput = expectedHeaderBytes + expectedSeparatorBytes + expectedEncodedPayloadBytes + + let signingInput: [UInt8] = Array(try JWSSigningInput(header: header, payload: payload).signingInput()) + XCTAssertEqual(signingInput, expectedSigningInput) + } + + func testSigningInputComputationWithUnencodedPayloadOptionDoesNotEncodePayload() throws { + let header: JWSHeader = JWSHeader(""" + { + "alg": "HS256", + "b64": false, + "crit": [ + "b64" + ] + } + """.data(using: .utf8)!)! + let unencodedPayloadBytes: [UInt8] = Array(repeating: 0, count: 32) + let unencodedPayload: Payload = Payload(Data(unencodedPayloadBytes)) + + let expectedHeaderBytes: [UInt8] = [ + 101, 119, 111, 103, 73, 67, 74, 104, 98, 71, 99, 105, 79, 105, 65, 105, + 83, 70, 77, 121, 78, 84, 89, 105, 76, 65, 111, 103, 73, 67, 74, 105, + 78, 106, 81, 105, 79, 105, 66, 109, 89, 87, 120, 122, 90, 83, 119, 75, + 73, 67, 65, 105, 89, 51, 74, 112, 100, 67, 73, 54, 73, 70, 115, 75, + 73, 67, 65, 103, 73, 67, 74, 105, 78, 106, 81, 105, 67, 105, 65, 103, + 88, 81, 112, 57 + ] + let expectedSigningInput = expectedHeaderBytes + expectedSeparatorBytes + unencodedPayloadBytes + + let signingInput: [UInt8] = Array(try JWSSigningInput(header: header, payload: unencodedPayload).signingInput()) + + XCTAssertEqual(signingInput, expectedSigningInput) + } + + func testSigningInputComputationWithExplicitEncodedPayloadOptionEncodesPayload() throws { + let header: JWSHeader = JWSHeader(""" + { + "alg": "HS256", + "b64": true, + "crit": [ + "b64" + ] + } + """.data(using: .utf8)!)! + + let expectedHeaderBytes: [UInt8] = [ + 101, 119, 111, 103, 73, 67, 74, 104, 98, 71, 99, 105, 79, 105, 65, 105, + 83, 70, 77, 121, 78, 84, 89, 105, 76, 65, 111, 103, 73, 67, 74, 105, + 78, 106, 81, 105, 79, 105, 66, 48, 99, 110, 86, 108, 76, 65, 111, 103, + 73, 67, 74, 106, 99, 109, 108, 48, 73, 106, 111, 103, 87, 119, 111, 103, + 73, 67, 65, 103, 73, 109, 73, 50, 78, 67, 73, 75, 73, 67, 66, 100, + 67, 110, 48 + ] + let expectedSigningInput: [UInt8] = expectedHeaderBytes + expectedSeparatorBytes + expectedEncodedPayloadBytes + let signingInput: [UInt8] = Array(try JWSSigningInput(header: header, payload: payload).signingInput()) + XCTAssertEqual(signingInput, expectedSigningInput) } + func testSigningInputComputationWithUnencodedPayloadOptionAndJwtThrows() throws { + let header: JWSHeader = JWSHeader(""" + { + "typ": "JWT", + "alg": "HS256", + "b64": false, + "crit": [ + "b64" + ] + } + """.data(using: .utf8)!)! + + do { + _ = try JWSSigningInput(header: header, payload: payload).signingInput() + XCTFail("Expected to throw") + } catch { + XCTAssertEqual(error as? JWSError, JWSError.unencodedPayloadOptionMustNotBeUsedWithJWT) + } + } + } From ad7e47c6d66725513e638f1cd68af07cc2f2e1c5 Mon Sep 17 00:00:00 2001 From: Visa Varjus Date: Wed, 27 Apr 2022 09:21:17 +0300 Subject: [PATCH 3/5] Add support for JWS detached payload for unencoded payload option --- JOSESwift/Sources/JWS.swift | 54 ++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/JOSESwift/Sources/JWS.swift b/JOSESwift/Sources/JWS.swift index fc04e7d5..a95386e6 100644 --- a/JOSESwift/Sources/JWS.swift +++ b/JOSESwift/Sources/JWS.swift @@ -111,6 +111,52 @@ public struct JWS { self = try JOSEDeserializer().deserialize(JWS.self, fromCompactSerialization: compactSerializationString) } + /// Constructs a JWS object from a given compact serialization string and a [detached payload](https://www.rfc-editor.org/rfc/rfc7515.html#appendix-F). + /// + /// If `compactSerialization` contains non-empty payload, `detachedPayload` is omitted. + /// + /// - Parameters: + /// - compactSerialization: A compact serialized JWS object in string format as received e.g. from the server. + /// - detachedPayload: A detached payload delivered outside the JWS context. + /// - Throws: + /// - `JOSESwiftError.invalidCompactSerializationComponentCount(count: Int)`: + /// If the component count of the compact serialization is wrong. + /// - `JOSESwiftError.componentNotValidBase64URL(component: String)`: + /// If the component is not a valid base64URL string. + /// - `JOSESwiftError.componentCouldNotBeInitializedFromData(data: Data)`: + /// If a component cannot be initialized from its data object. + public init(compactSerialization: String, detachedPayload: Payload) throws { + let jws = try JOSEDeserializer().deserialize(JWS.self, fromCompactSerialization: compactSerialization) + if jws.payload.data().isEmpty { + self.init(header: jws.header, payload: detachedPayload, signature: jws.signature) + } else { + self = jws + } + } + + /// Constructs a JWS object from a given compact serialization data and a [detached payload](https://www.rfc-editor.org/rfc/rfc7515.html#appendix-F). + /// + /// If `compactSerialization` contains non-empty payload, `detachedPayload` is omitted. + /// + /// - Parameters: + /// - compactSerialization: A compact serialized JWS object as data object as received e.g. from the server. + /// - detachedPayload: A detached payload delivered outside the JWS context. + /// - Throws: + /// - `JOSESwiftError.wrongDataEncoding(data: Data)`: + /// If the compact serialization data object is not convertible to string. + /// - `JOSESwiftError.invalidCompactSerializationComponentCount(count: Int)`: + /// If the component count of the compact serialization is wrong. + /// - `JOSESwiftError.componentNotValidBase64URL(component: String)`: + /// If the component is not a valid base64URL string. + /// - `JOSESwiftError.componentCouldNotBeInitializedFromData(data: Data)`: + /// If a component cannot be initialized from its data object. + public init(compactSerialization: Data, detachedPayload: Payload) throws { + guard let compactSerializationString = String(data: compactSerialization, encoding: .utf8) else { + throw JOSESwiftError.wrongDataEncoding(data: compactSerialization) + } + try self.init(compactSerialization: compactSerializationString, detachedPayload: detachedPayload) + } + fileprivate init(header: JWSHeader, payload: Payload, signature: Data) { self.header = header self.payload = payload @@ -208,7 +254,13 @@ public struct JWS { extension JWS: CompactSerializable { public func serialize(to serializer: inout CompactSerializer) { serializer.serialize(header) - serializer.serialize(payload) + + if header.crit?.contains("b64") == true, header.b64 == false { + serializer.serialize(Data()) // Detached payload. + } else { + serializer.serialize(payload) + } + serializer.serialize(signature) } } From 5fa25e700deca054ce3d689f2fb4a2e564d3a545 Mon Sep 17 00:00:00 2001 From: Visa Varjus Date: Wed, 27 Apr 2022 09:22:22 +0300 Subject: [PATCH 4/5] Add tests for JWS unencoded detached payload --- JOSESwift.xcodeproj/project.pbxproj | 4 ++ Tests/JWSUnencodedPayloadTests.swift | 103 +++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 Tests/JWSUnencodedPayloadTests.swift diff --git a/JOSESwift.xcodeproj/project.pbxproj b/JOSESwift.xcodeproj/project.pbxproj index f689aa80..73ee8820 100644 --- a/JOSESwift.xcodeproj/project.pbxproj +++ b/JOSESwift.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 5C0F564327FF18C8006328D1 /* JWSSigningInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0F564227FF18C8006328D1 /* JWSSigningInput.swift */; }; + 5C17840528181AD30072294E /* JWSUnencodedPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C17840428181AD30072294E /* JWSUnencodedPayloadTests.swift */; }; 5FB760497BB7711EFB470B5A /* ECKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB76A41AECF99F3673C1C40 /* ECKeys.swift */; }; 5FB76093CE81BF1F8E7C254A /* JWKECDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7625CF5EF5D8F2135967F /* JWKECDecodingTests.swift */; }; 5FB7628EC6EA2C4263853DE9 /* DataECPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */; }; @@ -141,6 +142,7 @@ /* Begin PBXFileReference section */ 5C0F564227FF18C8006328D1 /* JWSSigningInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWSSigningInput.swift; sourceTree = ""; }; + 5C17840428181AD30072294E /* JWSUnencodedPayloadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWSUnencodedPayloadTests.swift; sourceTree = ""; }; 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataECPrivateKey.swift; sourceTree = ""; }; 5FB760DB390F90F91102DB74 /* EC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EC.swift; sourceTree = ""; }; 5FB7625CF5EF5D8F2135967F /* JWKECDecodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWKECDecodingTests.swift; sourceTree = ""; }; @@ -295,6 +297,7 @@ 65125A311FBF85FA007CF3AE /* JWSDeserializationTests.swift */, 5FB765151D1E18BD7BFB95B8 /* JWSECTests.swift */, 7402BEB426288DCD0012801E /* JWSHMACTests.swift */, + 5C17840428181AD30072294E /* JWSUnencodedPayloadTests.swift */, ); name = JWS; sourceTree = ""; @@ -780,6 +783,7 @@ 65125A321FBF85FA007CF3AE /* JWSDeserializationTests.swift in Sources */, 65A103A3202B0CDF00D22BF5 /* DataRSAPublicKeyTests.swift in Sources */, C84BDE171FAB1CB60002B5D0 /* RSASignerTests.swift in Sources */, + 5C17840528181AD30072294E /* JWSUnencodedPayloadTests.swift in Sources */, C8EE14541FAC797500A616E4 /* RSAVerifierTests.swift in Sources */, 658261492029E2D200B594ED /* SecKeyRSAPublicKeyTests.swift in Sources */, C86AC8CB1FCEC20F0007E611 /* AESCBCContentEncryptionTests.swift in Sources */, diff --git a/Tests/JWSUnencodedPayloadTests.swift b/Tests/JWSUnencodedPayloadTests.swift new file mode 100644 index 00000000..708cd97a --- /dev/null +++ b/Tests/JWSUnencodedPayloadTests.swift @@ -0,0 +1,103 @@ +// swiftlint:disable force_unwrapping + +import XCTest +@testable import JOSESwift + +class JWSUnencodedPayloadTests: ECCryptoTestCase { + + let detachedPayload = Payload("...DETACHED_ASCII_PAYLOAD...".data(using: .ascii)!) + + let encodedPayload = "Li4uREVUQUNIRURfQVNDSUlfUEFZTE9BRC4uLg" + + var signer: Signer { + Signer(signingAlgorithm: .ES256, key: allTestData.first!.privateKey)! + } + + var verifier: Verifier { + Verifier(verifyingAlgorithm: .ES256, key: allTestData.first!.publicKey)! + } + + func testCompactSerializationWithUnencodedPayloadOptionHasDetachedPayload() throws { + let header = try JWSHeader(parameters: [ + "b64": false, // unencoded payload + "crit": ["b64"], + "alg": "ES256" + ]) + + let compactSerialization = try JWS(header: header, payload: detachedPayload, signer: signer).compactSerializedString + let components = compactSerialization.components(separatedBy: ".") + + XCTAssertEqual(components.count, 3) + XCTAssertEqual(components[1], "") // payload is detached + } + + func testCompactSerializationWithMissingCriticalHeaderHasEncodedPayload() throws { + let header = try JWSHeader(parameters: [ + "b64": false, // unencoded payload + "crit": [], // "b64" not set + "alg": "ES256" + ]) + + let compactSerialization = try JWS(header: header, payload: detachedPayload, signer: signer).compactSerializedString + let components = compactSerialization.components(separatedBy: ".") + + XCTAssertEqual(components[1], encodedPayload) + } + + func testCompactSerializationWithExplicitlyEncodedPayloadHasEncodedPayload() throws { + let header = try JWSHeader(parameters: [ + "b64": true, // explicitly encoded payload + "crit": ["b64"], + "alg": "ES256" + ]) + + let compactSerialization = try JWS(header: header, payload: detachedPayload, signer: signer).compactSerializedString + let components = compactSerialization.components(separatedBy: ".") + + XCTAssertEqual(components[1], encodedPayload) + } + + func testDeserializationAndValidityWithUnencodedDetachedPayload() throws { + let header = try JWSHeader(parameters: [ + "b64": false, // unencoded payload + "crit": ["b64"], + "alg": "ES256" + ]) + + let compactSerialization = try JWS(header: header, payload: detachedPayload, signer: signer).compactSerializedString + + let decoded = try JWS(compactSerialization: compactSerialization, detachedPayload: detachedPayload) + + XCTAssertEqual(decoded.payload.data(), detachedPayload.data()) + XCTAssertTrue(decoded.isValid(for: verifier)) + } + + func testDeserializationAndValidityWithEncodedDetachedPayload() throws { + let header = try JWSHeader(parameters: [ + "alg": "ES256" + ]) + + let compactSerialization = try JWS(header: header, payload: detachedPayload, signer: signer).compactSerializedString + let components = compactSerialization.components(separatedBy: ".") + let compactSerializationWithDetachedPayload = "\(components[0])..\(components[2])" + + let decoded = try JWS(compactSerialization: compactSerializationWithDetachedPayload, detachedPayload: detachedPayload) + + XCTAssertEqual(decoded.payload.data(), detachedPayload.data()) + XCTAssertTrue(decoded.isValid(for: verifier)) + } + + func testInitCompactSerializationWithPayloadAndDetachedPayloadIgnoresDetachedPayload() throws { + let header = try JWSHeader(parameters: [ + "alg": "ES256" + ]) + + let payload = Payload("payload".data(using: .ascii)!) + let compactSerializationWithPayload = try JWS(header: header, payload: payload, signer: signer).compactSerializedString + + let decoded = try JWS(compactSerialization: compactSerializationWithPayload, detachedPayload: detachedPayload) + + XCTAssertEqual(decoded.payload.data(), payload.data()) + } + +} From d8c544f41ee4a9f2f8770c8bb1f8e267872e3117 Mon Sep 17 00:00:00 2001 From: Visa Varjus Date: Wed, 27 Apr 2022 16:15:24 +0300 Subject: [PATCH 5/5] Link to unencoded payload option in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5e71fe03..beadb1c0 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,7 @@ You can find detailed information about the relevant JOSE standards in the respe - [RFC-7516:](https://tools.ietf.org/html/rfc7516) JSON Web Encryption (JWE) - [RFC-7517:](https://tools.ietf.org/html/rfc7517) JSON Web Key (JWK) - [RFC-7518:](https://tools.ietf.org/html/rfc7518) JSON Web Algorithms (JWA) +- [RFC-7797:](https://datatracker.ietf.org/doc/html/rfc7797) JSON Web Signature (JWS) Unencoded Payload Option Don’t forget to check our [our wiki](https://github.com/mohemian/jose-ios/wiki) for more detailed documentation.