From 88708c593d94a792d3422007b82a85233cc39128 Mon Sep 17 00:00:00 2001 From: Goncalo Frade Date: Thu, 30 May 2024 16:56:35 +0100 Subject: [PATCH] feat(jwa)!: add key representable BREAKING CHANGE: This feature will make the library easier to use, now there is no need parse a key to JWK, just Key just requires to support KeyRepresentable In the future this will enable to add other well known formats like PEM and DER. --- README.md | 33 +- Sources/JSONWebAlgorithms/CryptoError.swift | 4 +- .../KeyManagement/JWKRepresentable.swift | 13 + .../KeyManagement/KeyRepresentable.swift | 136 ------- .../KeyRepresentable/DataKey.swift | 146 +++++++ .../KeyRepresentable/KeyRepresentable.swift | 85 ++++ .../KeyRepresentable/SecKeyExtended.swift | 171 ++++++++ .../Signatures/RSA/Helpers/RSA+Security.swift | 2 +- Sources/JSONWebEncryption/JWE+Decrypt.swift | 53 ++- Sources/JSONWebEncryption/JWE+Encrypt.swift | 83 +++- Sources/JSONWebSignature/JWS+Sign.swift | 366 ++++++------------ Sources/JSONWebSignature/JWS+Verify.swift | 102 ++--- Sources/JSONWebToken/JWT+Encryption.swift | 55 ++- Sources/JSONWebToken/JWT+Signing.swift | 213 ++-------- Sources/JSONWebToken/JWT+Verification.swift | 48 ++- Tests/JWSTests/JWSJsonTests.swift | 23 +- Tests/JWSTests/JWSTests.swift | 5 + Tests/JWSTests/Mocks/JWK+Testing.swift | 17 + Tests/JWSTests/RFC7515Tests.swift | 4 +- 19 files changed, 820 insertions(+), 739 deletions(-) delete mode 100644 Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable.swift create mode 100644 Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/DataKey.swift create mode 100644 Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/KeyRepresentable.swift create mode 100644 Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/SecKeyExtended.swift diff --git a/README.md b/README.md index 5d110bb..1ff4f8c 100644 --- a/README.md +++ b/README.md @@ -272,9 +272,8 @@ Example: ```swift let payload = "Hello world".data(using: .utf8)! let key = secp256k1.Signing.PrivateKey() -let keyJWK = key.jwkRepresentation -let jws = try JWS(payload: payload, key: keyJWK) +let jws = try JWS(payload: payload, key: key) let jwsString = jws.compactSerialization @@ -288,6 +287,7 @@ let rsaKeyId = "Hello-keyId" var header = DefaultJWSHeaderImpl() header.keyID = rsaKeyId header.algorithm = .rsa512 + let keyJWK = JWK(keyType: .rsa, algorithm: "RSA512", keyID: rsaKeyId, e: rsaKeyExponent, n: rsaKeyModulus) let jwe = try JWS(payload: payload, protectedHeader: header, key: jwk) ``` @@ -334,7 +334,7 @@ JWE represents encrypted content using JSON-based data structures, following the 3. **Compression Algorithms**: - DEFLATE (zip) -Example: +Example1: ```swift let payload = "Hello world".data(using: .utf8)! @@ -355,6 +355,27 @@ let jwe = try JWE(compactString: compact) let decrypted = try jwe.decrypt(recipientKey: recipientJWK) ``` +Example2: + +```swift +let payload = "Hello world".data(using: .utf8)! +let key = P256.Signing.PrivateKey() + + +let serialization = try JWE( + payload: payload, + keyManagementAlg: .a256KW, + encryptionAlgorithm: .a256GCM, + compressionAlgorithm: .zip, + recipientKey: key +) + +let compact = serialization.compactSerialization() + +let jwe = try JWE(compactString: compact) +let decrypted = try jwe.decrypt(recipientKey: recipientJWK) +``` + If you want to add additional headers beyond the default to the JWE: ```swift @@ -403,7 +424,6 @@ Example: ```swift let key = P256.Signing.PrivateKey() -let keyJWK = key.jwkRepresentation let mockClaims = DefaultJWTClaims( iss: "testAlice", sub: "Alice", @@ -426,7 +446,6 @@ let verifiedPayload = verifiedJWT.payload ```swift let key = Curve25519.KeyAgreement.PrivateKey() -let keyJWK = key.jwkRepresentation let mockClaims = DefaultJWTClaims( iss: "testAlice", sub: "Alice", @@ -436,7 +455,7 @@ let mockClaims = DefaultJWTClaims( let jwt = try JWT.encrypt( payload: payload, protectedHeader: DefaultJWSHeaderImpl(keyManagementAlgorithm: .a128KW, encodingAlgorithm: .a128CBCHS256), - recipientKey: keyJWK + recipientKey: key ) let jwtString = jwt.jwtString @@ -449,7 +468,7 @@ let verifiedPayload = verifiedJWT.payload - Standard Claims on signing a JWT ```swift - let key = JWK.testingES256Pair + let key = P256.Signing.PrivateKey() let jwt = try JWT.signed( payload: { diff --git a/Sources/JSONWebAlgorithms/CryptoError.swift b/Sources/JSONWebAlgorithms/CryptoError.swift index e6bdaf3..2a6feea 100644 --- a/Sources/JSONWebAlgorithms/CryptoError.swift +++ b/Sources/JSONWebAlgorithms/CryptoError.swift @@ -17,7 +17,7 @@ import Foundation /// `CryptoError` is an enumeration representing various errors that can occur in cryptographic operations. -enum CryptoError: LocalizedError { +public enum CryptoError: LocalizedError { /// Error indicating that the initialization vector is missing for an operation that requires it. case missingInitializationVector @@ -77,4 +77,6 @@ enum CryptoError: LocalizedError { /// - type: The key type. /// - curve: Optional curve name, if applicable. case cannotGenerateKeyForTypeAndCurve(type: String, curve: String?) + + case keyFormatNotSupported(format: String, supportedFormats: [String]) } diff --git a/Sources/JSONWebAlgorithms/KeyManagement/JWKRepresentable.swift b/Sources/JSONWebAlgorithms/KeyManagement/JWKRepresentable.swift index 64f62c4..2a5aac7 100644 --- a/Sources/JSONWebAlgorithms/KeyManagement/JWKRepresentable.swift +++ b/Sources/JSONWebAlgorithms/KeyManagement/JWKRepresentable.swift @@ -7,6 +7,7 @@ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import CryptoKit +import CryptoSwift import Foundation import JSONWebKey import secp256k1 @@ -351,3 +352,15 @@ extension Curve25519.Signing.PublicKey: JWKRepresentable { ) } } + +extension CryptoSwift.RSA: JWKRepresentable { + /// Returns the JWK representation of a `RSA` key instance. + public var jwkRepresentation: JWK { + JWK( + keyType: .rsa, + e: e.serialize(), + n: n.serialize(), + d: d?.serialize() + ) + } +} diff --git a/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable.swift b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable.swift deleted file mode 100644 index 8a686d8..0000000 --- a/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable.swift +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2024 Gonçalo Frade - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import CryptoSwift -import CryptoKit -import Foundation -import JSONWebKey -import secp256k1 - -extension JWK { - public enum KeyFormat { - case rsa - case curve25519 - case p256 - case p384 - case p512 - case secp256k1 - case octSequence - } - - public static func JWKFrom(format: KeyFormat, isPrivate: Bool, isKeyAgreement: Bool, key: Key) throws -> JWK { - switch key { - case let value as Data: - return try getJWKFromData(format: format, isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, value: value) - case let value as SecKey: - return try getJWKFromSecKey(format: format, isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, value: value) - case let value as JWK: - return value - default: - throw isPrivate ? CryptoError.notValidPrivateKey : CryptoError.notValidPublicKey - } - } -} - -private func getJWKFromData(format: JWK.KeyFormat, isPrivate: Bool, isKeyAgreement: Bool, value: Data) throws -> JWK { - switch format { - case .rsa: - let rsaKey = try CryptoSwift.RSA(rawRepresentation: value) - let n = rsaKey.n.serialize() - let e = rsaKey.e.serialize() - let d = rsaKey.d?.serialize() - - return JWK(keyType: .rsa, e: e, n: n, d: d) - case .curve25519 where isKeyAgreement == false: - if isPrivate { - return try Curve25519.Signing.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try Curve25519.Signing.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .curve25519 where isKeyAgreement == true: - if isPrivate { - return try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try Curve25519.KeyAgreement.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .p256 where isKeyAgreement == false: - if isPrivate { - return try P256.Signing.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try P256.Signing.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .p256 where isKeyAgreement == true: - if isPrivate { - return try P256.KeyAgreement.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try P256.KeyAgreement.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .p384 where isKeyAgreement == false: - if isPrivate { - return try P384.Signing.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try P384.Signing.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .p384 where isKeyAgreement == true: - if isPrivate { - return try P384.KeyAgreement.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try P384.KeyAgreement.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .p512 where isKeyAgreement == false: - if isPrivate { - return try P521.Signing.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try P521.Signing.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .p512 where isKeyAgreement == true: - if isPrivate { - return try P521.KeyAgreement.PrivateKey(rawRepresentation: value).jwkRepresentation - } else { - return try P521.KeyAgreement.PublicKey(rawRepresentation: value).jwkRepresentation - } - case .secp256k1 where isKeyAgreement == false: - if isPrivate { - return try secp256k1.Signing.PrivateKey(dataRepresentation: value).jwkRepresentation - } else { - return try secp256k1.Signing.PublicKey(dataRepresentation: value, format: .uncompressed).jwkRepresentation - } - case .secp256k1 where isKeyAgreement == true: - if isPrivate { - return try secp256k1.KeyAgreement.PrivateKey(dataRepresentation: value).jwkRepresentation - } else { - return try secp256k1.KeyAgreement.PublicKey(dataRepresentation: value, format: .uncompressed).jwkRepresentation - } - case .octSequence: - return JWK(keyType: .octetSequence, key: value) - default: - throw isPrivate ? CryptoError.notValidPrivateKey : CryptoError.notValidPublicKey - } -} - -private func getJWKFromSecKey(format: JWK.KeyFormat, isPrivate: Bool, isKeyAgreement: Bool, value: SecKey) throws -> JWK { - let data = try convertSecKeyToData(value) - return try getJWKFromData(format: format, isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, value: data) -} - -private func convertSecKeyToData(_ secKey: SecKey) throws -> Data { - var error: Unmanaged? - guard let keyData = SecKeyCopyExternalRepresentation(secKey, &error) else { - throw CryptoError.securityLayerError(internalStatus: nil, internalError: error?.takeRetainedValue()) - } - - return keyData as Data -} diff --git a/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/DataKey.swift b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/DataKey.swift new file mode 100644 index 0000000..bcd0c7c --- /dev/null +++ b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/DataKey.swift @@ -0,0 +1,146 @@ +/* + * Copyright 2024 Gonçalo Frade + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import CryptoSwift +import CryptoKit +import Foundation +import JSONWebKey +import secp256k1 + +public struct DataKey { + public enum SupportedKeyType { + case rsa + case curve25519 + case p256 + case p384 + case p521 + case secp256k1 + case octSequence + } + + public let jwk: JWK + + public init(type: SupportedKeyType, isPrivate: Bool, isKeyAgreement: Bool, key: Data) throws { + self.jwk = try buildJWK(type: type, isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, key: key) + } +} + +private func buildJWK(type: DataKey.SupportedKeyType, isPrivate: Bool, isKeyAgreement: Bool, key: Data) throws -> JWK { + switch type { + case .rsa: + return try buildRSA(keyData: key) + case .p256: + return try buildP256Key(isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, keyData: key) + case .p384: + return try buildP384Key(isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, keyData: key) + case .p521: + return try buildP521Key(isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, keyData: key) + case .curve25519: + return try buildCurve25519Key(isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, keyData: key) + case .secp256k1: + return try buildSecp256k1Key(isPrivate: isPrivate, isKeyAgreement: isKeyAgreement, keyData: key) + case .octSequence: + return buildOctetSequence(keyData: key) + } +} + +private func buildOctetSequence(keyData: Data) -> JWK { + return JWK(keyType: .octetSequence, key: keyData) +} + +private func buildRSA(keyData: Data) throws -> JWK { + return try CryptoSwift.RSA(rawRepresentation: keyData).jwkRepresentation +} + +private func buildP256Key(isPrivate: Bool, isKeyAgreement: Bool, keyData: Data) throws -> JWK { + if isPrivate { + if !isKeyAgreement { + return try P256.Signing.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try P256.KeyAgreement.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } + } + + if !isKeyAgreement { + return try P256.Signing.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try P256.KeyAgreement.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } +} + +private func buildP384Key(isPrivate: Bool, isKeyAgreement: Bool, keyData: Data) throws -> JWK { + if isPrivate { + if !isKeyAgreement { + return try P384.Signing.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try P384.KeyAgreement.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } + } + + if !isKeyAgreement { + return try P384.Signing.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try P384.KeyAgreement.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } +} + +private func buildP521Key(isPrivate: Bool, isKeyAgreement: Bool, keyData: Data) throws -> JWK { + if isPrivate { + if !isKeyAgreement { + return try P521.Signing.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try P521.KeyAgreement.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } + } + + if !isKeyAgreement { + return try P521.Signing.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try P521.KeyAgreement.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } +} + +private func buildCurve25519Key(isPrivate: Bool, isKeyAgreement: Bool, keyData: Data) throws -> JWK { + if isPrivate { + if !isKeyAgreement { + return try Curve25519.Signing.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try Curve25519.KeyAgreement.PrivateKey.init(rawRepresentation: keyData).jwkRepresentation + } + } + + if !isKeyAgreement { + return try Curve25519.Signing.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } else { + return try Curve25519.KeyAgreement.PublicKey.init(rawRepresentation: keyData).jwkRepresentation + } +} + +private func buildSecp256k1Key(isPrivate: Bool, isKeyAgreement: Bool, keyData: Data) throws -> JWK { + if isPrivate { + if !isKeyAgreement { + return try secp256k1.Signing.PrivateKey.init(dataRepresentation: keyData, format: .uncompressed).jwkRepresentation + } else { + return try secp256k1.KeyAgreement.PrivateKey.init(dataRepresentation: keyData, format: .uncompressed).jwkRepresentation + } + } + + if !isKeyAgreement { + return try secp256k1.Signing.PublicKey.init(dataRepresentation: keyData, format: .uncompressed).jwkRepresentation + } else { + return try secp256k1.KeyAgreement.PublicKey.init(dataRepresentation: keyData, format: .uncompressed).jwkRepresentation + } +} diff --git a/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/KeyRepresentable.swift b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/KeyRepresentable.swift new file mode 100644 index 0000000..6ec5585 --- /dev/null +++ b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/KeyRepresentable.swift @@ -0,0 +1,85 @@ +import CryptoKit +import CryptoSwift +import Foundation +import JSONWebKey +import secp256k1 + +public protocol KeyRepresentable { + var jwk: JWK { get throws } +} + +extension JWK: KeyRepresentable { + public var jwk: JWK { self } +} + +extension RSA: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} + +extension Curve25519.Signing.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension Curve25519.Signing.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension Curve25519.KeyAgreement.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension Curve25519.KeyAgreement.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P256.Signing.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P256.Signing.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P256.KeyAgreement.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P256.KeyAgreement.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P384.Signing.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P384.Signing.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P384.KeyAgreement.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P384.KeyAgreement.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P521.Signing.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P521.Signing.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P521.KeyAgreement.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension P521.KeyAgreement.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension secp256k1.Signing.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension secp256k1.Signing.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension secp256k1.KeyAgreement.PrivateKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension secp256k1.KeyAgreement.PublicKey: KeyRepresentable { + public var jwk: JWK { self.jwkRepresentation } +} +extension SecKey: KeyRepresentable { + public var jwk: JWK { + get throws { + try SecKeyExtended(secKey: self).jwk() + } + } +} diff --git a/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/SecKeyExtended.swift b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/SecKeyExtended.swift new file mode 100644 index 0000000..847116e --- /dev/null +++ b/Sources/JSONWebAlgorithms/KeyManagement/KeyRepresentable/SecKeyExtended.swift @@ -0,0 +1,171 @@ +import CryptoKit +import CryptoSwift +import Foundation +import JSONWebKey +import Security + +public struct SecKeyExtended { + public enum SupportedKeyType { + case rsa + case p256 + case p384 + case p521 + } + + public enum KeyUsage { + case sign + case keyAgreement + case unknown + } + + public enum SecKeyError: Error { + case invalidKey + case unsupportedKeyType + case keyDataExtractionFailed + } + + public let key: SecKey + public let keyType: SupportedKeyType + public let isPrivate: Bool + public let keyData: Data + public let keyUsage: KeyUsage + + public init(secKey: SecKey) throws { + self.key = secKey + + // Get key attributes + guard let attributes = SecKeyCopyAttributes(secKey) as? [String: Any] else { + throw SecKeyError.invalidKey + } + + // Determine key type + guard let keyTypeString = attributes[kSecAttrKeyType as String] as? String else { + throw SecKeyError.unsupportedKeyType + } + + switch keyTypeString as CFString { + case kSecAttrKeyTypeRSA: + self.keyType = .rsa + case kSecAttrKeyTypeECSECPrimeRandom: + guard let keySize = attributes[kSecAttrKeySizeInBits as String] as? Int else { + throw SecKeyError.unsupportedKeyType + } + switch keySize { + case 256: + self.keyType = .p256 + case 384: + self.keyType = .p384 + case 521: + self.keyType = .p521 + default: + throw SecKeyError.unsupportedKeyType + } + default: + throw SecKeyError.unsupportedKeyType + } + + // Determine if the key is private or public + guard let keyClass = attributes[kSecAttrKeyClass as String] as? String else { + throw SecKeyError.invalidKey + } + self.isPrivate = (keyClass == (kSecAttrKeyClassPrivate as String)) + + // Extract key data + var error: Unmanaged? + guard let keyData = SecKeyCopyExternalRepresentation(secKey, &error) as Data? else { + throw CryptoError.securityLayerError( + internalStatus: (error?.takeUnretainedValue() as? NSError)?.code, + internalError: (error?.takeUnretainedValue() as? NSError) + ) + } + + self.keyData = keyData + + // Determine key usage + let canSign = (attributes[kSecAttrCanSign as String] as? Bool) ?? false + let canDerive = (attributes[kSecAttrCanDerive as String] as? Bool) ?? false + + if canSign { + self.keyUsage = .sign + } else if canDerive { + self.keyUsage = .keyAgreement + } else { + self.keyUsage = .unknown + } + } + + public func jwk() throws -> JWK { + return try buildJWK() + } + + private func buildJWK() throws -> JWK { + switch keyType { + case .rsa: + return try buildRSA() + case .p256: + return try buildP256Key() + case .p384: + return try buildP384Key() + case .p521: + return try buildP521Key() + } + } + + private func buildRSA() throws -> JWK { + return try CryptoSwift.RSA(rawRepresentation: keyData).jwkRepresentation + } + + private func buildP256Key() throws -> JWK { + if isPrivate { + switch keyUsage { + case .sign, .unknown: + return try P256.Signing.PrivateKey.init(x963Representation: keyData).jwkRepresentation + case .keyAgreement: + return try P256.KeyAgreement.PrivateKey.init(x963Representation: keyData).jwkRepresentation + } + } + + switch keyUsage { + case .sign, .unknown: + return try P256.Signing.PublicKey.init(x963Representation: keyData).jwkRepresentation + case .keyAgreement: + return try P256.KeyAgreement.PublicKey.init(x963Representation: keyData).jwkRepresentation + } + } + + private func buildP384Key() throws -> JWK { + if isPrivate { + switch keyUsage { + case .sign, .unknown: + return try P384.Signing.PrivateKey.init(x963Representation: keyData).jwkRepresentation + case .keyAgreement: + return try P384.KeyAgreement.PrivateKey.init(x963Representation: keyData).jwkRepresentation + } + } + + switch keyUsage { + case .sign, .unknown: + return try P384.Signing.PublicKey.init(x963Representation: keyData).jwkRepresentation + case .keyAgreement: + return try P384.KeyAgreement.PublicKey.init(x963Representation: keyData).jwkRepresentation + } + } + + private func buildP521Key() throws -> JWK { + if isPrivate { + switch keyUsage { + case .sign, .unknown: + return try P521.Signing.PrivateKey.init(x963Representation: keyData).jwkRepresentation + case .keyAgreement: + return try P521.KeyAgreement.PrivateKey.init(x963Representation: keyData).jwkRepresentation + } + } + + switch keyUsage { + case .sign, .unknown: + return try P521.Signing.PublicKey.init(x963Representation: keyData).jwkRepresentation + case .keyAgreement: + return try P521.KeyAgreement.PublicKey.init(x963Representation: keyData).jwkRepresentation + } + } +} diff --git a/Sources/JSONWebAlgorithms/Signatures/RSA/Helpers/RSA+Security.swift b/Sources/JSONWebAlgorithms/Signatures/RSA/Helpers/RSA+Security.swift index 4c5561b..d877f7e 100644 --- a/Sources/JSONWebAlgorithms/Signatures/RSA/Helpers/RSA+Security.swift +++ b/Sources/JSONWebAlgorithms/Signatures/RSA/Helpers/RSA+Security.swift @@ -21,7 +21,7 @@ import Security extension RSA { func getSecKey() throws -> SecKey { let raw = try externalRepresentation() - let attributes: [String:Any] = [ + let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeRSA, kSecAttrKeyClass as String: d != nil ? kSecAttrKeyClassPrivate : kSecAttrKeyClassPublic, kSecAttrKeySizeInBits as String: keySize, diff --git a/Sources/JSONWebEncryption/JWE+Decrypt.swift b/Sources/JSONWebEncryption/JWE+Decrypt.swift index 5bb1206..ea28a28 100644 --- a/Sources/JSONWebEncryption/JWE+Decrypt.swift +++ b/Sources/JSONWebEncryption/JWE+Decrypt.swift @@ -22,6 +22,7 @@ import Tools extension JWE { /// Initializes a `JWE` object from a compact serialization string. /// This method decodes the serialized string into its respective components. + /// /// - Parameters: /// - compactString: The compact serialization string of the JWE. /// - encryptionModule: The encryption module to use, defaulting to the standard module. @@ -48,6 +49,10 @@ extension JWE { /// This method decrypts a `JWE` object using the provided keys and optional password. /// It determines the appropriate decryption algorithm based on the JWE headers. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - senderKey: The sender's key, if applicable. Used in certain key agreement protocols. /// - recipientKey: The recipient's key, if applicable. Typically used for asymmetric decryption. @@ -56,9 +61,9 @@ extension JWE { /// - Returns: The decrypted data as `Data`. /// - Throws: `JWEError` for errors related to missing algorithms, keys, or failed decryption. public func decrypt( - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, password: Data? = nil ) throws -> Data { guard let alg = getKeyAlgorithm( @@ -77,9 +82,9 @@ extension JWE { initializationVector: initializationVector, authenticationTag: authenticationTag, additionalAuthenticationData: additionalAuthenticatedData, - senderKey: senderKey, - recipientKey: recipientKey, - sharedKey: sharedKey, + senderKey: senderKey.map { try prepareJWK(key: $0) }, + recipientKey: recipientKey.map { try prepareJWK(key: $0) }, + sharedKey: sharedKey.map { try prepareJWK(key: $0) }, password: password ) } @@ -88,6 +93,10 @@ extension JWE { /// /// This method is used to decrypt a `JWE` that is represented as a compact serialization string. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - compactString: The compact serialization string of the JWE. /// - senderKey: The sender's key, if applicable. @@ -98,9 +107,9 @@ extension JWE { /// - Throws: `JWEError` for errors related to parsing the compact string, missing algorithms, keys, or failed decryption. public static func decrypt( compactString: String, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, password: Data? = nil ) throws -> Data { try JWE(compactString: compactString) @@ -116,6 +125,10 @@ extension JWE { /// /// This method is used to decrypt a `JWE` that is represented as JSON data. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - jweJson: The JSON data representing the JWE. /// - senderKey: The sender's key, if applicable. @@ -127,9 +140,9 @@ extension JWE { /// - Throws: `JWEError` for errors related to parsing the JSON data, missing algorithms, keys, or failed decryption. public static func decrypt( jweJson: Data, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, password: Data? = nil, tryAllRecipients: Bool = false ) throws -> Data { @@ -148,6 +161,10 @@ extension JWE { /// /// This method allows for decryption of a `JWE` represented as a `JWEJson` object with custom header types. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - jweJson: The `JWEJson` object representing the JWE. /// - senderKey: The sender's key, if applicable. @@ -163,9 +180,9 @@ extension JWE { R: JWERegisteredFieldsHeader >( jweJson: JWEJson, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, password: Data? = nil, tryAllRecipients: Bool = false ) throws -> Data { @@ -181,9 +198,9 @@ extension JWE { recipients: jweJson.recipients.map { ($0.header, $0.encryptedKey)}, initializationVector: jweJson.initializationVector, authenticationTag: jweJson.authenticationTag, - senderKey: senderKey, - recipientKey: recipientKey, - sharedKey: sharedKey, + senderKey: senderKey.map { try prepareJWK(key: $0) }, + recipientKey: recipientKey.map { try prepareJWK(key: $0) }, + sharedKey: sharedKey.map { try prepareJWK(key: $0) }, additionalAuthenticationData: aad, tryAllRecipients: tryAllRecipients, password: password diff --git a/Sources/JSONWebEncryption/JWE+Encrypt.swift b/Sources/JSONWebEncryption/JWE+Encrypt.swift index 16ddc90..e90c450 100644 --- a/Sources/JSONWebEncryption/JWE+Encrypt.swift +++ b/Sources/JSONWebEncryption/JWE+Encrypt.swift @@ -23,6 +23,10 @@ extension JWE { /// Initializes a `JWE` object for encryption, given the payload and various encryption parameters. /// /// This method configures a `JWE` object with specified encryption settings, preparing it for data encryption. + /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - payload: The data to be encrypted. @@ -41,8 +45,8 @@ extension JWE { payload: Data, keyManagementAlg: KeyManagementAlgorithm, encryptionAlgorithm: ContentEncryptionAlgorithm, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, @@ -58,8 +62,8 @@ extension JWE { let parts = try JWE.encryptionModule.encryptor(alg: keyManagementAlg).encrypt( payload: payload, - senderKey: senderKey, - recipientKey: recipientKey, + senderKey: senderKey.map { try prepareJWK(key: $0) }, + recipientKey: recipientKey.map { try prepareJWK(key: $0) }, protectedHeader: protectedHeader, cek: cek, initializationVector: initializationVector, @@ -84,6 +88,10 @@ extension JWE { /// /// This method allows for a high level of customization of the `JWE` header parameters and encryption settings. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The data to be encrypted. /// - protectedHeader: Optional protected header, specifying encryption parameters. @@ -101,8 +109,8 @@ extension JWE { payload: Data, protectedHeader: P? = nil as DefaultJWEHeaderImpl?, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, @@ -122,8 +130,8 @@ extension JWE { let parts = try JWE.encryptionModule.encryptor(alg: alg).encrypt( payload: payload, - senderKey: senderKey, - recipientKey: recipientKey, + senderKey: senderKey.map { try prepareJWK(key: $0) }, + recipientKey: recipientKey.map { try prepareJWK(key: $0) }, protectedHeader: protectedHeader, unprotectedHeader: unprotectedHeader, cek: cek, @@ -154,6 +162,10 @@ extension JWE { /// This method is tailored for scenarios requiring a specific combination of encryption and key management algorithms, /// while also allowing a custom unprotected header. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The data to be encrypted. /// - keyManagementAlg: The key management algorithm. @@ -173,8 +185,8 @@ extension JWE { keyManagementAlg: KeyManagementAlgorithm, encryptionAlgorithm: ContentEncryptionAlgorithm, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, cek: Data? = nil, additionalAuthenticationData: Data? = nil, password: Data? = nil, @@ -204,6 +216,10 @@ extension JWE { /// /// This method allows for a high degree of flexibility by accepting generic header types and a list of recipients. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The data to be encrypted. /// - protectedHeader: Optional custom protected header. @@ -226,8 +242,8 @@ extension JWE { payload: Data, protectedHeader: P? = nil as DefaultJWEHeaderImpl?, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipients: [(header: R, key: JWK)], + senderKey: KeyRepresentable? = nil, + recipients: [(header: R, key: KeyRepresentable)], cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, @@ -237,8 +253,8 @@ extension JWE { ) throws -> JWEJson { let recipientParts = try encryptionModule.multiEncryptor.encrypt( payload: payload, - senderKey: senderKey, - recipients: recipients.map { ($0.header, $0.key) }, + senderKey: senderKey.map { try prepareJWK(key: $0) }, + recipients: try recipients.map { ($0.header, try prepareJWK(key: $0.key)) }, protectedHeader: protectedHeader, unprotectedHeader: unprotectedHeader, cek: cek, @@ -277,6 +293,10 @@ extension JWE { /// This method allows for specifying a custom shared unprotected header while using default headers for the protected /// and recipient-specific headers. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The data to be encrypted. /// - encryptionAlgorithm: The content encryption algorithm to be used. @@ -295,8 +315,8 @@ extension JWE { payload: Data, encryptionAlgorithm: ContentEncryptionAlgorithm, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipients: [(alg: KeyManagementAlgorithm, key: JWK)], + senderKey: KeyRepresentable? = nil, + recipients: [(alg: KeyManagementAlgorithm, key: KeyRepresentable)], cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, @@ -330,6 +350,10 @@ extension JWE { /// /// This method is particularly used when you have multiple recipients and a single encryption algorithm. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The data to be encrypted. /// - encryptionAlgorithm: The content encryption algorithm to be used. @@ -346,8 +370,8 @@ extension JWE { public static func jsonSerialization( payload: Data, encryptionAlgorithm: ContentEncryptionAlgorithm, - senderKey: JWK? = nil, - recipients: [(alg: KeyManagementAlgorithm, key: JWK)], + senderKey: KeyRepresentable? = nil, + recipients: [(alg: KeyManagementAlgorithm, key: KeyRepresentable)], cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, @@ -374,6 +398,10 @@ extension JWE { /// /// This method is useful for encrypting data for multiple recipients, each potentially using different encryption keys. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The data to be encrypted. /// - protectedHeader: Optional protected header. It should conform to `JWERegisteredFieldsHeader`. @@ -395,8 +423,8 @@ extension JWE { payload: Data, protectedHeader: P? = nil as DefaultJWEHeaderImpl?, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipientKeys: [JWK], + senderKey: KeyRepresentable? = nil, + recipientKeys: [KeyRepresentable], cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, @@ -409,7 +437,7 @@ extension JWE { protectedHeader: protectedHeader, unprotectedHeader: unprotectedHeader, senderKey: senderKey, - recipients: recipientKeys.map { (DefaultJWEHeaderImpl(from: $0), $0)}, + recipients: try recipientKeys.map { (DefaultJWEHeaderImpl(from: try prepareJWK(key: $0)), $0)}, cek: cek, initializationVector: initializationVector, additionalAuthenticationData: additionalAuthenticationData, @@ -419,3 +447,16 @@ extension JWE { ) } } + +func prepareJWK(key: KeyRepresentable?) throws -> JWK { + switch key { + case let value as SecKey: + return try SecKeyExtended(secKey: value).jwk() + case let value as JWK: + return value + case let value as JWKRepresentable: + return value.jwkRepresentation + default: + throw CryptoError.keyFormatNotSupported(format: String(describing: key.self), supportedFormats: ["JWK", "SecKey", "JWKRepresentable"]) + } +} diff --git a/Sources/JSONWebSignature/JWS+Sign.swift b/Sources/JSONWebSignature/JWS+Sign.swift index 7ce89df..73fed19 100644 --- a/Sources/JSONWebSignature/JWS+Sign.swift +++ b/Sources/JSONWebSignature/JWS+Sign.swift @@ -20,49 +20,22 @@ import JSONWebKey import Tools extension JWS { - - /// Initializes a new `JWS` instance using raw header data, payload data, and a JSON Web Key (JWK). - /// The header is prepared for the JWK, and the signature is generated using the provided key. - /// - /// - Parameters: - /// - payload: The payload data. - /// - protectedHeaderData: The raw header data. - /// - key: The `JWK` used for signing. - /// - Throws: An error if the signing process fails, or if the key is missing. - public init(payload: Data, protectedHeaderData: Data, key: JWK?) throws { - let signature: Data - let header = try prepareHeaderForJWK(header: protectedHeaderData, jwk: key) - let protectedHeader = try JSONDecoder().decode(DefaultJWSHeaderImpl.self, from: header) - if let signer = protectedHeader.algorithm?.cryptoSigner { - guard let key else { - throw JWSError.missingKey - } - let signingData = try JWS.buildSigningData(header: header, data: payload) - signature = try signer.sign(data: signingData, key: key) - } else { - signature = Data() - } - self.protectedHeaderData = header - self.protectedHeader = protectedHeader - self.payload = payload - self.signature = signature - self.compactSerialization = try JWS.buildJWSString(header: header, data: payload, signature: signature) - } - /// Initializes a new JWS (JSON Web Signature) instance with the given payload, protected header data, and key. /// - /// This initializer supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// When using `Data` as the key type, the `alg` (algorithm) field must be set in the header. /// /// - Parameters: /// - payload: The data to be signed and included in the JWS. /// - protectedHeaderData: The data for the protected header, which must be JSON encoded. - /// - key: The cryptographic key used for signing, which can be of type `Data`, `SecKey`, or `JWK`. + /// - 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 { let signature: Data - let key = try prepareJWKFromHeader(header: protectedHeaderData, key: key, isPrivate: true) + let key = try key.map { try prepareJWK(header: protectedHeaderData, key: $0, isPrivate: true) } let protectedHeader = try JSONDecoder().decode(DefaultJWSHeaderImpl.self, from: protectedHeaderData) if let signer = protectedHeader.algorithm?.cryptoSigner { guard let key else { @@ -80,48 +53,22 @@ extension JWS { self.compactSerialization = try JWS.buildJWSString(header: protectedHeaderData, data: payload, signature: signature) } - /// Initializes a new `JWS` instance using a `JWSProtectedFieldsHeader` instance, payload data, and a JSON Web Key (JWK). - /// The header is encoded and then prepared for the JWK, and the signature is generated using the provided key. - /// - /// - Parameters: - /// - header: The `JWSProtectedFieldsHeader` instance. - /// - data: The payload data. - /// - key: The `JWK` used for signing. - /// - Throws: An error if the signing process fails, or if the key is missing. - public init(payload: Data, protectedHeader: JWSRegisteredFieldsHeader, key: JWK?) throws { - let signature: Data - let headerData = try JSONEncoder.jose.encode(protectedHeader) - let header = try prepareHeaderForJWK(header: headerData, jwk: key) - if let signer = protectedHeader.algorithm?.cryptoSigner { - guard let key else { - throw JWSError.missingKey - } - let signingData = try JWS.buildSigningData(header: headerData, data: payload) - signature = try signer.sign(data: signingData, key: key) - } else { - signature = Data() - } - self.protectedHeaderData = header - self.protectedHeader = protectedHeader - self.payload = payload - self.signature = signature - self.compactSerialization = try JWS.buildJWSString(header: headerData, data: payload, signature: signature) - } - /// Initializes a new `JWS` instance using a `JWSProtectedFieldsHeader` instance, payload data, and a JSON Web Key (JWK). /// - /// This initializer supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// When using `Data` as the key type, the `alg` (algorithm) field must be set in the header. /// /// - Parameters: /// - header: The `JWSProtectedFieldsHeader` instance. /// - data: The payload data. - /// - key: The cryptographic key used for signing, which can be of type `Data`, `SecKey`, or `JWK`. + /// - 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 { let signature: Data let headerData = try JSONEncoder.jose.encode(protectedHeader) - let key = try prepareJWKFromHeader(header: headerData, key: key, isPrivate: true) + let key = try key.map { try prepareJWK(header: headerData, key: $0, isPrivate: true) } if let signer = protectedHeader.algorithm?.cryptoSigner { guard let key else { throw JWSError.missingKey @@ -141,12 +88,18 @@ extension JWS { /// Convenience initializer to create a `JWS` instance using payload data and a JSON Web Key (JWK). /// The signing algorithm is determined from the key, and a default header is created and used. /// + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// When using `Data` as the key type, the `alg` (algorithm) field must be set in the header. + /// /// - Parameters: /// - data: The payload data. - /// - key: The `JWK` used for signing. + /// - 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: JWK) throws { - let algorithm = try key.signingAlgorithm() + public init(payload: Data, key: Key) 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) } @@ -154,19 +107,24 @@ extension JWS { /// Generates a JSON serialization of the JWS object with multiple signatures, each corresponding to a different key in the provided array. /// This method is used when a payload needs to be signed with multiple keys. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The payload data to be signed. - /// - keys: An array of `JWK`s used for signing. + /// - keys: An array of cryptographic keys used for signing, each of which can be of type `KeyRepresentable`. /// - Returns: A `JWSJson` object representing the signed payload with multiple signatures. /// - Throws: An error if the signing process fails. static func jsonSerialization( payload: Data, - keys: [JWK] + keys: [KeyRepresentable] ) throws -> JWSJson { let signatures = try keys .map { + let key = try $0.jwk let jws = try JWS.init(payload: payload, key: $0) - let header = $0.keyID != nil ? DefaultJWSHeaderImpl(from: $0) : jws.protectedHeader + let header = key.keyID != nil ? DefaultJWSHeaderImpl(from: key) : jws.protectedHeader // This should never be triggered, I just feel the JWS interface is quite right, and dont want to add any generics. guard @@ -190,14 +148,18 @@ extension JWS { /// Encodes the JWS object with multiple signatures into JSON data. /// This is a wrapper around the `jsonSerialization(payload:keys:)` method that encodes the result into JSON. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The payload data to be signed. - /// - keys: An array of `JWK`s used for signing. + /// - keys: An array of cryptographic keys used for signing, each of which can be of type `KeyRepresentable`. /// - Returns: JSON encoded data representing the signed payload with multiple signatures. /// - Throws: An error if the JSON encoding process fails. public static func jsonSerialization( payload: Data, - keys: [JWK] + keys: [KeyRepresentable] ) throws -> Data { let json: JWSJson = try jsonSerialization(payload: payload, keys: keys) return try JSONEncoder.jose.encode(json) @@ -206,18 +168,22 @@ extension JWS { /// Generates a JSON serialization of the JWS object with signatures for a given payload, protected header, header, and keys. /// This method allows for specifying custom types for the protected header and header. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The payload data. /// - protectedHeader: The protected header instance. /// - unprotectedHeader: An optional header instance. - /// - keys: An array of `JWK`s used for signing. + /// - keys: An array of cryptographic keys used for signing, each of which can be of type `KeyRepresentable`. /// - Returns: A `JWSJson` object with the specified header types. /// - Throws: An error if the signing process fails. static func jsonSerialization( payload: Data, protectedHeader: P, unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - keys: [JWK] + keys: [KeyRepresentable] ) throws -> JWSJson { let signatures = try keys .map { @@ -240,91 +206,25 @@ extension JWS { return try jsonSerialization(payload: payload, signatures: signatures) } - /// Creates a JSON Web Signature (JWS) object using the provided payload, headers, and keys. - /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. - /// - /// - Parameters: - /// - payload: The data to be signed and included in the JWS. - /// - protectedHeader: The protected header fields conforming to `JWSRegisteredFieldsHeader`. - /// - unprotectedHeader: The optional unprotected header fields, defaulting to `nil`. - /// - keys: An array of cryptographic keys used for signing, each of which can be of type `Data`, `SecKey`, or `JWK`. - /// - /// - Throws: An error if the signing process or JSON serialization fails. - /// - Returns: A `JWSJson` object containing the payload and signatures. - static func jsonSerialization( - payload: Data, - protectedHeader: P, - unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - keys: [Key] - ) throws -> JWSJson { - let signatures = try keys - .map { - let headerData = try JSONEncoder.jose.encode(protectedHeader) - let key = try prepareJWKFromHeader(header: headerData, key: $0, isPrivate: true) - let jws = try JWS.init(payload: payload, protectedHeader: protectedHeader, key: key) - // This should never be triggered, I just feel the JWS interface is quite right, and dont want to add any generics. - guard - let typedProtected = jws.protectedHeader as? P - else { - throw JWSError.somethingWentWrong - } - - return try JWSJson.Signature( - protectedData: jws.protectedHeaderData, - protected: typedProtected, - header: unprotectedHeader, - signature: jws.signature - ) - } - - return try jsonSerialization(payload: payload, signatures: signatures) - } - - /// Encodes the JWS object into JSON data, allowing for custom protected header and header types. - /// This method provides a way to serialize the JWS object with specified header types into JSON. - /// - /// - Parameters: - /// - payload: The payload data. - /// - protectedHeader: The protected header instance. - /// - unprotectedHeader: An optional header instance. - /// - keys: An array of `JWK`s used for signing. - /// - Returns: JSON encoded data with the specified header types. - /// - Throws: An error if the JSON encoding process fails. - public static func jsonSerialization( - payload: Data, - protectedHeader: P, - unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - keys: [JWK] - ) throws -> Data { - let json: JWSJson = try jsonSerialization( - payload: payload, - protectedHeader: protectedHeader, - unprotectedHeader: unprotectedHeader, - keys: keys - ) - return try JSONEncoder.jose.encode(json) - } - /// Creates a JSON Web Signature (JWS) object in JSON format using the provided payload, headers, and keys. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - payload: The data to be signed and included in the JWS. /// - protectedHeader: The protected header fields conforming to `JWSRegisteredFieldsHeader`. /// - unprotectedHeader: The optional unprotected header fields, defaulting to `nil`. - /// - keys: An array of cryptographic keys used for signing, each of which can be of type `Data`, `SecKey`, or `JWK`. + /// - keys: An array of cryptographic keys used for signing, each of which can be of type `KeyRepresentable`. /// /// - Throws: An error if the signing process or JSON serialization fails. /// - Returns: A `Data` object containing the JSON-encoded JWS. - public static func jsonSerialization( + public static func jsonSerialization( payload: Data, protectedHeader: P, unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - keys: [Key] + keys: [KeyRepresentable] ) throws -> Data { let json: JWSJson = try jsonSerialization( payload: payload, @@ -353,63 +253,42 @@ extension JWS { /// Generates a flattened JSON serialization of the JWS object for a single key. /// This method is useful when there is only one signer and a compact JSON representation is preferred. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The payload data to be signed. - /// - key: The `JWK` used for signing. + /// - key: The cryptographic key used for signing, which can be of type `KeyRepresentable`. /// - Returns: Flattened JSON encoded data representing the signed payload. /// - Throws: An error if the signing or JSON encoding process fails. public static func jsonSerializationFlattened( payload: Data, - key: JWK + key: KeyRepresentable ) throws -> Data { let json: JWSJson = try jsonSerialization(payload: payload, keys: [key]) return try JSONEncoder.jose.encode(json.flattened()) } - /// Generates a flattened JSON serialization of the JWS object for a single key, allowing for custom protected header and header types. - /// This method is similar to `jsonSerializationFlattened(payload:key:)` but allows specifying custom header types. - /// - /// - Parameters: - /// - payload: The payload data. - /// - protectedHeader: The protected header instance. - /// - unprotectedHeader: An optional header instance. - /// - key: The `JWK` used for signing. - /// - Returns: Flattened JSON encoded data with the specified header types. - /// - Throws: An error if the signing or JSON encoding process fails. - public static func jsonSerializationFlattened( - payload: Data, - protectedHeader: P, - unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - key: JWK - ) throws -> Data { - let json: JWSJson = try jsonSerialization( - payload: payload, - protectedHeader: protectedHeader, - unprotectedHeader: unprotectedHeader, - keys: [key] - ) - - return try JSONEncoder.jose.encode(json.flattened()) - } - /// Creates a flattened JSON Web Signature (JWS) object in JSON format using the provided payload, headers, and a key. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - payload: The data to be signed and included in the JWS. /// - protectedHeader: The protected header fields conforming to `JWSRegisteredFieldsHeader`. /// - unprotectedHeader: The optional unprotected header fields, defaulting to `nil`. - /// - key: The cryptographic key used for signing, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `KeyRepresentable`. /// /// - Throws: An error if the signing process or JSON serialization fails. /// - Returns: A `Data` object containing the flattened JSON-encoded JWS. - public static func jsonSerializationFlattened( + public static func jsonSerializationFlattened( payload: Data, protectedHeader: P, unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - key: Key + key: KeyRepresentable ) throws -> Data { let json: JWSJson = try jsonSerialization( payload: payload, @@ -421,36 +300,11 @@ extension JWS { return try JSONEncoder.jose.encode(json.flattened()) } - /// Generates a flattened JSON serialization of the JWS object for a single key, allowing for custom protected header and header types. - /// This method is similar to `jsonSerializationFlattened(payload:key:)` but allows specifying custom header types. - /// - /// - Parameters: - /// - payload: The payload data. - /// - protectedHeader: The protected header instance. - /// - unprotectedHeader: An optional header instance. - /// - key: The `JWK` used for signing. - /// - Returns: A `JWSJsonFlattened` object with the specified header types. - /// - Throws: An error if the signing or JSON encoding process fails. - public static func jsonSerializationFlattened( - payload: Data, - protectedHeader: P, - unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - key: JWK - ) throws -> JWSJsonFlattened { - let json: JWSJson = try jsonSerialization( - payload: payload, - protectedHeader: protectedHeader, - unprotectedHeader: unprotectedHeader, - keys: [key] - ) - - return try json.flattened() - } - /// Creates a flattened JSON Web Signature (JWS) object using the provided payload, headers, and a key. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - payload: The data to be signed and included in the JWS. @@ -460,11 +314,11 @@ extension JWS { /// /// - Throws: An error if the signing process or JSON serialization fails. /// - Returns: A `JWSJsonFlattened` object containing the flattened JSON-encoded JWS. - public static func jsonSerializationFlattened( + public static func jsonSerializationFlattened( payload: Data, protectedHeader: P, unprotectedHeader: H? = nil as DefaultJWSHeaderImpl?, - key: Key + key: KeyRepresentable ) throws -> JWSJsonFlattened { let json: JWSJson = try jsonSerialization( payload: payload, @@ -489,49 +343,53 @@ private func prepareHeaderForJWK(header: Data, jwk: JWK?) throws -> Data { } } -func prepareJWKFromHeader(header: Data, key: Key?, isPrivate: Bool = false) throws -> JWK? { - guard let key else { return nil } - - guard - let jsonObj = try JSONSerialization.jsonObject(with: header) as? [String: Any], - let algStr = jsonObj["alg"] as? String, - let signingAlg = SigningAlgorithm(rawValue: algStr) - else { - throw JWS.JWSError.missingAlgorithm - } - - switch signingAlg { - case .HS256: - return try JWK.JWKFrom(format: .octSequence, isPrivate: false, isKeyAgreement: false, key: key) - case .HS384: - return try JWK.JWKFrom(format: .octSequence, isPrivate: false, isKeyAgreement: false, key: key) - case .HS512: - return try JWK.JWKFrom(format: .octSequence, isPrivate: false, isKeyAgreement: false, key: key) - case .RS256: - return try JWK.JWKFrom(format: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .RS384: - return try JWK.JWKFrom(format: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .RS512: - return try JWK.JWKFrom(format: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .ES256: - return try JWK.JWKFrom(format: .p256, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .ES384: - return try JWK.JWKFrom(format: .p384, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .ES512: - return try JWK.JWKFrom(format: .p512, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .ES256K: - return try JWK.JWKFrom(format: .secp256k1, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .PS256: - return try JWK.JWKFrom(format: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .PS384: - return try JWK.JWKFrom(format: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .PS512: - return try JWK.JWKFrom(format: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .EdDSA: - return try JWK.JWKFrom(format: .curve25519, isPrivate: isPrivate, isKeyAgreement: false, key: key) - case .none: - return nil - case .invalid: - throw JWS.JWSError.unsupportedAlgorithm(keyType: nil, algorithm: algStr, curve: nil) +func prepareJWK(header: Data?, key: Key, isPrivate: Bool = false) throws -> JWK { + switch key { + case let value as Data: + guard + let header, + let jsonObj = try JSONSerialization.jsonObject(with: header) as? [String: Any], + let algStr = jsonObj["alg"] as? String, + let signingAlg = SigningAlgorithm(rawValue: algStr) + else { + throw JWS.JWSError.missingAlgorithm + } + + switch signingAlg { + case .HS256: + return try DataKey(type: .octSequence, isPrivate: false, isKeyAgreement: false, key: value).jwk + case .HS384: + return try DataKey(type: .octSequence, isPrivate: false, isKeyAgreement: false, key: value).jwk + case .HS512: + return try DataKey(type: .octSequence, isPrivate: false, isKeyAgreement: false, key: value).jwk + case .RS256: + return try DataKey(type: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .RS384: + return try DataKey(type: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .RS512: + return try DataKey(type: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .ES256: + return try DataKey(type: .p256, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .ES384: + return try DataKey(type: .p384, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .ES512: + return try DataKey(type: .p521, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .ES256K: + return try DataKey(type: .secp256k1, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .PS256: + return try DataKey(type: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .PS384: + return try DataKey(type: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .PS512: + return try DataKey(type: .rsa, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .EdDSA: + return try DataKey(type: .curve25519, isPrivate: isPrivate, isKeyAgreement: false, key: value).jwk + case .invalid, .none: + throw JWS.JWSError.unsupportedAlgorithm(keyType: nil, algorithm: algStr, curve: nil) + } + case let value as KeyRepresentable: + return try value.jwk + default: + throw CryptoError.keyFormatNotSupported(format: String(describing: key.self), supportedFormats: ["Data", "KeyRepresentable"]) } } diff --git a/Sources/JSONWebSignature/JWS+Verify.swift b/Sources/JSONWebSignature/JWS+Verify.swift index 74d896c..fe904d2 100644 --- a/Sources/JSONWebSignature/JWS+Verify.swift +++ b/Sources/JSONWebSignature/JWS+Verify.swift @@ -19,47 +19,14 @@ import JSONWebAlgorithms import JSONWebKey extension JWS { - /// Verifies the signature of the JWS instance using the provided JSON Web Key (JWK). - /// - /// - Parameter key: The `JWK` used for verification. - /// - Returns: `true` if the signature is valid, `false` otherwise. - /// - Throws: `JWSError` if there's a mismatch in algorithms between the key and the header, - /// if the algorithm is unsupported, or other errors encountered during verification. - public func verify(key: JWK?) throws -> Bool { - guard SigningAlgorithm.none != protectedHeader.algorithm else { - return true - } - guard let key else { - throw JWSError.missingKey - } - try key.algorithm.map { - guard $0 == protectedHeader.algorithm?.rawValue else { - throw JWSError.keyAlgorithmAndHeaderAlgorithmAreNotEqual( - header: protectedHeader.algorithm?.rawValue ?? "", - key: $0 - ) - } - } - - guard - let verifier = protectedHeader.algorithm?.cryptoVerifier - else { - throw JWSError.unsupportedAlgorithm( - keyType: protectedHeader.jwk?.keyType.rawValue, - algorithm: protectedHeader.algorithm?.rawValue, - curve: protectedHeader.jwk?.curve?.rawValue - ) - } - let signingData = try JWS.buildSigningData(header: protectedHeaderData, data: payload) - return try verifier.verify(data: signingData, signature: signature, key: key) - } - /// Verifies the signature of the JWS using the provided key. /// - /// This method supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: - /// - key: The cryptographic key used for verification, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`. /// /// - Throws: An error if the verification process fails due to a missing key, unsupported algorithm, or other issues. /// - Returns: A Boolean value indicating whether the signature is valid (`true`) or not (`false`). @@ -67,12 +34,8 @@ extension JWS { guard SigningAlgorithm.none != protectedHeader.algorithm else { return true } - guard - let key, - let jwkKey = try prepareJWKFromHeader(header: protectedHeaderData, key: key) - else { - throw JWSError.missingKey - } + guard let key else { throw JWSError.missingKey } + let jwkKey = try prepareJWK(header: protectedHeaderData, key: key) try jwkKey.algorithm.map { guard $0 == protectedHeader.algorithm?.rawValue else { throw JWSError.keyAlgorithmAndHeaderAlgorithmAreNotEqual( @@ -98,47 +61,53 @@ extension JWS { /// Verifies the signature of a JWS JSON object using a single JSON Web Key (JWK). /// Can validate either all signatures or just one, depending on the `validateAll` parameter. /// + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - jwsJson: The JWS JSON data to be verified. - /// - jwk: The `JWK` used for verification. + /// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`. /// - validateAll: If `true`, validates all signatures; otherwise, validates at least one. /// - Returns: `true` if the signature(s) are valid according to the provided parameters, `false` otherwise. /// - Throws: `JWSError` for errors encountered during verification. - public static func verify(jwsJson: Data, jwk: JWK, validateAll: Bool = false) throws -> Bool { + public static func verify(jwsJson: Data, key: Key, validateAll: Bool = false) throws -> Bool { let json: JWSJson = try decodeFullOrFlattenedJson(json: jwsJson) - + let jwkKey = try prepareJWK(header: JSONEncoder.jose.encode(json), key: key) if validateAll { guard try json.signatures .map({ try $0.jws(payload: json.payload) }) - .contains(where: { (try? $0.verify(key: jwk)) ?? false }) + .contains(where: { (try? $0.verify(key: jwkKey)) ?? false }) else { return false } return true } else { - let filteredSignatures = json.findSignaturesForJWK(jwk: jwk) + let filteredSignatures = json.findSignaturesForJWK(jwk: jwkKey) guard !filteredSignatures.isEmpty else { - throw JWSError.noSignatureForJWK(jwkAlg: jwk.algorithm, jwkKid: jwk.keyID) + throw JWSError.noSignatureForJWK(jwkAlg: jwkKey.algorithm, jwkKid: jwkKey.keyID) } - return try filteredSignatures.map { try $0.jws(payload: json.payload) }.allSatisfy { try $0.verify(key: jwk) } + return try filteredSignatures.map { try $0.jws(payload: json.payload) }.allSatisfy { try $0.verify(key: jwkKey) } } } /// Verifies the signature of a JSON Web Signature (JWS) object using the provided key. /// - /// This method supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - jwsJson: The JSON-encoded JWS object as `Data`. - /// - jwk: The cryptographic key used for verification, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `Data` and `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 at least one signature in the JWS is valid (`true`) or not (`false`). - public static func verify(jwsJson: Data, jwk: Key) throws -> Bool { + public static func verify(jwsJson: Data, key: Key) throws -> Bool { let json: JWSJson = try decodeFullOrFlattenedJson(json: jwsJson) guard try json.signatures .map({ try $0.jws(payload: json.payload) }) - .contains(where: { (try? $0.verify(key: jwk)) ?? false }) + .contains(where: { (try? $0.verify(key: key)) ?? false }) else { return false } @@ -149,36 +118,43 @@ extension JWS { /// Depending on the `allNeedToVerify` parameter, either all keys need to verify the signature successfully, /// or at least one key needs to succeed. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - jwsJson: The JWS JSON data to be verified. - /// - jwks: An array of `JWK`s used for verification. + /// - keys: An array of cryptographic keys used for signing, each of which can be of type `KeyRepresentable`. /// - allNeedToVerify: If `true`, all keys must verify the signature successfully; otherwise, at least one key must succeed. /// - Returns: `true` if the signature(s) are valid according to the provided parameters, `false` otherwise. /// - Throws: `JWSError` for errors encountered during verification. - public static func verify(jwsJson: Data, jwks: [JWK], allNeedToVerify: Bool = false) throws -> Bool { + public static func verify(jwsJson: Data, keys: [KeyRepresentable], allNeedToVerify: Bool = false) throws -> Bool { if allNeedToVerify { - return try jwks.allSatisfy { try JWS.verify(jwsJson: jwsJson, jwk: $0) } + return try keys.allSatisfy { try JWS.verify(jwsJson: jwsJson, key: $0) } } else { - return try jwks.contains { try JWS.verify(jwsJson: jwsJson, jwk: $0) } + return try keys.contains { try JWS.verify(jwsJson: jwsJson, key: $0) } } } /// Verifies the signature of a JSON Web Signature (JWS) object using an array of provided keys. /// - /// This method supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// When using `Data` as the key type, the `alg` (algorithm) field must be set in the header. /// /// - Parameters: /// - jwsJson: The JSON-encoded JWS object as `Data`. - /// - jwks: An array of cryptographic keys used for verification, each of which can be of type `Data`, `SecKey`, or `JWK`. + /// - keys: An array of cryptographic keys used for signing, each of which can be of type `KeyRepresentable`. /// - allNeedToVerify: A Boolean value indicating whether all signatures need to be verified (`true`) or if at least one valid signature is sufficient (`false`). Default is `false`. /// /// - 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 verification criteria are met. If `allNeedToVerify` is `true`, returns `true` if all signatures are valid. If `allNeedToVerify` is `false`, returns `true` if at least one signature is valid. - public static func verify(jwsJson: Data, jwks: [Key], allNeedToVerify: Bool = false) throws -> Bool { + public static func verify(jwsJson: Data, keys: [Key], allNeedToVerify: Bool = false) throws -> Bool { if allNeedToVerify { - return try jwks.allSatisfy { try JWS.verify(jwsJson: jwsJson, jwk: $0) } + return try keys.allSatisfy { try JWS.verify(jwsJson: jwsJson, key: $0) } } else { - return try jwks.contains { try JWS.verify(jwsJson: jwsJson, jwk: $0) } + return try keys.contains { try JWS.verify(jwsJson: jwsJson, key: $0) } } } } diff --git a/Sources/JSONWebToken/JWT+Encryption.swift b/Sources/JSONWebToken/JWT+Encryption.swift index 2218047..3057db1 100644 --- a/Sources/JSONWebToken/JWT+Encryption.swift +++ b/Sources/JSONWebToken/JWT+Encryption.swift @@ -15,6 +15,7 @@ */ import Foundation +import JSONWebAlgorithms import JSONWebKey import JSONWebEncryption @@ -22,6 +23,10 @@ extension JWT { /// Encrypts the payload of a JWT and returns it in JWE format. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The payload to encrypt, conforming to `JWTRegisteredFieldsClaims`. /// - protectedHeader: A header with fields that will be protected (encrypted). @@ -41,9 +46,9 @@ extension JWT { payload: Codable, protectedHeader: P, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK?, - recipientKey: JWK?, - sharedKey: JWK?, + senderKey: KeyRepresentable?, + recipientKey: KeyRepresentable?, + sharedKey: KeyRepresentable?, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil @@ -73,9 +78,9 @@ extension JWT { @JWTClaimsBuilder payload: () -> Claim, protectedHeader: P, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK?, - recipientKey: JWK?, - sharedKey: JWK?, + senderKey: KeyRepresentable?, + recipientKey: KeyRepresentable?, + sharedKey: KeyRepresentable?, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil @@ -103,6 +108,10 @@ extension JWT { /// This method is used for creating a nested JWT, where the payload is another JWT string. /// It encrypts the provided JWT string and wraps it in a new JWE structure. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - jwtString: The JWT string to be encrypted. /// - protectedHeader: A header with fields that will be protected (encrypted) in the outer JWE layer. @@ -122,9 +131,9 @@ extension JWT { jwt: JWT, protectedHeader: P, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil @@ -149,6 +158,10 @@ extension JWT { /// This method creates a nested JWE structure with two layers of encryption. The inner layer encrypts the payload, /// and the outer layer encrypts the resulting JWT from the inner encryption. /// + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - payload: The payload to encrypt, conforming to `JWTRegisteredFieldsClaims`. /// - protectedHeader: A header with fields that will be protected (encrypted) in the outer JWE layer. @@ -178,17 +191,17 @@ extension JWT { payload: Codable, protectedHeader: P, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, nestedProtectedHeader: NP, nestedUnprotectedHeader: NU? = nil as DefaultJWEHeaderImpl?, - nestedSenderKey: JWK? = nil, - nestedRecipientKey: JWK? = nil, - nestedSharedKey: JWK? = nil, + nestedSenderKey: KeyRepresentable? = nil, + nestedRecipientKey: KeyRepresentable? = nil, + nestedSharedKey: KeyRepresentable? = nil, nestedCek: Data? = nil, nestedInitializationVector: Data? = nil, nestedAdditionalAuthenticationData: Data? = nil @@ -227,17 +240,17 @@ extension JWT { @JWTClaimsBuilder payload: () -> Claim, protectedHeader: P, unprotectedHeader: U? = nil as DefaultJWEHeaderImpl?, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - sharedKey: JWK? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + sharedKey: KeyRepresentable? = nil, cek: Data? = nil, initializationVector: Data? = nil, additionalAuthenticationData: Data? = nil, nestedProtectedHeader: NP, nestedUnprotectedHeader: NU? = nil as DefaultJWEHeaderImpl?, - nestedSenderKey: JWK? = nil, - nestedRecipientKey: JWK? = nil, - nestedSharedKey: JWK? = nil, + nestedSenderKey: KeyRepresentable? = nil, + nestedRecipientKey: KeyRepresentable? = nil, + nestedSharedKey: KeyRepresentable? = nil, nestedCek: Data? = nil, nestedInitializationVector: Data? = nil, nestedAdditionalAuthenticationData: Data? = nil diff --git a/Sources/JSONWebToken/JWT+Signing.swift b/Sources/JSONWebToken/JWT+Signing.swift index d3fb6c7..be1410b 100644 --- a/Sources/JSONWebToken/JWT+Signing.swift +++ b/Sources/JSONWebToken/JWT+Signing.swift @@ -15,47 +15,22 @@ */ import Foundation +import JSONWebAlgorithms import JSONWebKey import JSONWebSignature extension JWT { - /// Creates a signed JWT using the provided payload, header, and key. - /// - /// This method signs the payload and creates a JWT in JWS (JSON Web Signature) format. - /// - /// - Parameters: - /// - payload: The payload to be included in the JWT, conforming to `JWTRegisteredFieldsClaims`. - /// - protectedHeader: A `JWSRegisteredFieldsHeader` containing header fields that will be protected in the JWS. - /// - key: The `JWK` (JSON Web Key) used for signing the payload. - /// - Returns: A `JWT` instance in JWS format with the signed payload. - /// - Throws: An error if the signing process fails. - public static func signed( - payload: Codable, - protectedHeader: P, - key: JWK? - ) throws -> JWT { - var protectedHeader = protectedHeader - protectedHeader.type = "JWT" - let encodedPayload = try JSONEncoder.jwt.encode(payload) - return JWT( - payload: encodedPayload, - format: .jws(try JWS( - payload: encodedPayload, - protectedHeader: protectedHeader, - key: key - )) - ) - } - /// Creates a signed JSON Web Token (JWT) using the provided payload, header, and key. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// When using `Data` as the key type, the `alg` (algorithm) field must be set in the header. /// /// - Parameters: /// - payload: The payload to be included in the JWT, conforming to `Codable`. /// - protectedHeader: The protected header fields conforming to `JWSRegisteredFieldsHeader`. - /// - key: The cryptographic key used for signing, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`. /// /// - Throws: An error if the signing process or encoding fails. /// - Returns: A `JWT` instance in JWS format with the signed payload. @@ -77,44 +52,17 @@ extension JWT { ) } - /// Creates a signed JSON Web Token (JWT) using the provided claims, protected header, and key. - /// - /// This function supports signing the claims using a `JWK` key. The claims are built using the `JWTClaimsBuilder` and the resulting JWT is signed with the specified key and protected header. - /// - /// - Parameters: - /// - payload: A closure that returns the claims to be included in the JWT, using the `JWTClaimsBuilder`. - /// - protectedHeader: The protected header fields for the JWS, conforming to `JWSRegisteredFieldsHeader`. - /// - key: The `JWK` used for signing the JWS. - /// - /// - Throws: An error if the signing process or encoding fails. - /// - Returns: A `JWT` instance representing the signed JWT. - public static func signed( - @JWTClaimsBuilder payload: () -> Claim, - protectedHeader: P, - key: JWK? - ) throws -> JWT { - var protectedHeader = protectedHeader - protectedHeader.type = "JWT" - let encodedPayload = try JSONEncoder.jwt.encode(payload().value) - return JWT( - payload: encodedPayload, - format: .jws(try JWS( - payload: encodedPayload, - protectedHeader: protectedHeader, - key: key - )) - ) - } - /// Creates a signed JSON Web Token (JWT) using the provided claims, header, and key. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `Key` parameter, including `Data`, and `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// When using `Data` as the key type, the `alg` (algorithm) field must be set in the header. /// /// - Parameters: /// - payload: A closure that returns the claims to be included in the JWT, using the `JWTClaimsBuilder`. /// - protectedHeader: The protected header fields conforming to `JWSRegisteredFieldsHeader`. - /// - key: The cryptographic key used for signing, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `Data` and `KeyRepresentable`. /// /// - Throws: An error if the signing process or encoding fails. /// - Returns: A `JWT` instance in JWS format with the signed claims. @@ -136,90 +84,18 @@ extension JWT { ) } - /// Signs a JWT payload as a nested JWT in JWS format with distinct inner and outer JWS headers. - /// - /// This method creates a nested JWS structure where the payload is first signed using the inner header and key, - /// then the resulting JWT string is signed again using the outer header and key. - /// - /// - Parameters: - /// - payload: The payload to be signed, conforming to `JWTRegisteredFieldsClaims`. - /// - protectedHeader: A `JWSRegisteredFieldsHeader` containing header fields for the outer JWS layer. - /// - key: The `JWK` used for signing the outer JWT string. - /// - nestedProtectedHeader: A `JWSRegisteredFieldsHeader` containing header fields for the inner JWS layer. - /// - nestedKey: The `JWK` used for signing the inner JWT payload. - /// - Returns: A `JWS` instance representing the doubly signed nested JWT. - /// - Throws: An error if the signing process fails. - public static func signedAsNested< - P: JWSRegisteredFieldsHeader, - NP: JWSRegisteredFieldsHeader - >( - payload: Codable, - protectedHeader: P, - key: JWK?, - nestedProtectedHeader: NP, - nestedKey: JWK? - ) throws -> JWS { - let jwt = try signed( - payload: payload, - protectedHeader: nestedProtectedHeader, - key: nestedKey - ) - - return try signedAsNested( - jwtString: jwt.jwtString, - protectedHeader: protectedHeader, - key: key - ) - } - /// Creates a nested JSON Web Signature (JWS) object by first signing the payload as a JWT and then nesting it inside another JWS. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - payload: The payload to be included in the inner JWT, conforming to `Codable`. /// - protectedHeader: The protected header fields for the outer JWS, conforming to `JWSRegisteredFieldsHeader`. - /// - key: The cryptographic key used for signing the outer JWS, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `KeyRepresentable`. /// - nestedProtectedHeader: The protected header fields for the inner JWT, conforming to `JWSRegisteredFieldsHeader`. - /// - nestedKey: The cryptographic key used for signing the inner JWT, which can be of type `Data`, `SecKey`, or `JWK`. - /// - /// - Throws: An error if the signing process or encoding fails. - /// - Returns: A `JWS` instance representing the nested signed JWT. - public static func signedAsNested< - P: JWSRegisteredFieldsHeader, - NP: JWSRegisteredFieldsHeader, - Key - >( - payload: Codable, - protectedHeader: P, - key: Key?, - nestedProtectedHeader: NP, - nestedKey: Key? - ) throws -> JWS { - let jwt = try signed( - payload: payload, - protectedHeader: nestedProtectedHeader, - key: nestedKey - ) - - return try signedAsNested( - jwtString: jwt.jwtString, - protectedHeader: protectedHeader, - key: key - ) - } - - /// Creates a nested JSON Web Signature (JWS) object by first signing the claims as a JWT and then nesting it inside another JWS. - /// - /// This function signs the claims using the nested key and nested protected header, and then nests the resulting JWT string inside another JWS using the outer key and protected header. - /// - /// - Parameters: - /// - payload: A closure that returns the claims to be included in the inner JWT, using the `JWTClaimsBuilder`. - /// - protectedHeader: The protected header fields for the outer JWS, conforming to `JWSRegisteredFieldsHeader`. - /// - key: The `JWK` used for signing the outer JWS. - /// - nestedProtectedHeader: The protected header fields for the inner JWT, conforming to `JWSRegisteredFieldsHeader`. - /// - nestedKey: The `JWK` used for signing the inner JWT. + /// - nestedKey: The cryptographic key used for signing, which can be of type `KeyRepresentable`. /// /// - Throws: An error if the signing process or encoding fails. /// - Returns: A `JWS` instance representing the nested signed JWT. @@ -227,11 +103,11 @@ extension JWT { P: JWSRegisteredFieldsHeader, NP: JWSRegisteredFieldsHeader >( - @JWTClaimsBuilder payload: () -> Claim, + payload: Codable, protectedHeader: P, - key: JWK?, + key: KeyRepresentable?, nestedProtectedHeader: NP, - nestedKey: JWK? + nestedKey: KeyRepresentable? ) throws -> JWS { let jwt = try signed( payload: payload, @@ -248,13 +124,14 @@ extension JWT { /// Creates a nested JSON Web Signature (JWS) object by first signing the claims as a JWT and then nesting it inside another JWS. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - payload: A closure that returns the claims to be included in the inner JWT, using the `JWTClaimsBuilder`. /// - protectedHeader: The protected header fields for the outer JWS, conforming to `JWSRegisteredFieldsHeader`. - /// - key: The cryptographic key used for signing the outer JWS, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `KeyRepresentable`. /// - nestedProtectedHeader: The protected header fields for the inner JWT, conforming to `JWSRegisteredFieldsHeader`. /// - nestedKey: The cryptographic key used for signing the inner JWT, which can be of type `Data`, `SecKey`, or `JWK`. /// @@ -262,14 +139,13 @@ extension JWT { /// - Returns: A `JWS` instance representing the nested signed JWT. public static func signedAsNested< P: JWSRegisteredFieldsHeader, - NP: JWSRegisteredFieldsHeader, - Key + NP: JWSRegisteredFieldsHeader >( @JWTClaimsBuilder payload: () -> Claim, protectedHeader: P, - key: Key?, + key: KeyRepresentable?, nestedProtectedHeader: NP, - nestedKey: Key? + nestedKey: KeyRepresentable? ) throws -> JWS { let jwt = try signed( payload: payload, @@ -284,48 +160,23 @@ extension JWT { ) } - /// Signs a JWT string as a nested JWT in JWS format. - /// - /// This method is used for creating a nested JWT, where the payload is another JWT string. - /// It signs the provided JWT string and wraps it in a new JWS structure. - /// - /// - Parameters: - /// - jwtString: The JWT string to be signed. - /// - protectedHeader: A `JWSRegisteredFieldsHeader` containing header fields that will be protected in the JWS. - /// - key: The `JWK` used for signing the JWT string. - /// - Returns: A string representing the signed JWT in JWS format. - /// - Throws: An error if the signing process fails. - public static func signedAsNested( - jwtString: String, - protectedHeader: P, - key: JWK? - ) throws -> JWS { - var protectedHeader = protectedHeader - protectedHeader.contentType = "JWT" - - return try JWS( - payload: JSONEncoder.jwt.encode(jwtString.tryToData()), - protectedHeader: protectedHeader, - key: key - ) - } - /// Creates a nested JSON Web Signature (JWS) object by wrapping an existing JWT string inside another JWS. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This initializer supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - jwtString: The existing JWT string to be nested inside the outer JWS. /// - protectedHeader: The protected header fields for the outer JWS, conforming to `JWSRegisteredFieldsHeader`. - /// - key: The cryptographic key used for signing the outer JWS, which can be of type `Data`, `SecKey`, or `JWK`. + /// - key: The cryptographic key used for signing, which can be of type `KeyRepresentable`. /// /// - Throws: An error if the signing process or encoding fails. /// - Returns: A `JWS` instance representing the nested signed JWT. - public static func signedAsNested( + public static func signedAsNested( jwtString: String, protectedHeader: P, - key: Key? + key: KeyRepresentable? ) throws -> JWS { var protectedHeader = protectedHeader protectedHeader.contentType = "JWT" diff --git a/Sources/JSONWebToken/JWT+Verification.swift b/Sources/JSONWebToken/JWT+Verification.swift index 66d5b7d..c159327 100644 --- a/Sources/JSONWebToken/JWT+Verification.swift +++ b/Sources/JSONWebToken/JWT+Verification.swift @@ -15,6 +15,7 @@ */ import Foundation +import JSONWebAlgorithms import JSONWebEncryption import JSONWebKey import JSONWebSignature @@ -25,20 +26,24 @@ extension JWT { /// /// This method supports both JWS (JSON Web Signature) and JWE (JSON Web Encryption) formats. It first determines the format of the JWT based on the number of components separated by dots in the JWT string. The method also handles nested JWTs, verifying each layer as needed. /// + /// This method supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. + /// /// - Parameters: /// - jwtString: The JWT string to be verified and decoded. - /// - senderKey: An optional `JWK` representing the sender's key, used for verifying a JWS. - /// - recipientKey: An optional `JWK` representing the recipient's key, used for decrypting a JWE. - /// - nestedKeys: An array of `JWK` used for verifying nested JWTs. + /// - senderKey: An optional `KeyRepresentable` representing the sender's key, used for verifying a JWS. + /// - recipientKey: An optional `KeyRepresentable` representing the recipient's key, used for decrypting a JWE. + /// - nestedKeys: An array of `KeyRepresentable` used for verifying nested JWTs. /// - expectedIssuer: An optional expected issuer (`iss` claim) to validate. /// - expectedAudience: An optional expected audience (`aud` claim) to validate. /// - Returns: A `JWT` instance containing the payload and format. /// - Throws: `JWTError` if verification fails, the signature is invalid, claims validation fails, the JWT format is incorrect, or if nested JWT keys are missing. public static func verify( jwtString: String, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - nestedKeys: [JWK] = [], + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + nestedKeys: [KeyRepresentable] = [], expectedIssuer: String? = nil, expectedAudience: String? = nil ) throws -> JWT { @@ -48,7 +53,7 @@ extension JWT { let jws = try JWS(jwsString: jwtString) if jws.protectedHeader.contentType == "JWT" { guard let key = getKeyForJWSHeader( - keys: nestedKeys, + keys: try nestedKeys.map { try $0.jwk }, header: jws.protectedHeader ) else { throw JWTError.missingNestedJWTKey } @@ -82,7 +87,7 @@ extension JWT { if jwe.protectedHeader.contentType == "JWT" { guard let key = getKeyForJWEHeader( - keys: nestedKeys, + keys: try nestedKeys.map { try $0.jwk }, header: jwe.protectedHeader ) else { throw JWTError.missingNestedJWTKey } @@ -109,26 +114,27 @@ extension JWT { } /// Verifies a JSON Web Token (JWT) string by checking its signature and claims. /// - /// This function supports different types for the `Key` parameter, including `Data`, `SecKey`, and `JWK`. - /// When using `Data` or `SecKey` as the key type, the `alg` (algorithm) field must be set in the header. + /// This method supports different types for the `KeyRepresentable`. + /// The following types by default extend `KeyRepresentable` and can be used as the Key `JWK`, `SecKey`, `CryptoSwift.RSA` + /// and CriptoKit EC Keys and Curve25519. /// /// - Parameters: /// - jwtString: The JWT string to be verified. - /// - signerKey: The cryptographic key used for verifying the JWS, which can be of type `Data`, `SecKey`, or `JWK`. - /// - senderKey: The JWK used for decrypting the JWE, if applicable. - /// - recipientKey: The JWK used for decrypting the JWE, if applicable. - /// - nestedKeys: An array of JWKs used for verifying nested JWTs, if applicable. + /// - signerKey: The cryptographic key used for verifying the JWS, which can be of type `KeyRepresentable`. + /// - senderKey: The cryptographic key used for verifying the JWS, which can be of type `KeyRepresentable`. + /// - recipientKey: The cryptographic key used for verifying the JWS, which can be of type `KeyRepresentable`. + /// - nestedKeys: An array of `KeyRepresentable` used for verifying nested JWTs, if applicable. /// - expectedIssuer: The expected issuer (`iss`) claim in the JWT payload. /// - expectedAudience: The expected audience (`aud`) claim in the JWT payload. /// /// - Throws: An error if the verification process fails. /// - Returns: A `JWT` instance representing the verified JWT. - public static func verify( + public static func verify( jwtString: String, - signerKey: Key? = nil, - senderKey: JWK? = nil, - recipientKey: JWK? = nil, - nestedKeys: [JWK] = [], + signerKey: KeyRepresentable? = nil, + senderKey: KeyRepresentable? = nil, + recipientKey: KeyRepresentable? = nil, + nestedKeys: [KeyRepresentable] = [], expectedIssuer: String? = nil, expectedAudience: String? = nil ) throws -> JWT { @@ -138,7 +144,7 @@ extension JWT { let jws = try JWS(jwsString: jwtString) if jws.protectedHeader.contentType == "JWT" { guard let key = getKeyForJWSHeader( - keys: nestedKeys, + keys: try nestedKeys.map { try $0.jwk }, header: jws.protectedHeader ) else { throw JWTError.missingNestedJWTKey } @@ -172,7 +178,7 @@ extension JWT { if jwe.protectedHeader.contentType == "JWT" { guard let key = getKeyForJWEHeader( - keys: nestedKeys, + keys: try nestedKeys.map { try $0.jwk }, header: jwe.protectedHeader ) else { throw JWTError.missingNestedJWTKey } diff --git a/Tests/JWSTests/JWSJsonTests.swift b/Tests/JWSTests/JWSJsonTests.swift index 9b61493..9f458fb 100644 --- a/Tests/JWSTests/JWSJsonTests.swift +++ b/Tests/JWSTests/JWSJsonTests.swift @@ -35,7 +35,7 @@ final class JWSJsonTests: XCTestCase { XCTAssertEqual(jsonSerilization.signatures.count, 1) XCTAssertEqual(try jsonSerilization.signatures.first!.validateAlg(), .ES256) XCTAssertEqual(try jsonSerilization.signatures.first!.getKid(), "1") - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: jwk)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: jwk)) } func testJsonSerializationTwoKeysES256() throws { @@ -57,8 +57,8 @@ final class JWSJsonTests: XCTestCase { XCTAssertEqual(try jsonSerilization.signatures.filter { try $0.validateAlg() == .ES256 }.count, 2) XCTAssertTrue(try jsonSerilization.signatures.contains { try $0.getKid() == "1"} ) XCTAssertTrue(try jsonSerilization.signatures.contains { try $0.getKid() == "2"} ) - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: jwk1)) - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: jwk2)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: jwk1)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: jwk2)) } func testJsonSerializationOneKeyES256_OtherES521() throws { @@ -81,8 +81,8 @@ final class JWSJsonTests: XCTestCase { XCTAssertEqual(try jsonSerilization.signatures.filter { try $0.validateAlg() == .ES512 }.count, 1) XCTAssertTrue(try jsonSerilization.signatures.contains { try $0.getKid() == "1"} ) XCTAssertTrue(try jsonSerilization.signatures.contains { try $0.getKid() == "2"} ) - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: jwk1)) - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: jwk2)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: jwk1)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: jwk2)) } func testJsonSerializationTrueES256Verification_FailES521VerificationWithRandomKey() throws { @@ -95,8 +95,8 @@ final class JWSJsonTests: XCTestCase { let jwkRandomKey = JWK.testingES521Pair - XCTAssertTrue(try JWS.verify(jwsJson: jws.data(using: .utf8)!, jwk: jwk1)) - XCTAssertFalse(try JWS.verify(jwsJson: jws.data(using: .utf8)!, jwk: jwkRandomKey, validateAll: true)) + XCTAssertTrue(try JWS.verify(jwsJson: jws.data(using: .utf8)!, key: jwk1)) + XCTAssertFalse(try JWS.verify(jwsJson: jws.data(using: .utf8)!, key: jwkRandomKey, validateAll: true)) } func testJsonSerializationVerificationTrueWhenKeyIsValidWithoutKidAndValidateAllTrue() throws { @@ -110,13 +110,10 @@ final class JWSJsonTests: XCTestCase { {"payload":"eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ==","signatures":[{"header":{"kid":"1"},"protected":"eyJhbGciOiJFUzI1NiJ9","signature":"vlYj-Vt5onyW56JMnWA82dlylnf2ELGfrGXP7P_JVUY3Dftecm83ceW9w6FYF4ApacRym6Mu5n_NtWDPgK35yg"},{"header":{"kid":"2"},"protected":"eyJhbGciOiJFUzUxMiJ9","signature":"AST-iRjis7O62AjCJBdOk-n54P73JZ_hCJZHBMTcqbrBD7Nhd0PysbDGZQf1IsD2LHcAvL_H2LR-p-QsmDViooHQAI9LaK8abwQYIDrYNc9fGSaVdWw42qzqj_m9qGhM5jLEcGW-PrNYUGsJSsBC4daBXnxEUbCR7iR0UVaR00ngb4Ma"}]} """ - XCTAssertTrue(try JWS.verify(jwsJson: jws.data(using: .utf8)!, jwk: jwkWithoutKid, validateAll: true)) + XCTAssertTrue(try JWS.verify(jwsJson: jws.data(using: .utf8)!, key: jwkWithoutKid, validateAll: true)) } func testJsonSerializationVerificationFalseWhenKeyHasNoKid() throws { - let keyJWK = "{\"kty\":\"EC\",\"kid\":\"1\",\"crv\":\"P-256\",\"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\",\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\",\"d\":\"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI\"}" - let jwk1 = try JSONDecoder().decode(JWK.self, from: keyJWK.data(using: .utf8)!) - let keyJWKWithoutKid = "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\",\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\",\"d\":\"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI\"}" let jwkWithoutKid = try JSONDecoder().decode(JWK.self, from: keyJWKWithoutKid.data(using: .utf8)!) @@ -124,7 +121,7 @@ final class JWSJsonTests: XCTestCase { {"payload":"eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ==","signatures":[{"header":{"kid":"1"},"protected":"eyJhbGciOiJFUzI1NiJ9","signature":"vlYj-Vt5onyW56JMnWA82dlylnf2ELGfrGXP7P_JVUY3Dftecm83ceW9w6FYF4ApacRym6Mu5n_NtWDPgK35yg"},{"header":{"kid":"2"},"protected":"eyJhbGciOiJFUzUxMiJ9","signature":"AST-iRjis7O62AjCJBdOk-n54P73JZ_hCJZHBMTcqbrBD7Nhd0PysbDGZQf1IsD2LHcAvL_H2LR-p-QsmDViooHQAI9LaK8abwQYIDrYNc9fGSaVdWw42qzqj_m9qGhM5jLEcGW-PrNYUGsJSsBC4daBXnxEUbCR7iR0UVaR00ngb4Ma"}]} """ - XCTAssertThrowsError(try JWS.verify(jwsJson: jws.data(using: .utf8)!, jwk: jwkWithoutKid)) + XCTAssertThrowsError(try JWS.verify(jwsJson: jws.data(using: .utf8)!, key: jwkWithoutKid, validateAll: false)) } func testJsonSerializationOneKeyOnlyEdDSA() throws { @@ -141,6 +138,6 @@ final class JWSJsonTests: XCTestCase { XCTAssertEqual(jsonSerilization.signatures.count, 1) XCTAssertEqual(try jsonSerilization.signatures.first!.validateAlg(), .EdDSA) XCTAssertEqual(try jsonSerilization.signatures.first!.getKid(), "1") - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: keyJWK)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: keyJWK)) } } diff --git a/Tests/JWSTests/JWSTests.swift b/Tests/JWSTests/JWSTests.swift index 35524a7..5edc748 100644 --- a/Tests/JWSTests/JWSTests.swift +++ b/Tests/JWSTests/JWSTests.swift @@ -98,6 +98,11 @@ final class JWSTests: XCTestCase { XCTAssertNoThrow(try JWS(payload: "test".data(using: .utf8)!, protectedHeader: DefaultJWSHeaderImpl(algorithm: .ES256), key: keyPair)) } + func testES256SigningWithSecKey() throws { + let keyPair = JWK.testingES256PairSecKey + XCTAssertNoThrow(try JWS(payload: "test".data(using: .utf8)!, protectedHeader: DefaultJWSHeaderImpl(algorithm: .ES256), key: keyPair)) + } + func testES384SigningWithDataKey() throws { let keyPair = JWK.testingES384PairData XCTAssertNoThrow(try JWS(payload: "test".data(using: .utf8)!, protectedHeader: DefaultJWSHeaderImpl(algorithm: .ES384), key: keyPair)) diff --git a/Tests/JWSTests/Mocks/JWK+Testing.swift b/Tests/JWSTests/Mocks/JWK+Testing.swift index f082db2..3446b9e 100644 --- a/Tests/JWSTests/Mocks/JWK+Testing.swift +++ b/Tests/JWSTests/Mocks/JWK+Testing.swift @@ -30,6 +30,23 @@ extension JWK { return privateKey.rawRepresentation } + static var testingES256PairSecKey: SecKey { + let p256Data = P256.Signing.PrivateKey().x963Representation + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECDSA, + kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, + kSecAttrKeySizeInBits as String: 256 + ] + + var error: Unmanaged? + if let secKey = SecKeyCreateWithData(p256Data as CFData, attributes as CFDictionary, &error) { + return secKey + } else if let error = error?.takeRetainedValue() { + fatalError() + } + fatalError() + } + static var testingES384Pair: JWK { let privateKey = P384.Signing.PrivateKey() return privateKey.jwkRepresentation diff --git a/Tests/JWSTests/RFC7515Tests.swift b/Tests/JWSTests/RFC7515Tests.swift index 28d4834..d5db3fe 100644 --- a/Tests/JWSTests/RFC7515Tests.swift +++ b/Tests/JWSTests/RFC7515Tests.swift @@ -248,7 +248,7 @@ final class RFC7515Tests: XCTestCase { XCTAssertEqual(esSignature!.unprotectedHeader!.keyID, "e9bc097a-ce51-4036-9562-d2ade882db0d") // We cannot test if the ES256 signature is equal since the value is always different, // instead we verify with the key - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: es256KeyJWK)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: es256KeyJWK)) } func testJWS_RFC7515_A7() throws { @@ -271,6 +271,6 @@ final class RFC7515Tests: XCTestCase { XCTAssertEqual(jsonSerilization.unprotectedHeader!.keyID, "e9bc097a-ce51-4036-9562-d2ade882db0d") // We cannot test if the ES256 signature is equal since the value is always different, // instead we verify with the key - XCTAssertTrue(try JWS.verify(jwsJson: jws, jwk: es256KeyJWK)) + XCTAssertTrue(try JWS.verify(jwsJson: jws, key: es256KeyJWK)) } }