|
| 1 | +/* |
| 2 | + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"). |
| 5 | + * You may not use this file except in compliance with the License. |
| 6 | + * A copy of the License is located at |
| 7 | + * |
| 8 | + * http://aws.amazon.com/apache2.0 |
| 9 | + * |
| 10 | + * or in the "license" file accompanying this file. This file is distributed |
| 11 | + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either |
| 12 | + * express or implied. See the License for the specific language governing |
| 13 | + * permissions and limitations under the License. |
| 14 | + */ |
| 15 | + |
| 16 | +package software.amazon.awssdk.services.signin.auth.internal; |
| 17 | + |
| 18 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 19 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 20 | +import static org.junit.jupiter.api.Assertions.assertTrue; |
| 21 | + |
| 22 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 23 | +import java.math.BigInteger; |
| 24 | +import java.nio.charset.StandardCharsets; |
| 25 | +import java.security.KeyFactory; |
| 26 | +import java.security.Signature; |
| 27 | +import java.security.interfaces.ECPublicKey; |
| 28 | +import java.security.spec.ECGenParameterSpec; |
| 29 | +import java.security.spec.ECParameterSpec; |
| 30 | +import java.security.spec.ECPoint; |
| 31 | +import java.security.spec.ECPublicKeySpec; |
| 32 | +import java.time.Instant; |
| 33 | +import java.util.Base64; |
| 34 | +import java.util.Map; |
| 35 | +import java.util.UUID; |
| 36 | +import org.junit.Test; |
| 37 | +import software.amazon.awssdk.services.signin.internal.DpopHeaderGenerator; |
| 38 | + |
| 39 | +public class DpopHeaderGeneratorTest { |
| 40 | + private static final String VALID_TEST_PEM = "-----BEGIN EC PRIVATE KEY-----\n" |
| 41 | + + "MHcCAQEEICeY73qhQO/3o1QnrL5Nu3HMDB9h3kVW6imRdcHks0tboAoGCCqGSM49" |
| 42 | + + "AwEHoUQDQgAEbefyxjd/UlGwAPF6hy0k4yCW7dSghc6yPd4To0sBqX0tPS/aoLrl" |
| 43 | + + "QnPjfDslgD29p4+Pgwxj1s8cFHVeDKdKTQ==\n" |
| 44 | + + "-----END EC PRIVATE KEY-----"; |
| 45 | + private static final ObjectMapper MAPPER = new ObjectMapper(); |
| 46 | + |
| 47 | + @Test |
| 48 | + public void testGenerateAndVerifyDPoPHeader() throws Exception { |
| 49 | + String endpoint = "https://ap-northeast-1.aws-signin-testing.amazon.com/v1/token"; |
| 50 | + long epochSeconds = Instant.now().getEpochSecond(); |
| 51 | + String uuid = UUID.randomUUID().toString(); |
| 52 | + |
| 53 | + // Generate the DPoP proof JWT |
| 54 | + String dpop = DpopHeaderGenerator.generateDPoPProofHeader(VALID_TEST_PEM, endpoint, epochSeconds, uuid); |
| 55 | + assertNotNull(dpop, "DPoP header should not be null"); |
| 56 | + |
| 57 | + // JWT should be in form header.payload.signature |
| 58 | + String[] parts = dpop.split("\\."); |
| 59 | + assertEquals(3, parts.length, "DPoP header must have 3 JWT parts"); |
| 60 | + |
| 61 | + // Decode and parse header |
| 62 | + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); |
| 63 | + Map<String, Object> header = MAPPER.readValue(headerJson, Map.class); |
| 64 | + assertEquals("ES256", header.get("alg")); |
| 65 | + assertEquals("dpop+jwt", header.get("typ")); |
| 66 | + assertTrue(header.containsKey("jwk")); |
| 67 | + |
| 68 | + Map<String, String> jwk = (Map<String, String>) header.get("jwk"); |
| 69 | + assertEquals("EC", jwk.get("kty")); |
| 70 | + assertEquals("P-256", jwk.get("crv")); |
| 71 | + assertNotNull(jwk.get("x")); |
| 72 | + assertNotNull(jwk.get("y")); |
| 73 | + |
| 74 | + // Decode and parse payload |
| 75 | + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); |
| 76 | + Map<String, Object> payload = MAPPER.readValue(payloadJson, Map.class); |
| 77 | + assertEquals(uuid, payload.get("jti")); |
| 78 | + assertEquals("POST", payload.get("htm")); |
| 79 | + assertEquals(endpoint, payload.get("htu")); |
| 80 | + assertEquals(((Number) payload.get("iat")).longValue(), epochSeconds); |
| 81 | + |
| 82 | + // Verify the ES256 signature using the public key from JWK |
| 83 | + boolean verified = verifySignature(jwk, parts[0], parts[1], parts[2]); |
| 84 | + assertTrue(verified, "DPoP ES256 signature should verify correctly"); |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Verifies an ES256 signature given base64url-encoded JWT parts and the JWK public key. |
| 89 | + */ |
| 90 | + private static boolean verifySignature(Map<String, String> jwk, String encodedHeader, String encodedPayload, String encodedSignature) throws Exception { |
| 91 | + byte[] sigBytes = Base64.getUrlDecoder().decode(encodedSignature); |
| 92 | + byte[] message = (encodedHeader + "." + encodedPayload).getBytes(StandardCharsets.UTF_8); |
| 93 | + |
| 94 | + // Convert x and y to BigIntegers |
| 95 | + BigInteger x = new BigInteger(1, Base64.getUrlDecoder().decode(jwk.get("x"))); |
| 96 | + BigInteger y = new BigInteger(1, Base64.getUrlDecoder().decode(jwk.get("y"))); |
| 97 | + ECPoint w = new ECPoint(x, y); |
| 98 | + |
| 99 | + // Use the NIST P-256 curve |
| 100 | + KeyFactory kf = KeyFactory.getInstance("EC"); |
| 101 | + ECParameterSpec ecSpec = getECParameterSpec("secp256r1"); |
| 102 | + ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, ecSpec); |
| 103 | + ECPublicKey pubKey = (ECPublicKey) kf.generatePublic(pubSpec); |
| 104 | + |
| 105 | + // Convert JWS (R||S) signature to DER for Java verification |
| 106 | + byte[] derSignature = jwsToDer(sigBytes); |
| 107 | + |
| 108 | + Signature verifier = Signature.getInstance("SHA256withECDSA"); |
| 109 | + verifier.initVerify(pubKey); |
| 110 | + verifier.update(message); |
| 111 | + return verifier.verify(derSignature); |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + * Converts a 64-byte JWS (R||S) ECDSA signature to DER format. |
| 116 | + */ |
| 117 | + private static byte[] jwsToDer(byte[] jwsSignature) throws Exception { |
| 118 | + if (jwsSignature.length != 64) { |
| 119 | + throw new IllegalArgumentException("Invalid ES256 signature length"); |
| 120 | + } |
| 121 | + byte[] r = new byte[32]; |
| 122 | + byte[] s = new byte[32]; |
| 123 | + System.arraycopy(jwsSignature, 0, r, 0, 32); |
| 124 | + System.arraycopy(jwsSignature, 32, s, 0, 32); |
| 125 | + |
| 126 | + BigInteger R = new BigInteger(1, r); |
| 127 | + BigInteger S = new BigInteger(1, s); |
| 128 | + |
| 129 | + // ASN.1 encode sequence of two INTEGERs |
| 130 | + byte[] derR = encodeDerInteger(R); |
| 131 | + byte[] derS = encodeDerInteger(S); |
| 132 | + int len = derR.length + derS.length; |
| 133 | + byte[] der = new byte[len + 2]; |
| 134 | + der[0] = 0x30; |
| 135 | + der[1] = (byte) len; |
| 136 | + System.arraycopy(derR, 0, der, 2, derR.length); |
| 137 | + System.arraycopy(derS, 0, der, 2 + derR.length, derS.length); |
| 138 | + return der; |
| 139 | + } |
| 140 | + |
| 141 | + private static byte[] encodeDerInteger(BigInteger val) { |
| 142 | + byte[] raw = val.toByteArray(); |
| 143 | + int len = raw.length; |
| 144 | + byte[] out = new byte[len + 2]; |
| 145 | + out[0] = 0x02; |
| 146 | + out[1] = (byte) len; |
| 147 | + System.arraycopy(raw, 0, out, 2, len); |
| 148 | + return out; |
| 149 | + } |
| 150 | + |
| 151 | + private static ECParameterSpec getECParameterSpec(String name) throws Exception { |
| 152 | + java.security.AlgorithmParameters parameters = java.security.AlgorithmParameters.getInstance("EC"); |
| 153 | + parameters.init(new ECGenParameterSpec(name)); |
| 154 | + return parameters.getParameterSpec(ECParameterSpec.class); |
| 155 | + } |
| 156 | +} |
0 commit comments