|
4 | 4 | "crypto/ed25519" |
5 | 5 | "crypto/x509" |
6 | 6 | "encoding/base64" |
| 7 | + "encoding/json" |
7 | 8 | "encoding/pem" |
8 | 9 | "testing" |
9 | 10 |
|
@@ -193,3 +194,141 @@ func TestConvertJWKToPEM(t *testing.T) { |
193 | 194 | }) |
194 | 195 | } |
195 | 196 | } |
| 197 | + |
| 198 | +func TestConvertPEMToJWK(t *testing.T) { |
| 199 | + tests := []struct { |
| 200 | + name string |
| 201 | + pemData string |
| 202 | + wantErr bool |
| 203 | + errMsg string |
| 204 | + wantKty string |
| 205 | + wantCrv string |
| 206 | + validateFn func(t *testing.T, jwkJSON string) |
| 207 | + }{ |
| 208 | + { |
| 209 | + name: "valid Ed25519 PEM key", |
| 210 | + pemData: `-----BEGIN PRIVATE KEY----- |
| 211 | +MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF |
| 212 | +-----END PRIVATE KEY-----`, |
| 213 | + wantErr: false, |
| 214 | + wantKty: "OKP", |
| 215 | + wantCrv: "Ed25519", |
| 216 | + validateFn: func(t *testing.T, jwkJSON string) { |
| 217 | + // Parse the JWK |
| 218 | + var jwk jwkKey |
| 219 | + err := json.Unmarshal([]byte(jwkJSON), &jwk) |
| 220 | + require.NoError(t, err) |
| 221 | + |
| 222 | + // Verify structure |
| 223 | + require.NotEmpty(t, jwk.D, "Private key component 'd' should not be empty") |
| 224 | + require.NotEmpty(t, jwk.X, "Public key component 'x' should not be empty") |
| 225 | + |
| 226 | + // Verify the private key can be decoded |
| 227 | + privKeyBytes, err := base64.RawURLEncoding.DecodeString(jwk.D) |
| 228 | + require.NoError(t, err) |
| 229 | + assert.Len(t, privKeyBytes, ed25519.SeedSize, "Private key should be 32 bytes") |
| 230 | + |
| 231 | + // Verify the public key can be decoded |
| 232 | + pubKeyBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) |
| 233 | + require.NoError(t, err) |
| 234 | + assert.Len(t, pubKeyBytes, ed25519.PublicKeySize, "Public key should be 32 bytes") |
| 235 | + }, |
| 236 | + }, |
| 237 | + { |
| 238 | + name: "invalid PEM format", |
| 239 | + pemData: "not a pem key", |
| 240 | + wantErr: true, |
| 241 | + errMsg: "failed to decode PEM block", |
| 242 | + }, |
| 243 | + { |
| 244 | + name: "wrong PEM type (public key)", |
| 245 | + pemData: `-----BEGIN PUBLIC KEY----- |
| 246 | +MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs= |
| 247 | +-----END PUBLIC KEY-----`, |
| 248 | + wantErr: true, |
| 249 | + errMsg: "invalid PEM type", |
| 250 | + }, |
| 251 | + { |
| 252 | + name: "invalid PKCS8 data", |
| 253 | + pemData: `-----BEGIN PRIVATE KEY----- |
| 254 | +aW52YWxpZCBkYXRh |
| 255 | +-----END PRIVATE KEY-----`, |
| 256 | + wantErr: true, |
| 257 | + errMsg: "failed to parse PKCS#8 private key", |
| 258 | + }, |
| 259 | + } |
| 260 | + |
| 261 | + for _, tt := range tests { |
| 262 | + t.Run(tt.name, func(t *testing.T) { |
| 263 | + jwkJSON, err := ConvertPEMToJWK(tt.pemData) |
| 264 | + if tt.wantErr { |
| 265 | + require.Error(t, err) |
| 266 | + if tt.errMsg != "" { |
| 267 | + assert.Contains(t, err.Error(), tt.errMsg) |
| 268 | + } |
| 269 | + return |
| 270 | + } |
| 271 | + |
| 272 | + require.NoError(t, err) |
| 273 | + require.NotEmpty(t, jwkJSON) |
| 274 | + |
| 275 | + // Parse and verify JWK structure |
| 276 | + var jwk jwkKey |
| 277 | + err = json.Unmarshal([]byte(jwkJSON), &jwk) |
| 278 | + require.NoError(t, err, "JWK should be valid JSON") |
| 279 | + |
| 280 | + // Verify basic fields |
| 281 | + assert.Equal(t, tt.wantKty, jwk.Kty, "Key type should be OKP") |
| 282 | + assert.Equal(t, tt.wantCrv, jwk.Crv, "Curve should be Ed25519") |
| 283 | + |
| 284 | + // Run custom validation if provided |
| 285 | + if tt.validateFn != nil { |
| 286 | + tt.validateFn(t, jwkJSON) |
| 287 | + } |
| 288 | + }) |
| 289 | + } |
| 290 | +} |
| 291 | + |
| 292 | +func TestPEMToJWKRoundtrip(t *testing.T) { |
| 293 | + // Test that converting PEM -> JWK -> PEM produces equivalent keys |
| 294 | + originalPEM := `-----BEGIN PRIVATE KEY----- |
| 295 | +MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF |
| 296 | +-----END PRIVATE KEY-----` |
| 297 | + |
| 298 | + // Convert PEM to JWK |
| 299 | + jwkJSON, err := ConvertPEMToJWK(originalPEM) |
| 300 | + require.NoError(t, err) |
| 301 | + require.NotEmpty(t, jwkJSON) |
| 302 | + |
| 303 | + // Convert JWK back to PEM |
| 304 | + pemData, err := ConvertJWKToPEM(jwkJSON) |
| 305 | + require.NoError(t, err) |
| 306 | + require.NotEmpty(t, pemData) |
| 307 | + |
| 308 | + // Parse both keys and verify they're the same |
| 309 | + block1, _ := pem.Decode([]byte(originalPEM)) |
| 310 | + require.NotNil(t, block1) |
| 311 | + key1, err := x509.ParsePKCS8PrivateKey(block1.Bytes) |
| 312 | + require.NoError(t, err) |
| 313 | + ed25519Key1 := key1.(ed25519.PrivateKey) |
| 314 | + |
| 315 | + block2, _ := pem.Decode(pemData) |
| 316 | + require.NotNil(t, block2) |
| 317 | + key2, err := x509.ParsePKCS8PrivateKey(block2.Bytes) |
| 318 | + require.NoError(t, err) |
| 319 | + ed25519Key2 := key2.(ed25519.PrivateKey) |
| 320 | + |
| 321 | + // Compare private keys |
| 322 | + assert.Equal(t, ed25519Key1, ed25519Key2, "Keys should be identical after roundtrip") |
| 323 | + |
| 324 | + // Verify signatures match |
| 325 | + message := []byte("test message for signature verification") |
| 326 | + sig1 := ed25519.Sign(ed25519Key1, message) |
| 327 | + sig2 := ed25519.Sign(ed25519Key2, message) |
| 328 | + |
| 329 | + pubKey1 := ed25519Key1.Public().(ed25519.PublicKey) |
| 330 | + pubKey2 := ed25519Key2.Public().(ed25519.PublicKey) |
| 331 | + |
| 332 | + assert.True(t, ed25519.Verify(pubKey1, message, sig2), "Key1 should verify signature from Key2") |
| 333 | + assert.True(t, ed25519.Verify(pubKey2, message, sig1), "Key2 should verify signature from Key1") |
| 334 | +} |
0 commit comments