From c3988654472040522fd7caf2290f27d41bf073c7 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 7 Dec 2025 10:29:48 +1100 Subject: [PATCH 1/8] Try supporting more x5c signatures --- Sources/JWTKit/X5C/X5CVerifier.swift | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/JWTKit/X5C/X5CVerifier.swift b/Sources/JWTKit/X5C/X5CVerifier.swift index 14844afa..5bd90fc8 100644 --- a/Sources/JWTKit/X5C/X5CVerifier.swift +++ b/Sources/JWTKit/X5C/X5CVerifier.swift @@ -148,8 +148,7 @@ public struct X5CVerifier: Sendable { let parser = DefaultJWTParser(jsonDecoder: jsonDecoder) let (header, payload, _) = try parser.parse(token, as: Payload.self) - // Ensure the algorithm used is ES256, as it's the only supported one (for now) - guard let headerAlg = header.alg, headerAlg == "ES256" else { + guard let rawAlg = header.alg, let headerAlg = JWK.Algorithm(rawValue: rawAlg) else { throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: header.alg))") } @@ -201,9 +200,29 @@ public struct X5CVerifier: Sendable { } // Assuming the chain is valid, verify the token was signed by the valid certificate - let ecdsaKey = try ES256PublicKey(certificate: certificates[0].serializeAsPEM().pemString) - let signer = JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) + //let ecdsaKey = try ES256PublicKey(certificate: certificates[0].serializeAsPEM().pemString) + //let signer = JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) + let signer = try getSigner(for: headerAlg, certificate: certificates[0], parser: parser) return try await signer.verify(token) } } + +extension X5CVerifier { + func getSigner(for alg: JWK.Algorithm, certificate: Certificate, parser: any JWTParser) throws -> JWTSigner { + let pem = try certificate.serializeAsPEM().pemString + switch alg { + case .eddsa: + let eddsaKey = try EdDSA.PublicKey(pem: pem) + return JWTSigner(algorithm: EdDSASigner(key: eddsaKey), parser: parser) + case .es256: + let ecdsaKey = try ES256PublicKey(certificate: pem) + return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) + case .es384: + let ecdsaKey = try ES384PublicKey(certificate: pem) + return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) + default: + throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: alg))") + } + } +} From d2ac1c7c43bbdbcbef8751580f302aea5595a405 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 7 Dec 2025 12:04:33 +1100 Subject: [PATCH 2/8] Try supporting more x5c signatures --- Package.swift | 3 ++- Sources/JWTKit/X5C/X5CVerifier.swift | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 296ccdf3..b5670dda 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,8 @@ let package = Package( .library(name: "JWTKit", targets: ["JWTKit"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", from: "4.1.0"), + //.package(url: "https://github.com/apple/swift-crypto.git", from: "4.1.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "3.8.0"..<"5.0.0"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.15.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], diff --git a/Sources/JWTKit/X5C/X5CVerifier.swift b/Sources/JWTKit/X5C/X5CVerifier.swift index 5bd90fc8..6e292b60 100644 --- a/Sources/JWTKit/X5C/X5CVerifier.swift +++ b/Sources/JWTKit/X5C/X5CVerifier.swift @@ -221,6 +221,9 @@ extension X5CVerifier { case .es384: let ecdsaKey = try ES384PublicKey(certificate: pem) return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) + case .es512: + let ecdsaKey = try ES512PublicKey(certificate: pem) + return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) default: throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: alg))") } From 57b957abf2bdb77df8d1ae7ae5191356b84a48eb Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 7 Dec 2025 12:39:16 +1100 Subject: [PATCH 3/8] Try supporting more x5c signatures --- Package.swift | 3 +- Sources/JWTKit/X5C/X5CVerifier.swift | 15 ++--- Tests/JWTKitTests/X5CEdDSATests.swift | 90 +++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 Tests/JWTKitTests/X5CEdDSATests.swift diff --git a/Package.swift b/Package.swift index b5670dda..296ccdf3 100644 --- a/Package.swift +++ b/Package.swift @@ -13,8 +13,7 @@ let package = Package( .library(name: "JWTKit", targets: ["JWTKit"]) ], dependencies: [ - //.package(url: "https://github.com/apple/swift-crypto.git", from: "4.1.0"), - .package(url: "https://github.com/apple/swift-crypto.git", "3.8.0"..<"5.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", from: "4.1.0"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.15.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], diff --git a/Sources/JWTKit/X5C/X5CVerifier.swift b/Sources/JWTKit/X5C/X5CVerifier.swift index 6e292b60..3a1f52a6 100644 --- a/Sources/JWTKit/X5C/X5CVerifier.swift +++ b/Sources/JWTKit/X5C/X5CVerifier.swift @@ -200,8 +200,6 @@ public struct X5CVerifier: Sendable { } // Assuming the chain is valid, verify the token was signed by the valid certificate - //let ecdsaKey = try ES256PublicKey(certificate: certificates[0].serializeAsPEM().pemString) - //let signer = JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) let signer = try getSigner(for: headerAlg, certificate: certificates[0], parser: parser) return try await signer.verify(token) @@ -210,20 +208,19 @@ public struct X5CVerifier: Sendable { extension X5CVerifier { func getSigner(for alg: JWK.Algorithm, certificate: Certificate, parser: any JWTParser) throws -> JWTSigner { - let pem = try certificate.serializeAsPEM().pemString switch alg { - case .eddsa: - let eddsaKey = try EdDSA.PublicKey(pem: pem) - return JWTSigner(algorithm: EdDSASigner(key: eddsaKey), parser: parser) case .es256: - let ecdsaKey = try ES256PublicKey(certificate: pem) + let ecdsaKey = try ES256PublicKey(certificate: certificate.serializeAsPEM().pemString) return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) case .es384: - let ecdsaKey = try ES384PublicKey(certificate: pem) + let ecdsaKey = try ES384PublicKey(certificate: certificate.serializeAsPEM().pemString) return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) case .es512: - let ecdsaKey = try ES512PublicKey(certificate: pem) + let ecdsaKey = try ES512PublicKey(certificate: certificate.serializeAsPEM().pemString) return JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) + case .eddsa: + let eddsaKey = try EdDSA.PublicKey(pem: certificate.publicKey.serializeAsPEM().pemString) + return JWTSigner(algorithm: EdDSASigner(key: eddsaKey), parser: parser) default: throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: alg))") } diff --git a/Tests/JWTKitTests/X5CEdDSATests.swift b/Tests/JWTKitTests/X5CEdDSATests.swift new file mode 100644 index 00000000..f504280f --- /dev/null +++ b/Tests/JWTKitTests/X5CEdDSATests.swift @@ -0,0 +1,90 @@ +#if canImport(Testing) +import Testing +import JWTKit +@_spi(FixedExpiryValidationTime) import X509 + +#if !canImport(Darwin) +import FoundationEssentials +#else +import Foundation +#endif + +/// Test the x5c verification abilities of JWTSigners. +/// +/// In these tests, there are 4 certificates: +/// - Root +/// - Intermediate +/// - Leaf +/// - Leaf expired +/// +/// All tokens in these tests have been signed with the "Leaf" private key. +/// "Root" is the trusted, self-signed certificate. "Intermediate" is signed by +/// "Root" and "Leaf" is signed by "Intermediate." +/// +/// "Leaf expired" has the same private key as "Leaf" but is meant to expire Oct 30 16:06:22 2022 GMT. +/// +/// Only tokens with an x5c chain that starts with "Leaf" +/// and ends in either "Intermediate" or "Root" should +/// successfully be verified. +/// +/// Note: if the certificates are expired and need updating, see the `scripts/generateTokens.swift` file. +@Suite("X5CTests") +struct X5CEdDSATests { + + @Test("Test signing with x5c chain") + func signWithX5CChain() async throws { + let key = try EdDSA.PrivateKey() + let privateKey = try Certificate.PrivateKey(pemEncoded: key.pemRepresentation) + let subjectName = try DistinguishedName { + CommonName("Nuw") + } + let cert = try Certificate( + version: .v3, + serialNumber: .init(), + publicKey: privateKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date().addingTimeInterval(3600), + issuer: subjectName, + subject: subjectName, + extensions: try Certificate.Extensions { + Critical( + BasicConstraints.isCertificateAuthority(maxPathLength: nil) + ) + Critical( + KeyUsage(digitalSignature: true, keyCertSign: true) + ) + //SubjectAlternativeNames([.dnsName("localhost")]) + }, + issuerPrivateKey: privateKey + ) + let keyCollection = await JWTKeyCollection() + .add(eddsa: key) + /*.add( + ecdsa: ES256PrivateKey(pem: x5cLeafCertKey) + )*/ + + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + + let certs = [try cert.serializeAsPEM().pemString] + let header: JWTHeader = ["x5c": .array(certs.map(JWTHeaderField.string))] + let token = try await keyCollection.sign(payload, header: header) + print("\n **** token=\(token)") + let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) + + let x5c = try #require(parsed.header.x5c) + //let pemCerts = try x5c.map(getPEMString) + //#expect(pemCerts == x5cCerts) + let verifier = try X5CVerifier(rootCertificates: certs) + await #expect(throws: Never.self) { + try await verifier.verifyJWS(token, as: TestPayload.self) + } + } +} + + +#endif // canImport(Testing) From 97ac855edbd0e8e4345f5ca1b856b782075adb7f Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 7 Dec 2025 13:26:33 +1100 Subject: [PATCH 4/8] Add EdDSA signing/verification test --- Tests/JWTKitTests/X5CEdDSATests.swift | 90 --------------------------- Tests/JWTKitTests/X5CTests.swift | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+), 90 deletions(-) delete mode 100644 Tests/JWTKitTests/X5CEdDSATests.swift diff --git a/Tests/JWTKitTests/X5CEdDSATests.swift b/Tests/JWTKitTests/X5CEdDSATests.swift deleted file mode 100644 index f504280f..00000000 --- a/Tests/JWTKitTests/X5CEdDSATests.swift +++ /dev/null @@ -1,90 +0,0 @@ -#if canImport(Testing) -import Testing -import JWTKit -@_spi(FixedExpiryValidationTime) import X509 - -#if !canImport(Darwin) -import FoundationEssentials -#else -import Foundation -#endif - -/// Test the x5c verification abilities of JWTSigners. -/// -/// In these tests, there are 4 certificates: -/// - Root -/// - Intermediate -/// - Leaf -/// - Leaf expired -/// -/// All tokens in these tests have been signed with the "Leaf" private key. -/// "Root" is the trusted, self-signed certificate. "Intermediate" is signed by -/// "Root" and "Leaf" is signed by "Intermediate." -/// -/// "Leaf expired" has the same private key as "Leaf" but is meant to expire Oct 30 16:06:22 2022 GMT. -/// -/// Only tokens with an x5c chain that starts with "Leaf" -/// and ends in either "Intermediate" or "Root" should -/// successfully be verified. -/// -/// Note: if the certificates are expired and need updating, see the `scripts/generateTokens.swift` file. -@Suite("X5CTests") -struct X5CEdDSATests { - - @Test("Test signing with x5c chain") - func signWithX5CChain() async throws { - let key = try EdDSA.PrivateKey() - let privateKey = try Certificate.PrivateKey(pemEncoded: key.pemRepresentation) - let subjectName = try DistinguishedName { - CommonName("Nuw") - } - let cert = try Certificate( - version: .v3, - serialNumber: .init(), - publicKey: privateKey.publicKey, - notValidBefore: Date(), - notValidAfter: Date().addingTimeInterval(3600), - issuer: subjectName, - subject: subjectName, - extensions: try Certificate.Extensions { - Critical( - BasicConstraints.isCertificateAuthority(maxPathLength: nil) - ) - Critical( - KeyUsage(digitalSignature: true, keyCertSign: true) - ) - //SubjectAlternativeNames([.dnsName("localhost")]) - }, - issuerPrivateKey: privateKey - ) - let keyCollection = await JWTKeyCollection() - .add(eddsa: key) - /*.add( - ecdsa: ES256PrivateKey(pem: x5cLeafCertKey) - )*/ - - let payload = TestPayload( - sub: "vapor", - name: "Foo", - admin: false, - exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) - ) - - let certs = [try cert.serializeAsPEM().pemString] - let header: JWTHeader = ["x5c": .array(certs.map(JWTHeaderField.string))] - let token = try await keyCollection.sign(payload, header: header) - print("\n **** token=\(token)") - let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) - - let x5c = try #require(parsed.header.x5c) - //let pemCerts = try x5c.map(getPEMString) - //#expect(pemCerts == x5cCerts) - let verifier = try X5CVerifier(rootCertificates: certs) - await #expect(throws: Never.self) { - try await verifier.verifyJWS(token, as: TestPayload.self) - } - } -} - - -#endif // canImport(Testing) diff --git a/Tests/JWTKitTests/X5CTests.swift b/Tests/JWTKitTests/X5CTests.swift index 4030c9f1..61f4c7a0 100644 --- a/Tests/JWTKitTests/X5CTests.swift +++ b/Tests/JWTKitTests/X5CTests.swift @@ -332,6 +332,70 @@ struct X5CTests { } } + @Test("Test signing with EdDSA x5c chain") + func signWithEdDSAX5CChain() async throws { + let caKey = try EdDSA.PrivateKey() + let caPrivateKey = try Certificate.PrivateKey(pemEncoded: caKey.pemRepresentation) + let caSubjectName = try DistinguishedName { + CommonName("CA") + } + let caCert = try Certificate( + version: .v3, + serialNumber: .init(), + publicKey: caPrivateKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date().addingTimeInterval(3600), + issuer: caSubjectName, + subject: caSubjectName, + extensions: try Certificate.Extensions { + Critical(BasicConstraints.isCertificateAuthority(maxPathLength: nil)) + Critical(KeyUsage(digitalSignature: true, keyCertSign: true)) + }, + issuerPrivateKey: caPrivateKey + ) + + let key = try EdDSA.PrivateKey() + let privateKey = try Certificate.PrivateKey(pemEncoded: key.pemRepresentation) + let subjectName = try DistinguishedName { + CommonName("Signer") + } + let cert = try Certificate( + version: .v3, + serialNumber: .init(), + publicKey: privateKey.publicKey, + notValidBefore: Date(), + notValidAfter: Date().addingTimeInterval(3600), + issuer: caSubjectName, + subject: subjectName, + extensions: try Certificate.Extensions { + Critical(KeyUsage(digitalSignature: true, keyCertSign: true)) + }, + issuerPrivateKey: caPrivateKey + ) + + let keyCollection = await JWTKeyCollection() + .add(eddsa: key) + + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + + let caCerts = try [caCert.serializeAsPEM().pemString] + let certs = try [cert.serializeAsPEM().pemString] + let header: JWTHeader = ["x5c": .array(certs.map(JWTHeaderField.string))] + let token = try await keyCollection.sign(payload, header: header) + let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) + + let x5c = try #require(parsed.header.x5c) + let verifier = try X5CVerifier(rootCertificates: caCerts) + await #expect(throws: Never.self) { + try await verifier.verifyJWS(token, as: TestPayload.self) + } + } + // MARK: Private private func getPEMString(from der: String) throws -> String { From cbb2d5b15dec4b9e0d910d5e93c145d6a32b3c84 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 20 Dec 2025 08:31:24 +1100 Subject: [PATCH 5/8] Improve algorithm-related error and x5c tests --- Sources/JWTKit/JWTError.swift | 11 +- Sources/JWTKit/X5C/X5CVerifier.swift | 4 +- Tests/JWTKitTests/X5CTests.swift | 236 ++++++++++++++------------- 3 files changed, 135 insertions(+), 116 deletions(-) diff --git a/Sources/JWTKit/JWTError.swift b/Sources/JWTKit/JWTError.swift index 31cea7f9..e053b73d 100644 --- a/Sources/JWTKit/JWTError.swift +++ b/Sources/JWTKit/JWTError.swift @@ -21,6 +21,7 @@ public struct JWTError: Error, Sendable, Equatable { case invalidX5CChain case invalidHeaderField case unsupportedCurve + case unsupportedAlgorithm case generic } @@ -43,6 +44,7 @@ public struct JWTError: Error, Sendable, Equatable { public static let invalidX5CChain = Self(.invalidX5CChain) public static let invalidHeaderField = Self(.invalidHeaderField) public static let unsupportedCurve = Self(.unsupportedCurve) + public static let unsupportedAlgorithm = Self(.unsupportedAlgorithm) public static let generic = Self(.generic) public var description: String { @@ -59,6 +61,7 @@ public struct JWTError: Error, Sendable, Equatable { fileprivate let identifier: String? fileprivate let failedClaim: (any JWTClaim)? fileprivate var curve: (any ECDSACurveType)? + fileprivate var algorithm: String? init( errorType: ErrorType, @@ -68,7 +71,8 @@ public struct JWTError: Error, Sendable, Equatable { kid: JWKIdentifier? = nil, identifier: String? = nil, failedClaim: (any JWTClaim)? = nil, - curve: (any ECDSACurveType)? = nil + curve: (any ECDSACurveType)? = nil, + algorithm: String? = nil ) { self.errorType = errorType self.name = name @@ -78,6 +82,7 @@ public struct JWTError: Error, Sendable, Equatable { self.identifier = identifier self.failedClaim = failedClaim self.curve = curve + self.algorithm = algorithm } static func == (lhs: JWTError.Backing, rhs: JWTError.Backing) -> Bool { @@ -148,6 +153,10 @@ public struct JWTError: Error, Sendable, Equatable { .init(backing: .init(errorType: .unsupportedCurve, curve: curve)) } + public static func unsupportedAlgorithm(alg: String?) -> Self { + .init(backing: .init(errorType: .unsupportedAlgorithm, algorithm: alg)) + } + public static func generic(identifier: String, reason: String) -> Self { .init(backing: .init(errorType: .generic, reason: reason)) } diff --git a/Sources/JWTKit/X5C/X5CVerifier.swift b/Sources/JWTKit/X5C/X5CVerifier.swift index 3a1f52a6..74b6b6c5 100644 --- a/Sources/JWTKit/X5C/X5CVerifier.swift +++ b/Sources/JWTKit/X5C/X5CVerifier.swift @@ -149,7 +149,7 @@ public struct X5CVerifier: Sendable { let (header, payload, _) = try parser.parse(token, as: Payload.self) guard let rawAlg = header.alg, let headerAlg = JWK.Algorithm(rawValue: rawAlg) else { - throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: header.alg))") + throw JWTError.unsupportedAlgorithm(alg: header.alg) } // Ensure the x5c header parameter is present and not empty @@ -222,7 +222,7 @@ extension X5CVerifier { let eddsaKey = try EdDSA.PublicKey(pem: certificate.publicKey.serializeAsPEM().pemString) return JWTSigner(algorithm: EdDSASigner(key: eddsaKey), parser: parser) default: - throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: alg))") + throw JWTError.unsupportedAlgorithm(alg: alg.rawValue) } } } diff --git a/Tests/JWTKitTests/X5CTests.swift b/Tests/JWTKitTests/X5CTests.swift index 61f4c7a0..f1f5a353 100644 --- a/Tests/JWTKitTests/X5CTests.swift +++ b/Tests/JWTKitTests/X5CTests.swift @@ -276,11 +276,11 @@ struct X5CTests { } } - @Test("Test signing with x5c chain") - func signWithX5CChain() async throws { + @Test("Test signing with ES256 x5c chain") + func signWithES256X5CChain() async throws { let keyCollection = try await JWTKeyCollection() .add( - ecdsa: ES256PrivateKey(pem: x5cLeafCertKey) + ecdsa: ES256PrivateKey(pem: x5cLeafCertKeys[.es256]!.serializeAsPEM().pemString) ) let payload = TestPayload( @@ -289,14 +289,14 @@ struct X5CTests { admin: false, exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) ) - let header: JWTHeader = ["x5c": .array(x5cCerts.map(JWTHeaderField.string))] + let header: JWTHeader = ["x5c": .array(x5cCerts[.es256]!.map(JWTHeaderField.string))] let token = try await keyCollection.sign(payload, header: header) let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) let x5c = try #require(parsed.header.x5c) let pemCerts = try x5c.map(getPEMString) - #expect(pemCerts == x5cCerts) - let verifier = try X5CVerifier(rootCertificates: [x5cCerts.last!]) + #expect(pemCerts == x5cCerts[.es256]!) + let verifier = try X5CVerifier(rootCertificates: [x5cCerts[.es256]!.last!]) await #expect(throws: Never.self) { try await verifier.verifyJWS(token, as: TestPayload.self) } @@ -306,7 +306,7 @@ struct X5CTests { func signWithInvalidX5CChain() async throws { let keyCollection = try await JWTKeyCollection() .add( - ecdsa: ES256PrivateKey(pem: x5cLeafCertKey) + ecdsa: ES256PrivateKey(pem: x5cLeafCertKeys[.es256]!.serializeAsPEM().pemString) ) let payload = TestPayload( @@ -317,7 +317,7 @@ struct X5CTests { ) // Remove the intermediate cert from the chain - let certs = x5cCerts.enumerated().filter { $0.offset != 1 }.map { $0.element } + let certs = x5cCerts[.es256]!.enumerated().filter { $0.offset != 1 }.map { $0.element } let header: JWTHeader = ["x5c": .array(certs.map(JWTHeaderField.string))] let token = try await keyCollection.sign(payload, header: header) @@ -332,49 +332,38 @@ struct X5CTests { } } - @Test("Test signing with EdDSA x5c chain") - func signWithEdDSAX5CChain() async throws { - let caKey = try EdDSA.PrivateKey() - let caPrivateKey = try Certificate.PrivateKey(pemEncoded: caKey.pemRepresentation) - let caSubjectName = try DistinguishedName { - CommonName("CA") - } - let caCert = try Certificate( - version: .v3, - serialNumber: .init(), - publicKey: caPrivateKey.publicKey, - notValidBefore: Date(), - notValidAfter: Date().addingTimeInterval(3600), - issuer: caSubjectName, - subject: caSubjectName, - extensions: try Certificate.Extensions { - Critical(BasicConstraints.isCertificateAuthority(maxPathLength: nil)) - Critical(KeyUsage(digitalSignature: true, keyCertSign: true)) - }, - issuerPrivateKey: caPrivateKey + @Test("Test signing with ES384 x5c chain") + func signWithES384X5CChain() async throws { + let keyCollection = try await JWTKeyCollection() + .add( + ecdsa: ES384PrivateKey(pem: x5cLeafCertKeys[.es384]!.serializeAsPEM().pemString) + ) + + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) ) + let header: JWTHeader = ["x5c": .array(x5cCerts[.es384]!.map(JWTHeaderField.string))] + let token = try await keyCollection.sign(payload, header: header) + let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) - let key = try EdDSA.PrivateKey() - let privateKey = try Certificate.PrivateKey(pemEncoded: key.pemRepresentation) - let subjectName = try DistinguishedName { - CommonName("Signer") + let x5c = try #require(parsed.header.x5c) + let pemCerts = try x5c.map(getPEMString) + #expect(pemCerts == x5cCerts[.es384]!) + let verifier = try X5CVerifier(rootCertificates: [x5cCerts[.es384]!.last!]) + await #expect(throws: Never.self) { + try await verifier.verifyJWS(token, as: TestPayload.self) } - let cert = try Certificate( - version: .v3, - serialNumber: .init(), - publicKey: privateKey.publicKey, - notValidBefore: Date(), - notValidAfter: Date().addingTimeInterval(3600), - issuer: caSubjectName, - subject: subjectName, - extensions: try Certificate.Extensions { - Critical(KeyUsage(digitalSignature: true, keyCertSign: true)) - }, - issuerPrivateKey: caPrivateKey - ) + } - let keyCollection = await JWTKeyCollection() - .add(eddsa: key) + @Test("Test signing with ES512 x5c chain") + func signWithES512X5CChain() async throws { + let keyCollection = try await JWTKeyCollection() + .add( + ecdsa: ES512PrivateKey(pem: x5cLeafCertKeys[.es512]!.serializeAsPEM().pemString) + ) let payload = TestPayload( sub: "vapor", @@ -382,15 +371,40 @@ struct X5CTests { admin: false, exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) ) + let header: JWTHeader = ["x5c": .array(x5cCerts[.es512]!.map(JWTHeaderField.string))] + let token = try await keyCollection.sign(payload, header: header) + let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) - let caCerts = try [caCert.serializeAsPEM().pemString] - let certs = try [cert.serializeAsPEM().pemString] - let header: JWTHeader = ["x5c": .array(certs.map(JWTHeaderField.string))] + let x5c = try #require(parsed.header.x5c) + let pemCerts = try x5c.map(getPEMString) + #expect(pemCerts == x5cCerts[.es512]!) + let verifier = try X5CVerifier(rootCertificates: [x5cCerts[.es512]!.last!]) + await #expect(throws: Never.self) { + try await verifier.verifyJWS(token, as: TestPayload.self) + } + } + + @Test("Test signing with EdDSA x5c chain") + func signWithEdDSAX5CChain() async throws { + let keyCollection = try await JWTKeyCollection() + .add( + eddsa: EdDSA.PrivateKey(pem: x5cLeafCertKeys[.eddsa]!.serializeAsPEM().pemString) + ) + + let payload = TestPayload( + sub: "vapor", + name: "Foo", + admin: false, + exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) + ) + let header: JWTHeader = ["x5c": .array(x5cCerts[.eddsa]!.map(JWTHeaderField.string))] let token = try await keyCollection.sign(payload, header: header) let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) let x5c = try #require(parsed.header.x5c) - let verifier = try X5CVerifier(rootCertificates: caCerts) + let pemCerts = try x5c.map(getPEMString) + #expect(pemCerts == x5cCerts[.eddsa]!) + let verifier = try X5CVerifier(rootCertificates: [x5cCerts[.eddsa]!.last!]) await #expect(throws: Never.self) { try await verifier.verifyJWS(token, as: TestPayload.self) } @@ -444,70 +458,64 @@ let missingIntermediateAndRootToken = """ eyJ4NWMiOlsiTUlJQ2ZqQ0NBaU9nQXdJQkFnSVVGeW9vWlJtc1wvU1M1SnZZTDBkbDZoSHdSNWxZd0NnWUlLb1pJemowRUF3SXdnYUV4Q3pBSkJnTlZCQVlUQWxWVE1SRXdEd1lEVlFRSURBaE9aWGNnV1c5eWF6RVJNQThHQTFVRUJ3d0lUbVYzSUZsdmNtc3hEakFNQmdOVkJBb01CVlpoY0c5eU1SUXdFZ1lEVlFRTERBdEZibWRwYm1WbGNtbHVaekVlTUJ3R0ExVUVBd3dWVm1Gd2IzSWdTVzUwWlhKdFpXUnBZWFJsSUVOQk1TWXdKQVlKS29aSWh2Y05BUWtCRmhkaFpHMXBia0IyWVhCdmNpNWxlR0Z0Y0d4bExtTnZiVEFlRncweU5UQXhNVEF3T1RJM01UaGFGdzB5TmpBeE1UQXdPVEkzTVRoYU1JR1dNUXN3Q1FZRFZRUUdFd0pWVXpFUk1BOEdBMVVFQ0F3SVRtVjNJRmx2Y21zeEVUQVBCZ05WQkFjTUNFNWxkeUJaYjNKck1RNHdEQVlEVlFRS0RBVldZWEJ2Y2pFVU1CSUdBMVVFQ3d3TFJXNW5hVzVsWlhKcGJtY3hFekFSQmdOVkJBTU1DbFpoY0c5eUlFeGxZV1l4SmpBa0Jna3Foa2lHOXcwQkNRRVdGMkZrYldsdVFIWmhjRzl5TG1WNFlXMXdiR1V1WTI5dE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVhMS1BzZDFQaXMxOFp6Nzc5TEkzamU0R0NcL0dMZWVKeHhTN1Y1bndGeFNBdFN4bUpYWlBsd0Q2Y2RGcnVYZHdKdnpKbk9QclhXRDRwWXZBTVBcLzdDUzZOQ01FQXdIUVlEVlIwT0JCWUVGSFwvY2tGdG1rSmJZeWl2eVZrb3RwMkZJcE5xV01COEdBMVVkSXdRWU1CYUFGSFBXempuN0h4R0kyeEh4Uk5FSmh5M1NSSWVWTUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDSVFEUndYOE02RFRIMEplY2RjUnQwWFU3V1JYV2ZGb0VGZmxka0xSSjlVNHZRUUloQUxwVlBYVUlaM0xMdjFVU2JZNzNKUTVjazBJNzkyY3U1XC9uYUNlVDZvOHJIIl0sImFsZyI6IkVTMjU2IiwidHlwIjoiSldUIn0.eyJjb29sIjp0cnVlfQ.PhxN-7AYea0WzTTL8GcBoVk48csux9oEvodMSuDbA4Ayxv1fO9rH-vwtSP7OO66F2DjPYUBKp5GGvTC4MA0R0g """ -let x5cCerts = [ - """ - -----BEGIN CERTIFICATE----- - MIICfTCCAiOgAwIBAgIUdSjHMW4Ee5DUpcOyHQx3KOEEoHwwCgYIKoZIzj0EAwIw - gaExCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3 - IFlvcmsxDjAMBgNVBAoMBVZhcG9yMRQwEgYDVQQLDAtFbmdpbmVlcmluZzEeMBwG - A1UEAwwVVmFwb3IgSW50ZXJtZWRpYXRlIENBMSYwJAYJKoZIhvcNAQkBFhdhZG1p - bkB2YXBvci5leGFtcGxlLmNvbTAeFw0yNTAxMTAwOTQxNDJaFw0yNjAxMTAwOTQx - NDJaMIGWMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcM - CE5ldyBZb3JrMQ4wDAYDVQQKDAVWYXBvcjEUMBIGA1UECwwLRW5naW5lZXJpbmcx - EzARBgNVBAMMClZhcG9yIExlYWYxJjAkBgkqhkiG9w0BCQEWF2FkbWluQHZhcG9y - LmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEe6mlYhBYd3VM - +yMXmW0ZwsJCzfUWU7RWUdkI35FyMcY/BQLMM2RFrrgyX8CuEVJsT6Bgzgg+hyDh - YMKwtMX1i6NCMEAwHQYDVR0OBBYEFEbhwCoMFqviogGLUVJrHgLiAMlAMB8GA1Ud - IwQYMBaAFF0UJCCJyb67oh5/1bbXdwB/nKKnMAoGCCqGSM49BAMCA0gAMEUCIAQZ - sKPKXPX1tD+rGyYQQu7Knedq1uZz8Vtoun7zx+kPAiEAv/HympBtgony5zIb3Wme - EAOpDqw6rP+TeYWgk0XyaJA= - -----END CERTIFICATE----- - """, - """ - -----BEGIN CERTIFICATE----- - MIICjjCCAjSgAwIBAgIULepBF8dIlNcyWDaMBGr29YX0bD4wCgYIKoZIzj0EAwIw - gZkxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3 - IFlvcmsxDjAMBgNVBAoMBVZhcG9yMRQwEgYDVQQLDAtFbmdpbmVlcmluZzEWMBQG - A1UEAwwNVmFwb3IgUm9vdCBDQTEmMCQGCSqGSIb3DQEJARYXYWRtaW5AdmFwb3Iu - ZXhhbXBsZS5jb20wHhcNMjUwMTEwMDk0MTQyWhcNMzAwMTA5MDk0MTQyWjCBoTEL - MAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9y - azEOMAwGA1UECgwFVmFwb3IxFDASBgNVBAsMC0VuZ2luZWVyaW5nMR4wHAYDVQQD - DBVWYXBvciBJbnRlcm1lZGlhdGUgQ0ExJjAkBgkqhkiG9w0BCQEWF2FkbWluQHZh - cG9yLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb+p+OAnz - 3W63DLze82XsWLqI75MJi6GGTdnnW9HtQhxDCMBiNkFHpUu6qtsaIEsm0PCiW640 - fLEf0hG+CmNfQaNQME4wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUXRQkIInJvrui - Hn/Vttd3AH+coqcwHwYDVR0jBBgwFoAUe3e998Fah3ndWj04w7r5VK9zhiAwCgYI - KoZIzj0EAwIDSAAwRQIhAO6Xr51C3jLaEN+gMWm0eeeK6cQFn2xIy/F8Se0jAyAo - AiAeSRkjHbrgK63cbza6Qz5ClaHYwg6WF/VNMglE10CDGw== - -----END CERTIFICATE----- - """, - """ - -----BEGIN CERTIFICATE----- - MIICiTCCAi+gAwIBAgIUcD7x8o9UAoMCY58OG6cNm0jqTJ0wCgYIKoZIzj0EAwIw - gZkxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3 - IFlvcmsxDjAMBgNVBAoMBVZhcG9yMRQwEgYDVQQLDAtFbmdpbmVlcmluZzEWMBQG - A1UEAwwNVmFwb3IgUm9vdCBDQTEmMCQGCSqGSIb3DQEJARYXYWRtaW5AdmFwb3Iu - ZXhhbXBsZS5jb20wHhcNMjUwMTEwMDk0MTQyWhcNMzUwMTA4MDk0MTQyWjCBmTEL - MAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9y - azEOMAwGA1UECgwFVmFwb3IxFDASBgNVBAsMC0VuZ2luZWVyaW5nMRYwFAYDVQQD - DA1WYXBvciBSb290IENBMSYwJAYJKoZIhvcNAQkBFhdhZG1pbkB2YXBvci5leGFt - cGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFUHLLDNUDFdH2TGjqHj - NKjvCsClKYEoWbmMXoypA6P2KHmWVVC3VSQ0hWVrpN8jza/tsLe03fjvfYrsf7IN - yLmjUzBRMB0GA1UdDgQWBBR7d733wVqHed1aPTjDuvlUr3OGIDAfBgNVHSMEGDAW - gBR7d733wVqHed1aPTjDuvlUr3OGIDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 - BAMCA0gAMEUCIEwkrw2Jx6BbuYnZb3LQ6I3hZZnjHA5Co4Re1IKf3sRBAiEAugsW - oXB0T7ftyoxbWj5qDUSnTPN+P27kWOf1GceWh3U= - -----END CERTIFICATE----- - """, +let x5cLeafCertKeys: [JWK.Algorithm:Certificate.PrivateKey] = try! [ + .es256: Certificate.PrivateKey(pemEncoded: ES256PrivateKey().pemRepresentation), + .es384: Certificate.PrivateKey(pemEncoded: ES384PrivateKey().pemRepresentation), + .es512: Certificate.PrivateKey(pemEncoded: ES512PrivateKey().pemRepresentation), + .eddsa: Certificate.PrivateKey(pemEncoded: EdDSA.PrivateKey().pemRepresentation), ] -let x5cLeafCertKey = """ - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEIJuYkleZWC5RrZUnepPFx25QI5msgOROv/KV97lknYzsoAoGCCqGSM49 - AwEHoUQDQgAEe6mlYhBYd3VM+yMXmW0ZwsJCzfUWU7RWUdkI35FyMcY/BQLMM2RF - rrgyX8CuEVJsT6Bgzgg+hyDhYMKwtMX1iw== - -----END EC PRIVATE KEY----- - """ +let x5cCerts: [JWK.Algorithm:[String]] = [ + .es256: getChain(alg: .es256), + .es384: getChain(alg: .es384), + .es512: getChain(alg: .es512), + .eddsa: getChain(alg: .eddsa), +] + +private func getChain(alg: JWK.Algorithm) -> [String] { + return try! [ + // Leaf + Certificate( + version: .v3, + serialNumber: .init(), + publicKey: x5cLeafCertKeys[alg]!.publicKey, + notValidBefore: Date(), + notValidAfter: Date().addingTimeInterval(3600), + issuer: try DistinguishedName { CommonName("Intermediate") }, + subject: try DistinguishedName { CommonName("Leaf") }, + extensions: .init(), + issuerPrivateKey: x5cLeafCertKeys[alg]! + ).serializeAsPEM().pemString, + // Intermediate + Certificate( + version: .v3, + serialNumber: .init(), + publicKey: x5cLeafCertKeys[alg]!.publicKey, + notValidBefore: Date(), + notValidAfter: Date().addingTimeInterval(3600), + issuer: try DistinguishedName { CommonName("CA") }, + subject: try DistinguishedName { CommonName("Intermediate") }, + extensions: try Certificate.Extensions { + Critical(BasicConstraints.isCertificateAuthority(maxPathLength: nil)) + }, + issuerPrivateKey: x5cLeafCertKeys[alg]! + ).serializeAsPEM().pemString, + // CA + Certificate( + version: .v3, + serialNumber: .init(), + publicKey: x5cLeafCertKeys[alg]!.publicKey, + notValidBefore: Date(), + notValidAfter: Date().addingTimeInterval(3600), + issuer: try DistinguishedName { CommonName("CA") }, + subject: try DistinguishedName { CommonName("CA") }, + extensions: try Certificate.Extensions { + Critical(BasicConstraints.isCertificateAuthority(maxPathLength: nil)) + }, + issuerPrivateKey: x5cLeafCertKeys[alg]! + ).serializeAsPEM().pemString, + ] +} let rootCA = try! Certificate( derEncoded: Array( @@ -549,3 +557,5 @@ private struct TokenPayload: JWTPayload { } } #endif // canImport(Testing) + +extension JWK.Algorithm: Hashable {} From 297ee38cfe27cb0c506ad987bbdca5ae0971e3c0 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Thu, 9 Apr 2026 18:47:25 +0200 Subject: [PATCH 6/8] Parameterized tests --- Tests/JWTKitTests/X5CTests.swift | 41 +++++++++++--------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/Tests/JWTKitTests/X5CTests.swift b/Tests/JWTKitTests/X5CTests.swift index aa64a91c..2ca858cd 100644 --- a/Tests/JWTKitTests/X5CTests.swift +++ b/Tests/JWTKitTests/X5CTests.swift @@ -281,7 +281,7 @@ struct X5CTests { @Test("Test signing with invalid x5c chain") func signWithInvalidX5CChain() async throws { let keyCollection = try await JWTKeyCollection().add( - ecdsa: ES256PrivateKey(backing: x5cLeafCertKeys[JWK.Algorithm.es256]!) + ecdsa: ES256PrivateKey(pem: x5cLeafCertKeys[JWK.Algorithm.es256]!.serializeAsPEM().pemString) ) let payload = TestPayload( @@ -312,11 +312,11 @@ struct X5CTests { let keyCollection = JWTKeyCollection() switch alg { case .es256: - try await keyCollection.add(ecdsa: ES256PrivateKey(backing: x5cLeafCertKeys[alg]!)) + try await keyCollection.add(ecdsa: ES256PrivateKey(pem: x5cLeafCertKeys[alg]!.serializeAsPEM().pemString)) case .es384: - try await keyCollection.add(ecdsa: ES384PrivateKey(backing: x5cLeafCertKeys[alg]!)) + try await keyCollection.add(ecdsa: ES384PrivateKey(pem: x5cLeafCertKeys[alg]!.serializeAsPEM().pemString)) case .es512: - try await keyCollection.add(ecdsa: ES512PrivateKey(backing: x5cLeafCertKeys[alg]!)) + try await keyCollection.add(ecdsa: ES512PrivateKey(pem: x5cLeafCertKeys[alg]!.serializeAsPEM().pemString)) default: return } @@ -340,7 +340,7 @@ struct X5CTests { } } - /*@Test("Test signing with EdDSA x5c chain") + @Test("Test signing with EdDSA x5c chain") func signWithEdDSAX5CChain() async throws { let keyCollection = try await JWTKeyCollection() .add( @@ -364,7 +364,7 @@ struct X5CTests { await #expect(throws: Never.self) { try await verifier.verifyJWS(token, as: TestPayload.self) } - }*/ + } // MARK: Private @@ -414,31 +414,18 @@ let missingLeafAndIntermediateToken = """ eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlDaVRDQ0FpK2dBd0lCQWdJVUFRbm5penpuXC9oSXJKQnkzdFBHXC9Cc1Q4emR3d0NnWUlLb1pJemowRUF3SXdnWmt4Q3pBSkJnTlZCQVlUQWxWVE1SRXdEd1lEVlFRSURBaE9aWGNnV1c5eWF6RVJNQThHQTFVRUJ3d0lUbVYzSUZsdmNtc3hEakFNQmdOVkJBb01CVlpoY0c5eU1SUXdFZ1lEVlFRTERBdEZibWRwYm1WbGNtbHVaekVXTUJRR0ExVUVBd3dOVm1Gd2IzSWdVbTl2ZENCRFFURW1NQ1FHQ1NxR1NJYjNEUUVKQVJZWFlXUnRhVzVBZG1Gd2IzSXVaWGhoYlhCc1pTNWpiMjB3SGhjTk1qWXdNakE1TVRJeE5URTJXaGNOTXpZd01qQTNNVEl4TlRFMldqQ0JtVEVMTUFrR0ExVUVCaE1DVlZNeEVUQVBCZ05WQkFnTUNFNWxkeUJaYjNKck1SRXdEd1lEVlFRSERBaE9aWGNnV1c5eWF6RU9NQXdHQTFVRUNnd0ZWbUZ3YjNJeEZEQVNCZ05WQkFzTUMwVnVaMmx1WldWeWFXNW5NUll3RkFZRFZRUUREQTFXWVhCdmNpQlNiMjkwSUVOQk1TWXdKQVlKS29aSWh2Y05BUWtCRmhkaFpHMXBia0IyWVhCdmNpNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQXRtQW9kUGZhbEo2bEJ3ak90d1VXXC9uMGM5MnRCV0R0T2RiK1NnSFVIS2JxNXZYcmxtRHdrY2plNDRmWGdVYkgwZmNcL1d0TFwvNnc1cHE1NVViek5DMmVqVXpCUk1CMEdBMVVkRGdRV0JCUjVKNURyWmZ1SjF2Nlp3c1hjYlN1QUZES3grakFmQmdOVkhTTUVHREFXZ0JSNUo1RHJaZnVKMXY2WndzWGNiU3VBRkRLeCtqQVBCZ05WSFJNQkFmOEVCVEFEQVFIXC9NQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUR0RnlkYkJoWUl4M1kxY1RvM09qelpadnNFalpRYVwvMXlIR3lrXC91QVwvalwvd0lnTWVKU054UURMVEdMdkhvWVp4dHFPNGkya2MrWjNLZWRaMGtpK1ppNEZrMD0iXX0.eyJjb29sIjp0cnVlfQ.nBm2ew-bknfjH8DyHdLJOL4I7T6gJtvxEpLypg6jel1ELz2eYQxljNr6l1ra83W1IEi9lSNFAYeq9mR3uPVwWg """ -/*let x5cLeafCertKeys: [JWK.Algorithm:Certificate.PrivateKey] = try! [ +let x5cLeafCertKeys: [JWK.Algorithm:Certificate.PrivateKey] = try! [ .es256: Certificate.PrivateKey(pemEncoded: ES256PrivateKey().pemRepresentation), .es384: Certificate.PrivateKey(pemEncoded: ES384PrivateKey().pemRepresentation), .es512: Certificate.PrivateKey(pemEncoded: ES512PrivateKey().pemRepresentation), .eddsa: Certificate.PrivateKey(pemEncoded: EdDSA.PrivateKey().pemRepresentation), -]*/ - -let x5cLeafCertKeys: [JWK.Algorithm: any ECDSAPrivateKey] = [ - .es256: Crypto.P256.Signing.PrivateKey(), - .es384: Crypto.P384.Signing.PrivateKey(), - .es512: Crypto.P521.Signing.PrivateKey(), - //.es256: ES256PrivateKey(), - //.es384: ES384PrivateKey(), - //.es512: ES512PrivateKey(), ] -/*let x5cLeafCertKeys3: [JWK.Algorithm: EdDSA.PrivateKey] = [ - .eddsa: try! .init(curve: .ed25519) -]*/ - let x5cCerts: [JWK.Algorithm:[String]] = [ .es256: getChain(alg: .es256), .es384: getChain(alg: .es384), .es512: getChain(alg: .es512), - //.eddsa: getChain(alg: .eddsa), + .eddsa: getChain(alg: .eddsa), ] private func getChain(alg: JWK.Algorithm) -> [String] { @@ -447,19 +434,19 @@ private func getChain(alg: JWK.Algorithm) -> [String] { Certificate( version: .v3, serialNumber: .init(), - publicKey: .init(pemEncoded: x5cLeafCertKeys[alg]!.publicKey.pemRepresentation), + publicKey: x5cLeafCertKeys[alg]!.publicKey, notValidBefore: Date(), notValidAfter: Date().addingTimeInterval(3600), issuer: try DistinguishedName { CommonName("Intermediate") }, subject: try DistinguishedName { CommonName("Leaf") }, extensions: .init(), - issuerPrivateKey: .init(pemEncoded: x5cLeafCertKeys[alg]!.pemRepresentation) + issuerPrivateKey: x5cLeafCertKeys[alg]! ).serializeAsPEM().pemString, // Intermediate Certificate( version: .v3, serialNumber: .init(), - publicKey: .init(pemEncoded: x5cLeafCertKeys[alg]!.publicKey.pemRepresentation), + publicKey: x5cLeafCertKeys[alg]!.publicKey, notValidBefore: Date(), notValidAfter: Date().addingTimeInterval(3600), issuer: try DistinguishedName { CommonName("CA") }, @@ -467,13 +454,13 @@ private func getChain(alg: JWK.Algorithm) -> [String] { extensions: try Certificate.Extensions { Critical(BasicConstraints.isCertificateAuthority(maxPathLength: nil)) }, - issuerPrivateKey: .init(pemEncoded: x5cLeafCertKeys[alg]!.pemRepresentation) + issuerPrivateKey: x5cLeafCertKeys[alg]! ).serializeAsPEM().pemString, // CA Certificate( version: .v3, serialNumber: .init(), - publicKey: .init(pemEncoded: x5cLeafCertKeys[alg]!.publicKey.pemRepresentation), + publicKey: x5cLeafCertKeys[alg]!.publicKey, notValidBefore: Date(), notValidAfter: Date().addingTimeInterval(3600), issuer: try DistinguishedName { CommonName("CA") }, @@ -481,7 +468,7 @@ private func getChain(alg: JWK.Algorithm) -> [String] { extensions: try Certificate.Extensions { Critical(BasicConstraints.isCertificateAuthority(maxPathLength: nil)) }, - issuerPrivateKey: .init(pemEncoded: x5cLeafCertKeys[alg]!.pemRepresentation) + issuerPrivateKey: x5cLeafCertKeys[alg]! ).serializeAsPEM().pemString, ] } From 7f276c2312800492e628337a5f0190176f03602a Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Thu, 9 Apr 2026 18:53:44 +0200 Subject: [PATCH 7/8] Parameterize signWithInvalidX5CChain --- Tests/JWTKitTests/X5CTests.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Tests/JWTKitTests/X5CTests.swift b/Tests/JWTKitTests/X5CTests.swift index 2ca858cd..58ddc9f1 100644 --- a/Tests/JWTKitTests/X5CTests.swift +++ b/Tests/JWTKitTests/X5CTests.swift @@ -278,11 +278,19 @@ struct X5CTests { } } - @Test("Test signing with invalid x5c chain") - func signWithInvalidX5CChain() async throws { - let keyCollection = try await JWTKeyCollection().add( - ecdsa: ES256PrivateKey(pem: x5cLeafCertKeys[JWK.Algorithm.es256]!.serializeAsPEM().pemString) - ) + @Test("Test signing with invalid x5c chain", arguments: [JWK.Algorithm.es256, .es384, .es512]) + func signWithInvalidX5CChain(_ alg: JWK.Algorithm) async throws { + let keyCollection = JWTKeyCollection() + switch alg { + case .es256: + try await keyCollection.add(ecdsa: ES256PrivateKey(pem: x5cLeafCertKeys[alg]!.serializeAsPEM().pemString)) + case .es384: + try await keyCollection.add(ecdsa: ES384PrivateKey(pem: x5cLeafCertKeys[alg]!.serializeAsPEM().pemString)) + case .es512: + try await keyCollection.add(ecdsa: ES512PrivateKey(pem: x5cLeafCertKeys[alg]!.serializeAsPEM().pemString)) + default: + return + } let payload = TestPayload( sub: "vapor", From 76c7f354630233015a5b5789041c08061739e6de Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Thu, 9 Apr 2026 18:58:42 +0200 Subject: [PATCH 8/8] swift-format --- Tests/JWTKitTests/X5CTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/JWTKitTests/X5CTests.swift b/Tests/JWTKitTests/X5CTests.swift index 58ddc9f1..12e92d49 100644 --- a/Tests/JWTKitTests/X5CTests.swift +++ b/Tests/JWTKitTests/X5CTests.swift @@ -422,14 +422,14 @@ let missingLeafAndIntermediateToken = """ eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlDaVRDQ0FpK2dBd0lCQWdJVUFRbm5penpuXC9oSXJKQnkzdFBHXC9Cc1Q4emR3d0NnWUlLb1pJemowRUF3SXdnWmt4Q3pBSkJnTlZCQVlUQWxWVE1SRXdEd1lEVlFRSURBaE9aWGNnV1c5eWF6RVJNQThHQTFVRUJ3d0lUbVYzSUZsdmNtc3hEakFNQmdOVkJBb01CVlpoY0c5eU1SUXdFZ1lEVlFRTERBdEZibWRwYm1WbGNtbHVaekVXTUJRR0ExVUVBd3dOVm1Gd2IzSWdVbTl2ZENCRFFURW1NQ1FHQ1NxR1NJYjNEUUVKQVJZWFlXUnRhVzVBZG1Gd2IzSXVaWGhoYlhCc1pTNWpiMjB3SGhjTk1qWXdNakE1TVRJeE5URTJXaGNOTXpZd01qQTNNVEl4TlRFMldqQ0JtVEVMTUFrR0ExVUVCaE1DVlZNeEVUQVBCZ05WQkFnTUNFNWxkeUJaYjNKck1SRXdEd1lEVlFRSERBaE9aWGNnV1c5eWF6RU9NQXdHQTFVRUNnd0ZWbUZ3YjNJeEZEQVNCZ05WQkFzTUMwVnVaMmx1WldWeWFXNW5NUll3RkFZRFZRUUREQTFXWVhCdmNpQlNiMjkwSUVOQk1TWXdKQVlKS29aSWh2Y05BUWtCRmhkaFpHMXBia0IyWVhCdmNpNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQXRtQW9kUGZhbEo2bEJ3ak90d1VXXC9uMGM5MnRCV0R0T2RiK1NnSFVIS2JxNXZYcmxtRHdrY2plNDRmWGdVYkgwZmNcL1d0TFwvNnc1cHE1NVViek5DMmVqVXpCUk1CMEdBMVVkRGdRV0JCUjVKNURyWmZ1SjF2Nlp3c1hjYlN1QUZES3grakFmQmdOVkhTTUVHREFXZ0JSNUo1RHJaZnVKMXY2WndzWGNiU3VBRkRLeCtqQVBCZ05WSFJNQkFmOEVCVEFEQVFIXC9NQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUR0RnlkYkJoWUl4M1kxY1RvM09qelpadnNFalpRYVwvMXlIR3lrXC91QVwvalwvd0lnTWVKU054UURMVEdMdkhvWVp4dHFPNGkya2MrWjNLZWRaMGtpK1ppNEZrMD0iXX0.eyJjb29sIjp0cnVlfQ.nBm2ew-bknfjH8DyHdLJOL4I7T6gJtvxEpLypg6jel1ELz2eYQxljNr6l1ra83W1IEi9lSNFAYeq9mR3uPVwWg """ -let x5cLeafCertKeys: [JWK.Algorithm:Certificate.PrivateKey] = try! [ +let x5cLeafCertKeys: [JWK.Algorithm: Certificate.PrivateKey] = try! [ .es256: Certificate.PrivateKey(pemEncoded: ES256PrivateKey().pemRepresentation), .es384: Certificate.PrivateKey(pemEncoded: ES384PrivateKey().pemRepresentation), .es512: Certificate.PrivateKey(pemEncoded: ES512PrivateKey().pemRepresentation), .eddsa: Certificate.PrivateKey(pemEncoded: EdDSA.PrivateKey().pemRepresentation), ] -let x5cCerts: [JWK.Algorithm:[String]] = [ +let x5cCerts: [JWK.Algorithm: [String]] = [ .es256: getChain(alg: .es256), .es384: getChain(alg: .es384), .es512: getChain(alg: .es512),