Skip to content

Commit d50d94c

Browse files
authored
Merge pull request #4429 from aws/alexwoo/master/loginCredentialProvider_buildDPoPHeader
Implement DPoP Header generation
2 parents 99be928 + 735b293 commit d50d94c

File tree

2 files changed

+455
-0
lines changed

2 files changed

+455
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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.internal;
17+
18+
import java.nio.charset.StandardCharsets;
19+
import java.security.InvalidKeyException;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.security.Signature;
22+
import java.security.SignatureException;
23+
import java.security.interfaces.ECPrivateKey;
24+
import java.security.interfaces.ECPublicKey;
25+
import java.security.spec.ECPoint;
26+
import java.util.Arrays;
27+
import java.util.Base64;
28+
import software.amazon.awssdk.annotations.SdkInternalApi;
29+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
30+
import software.amazon.awssdk.utils.Pair;
31+
import software.amazon.awssdk.utils.Validate;
32+
33+
/**
34+
* Utilities that implement rfc9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP)
35+
*/
36+
@SdkInternalApi
37+
public final class DpopHeaderGenerator {
38+
39+
private static final int ES256_SIGNATURE_BYTE_LENGTH = 64;
40+
private static final byte DER_SEQUENCE_TAG = 0x30;
41+
42+
private DpopHeaderGenerator() {
43+
44+
}
45+
46+
/**
47+
* Construct a rfc9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP) header.
48+
*
49+
* The DPoP HTTP header must be a signed JWT (RFC 7519: JSON Web Token), which includes a
50+
* JWK (RFC 7517: JSON Web Key).
51+
*
52+
* For reference, see:
53+
* <ul>
54+
* <li><a href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449 -
55+
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a></li>
56+
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519 - JSON Web Token (JWT)</a></li>
57+
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7517">RFC 7517 - JSON Web Key (JWK)</a></li>
58+
* </ul>
59+
*
60+
* @param pemContent - EC1 / RFC 5915 ASN.1 formated PEM contents
61+
* @param endpoint - The HTTP target URI (Section 7.1 of [RFC9110]) of the request to which the JWT is attached,
62+
* without query and fragment parts
63+
* @param httpMethod - the HTTP method of the request (eg: POST).
64+
* @param epochSeconds - creation time of the JWT in epoch seconds.
65+
* @param uuid - Unique identifier for the DPoP proof JWT - should be a UUID4 string.
66+
* @return DPoP header value
67+
*/
68+
public static String generateDPoPProofHeader(String pemContent, String endpoint, String httpMethod,
69+
long epochSeconds, String uuid) {
70+
Validate.paramNotBlank(pemContent, "pemContent");
71+
Validate.paramNotBlank(endpoint, "endpoint");
72+
Validate.paramNotBlank(httpMethod, "httpMethod");
73+
Validate.paramNotBlank(uuid, "uuid");
74+
75+
try {
76+
// Load EC public and private key from PEM
77+
Pair<ECPrivateKey, ECPublicKey> keys = EcKeyLoader.loadSec1Pem(pemContent);
78+
ECPrivateKey privateKey = keys.left();
79+
ECPublicKey publicKey = keys.right();
80+
81+
// Build JSON strings (header, payload) with JsonGenerator
82+
byte[] headerJson = buildHeaderJson(publicKey);
83+
byte[] payloadJson = buildPayloadJson(uuid, endpoint, httpMethod, epochSeconds);
84+
85+
// Base64URL encode header + payload
86+
String encodedHeader = base64UrlEncode(headerJson);
87+
String encodedPayload = base64UrlEncode(payloadJson);
88+
String message = encodedHeader + "." + encodedPayload;
89+
90+
// Sign (ES256)
91+
Signature signature = Signature.getInstance("SHA256withECDSA");
92+
signature.initSign(privateKey);
93+
signature.update(message.getBytes(StandardCharsets.UTF_8));
94+
byte[] signatureBytes = translateDerSignatureToJws(signature.sign(), ES256_SIGNATURE_BYTE_LENGTH);
95+
96+
// Combine into JWT
97+
String encodedSignature = base64UrlEncode(signatureBytes);
98+
return message + "." + encodedSignature;
99+
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
100+
throw new RuntimeException(e);
101+
}
102+
}
103+
104+
// build the JWT header which includes the public key
105+
// see: https://datatracker.ietf.org/doc/html/rfc9449#name-dpop-proof-jwt-syntax
106+
private static byte[] buildHeaderJson(ECPublicKey publicKey) {
107+
ECPoint pubPoint = publicKey.getW();
108+
String x = base64UrlEncode(stripLeadingZero(pubPoint.getAffineX().toByteArray()));
109+
String y = base64UrlEncode(stripLeadingZero(pubPoint.getAffineY().toByteArray()));
110+
JsonWriter jsonWriter = null;
111+
try {
112+
jsonWriter = JsonWriter.create();
113+
jsonWriter.writeStartObject();
114+
jsonWriter.writeFieldName("typ");
115+
jsonWriter.writeValue("dpop+jwt");
116+
117+
jsonWriter.writeFieldName("alg");
118+
jsonWriter.writeValue("ES256");
119+
120+
jsonWriter.writeFieldName("jwk");
121+
jsonWriter.writeStartObject();
122+
123+
jsonWriter.writeFieldName("crv") ;
124+
jsonWriter.writeValue("P-256");
125+
126+
jsonWriter.writeFieldName("kty");
127+
jsonWriter.writeValue("EC");
128+
129+
jsonWriter.writeFieldName("x");
130+
jsonWriter.writeValue(x);
131+
132+
jsonWriter.writeFieldName("y");
133+
jsonWriter.writeValue(y);
134+
jsonWriter.writeEndObject(); // end jwk
135+
jsonWriter.writeEndObject(); // end root
136+
137+
return jsonWriter.getBytes();
138+
} finally {
139+
if (jsonWriter != null) {
140+
jsonWriter.close();
141+
}
142+
}
143+
}
144+
145+
// build claims payload
146+
// see: https://datatracker.ietf.org/doc/html/rfc9449#name-dpop-proof-jwt-syntax
147+
private static byte[] buildPayloadJson(String uuid, String endpoint, String httpMethod, long epochSeconds) {
148+
JsonWriter jsonWriter = null;
149+
try {
150+
jsonWriter = JsonWriter.create();
151+
jsonWriter.writeStartObject();
152+
153+
jsonWriter.writeFieldName("jti");
154+
jsonWriter.writeValue(uuid);
155+
156+
jsonWriter.writeFieldName("htm");
157+
jsonWriter.writeValue(httpMethod);
158+
159+
jsonWriter.writeFieldName("htu");
160+
jsonWriter.writeValue(endpoint);
161+
162+
jsonWriter.writeFieldName("iat");
163+
jsonWriter.writeValue(epochSeconds);
164+
165+
jsonWriter.writeEndObject();
166+
167+
return jsonWriter.getBytes();
168+
} finally {
169+
if (jsonWriter != null) {
170+
jsonWriter.close();
171+
}
172+
}
173+
}
174+
175+
/**
176+
* Java Signature from SHA256withECDSA produces an ASN.1/DER encoded signature.
177+
* This method translates that signature into the concatenated (R,S) format expected by JWS.
178+
*
179+
* An ECDSA signature always produces two big integers: R and S. The DER format encodes these in a variable
180+
* length sequence because the values are encoded without leading zeroes with a structure following:
181+
* [ SEQUENCE_TAG, total_length, INTEGER_TAG, length of R, (bytes of R), INTEGER_TAG, length OF S, ( bytes of S) ]
182+
*
183+
* The JWT/JOSE spec defines ECDSA signatures as two 32 byte, big-endian integer values for R and S (total of 64 bytes):
184+
* [ 32 bytes of R, 32 bytes of S]
185+
*
186+
* @param derSignature The ASN1/DER-encoded signature.
187+
* @param outputLength The expected length of the ECDSA JWS signature. This should be 64 for ES256
188+
*
189+
* @return The ECDSA JWS encoded signature (concatenated r,s values)
190+
**/
191+
private static byte[] translateDerSignatureToJws(byte[] derSignature, int outputLength) {
192+
193+
// validate DER signature format
194+
if (derSignature.length < 8 || derSignature[0] != DER_SEQUENCE_TAG) {
195+
throw new RuntimeException("Invalid ECDSA signature format");
196+
}
197+
198+
// the total length may be more than 1 byte
199+
// if the first byte is (0x81), its 2 bytes
200+
int offset; // point to the start of the first INTEGER_TAG
201+
if (derSignature[1] > 0) {
202+
offset = 2;
203+
} else if (derSignature[1] == (byte) 0x81) {
204+
offset = 3;
205+
} else {
206+
throw new RuntimeException("Invalid ECDSA signature format");
207+
}
208+
209+
// get the length of R as the byte after the first INTEGER_TAG
210+
byte rLength = derSignature[offset + 1];
211+
212+
// determine the number of significant (non-zero) bytes in R
213+
int i;
214+
int endOfR = offset + 2 + rLength;
215+
for (i = rLength; (i > 0) && (derSignature[endOfR - i] == 0); i--) {
216+
// do nothing
217+
}
218+
219+
// get the length of S as the byte after the second INTEGER_TAG which is:
220+
byte sLength = derSignature[endOfR + 1];
221+
222+
// determine number of significant bytes in S
223+
int j;
224+
int endOfS = endOfR + 2 + sLength;
225+
for (j = sLength; (j > 0) && (derSignature[endOfS - j] == 0); j--) {
226+
// do nothing
227+
}
228+
229+
int rawLen = Math.max(i, j);
230+
rawLen = Math.max(rawLen, outputLength / 2);
231+
232+
// sanity check, ensure the internal structure matches the DER spec.
233+
if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset
234+
|| (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength
235+
|| derSignature[offset] != 2
236+
|| derSignature[endOfR] != 2) {
237+
throw new RuntimeException("Invalid ECDSA signature format");
238+
}
239+
240+
byte[] jwsSignature = new byte[2 * rawLen];
241+
// copy the significant bytes of R (i bytes), removing any leading zeros, into the first half of output array.
242+
// Right aligned!
243+
System.arraycopy(derSignature, endOfR - i, jwsSignature, rawLen - i, i);
244+
// do the same for S to the second half of the output array. Also right aligned.
245+
System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, jwsSignature, 2 * rawLen - j, j);
246+
247+
return jwsSignature;
248+
}
249+
250+
private static String base64UrlEncode(byte[] data) {
251+
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
252+
}
253+
254+
private static byte[] stripLeadingZero(byte[] bytes) {
255+
if (bytes.length > 1 && bytes[0] == 0x00) {
256+
return Arrays.copyOfRange(bytes, 1, bytes.length);
257+
}
258+
return bytes;
259+
}
260+
}

0 commit comments

Comments
 (0)