Skip to content

Commit 2f8bfd3

Browse files
committed
Implement generation of DPoP header
1 parent c67ccdf commit 2f8bfd3

File tree

2 files changed

+195
-45
lines changed

2 files changed

+195
-45
lines changed

services/signin/src/main/java/software/amazon/awssdk/services/signin/internal/DpopHeaderGenerator.java

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import java.io.ByteArrayOutputStream;
1919
import java.io.IOException;
2020
import java.nio.charset.StandardCharsets;
21+
import java.security.InvalidKeyException;
22+
import java.security.NoSuchAlgorithmException;
2123
import java.security.Signature;
24+
import java.security.SignatureException;
2225
import java.security.interfaces.ECPrivateKey;
2326
import java.security.interfaces.ECPublicKey;
2427
import java.security.spec.ECPoint;
@@ -38,7 +41,9 @@ public final class DpopHeaderGenerator {
3841
public static final int ES256_SIGNATURE_BYTE_LENGTH = 64;
3942
public static final byte DER_SEQUENCE_TAG = 0x30;
4043

41-
private DpopHeaderGenerator() {}
44+
private DpopHeaderGenerator() {
45+
46+
}
4247

4348
/**
4449
* Construct a rfc9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP) header.
@@ -60,36 +65,39 @@ private DpopHeaderGenerator() {}
6065
* @param epochSeconds - creation time of the JWT in epoch seconds.
6166
* @param uuid - Unique identifier for the DPoP proof JWT - should be a UUID4 string.
6267
* @return DPoP header value
63-
* @throws Exception
6468
*/
65-
public static String generateDPoPProofHeader(String pemContent, String endpoint, long epochSeconds, String uuid)
66-
throws Exception {
67-
// Load EC public and private key from PEM
68-
Pair<ECPrivateKey, ECPublicKey> keys = EcKeyLoader.loadSec1Pem(pemContent);
69-
ECPrivateKey privateKey = keys.left();
70-
ECPublicKey publicKey = keys.right();
71-
72-
// Build JSON strings (header, payload) with JsonGenerator
73-
String headerJson = buildHeaderJson(publicKey);
74-
String payloadJson = buildPayloadJson(uuid, endpoint, epochSeconds);
75-
76-
// Base64URL encode header + payload
77-
String encodedHeader = base64UrlEncode(headerJson.getBytes(StandardCharsets.UTF_8));
78-
String encodedPayload = base64UrlEncode(payloadJson.getBytes(StandardCharsets.UTF_8));
79-
String message = encodedHeader + "." + encodedPayload;
80-
81-
// Sign (ES256)
82-
Signature signature = Signature.getInstance("SHA256withECDSA");
83-
signature.initSign(privateKey);
84-
signature.update(message.getBytes(StandardCharsets.UTF_8));
85-
byte[] signatureBytes = translateDerSignatureToJws(signature.sign(), ES256_SIGNATURE_BYTE_LENGTH);
86-
87-
// Combine into JWT
88-
String encodedSignature = base64UrlEncode(signatureBytes);
89-
return message + "." + encodedSignature;
69+
public static String generateDPoPProofHeader(String pemContent, String endpoint, long epochSeconds, String uuid) {
70+
try {
71+
// Load EC public and private key from PEM
72+
Pair<ECPrivateKey, ECPublicKey> keys = EcKeyLoader.loadSec1Pem(pemContent);
73+
ECPrivateKey privateKey = keys.left();
74+
ECPublicKey publicKey = keys.right();
75+
76+
// Build JSON strings (header, payload) with JsonGenerator
77+
String headerJson = buildHeaderJson(publicKey);
78+
String payloadJson = buildPayloadJson(uuid, endpoint, epochSeconds);
79+
80+
// Base64URL encode header + payload
81+
String encodedHeader = base64UrlEncode(headerJson.getBytes(StandardCharsets.UTF_8));
82+
String encodedPayload = base64UrlEncode(payloadJson.getBytes(StandardCharsets.UTF_8));
83+
String message = encodedHeader + "." + encodedPayload;
84+
85+
// Sign (ES256)
86+
Signature signature = Signature.getInstance("SHA256withECDSA");
87+
signature.initSign(privateKey);
88+
signature.update(message.getBytes(StandardCharsets.UTF_8));
89+
byte[] signatureBytes = translateDerSignatureToJws(signature.sign(), ES256_SIGNATURE_BYTE_LENGTH);
90+
91+
// Combine into JWT
92+
String encodedSignature = base64UrlEncode(signatureBytes);
93+
return message + "." + encodedSignature;
94+
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
95+
throw new RuntimeException(e);
96+
}
9097
}
9198

9299
// build the JWT header which includes the public key
100+
// see: https://datatracker.ietf.org/doc/html/rfc9449#name-dpop-proof-jwt-syntax
93101
private static String buildHeaderJson(ECPublicKey publicKey) throws IOException {
94102
ECPoint pubPoint = publicKey.getW();
95103
String x = base64UrlEncode(stripLeadingZero(pubPoint.getAffineX().toByteArray()));
@@ -112,6 +120,8 @@ private static String buildHeaderJson(ECPublicKey publicKey) throws IOException
112120
return out.toString();
113121
}
114122

123+
// build claims payload
124+
// see: https://datatracker.ietf.org/doc/html/rfc9449#name-dpop-proof-jwt-syntax
115125
private static String buildPayloadJson(String uuid, String endpoint, long epochSeconds) throws IOException {
116126
ByteArrayOutputStream out = new ByteArrayOutputStream();
117127
JsonFactory factory = new JsonFactory();
@@ -142,8 +152,7 @@ private static String buildPayloadJson(String uuid, String endpoint, long epochS
142152
*
143153
* @return The ECDSA JWS encoded signature (concatenated r,s values)
144154
**/
145-
private static byte[] translateDerSignatureToJws(byte[] derSignature, int outputLength)
146-
throws Exception {
155+
private static byte[] translateDerSignatureToJws(byte[] derSignature, int outputLength) {
147156

148157
// validate DER signature format
149158
if (derSignature.length < 8 || derSignature[0] != DER_SEQUENCE_TAG) {
@@ -192,7 +201,7 @@ private static byte[] translateDerSignatureToJws(byte[] derSignature, int output
192201
throw new RuntimeException("Invalid ECDSA signature format");
193202
}
194203

195-
final byte[] jwsSignature = new byte[2 * rawLen];
204+
byte[] jwsSignature = new byte[2 * rawLen];
196205
// copy the significant bytes of R (i bytes), removing any leading zeros, into the first half of output array.
197206
// Right aligned!
198207
System.arraycopy(derSignature, endOfR - i, jwsSignature, rawLen - i, i);
@@ -212,19 +221,4 @@ private static byte[] stripLeadingZero(byte[] bytes) {
212221
}
213222
return bytes;
214223
}
215-
216-
public static void main(String[] args) throws Exception {
217-
String pem = "-----BEGIN EC PRIVATE KEY-----\n"
218-
+ "MHcCAQEEICeY73qhQO/3o1QnrL5Nu3HMDB9h3kVW6imRdcHks0tboAoGCCqGSM49"
219-
+ "AwEHoUQDQgAEbefyxjd/UlGwAPF6hy0k4yCW7dSghc6yPd4To0sBqX0tPS/aoLrl"
220-
+ "QnPjfDslgD29p4+Pgwxj1s8cFHVeDKdKTQ==\n"
221-
+ "-----END EC PRIVATE KEY-----";
222-
String dpopHeader = DpopHeaderGenerator.generateDPoPProofHeader(
223-
pem, "https://ap-northeast-1.aws-signin-testing.amazon.com/v1/token",
224-
1760727856,
225-
"c7cf0359-f736-4a55-bbc1-70f6a0e2d55d"
226-
);
227-
228-
System.out.println("\n\nDPOP Proof header:\n" + dpopHeader);
229-
}
230224
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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

Comments
 (0)