diff --git a/README.md b/README.md
index aaebff5..e505538 100644
--- a/README.md
+++ b/README.md
@@ -117,11 +117,12 @@ This library provides comprehensive support for the Jose suite of standards, inc
JWS Supported Types | JWS Supported Algorithms |
-| Type | Supported |
-|----------------|------------------|
-| Compact String |:white_check_mark:|
-| JSON |:white_check_mark:|
-| JSON Flattened |:white_check_mark:|
+| Type | Supported |
+|---------------------|------------------|
+| Compact String |:white_check_mark:|
+| JSON |:white_check_mark:|
+| JSON Flattened |:white_check_mark:|
+| Unencoded Payload\* |:white_check_mark:|
|
@@ -144,6 +145,8 @@ This library provides comprehensive support for the Jose suite of standards, inc
|
+Note: JWS Unencoded payload as referenced in the [RFC-7797](https://datatracker.ietf.org/doc/html/rfc7797)
+
### JWK
@@ -298,6 +301,26 @@ let keyJWK = JWK(keyType: .rsa, algorithm: "RSA512", keyID: rsaKeyId, e: rsaKeyE
let jwe = try JWS(payload: payload, protectedHeader: header, key: jwk)
```
+### JWS with Unencoded payload (Compact string only)
+
+JWS also supports unencoded payloads, which is useful in scenarios where the payload is already in a compact, URL-safe form (such as in the case of small JSON objects or base64url-encoded strings). This can help reduce the overall size of the JWS and improve performance by avoiding redundant encoding steps.
+
+To create a JWS with an unencoded payload, you need to set the b64 header parameter to false and ensure the payload is in a compatible format.
+
+Example:
+
+```
+let payload = "Hello world".data(using: .utf8)!
+let key = secp256k1.Signing.PrivateKey()
+
+let jws = try JWS(payload: payload, key: key, options: [.unencodedPayload])
+
+let jwsString = jws.compactSerialization
+
+try JWS.verify(jwsString: jwsString, payload: payload.data(using: .utf8)!, key: key)
+```
+
+
### JWE (JSON Web Encryption)
JWE represents encrypted content using JSON-based data structures, following the guidelines of [RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516). This module includes functionalities for encrypting and decrypting data, managing encryption keys, and handling various encryption algorithms and methods.
diff --git a/Sources/JSONWebEncryption/DefaultJWEHeaderImpl+Codable.swift b/Sources/JSONWebEncryption/DefaultJWEHeaderImpl+Codable.swift
index c249bdd..4f3cd86 100644
--- a/Sources/JSONWebEncryption/DefaultJWEHeaderImpl+Codable.swift
+++ b/Sources/JSONWebEncryption/DefaultJWEHeaderImpl+Codable.swift
@@ -94,7 +94,7 @@ extension DefaultJWEHeaderImpl: Codable {
ephemeralPublicKey = try container.decodeIfPresent(JWK.self, forKey: .ephemeralPublicKey)
type = try container.decodeIfPresent(String.self, forKey: .type)
contentType = try container.decodeIfPresent(String.self, forKey: .contentType)
- critical = try container.decodeIfPresent(String.self, forKey: .critical)
+ critical = try container.decodeIfPresent([String].self, forKey: .critical)
senderKeyID = try container.decodeIfPresent(String.self, forKey: .senderKeyID)
let initializationVectorBase64Url = try container.decodeIfPresent(String.self, forKey: .initializationVector)
initializationVector = try initializationVectorBase64Url.map { try Base64URL.decode($0) }
diff --git a/Sources/JSONWebEncryption/JWERegisteredFieldsHeader.swift b/Sources/JSONWebEncryption/JWERegisteredFieldsHeader.swift
index 8992cef..b0a8a4f 100644
--- a/Sources/JSONWebEncryption/JWERegisteredFieldsHeader.swift
+++ b/Sources/JSONWebEncryption/JWERegisteredFieldsHeader.swift
@@ -58,7 +58,7 @@ public protocol JWERegisteredFieldsHeader: JWARegisteredFieldsHeader {
var contentType: String? { get set }
/// List of critical headers that must be understood and processed.
- var critical: String? { get set }
+ var critical: [String]? { get set }
/// Key ID of the sender's key, used in the `ECDH-1PU` key agreement algorithm.
var senderKeyID: String? { get set }
@@ -92,7 +92,7 @@ public protocol JWERegisteredFieldsHeader: JWARegisteredFieldsHeader {
x509CertificateSHA256Thumbprint: String?,
type: String?,
contentType: String?,
- critical: String?,
+ critical: [String]?,
ephemeralPublicKey: JWK?,
agreementPartyUInfo: Data?,
agreementPartyVInfo: Data?,
@@ -118,7 +118,7 @@ extension JWERegisteredFieldsHeader {
x509CertificateSHA256Thumbprint: String? = nil,
type: String? = nil,
contentType: String? = nil,
- critical: String? = nil,
+ critical: [String]? = nil,
ephemeralPublicKey: JWK? = nil,
agreementPartyUInfo: Data? = nil,
agreementPartyVInfo: Data? = nil,
@@ -227,7 +227,7 @@ public struct DefaultJWEHeaderImpl: JWERegisteredFieldsHeader {
public var x509CertificateSHA256Thumbprint: String?
public var type: String?
public var contentType: String?
- public var critical: String?
+ public var critical: [String]?
public var ephemeralPublicKey: JWK?
public var agreementPartyUInfo: Data?
public var agreementPartyVInfo: Data?
@@ -263,7 +263,7 @@ public struct DefaultJWEHeaderImpl: JWERegisteredFieldsHeader {
x509CertificateSHA256Thumbprint: String?,
type: String?,
contentType: String?,
- critical: String?,
+ critical: [String]?,
ephemeralPublicKey: JWK?,
agreementPartyUInfo: Data?,
agreementPartyVInfo: Data?,
diff --git a/Sources/JSONWebSignature/DefaultJWSHeaderImpl+Codable.swift b/Sources/JSONWebSignature/DefaultJWSHeaderImpl+Codable.swift
index 41907a8..ce7718a 100644
--- a/Sources/JSONWebSignature/DefaultJWSHeaderImpl+Codable.swift
+++ b/Sources/JSONWebSignature/DefaultJWSHeaderImpl+Codable.swift
@@ -39,6 +39,7 @@ extension DefaultJWSHeaderImpl: Codable {
case pbes2SaltInput = "p2s"
case pbes2Count = "p2c"
case senderKeyID = "skid"
+ case base64EncodedUrlPayload = "b64"
}
public func encode(to encoder: Encoder) throws {
@@ -54,6 +55,7 @@ extension DefaultJWSHeaderImpl: Codable {
try container.encodeIfPresent(type, forKey: .type)
try container.encodeIfPresent(contentType, forKey: .contentType)
try container.encodeIfPresent(critical, forKey: .critical)
+ try container.encodeIfPresent(base64EncodedUrlPayload, forKey: .base64EncodedUrlPayload)
}
public init(from decoder: Decoder) throws {
@@ -68,6 +70,7 @@ extension DefaultJWSHeaderImpl: Codable {
x509CertificateSHA256Thumbprint = try container.decodeIfPresent(String.self, forKey: .x509CertificateSHA256Thumbprint)
type = try container.decodeIfPresent(String.self, forKey: .type)
contentType = try container.decodeIfPresent(String.self, forKey: .contentType)
- critical = try container.decodeIfPresent(String.self, forKey: .critical)
+ critical = try container.decodeIfPresent([String].self, forKey: .critical)
+ base64EncodedUrlPayload = try container.decodeIfPresent(Bool.self, forKey: .base64EncodedUrlPayload)
}
}
diff --git a/Sources/JSONWebSignature/JWS+Helper.swift b/Sources/JSONWebSignature/JWS+Helper.swift
index 0a5d559..a4c288c 100644
--- a/Sources/JSONWebSignature/JWS+Helper.swift
+++ b/Sources/JSONWebSignature/JWS+Helper.swift
@@ -19,6 +19,10 @@ import Tools
extension JWS {
static func buildSigningData(header: Data, data: Data) throws -> Data {
+ if try unencodedBase64Payload(header: header ) {
+ let headerB64 = Base64URL.encode(header)
+ return try [headerB64, data.tryToString()].joined(separator: ".").tryToData()
+ }
guard let signingData = [header, data]
.map({ Base64URL.encode($0) })
.joined(separator: ".")
@@ -30,8 +34,23 @@ extension JWS {
}
static func buildJWSString(header: Data, data: Data, signature: Data) throws -> String {
- return [header, data, signature]
- .map({ Base64URL.encode($0) })
- .joined(separator: ".")
+ if try unencodedBase64Payload(header: header) {
+ return [header, Data(), signature]
+ .map({ Base64URL.encode($0) })
+ .joined(separator: ".")
+ } else {
+ return [header, data, signature]
+ .map({ Base64URL.encode($0) })
+ .joined(separator: ".")
+ }
+ }
+
+ static func unencodedBase64Payload(header: Data) throws -> Bool {
+ let headerFields = try JSONDecoder.jwt.decode(DefaultJWSHeaderImpl.self, from: header)
+ guard
+ let hasBase64Header = headerFields.base64EncodedUrlPayload,
+ !hasBase64Header
+ else { return false }
+ return true
}
}
diff --git a/Sources/JSONWebSignature/JWS+JsonFlattened.swift b/Sources/JSONWebSignature/JWS+JsonFlattened.swift
index 3d03b72..60f47f7 100644
--- a/Sources/JSONWebSignature/JWS+JsonFlattened.swift
+++ b/Sources/JSONWebSignature/JWS+JsonFlattened.swift
@@ -173,7 +173,14 @@ extension JWSJsonFlattened: Codable {
try container.encodeIfPresent(protectedHeaderData.map { Base64URL.encode($0) }, forKey: .protected)
try container.encodeIfPresent(Base64URL.encode(signature), forKey: .signature)
try container.encodeIfPresent(unprotectedHeader, forKey: .header)
- try container.encode(Base64URL.encode(payload), forKey: .payload)
+ if
+ let headerData = protectedHeaderData,
+ try JWS.unencodedBase64Payload(header: headerData)
+ {
+ try container.encode(payload.tryToString().addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), forKey: .payload)
+ } else {
+ try container.encode(Base64URL.encode(payload), forKey: .payload)
+ }
}
public init(from decoder: Decoder) throws {
@@ -190,7 +197,17 @@ extension JWSJsonFlattened: Codable {
self.unprotectedHeaderData = try header.map { try JSONEncoder.jose.encode($0) }
self.unprotectedHeader = header
- let payloadBase64 = try container.decode(String.self, forKey: .payload)
- self.payload = try Base64URL.decode(payloadBase64)
+ let payloadStr = try container.decode(String.self, forKey: .payload)
+ if
+ let headerData = protectedHeaderData,
+ try JWS.unencodedBase64Payload(header: headerData)
+ {
+ guard let payloadValue = payloadStr.removingPercentEncoding else {
+ throw JWS.JWSError.somethingWentWrong
+ }
+ self.payload = try payloadValue.tryToData()
+ } else {
+ self.payload = try Base64URL.decode(payloadStr)
+ }
}
}
diff --git a/Sources/JSONWebSignature/JWS+Sign.swift b/Sources/JSONWebSignature/JWS+Sign.swift
index 73fed19..553b5d8 100644
--- a/Sources/JSONWebSignature/JWS+Sign.swift
+++ b/Sources/JSONWebSignature/JWS+Sign.swift
@@ -19,6 +19,10 @@ import JSONWebAlgorithms
import JSONWebKey
import Tools
+public enum JWSSignOptions {
+ case unencodedPayload
+}
+
extension JWS {
/// Initializes a new JWS (JSON Web Signature) instance with the given payload, protected header data, and key.
///
@@ -33,10 +37,13 @@ extension JWS {
/// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`.
///
/// - Throws: An error if the initialization or signing process fails.
- public init(payload: Data, protectedHeaderData: Data, key: Key?) throws {
+ public init(payload: Data, protectedHeaderData: Data, key: Key?, options: [JWSSignOptions] = []) throws {
let signature: Data
let key = try key.map { try prepareJWK(header: protectedHeaderData, key: $0, isPrivate: true) }
- let protectedHeader = try JSONDecoder().decode(DefaultJWSHeaderImpl.self, from: protectedHeaderData)
+ let (protectedHeader, protectedHeaderData): (DefaultJWSHeaderImpl, Data) = try setHeaderForOptions(
+ header: protectedHeaderData,
+ options: Set(options)
+ )
if let signer = protectedHeader.algorithm?.cryptoSigner {
guard let key else {
throw JWSError.missingKey
@@ -65,15 +72,19 @@ extension JWS {
/// - data: The payload data.
/// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`.
/// - Throws: An error if the signing process fails, or if the key is missing.
- public init(payload: Data, protectedHeader: JWSRegisteredFieldsHeader, key: Key?) throws {
+ public init(payload: Data, protectedHeader: JWSRegisteredFieldsHeader, key: Key?, options: [JWSSignOptions] = []) throws {
let signature: Data
let headerData = try JSONEncoder.jose.encode(protectedHeader)
- let key = try key.map { try prepareJWK(header: headerData, key: $0, isPrivate: true) }
+ let (_, protectedHeaderData): (DefaultJWSHeaderImpl, Data) = try setHeaderForOptions(
+ header: headerData,
+ options: Set(options)
+ )
+ let key = try key.map { try prepareJWK(header: protectedHeaderData, key: $0, isPrivate: true) }
if let signer = protectedHeader.algorithm?.cryptoSigner {
guard let key else {
throw JWSError.missingKey
}
- let signingData = try JWS.buildSigningData(header: headerData, data: payload)
+ let signingData = try JWS.buildSigningData(header: protectedHeaderData, data: payload)
signature = try signer.sign(data: signingData, key: key)
} else {
signature = Data()
@@ -82,7 +93,7 @@ extension JWS {
self.protectedHeader = protectedHeader
self.payload = payload
self.signature = signature
- self.compactSerialization = try JWS.buildJWSString(header: headerData, data: payload, signature: signature)
+ self.compactSerialization = try JWS.buildJWSString(header: protectedHeaderData, data: payload, signature: signature)
}
/// Convenience initializer to create a `JWS` instance using payload data and a JSON Web Key (JWK).
@@ -97,11 +108,11 @@ extension JWS {
/// - data: The payload data.
/// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`.
/// - Throws: An error if the signing process fails or if the key is inappropriate for the determined algorithm.
- public init(payload: Data, key: Key) throws {
+ public init(payload: Data, key: Key, options: [JWSSignOptions] = []) throws {
let jwkKey = try prepareJWK(header: nil, key: key)
let algorithm = try jwkKey.signingAlgorithm()
let header = DefaultJWSHeaderImpl(algorithm: algorithm)
- try self.init(payload: payload, protectedHeader: header, key: key)
+ try self.init(payload: payload, protectedHeader: header, key: key, options: options)
}
/// Generates a JSON serialization of the JWS object with multiple signatures, each corresponding to a different key in the provided array.
@@ -331,7 +342,31 @@ extension JWS {
}
}
-private func prepareHeaderForJWK(header: Data, jwk: JWK?) throws -> Data {
+func setHeaderForOptions(header: Data, options: Set) throws -> (H, Data) {
+ var headerChanges = header
+ try options.forEach {
+ switch $0 {
+ case .unencodedPayload:
+ headerChanges = try setUnencodedPayloadHeader(header: headerChanges)
+ }
+ }
+ let jwsFieldsHeader = try JSONDecoder.jwt.decode(H.self, from: headerChanges)
+ return (jwsFieldsHeader, headerChanges)
+}
+
+func setUnencodedPayloadHeader(header: Data) throws -> Data {
+ guard
+ var json = try JSONSerialization.jsonObject(with: header) as? [String: Any]
+ else { throw JWS.JWSError.somethingWentWrong }
+ json["b64"] = false
+ var newCritical = (json["crit"] as? [String]).map { Set($0) } ?? Set()
+ newCritical.insert("b64")
+ json["crit"] = Array(newCritical)
+ let jsonData = try JSONSerialization.data(withJSONObject: json)
+ return jsonData
+}
+
+func prepareHeaderForJWK(header: Data, jwk: JWK?) throws -> Data {
if
var jsonObj = try JSONSerialization.jsonObject(with: header) as? [String: Any],
jsonObj["alg"] == nil
diff --git a/Sources/JSONWebSignature/JWS+Verify.swift b/Sources/JSONWebSignature/JWS+Verify.swift
index fe904d2..6ae4090 100644
--- a/Sources/JSONWebSignature/JWS+Verify.swift
+++ b/Sources/JSONWebSignature/JWS+Verify.swift
@@ -17,6 +17,7 @@
import Foundation
import JSONWebAlgorithms
import JSONWebKey
+import Tools
extension JWS {
/// Verifies the signature of the JWS using the provided key.
@@ -157,6 +158,35 @@ extension JWS {
return try keys.contains { try JWS.verify(jwsJson: jwsJson, key: $0) }
}
}
+
+ /// Verifies the signature of a JSON Web Signature (JWS) object when the payload is unencoded.
+ ///
+ /// This method handles JWS objects that have an unencoded payload, which is indicated by the `b64`
+ /// header parameter set to `false`. It first checks if the JWS header specifies an unencoded payload,
+ /// and then performs the verification accordingly.
+ ///
+ /// - Parameters:
+ /// - jwsString: The compact serialized JWS string.
+ /// - payload: The unencoded payload as `Data`.
+ /// - key: The cryptographic key used for signing, which can be of type `KeyRepresentable`.
+ ///
+ /// - Throws: An error if the verification process fails due to an invalid JWS format, missing key, or other issues.
+ /// - Returns: A Boolean value indicating whether the signature is valid (`true`) or not (`false`).
+ public static func verify(jwsString: String, payload: Data, key: Key?) throws -> Bool {
+ let components = jwsString.components(separatedBy: ".")
+ guard components.count == 3 else {
+ throw JWSError.invalidString
+ }
+ let header = try Base64URL.decode(components[0])
+ guard try unencodedBase64Payload(header: header) else {
+ return try JWS(jwsString: jwsString).verify(key: key)
+ }
+ return try JWS(
+ protectedHeaderData: header,
+ data: payload,
+ signature: try Base64URL.decode(components[2])
+ ).verify(key: key)
+ }
}
func decodeFullOrFlattenedJson<
diff --git a/Sources/JSONWebSignature/JWSRegisteredFieldsHeader.swift b/Sources/JSONWebSignature/JWSRegisteredFieldsHeader.swift
index 6b8a88f..63bc0aa 100644
--- a/Sources/JSONWebSignature/JWSRegisteredFieldsHeader.swift
+++ b/Sources/JSONWebSignature/JWSRegisteredFieldsHeader.swift
@@ -52,7 +52,9 @@ public protocol JWSRegisteredFieldsHeader: Codable {
var contentType: String? { get set }
/// Indicates extensions to this protocol that must be understood and processed.
- var critical: String? { get set }
+ var critical: [String]? { get set }
+
+ var base64EncodedUrlPayload: Bool? { get set }
}
/// `DefaultJWSHeaderImpl` is a default implementation of the `JWSProtectedFieldsHeader` protocol.
@@ -68,7 +70,8 @@ public struct DefaultJWSHeaderImpl: JWSRegisteredFieldsHeader {
public var x509CertificateSHA256Thumbprint: String?
public var type: String?
public var contentType: String?
- public var critical: String?
+ public var critical: [String]?
+ public var base64EncodedUrlPayload: Bool?
/// Initializes a new `DefaultJWSHeaderImpl` instance with optional parameters for each field.
/// - Parameters:
@@ -94,7 +97,8 @@ public struct DefaultJWSHeaderImpl: JWSRegisteredFieldsHeader {
x509CertificateSHA256Thumbprint: String? = nil,
type: String? = nil,
contentType: String? = nil,
- critical: String? = nil
+ critical: [String]? = nil,
+ base64EncodedUrlPayload: Bool? = nil
) {
self.algorithm = algorithm
self.keyID = keyID
@@ -107,6 +111,7 @@ public struct DefaultJWSHeaderImpl: JWSRegisteredFieldsHeader {
self.type = type
self.contentType = contentType
self.critical = critical
+ self.base64EncodedUrlPayload = base64EncodedUrlPayload
}
public init(from: JWK) {
diff --git a/Tests/JWSTests/JWSTests.swift b/Tests/JWSTests/JWSTests.swift
index 5edc748..f3de107 100644
--- a/Tests/JWSTests/JWSTests.swift
+++ b/Tests/JWSTests/JWSTests.swift
@@ -127,4 +127,12 @@ final class JWSTests: XCTestCase {
let keyPair = JWK.testingCurve25519KPair
XCTAssertThrowsError(try JWS(payload: "test".data(using: .utf8)!, protectedHeader: DefaultJWSHeaderImpl(algorithm: .ES512), key: keyPair))
}
+
+ func testJWSUnencodedPayloadCompactString() throws {
+ let payload = "$.02"
+ let keyPair = JWK.testingES256Pair
+ let testJWS = try JWS(payload: payload.data(using: .utf8)!, key: keyPair, options: [.unencodedPayload])
+ XCTAssertTrue(testJWS.compactSerialization.contains(".."))
+ XCTAssertTrue(try JWS.verify(jwsString: testJWS.compactSerialization, payload: payload.data(using: .utf8)!, key: keyPair))
+ }
}