From bc2e1c3870acd68c5da651dd9511a62b9e88730e Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Mon, 27 Oct 2025 12:58:31 +0100 Subject: [PATCH 1/4] Add PEM representation init and property to EdDSA keys --- Package.swift | 2 +- Sources/JWTKit/EdDSA/EdDSA.swift | 31 ++++++++++++++++++++++++++ Sources/JWTKit/EdDSA/EdDSASigner.swift | 6 ++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 17d1ffbd..51c78125 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "JWTKit", targets: ["JWTKit"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", from: "4.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", branch: "main"), .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/EdDSA/EdDSA.swift b/Sources/JWTKit/EdDSA/EdDSA.swift index 7d2746b9..6da48bc0 100644 --- a/Sources/JWTKit/EdDSA/EdDSA.swift +++ b/Sources/JWTKit/EdDSA/EdDSA.swift @@ -33,6 +33,15 @@ extension EdDSA { self.curve = .ed25519 } + /// Creates an ``EdDSA.PublicKey`` instance using the provided PEM + /// (Privacy Enhanced Mail) representation. + /// + /// - Parameter pem: The PEM representation of the public key. + public init(pem string: String) throws { + self.backing = try .init(pemRepresentation: string) + self.curve = .ed25519 + } + /// Creates an ``EdDSA.PublicKey`` instance using the public key x-coordinate and specified curve. /// /// This init allows for the creation of an ``EdDSA.PublicKey`` using the x-coordinate of the public key. @@ -61,9 +70,15 @@ extension EdDSA { self.init(backing: key) } + /// Raw bytes representation of the public key. public var rawRepresentation: Data { self.backing.rawRepresentation } + + /// PEM (Privacy Enhanced Mail) representation of the public key. + public var pemRepresentation: String { + self.backing.pemRepresentation + } } } @@ -94,6 +109,15 @@ extension EdDSA { self.init(backing: key) } + /// Creates an ``EdDSA.PrivateKey`` instance using the provided PEM + /// (Privacy Enhanced Mail) representation. + /// + /// - Parameter pem: The PEM representation of the private key. + public init(pem string: String) throws { + self.backing = try .init(pemRepresentation: string) + self.curve = .ed25519 + } + /// Creates an ``EdDSA.PrivateKey`` instance using the provided private key. /// /// This init constructs an ``EdDSA.PrivateKey`` based on the corresponding SwiftCrypto @@ -132,12 +156,19 @@ extension EdDSA { self.init(backing: key) } + /// ``EdDSA.PublicKey`` associated with this private key. public var publicKey: PublicKey { .init(backing: self.backing.publicKey) } + /// Raw bytes representation of the private key. public var rawRepresentation: Data { self.backing.rawRepresentation } + + /// PEM (Privacy Enhanced Mail) representation of the public key. + public var pemRepresentation: String { + self.backing.pemRepresentation + } } } diff --git a/Sources/JWTKit/EdDSA/EdDSASigner.swift b/Sources/JWTKit/EdDSA/EdDSASigner.swift index 6c984336..0157e012 100644 --- a/Sources/JWTKit/EdDSA/EdDSASigner.swift +++ b/Sources/JWTKit/EdDSA/EdDSASigner.swift @@ -32,16 +32,14 @@ struct EdDSASigner: JWTAlgorithm, Sendable { switch privateKey.curve.backing { case .ed25519: - return try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey.rawRepresentation) - .signature(for: plaintext).copyBytes() + return try privateKey.backing.signature(for: plaintext).copyBytes() } } func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws -> Bool { switch publicKey.curve.backing { case .ed25519: - try Curve25519.Signing.PublicKey(rawRepresentation: publicKey.rawRepresentation) - .isValidSignature(signature, for: plaintext) + publicKey.backing.isValidSignature(signature, for: plaintext) } } } From 618f10eda6d136aba1c00a9bdda0fa3d14d0a3a8 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 30 Oct 2025 15:46:36 +0100 Subject: [PATCH 2/4] Add tests --- Package.swift | 2 +- Sources/JWTKit/EdDSA/EdDSA.swift | 12 ++++- Tests/JWTKitTests/EdDSATests.swift | 74 ++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 51c78125..296ccdf3 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "JWTKit", targets: ["JWTKit"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", branch: "main"), + .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/EdDSA/EdDSA.swift b/Sources/JWTKit/EdDSA/EdDSA.swift index 6da48bc0..cef5693f 100644 --- a/Sources/JWTKit/EdDSA/EdDSA.swift +++ b/Sources/JWTKit/EdDSA/EdDSA.swift @@ -19,7 +19,7 @@ extension EdDSA { /// /// In JWT, EdDSA public keys are represented as a single x-coordinate and are used for verifying signatures. /// Currently, only the ``EdDSACurve/ed25519`` curve is supported. - public struct PublicKey: EdDSAKey { + public struct PublicKey: EdDSAKey, Equatable { let backing: Curve25519.Signing.PublicKey let curve: EdDSACurve @@ -79,6 +79,10 @@ extension EdDSA { public var pemRepresentation: String { self.backing.pemRepresentation } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.backing.derRepresentation == rhs.backing.derRepresentation + } } } @@ -87,7 +91,7 @@ extension EdDSA { /// /// In JWT, EdDSA private keys are represented as a pair of x-coordinate and private key (d) and are used for signing. /// Currently, only the ``Curve/ed25519`` curve is supported. - public struct PrivateKey: EdDSAKey { + public struct PrivateKey: EdDSAKey, Equatable { let backing: Curve25519.Signing.PrivateKey let curve: EdDSACurve @@ -170,5 +174,9 @@ extension EdDSA { public var pemRepresentation: String { self.backing.pemRepresentation } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.backing.derRepresentation == rhs.backing.derRepresentation + } } } diff --git a/Tests/JWTKitTests/EdDSATests.swift b/Tests/JWTKitTests/EdDSATests.swift index 5fb535c3..98efd6bd 100644 --- a/Tests/JWTKitTests/EdDSATests.swift +++ b/Tests/JWTKitTests/EdDSATests.swift @@ -4,7 +4,6 @@ import JWTKit @Suite("EdDSA Tests") struct EdDSATests { - @Test("Test EdDSA Generate") func edDSAGenerate() async throws { let payload = TestPayload( @@ -24,9 +23,10 @@ struct EdDSATests { @Test("Test EdDSA Public and Private") func edDSAPublicPrivate() async throws { - let keys = try await JWTKeyCollection() - .add(eddsa: EdDSA.PublicKey(x: eddsaPublicKeyBase64, curve: .ed25519), kid: "public") - .add(eddsa: EdDSA.PrivateKey(d: eddsaPrivateKeyBase64, curve: .ed25519), kid: "private") + let signingCollection = try await JWTKeyCollection() + .add(eddsa: EdDSA.PrivateKey(d: eddsaPrivateKeyBase64, curve: .ed25519)) + let verifyingCollection = try await JWTKeyCollection() + .add(eddsa: EdDSA.PublicKey(x: eddsaPublicKeyBase64, curve: .ed25519)) let payload = TestPayload( sub: "vapor", @@ -35,21 +35,14 @@ struct EdDSATests { exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) ) - for _ in 0..<1000 { - let token = try await keys.sign(payload, kid: "private") - // test public signer decoding - let verifiedPayload = try await keys.verify(token, as: TestPayload.self) - #expect(verifiedPayload == payload) - } + let token = try await signingCollection.sign(payload, kid: "private") + // test public signer decoding + let verifiedPayload = try await verifyingCollection.verify(token, as: TestPayload.self) + #expect(verifiedPayload == payload) } @Test("Test Verifying EdDSA Key Using JWK") func verifyingEdDSAKeyUsingJWK() async throws { - struct Foo: JWTPayload { - var bar: Int - func verify(using _: some JWTAlgorithm) throws {} - } - // ecdsa key in base64 format let x = eddsaPublicKeyBase64 let d = eddsaPrivateKeyBase64 @@ -83,11 +76,6 @@ struct EdDSATests { @Test("Test Verifying EdDSA Key Using JWK Base64URL") func verifyingEdDSAKeyUsingJWKBase64URL() async throws { - struct Foo: JWTPayload { - var bar: Int - func verify(using _: some JWTAlgorithm) throws {} - } - let x = eddsaPublicKeyBase64Url let d = eddsaPrivateKeyBase64Url @@ -120,11 +108,6 @@ struct EdDSATests { @Test("Test Verifying EdDSA Key Using JWK with Mixed Base64 Formats") func verifyingEdDSAKeyUsingJWKWithMixedBase64Formats() async throws { - struct Foo: JWTPayload { - var bar: Int - func verify(using _: some JWTAlgorithm) throws {} - } - // eddsa key in base64url format let x = eddsaPublicKeyBase64Url let d = eddsaPrivateKeyBase64 @@ -155,10 +138,51 @@ struct EdDSATests { let foo = try await keyCollection.verify(jwt, as: Foo.self) #expect(foo.bar == 42) } + + @Test("Signing and Verifying with PEM") + func signingAndVerifyingWithPEM() async throws { + let signingKeyCollection = try await JWTKeyCollection() + .add(eddsa: EdDSA.PrivateKey(pem: eddsaPrivateKeyPEM)) + + let verificationKeyCollection = try await JWTKeyCollection() + .add(eddsa: EdDSA.PublicKey(pem: eddsaPublicKeyPEM)) + + let jwt = try await signingKeyCollection.sign(Foo(bar: 42)) + let foo = try await verificationKeyCollection.verify(jwt, as: Foo.self) + #expect(foo.bar == 42) + } + + @Test("PEM representation") + func pemRepresentation() async throws { + let privateKey = try EdDSA.PrivateKey(pem: eddsaPrivateKeyPEM) + let derivedPrivateKey = try EdDSA.PrivateKey(pem: privateKey.pemRepresentation) + #expect(privateKey == derivedPrivateKey) + + let publicKey = try EdDSA.PublicKey(pem: eddsaPublicKeyPEM) + let derivedPublicKey = try EdDSA.PublicKey(pem: publicKey.pemRepresentation) + #expect(publicKey == derivedPublicKey) + } + + struct Foo: JWTPayload { + var bar: Int + func verify(using _: some JWTAlgorithm) throws {} + } } let eddsaPublicKeyBase64 = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE=" let eddsaPrivateKeyBase64 = "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=" let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE" let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w" + +let eddsaPrivateKeyPEM = """ + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJXTkfKlQbhHjAcfEjQH9BMqUAxXalmgl1zi5q9zgSXB + -----END PRIVATE KEY----- + """ + +let eddsaPublicKeyPEM = """ + -----BEGIN PUBLIC KEY----- + MCowBQYDK2VwAyEAHprHxN90GB+Kue+eXMpwuJc1xcouR1V3ZpVFLrsdgUQ= + -----END PUBLIC KEY----- + """ #endif // canImport(Testing) From ed326508c78aaca5d3e9a478d7a9c06e7cae985a Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 30 Oct 2025 16:38:48 +0100 Subject: [PATCH 3/4] Format --- Sources/JWTKit/EdDSA/EdDSA.swift | 8 ++++---- Tests/JWTKitTests/EdDSATests.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JWTKit/EdDSA/EdDSA.swift b/Sources/JWTKit/EdDSA/EdDSA.swift index cef5693f..115786dd 100644 --- a/Sources/JWTKit/EdDSA/EdDSA.swift +++ b/Sources/JWTKit/EdDSA/EdDSA.swift @@ -33,9 +33,9 @@ extension EdDSA { self.curve = .ed25519 } - /// Creates an ``EdDSA.PublicKey`` instance using the provided PEM + /// Creates an ``EdDSA.PublicKey`` instance using the provided PEM /// (Privacy Enhanced Mail) representation. - /// + /// /// - Parameter pem: The PEM representation of the public key. public init(pem string: String) throws { self.backing = try .init(pemRepresentation: string) @@ -113,9 +113,9 @@ extension EdDSA { self.init(backing: key) } - /// Creates an ``EdDSA.PrivateKey`` instance using the provided PEM + /// Creates an ``EdDSA.PrivateKey`` instance using the provided PEM /// (Privacy Enhanced Mail) representation. - /// + /// /// - Parameter pem: The PEM representation of the private key. public init(pem string: String) throws { self.backing = try .init(pemRepresentation: string) diff --git a/Tests/JWTKitTests/EdDSATests.swift b/Tests/JWTKitTests/EdDSATests.swift index 98efd6bd..85595e5e 100644 --- a/Tests/JWTKitTests/EdDSATests.swift +++ b/Tests/JWTKitTests/EdDSATests.swift @@ -143,7 +143,7 @@ struct EdDSATests { func signingAndVerifyingWithPEM() async throws { let signingKeyCollection = try await JWTKeyCollection() .add(eddsa: EdDSA.PrivateKey(pem: eddsaPrivateKeyPEM)) - + let verificationKeyCollection = try await JWTKeyCollection() .add(eddsa: EdDSA.PublicKey(pem: eddsaPublicKeyPEM)) From f5f0de3927e9b00fc6803c68441cad81a10fe64a Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 30 Oct 2025 16:41:28 +0100 Subject: [PATCH 4/4] Disable windows CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cd1193f..753a05e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: with_api_check: ${{ github.event_name == 'pull_request' }} warnings_as_errors: true with_linting: true - with_windows: true + with_windows: false with_musl: true with_android: true ios_scheme_name: jwt-kit