Skip to content

Commit d4fb299

Browse files
committed
(storage) WIP: Add V4 signing support
1 parent ab931d8 commit d4fb299

6 files changed

Lines changed: 295 additions & 73 deletions

File tree

google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/CanonicalExtensionHeadersSerializer.java

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,17 @@
3232
public class CanonicalExtensionHeadersSerializer {
3333

3434
private static final char HEADER_SEPARATOR = ':';
35+
private static final char HEADER_NAME_SEPARATOR = ';';
3536

3637
public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders) {
3738

3839
StringBuilder serializedHeaders = new StringBuilder();
3940

4041
if (canonicalizedExtensionHeaders == null || canonicalizedExtensionHeaders.isEmpty()) {
41-
4242
return serializedHeaders;
4343
}
4444

45-
// Make all custom header names lowercase.
46-
Map<String, String> lowercaseHeaders = new HashMap<>();
47-
for (String headerName : new ArrayList<>(canonicalizedExtensionHeaders.keySet())) {
48-
49-
String lowercaseHeaderName = headerName.toLowerCase();
50-
51-
// If present, remove the x-goog-encryption-key and x-goog-encryption-key-sha256 headers.
52-
if ("x-goog-encryption-key".equals(lowercaseHeaderName)
53-
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName)) {
54-
55-
continue;
56-
}
57-
58-
lowercaseHeaders.put(lowercaseHeaderName, canonicalizedExtensionHeaders.get(headerName));
59-
}
45+
Map<String, String> lowercaseHeaders = getLowercaseHeaders(canonicalizedExtensionHeaders);
6046

6147
// Sort all custom headers by header name using a lexicographical sort by code point value.
6248
List<String> sortedHeaderNames = new ArrayList<>(lowercaseHeaders.keySet());
@@ -81,4 +67,46 @@ public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders
8167
// Concatenate all custom headers
8268
return serializedHeaders;
8369
}
70+
71+
public StringBuilder serializeHeaderNames(Map<String, String> canonicalizedExtensionHeaders) {
72+
StringBuilder serializedHeaders = new StringBuilder();
73+
74+
if (canonicalizedExtensionHeaders == null || canonicalizedExtensionHeaders.isEmpty()) {
75+
return serializedHeaders;
76+
}
77+
78+
Map<String, String> lowercaseHeaders = getLowercaseHeaders(canonicalizedExtensionHeaders);
79+
80+
List<String> sortedHeaderNames = new ArrayList<>(lowercaseHeaders.keySet());
81+
Collections.sort(sortedHeaderNames);
82+
83+
for (String headerName : sortedHeaderNames) {
84+
serializedHeaders.append(headerName).append(HEADER_NAME_SEPARATOR);
85+
}
86+
87+
serializedHeaders.setLength(serializedHeaders.length() - 1); // remove trailing semicolon
88+
89+
return serializedHeaders;
90+
}
91+
92+
private Map<String, String> getLowercaseHeaders(
93+
Map<String, String> canonicalizedExtensionHeaders) {
94+
// Make all custom header names lowercase.
95+
Map<String, String> lowercaseHeaders = new HashMap<>();
96+
for (String headerName : new ArrayList<>(canonicalizedExtensionHeaders.keySet())) {
97+
98+
String lowercaseHeaderName = headerName.toLowerCase();
99+
100+
// If present, remove the x-goog-encryption-key and x-goog-encryption-key-sha256 headers.
101+
if ("x-goog-encryption-key".equals(lowercaseHeaderName)
102+
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName)) {
103+
104+
continue;
105+
}
106+
107+
lowercaseHeaders.put(lowercaseHeaderName, canonicalizedExtensionHeaders.get(headerName));
108+
}
109+
110+
return lowercaseHeaders;
111+
}
84112
}

google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/SignatureInfo.java

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616

1717
package com.google.cloud.storage;
1818

19+
import com.google.common.collect.ImmutableMap;
20+
import com.google.common.hash.Hashing;
21+
import com.google.common.net.UrlEscapers;
22+
1923
import static com.google.common.base.Preconditions.checkArgument;
2024

2125
import java.net.URI;
26+
import java.nio.charset.StandardCharsets;
27+
import java.text.SimpleDateFormat;
28+
import java.util.Date;
2229
import java.util.Map;
2330

2431
/**
@@ -31,30 +38,64 @@
3138
public class SignatureInfo {
3239

3340
public static final char COMPONENT_SEPARATOR = '\n';
41+
public static final String GOOG4_RSA_SHA256 = "GOOG4-RSA-SHA256";
42+
public static final String SCOPE = "/auto/storage/goog4_request";
3443

3544
private final HttpMethod httpVerb;
3645
private final String contentMd5;
3746
private final String contentType;
3847
private final long expiration;
3948
private final Map<String, String> canonicalizedExtensionHeaders;
4049
private final URI canonicalizedResource;
50+
private final Storage.SignUrlOption.SignatureVersion signatureVersion;
51+
private final String accountEmail;
52+
private final long timestamp;
53+
54+
private final String yearMonthDay;
55+
private final String exactDate;
4156

4257
private SignatureInfo(Builder builder) {
4358
this.httpVerb = builder.httpVerb;
4459
this.contentMd5 = builder.contentMd5;
4560
this.contentType = builder.contentType;
4661
this.expiration = builder.expiration;
47-
this.canonicalizedExtensionHeaders = builder.canonicalizedExtensionHeaders;
4862
this.canonicalizedResource = builder.canonicalizedResource;
63+
this.signatureVersion = builder.signatureVersion;
64+
this.accountEmail = builder.accountEmail;
65+
this.timestamp = builder.timestamp;
66+
67+
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)
68+
&& !builder.canonicalizedExtensionHeaders.containsKey("host")) {
69+
canonicalizedExtensionHeaders =
70+
new ImmutableMap.Builder<String, String>()
71+
.putAll(builder.canonicalizedExtensionHeaders)
72+
.put("host", "storage.googleapis.com")
73+
.build();
74+
} else {
75+
canonicalizedExtensionHeaders = builder.canonicalizedExtensionHeaders;
76+
}
77+
78+
Date date = new Date(timestamp);
79+
80+
yearMonthDay = new SimpleDateFormat("yyyyMMdd").format(date);
81+
exactDate = new SimpleDateFormat("yyyyMMdd'T'hhmmss'Z'").format(date);
4982
}
5083

5184
/**
5285
* Constructs payload to be signed.
5386
*
54-
* @return paylod to sign
87+
* @return payload to sign
5588
* @see <a href="https://cloud.google.com/storage/docs/access-control#Signed-URLs">Signed URLs</a>
5689
*/
5790
public String constructUnsignedPayload() {
91+
// TODO reverse order when V4 becomes default
92+
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)) {
93+
return constructV4UnsignedPayload();
94+
}
95+
return constructV2UnsignedPayload();
96+
}
97+
98+
private String constructV2UnsignedPayload() {
5899
StringBuilder payload = new StringBuilder();
59100

60101
payload.append(httpVerb.name()).append(COMPONENT_SEPARATOR);
@@ -80,6 +121,65 @@ public String constructUnsignedPayload() {
80121
return payload.toString();
81122
}
82123

124+
private String constructV4UnsignedPayload() {
125+
StringBuilder payload = new StringBuilder();
126+
127+
payload.append(GOOG4_RSA_SHA256).append(COMPONENT_SEPARATOR);
128+
129+
payload.append(exactDate).append(COMPONENT_SEPARATOR);
130+
131+
payload.append(yearMonthDay).append(SCOPE).append(COMPONENT_SEPARATOR);
132+
133+
payload.append(constructV4CanonicalRequestHash());
134+
135+
return payload.toString();
136+
}
137+
138+
private String constructV4CanonicalRequestHash() {
139+
StringBuilder canonicalRequest = new StringBuilder();
140+
141+
CanonicalExtensionHeadersSerializer serializer = new CanonicalExtensionHeadersSerializer();
142+
143+
canonicalRequest.append(httpVerb.name()).append(COMPONENT_SEPARATOR);
144+
145+
canonicalRequest.append(canonicalizedResource).append(COMPONENT_SEPARATOR);
146+
147+
canonicalRequest.append(constructV4QueryString()).append(COMPONENT_SEPARATOR);
148+
149+
canonicalRequest
150+
.append(serializer.serialize(canonicalizedExtensionHeaders))
151+
.append(COMPONENT_SEPARATOR);
152+
153+
canonicalRequest
154+
.append(serializer.serializeHeaderNames(canonicalizedExtensionHeaders))
155+
.append(COMPONENT_SEPARATOR);
156+
157+
canonicalRequest.append("UNSIGNED-PAYLOAD");
158+
159+
return Hashing.sha256()
160+
.hashString(canonicalRequest.toString(), StandardCharsets.UTF_8)
161+
.toString();
162+
}
163+
164+
public String constructV4QueryString() {
165+
StringBuilder signedHeaders =
166+
new CanonicalExtensionHeadersSerializer()
167+
.serializeHeaderNames(canonicalizedExtensionHeaders);
168+
169+
StringBuilder queryString = new StringBuilder();
170+
queryString.append("X-Goog-Algorithm=").append(GOOG4_RSA_SHA256).append("&");
171+
queryString.append(
172+
"X-Goog-Credential="
173+
+ UrlEscapers.urlFormParameterEscaper()
174+
.escape(accountEmail + "/" + yearMonthDay + SCOPE)
175+
+ "&");
176+
queryString.append("X-Goog-Date=" + exactDate + "&");
177+
queryString.append("X-Goog-Expires=" + expiration + "&");
178+
queryString.append("X-Goog-SignedHeaders=" + signedHeaders.toString());
179+
180+
return queryString.toString();
181+
}
182+
83183
public HttpMethod getHttpVerb() {
84184
return httpVerb;
85185
}
@@ -104,6 +204,18 @@ public URI getCanonicalizedResource() {
104204
return canonicalizedResource;
105205
}
106206

207+
public Storage.SignUrlOption.SignatureVersion getSignatureVersion() {
208+
return signatureVersion;
209+
}
210+
211+
public long getTimestamp() {
212+
return timestamp;
213+
}
214+
215+
public String getAccountEmail() {
216+
return accountEmail;
217+
}
218+
107219
public static final class Builder {
108220

109221
private final HttpMethod httpVerb;
@@ -112,6 +224,9 @@ public static final class Builder {
112224
private final long expiration;
113225
private Map<String, String> canonicalizedExtensionHeaders;
114226
private final URI canonicalizedResource;
227+
private Storage.SignUrlOption.SignatureVersion signatureVersion;
228+
private String accountEmail;
229+
private long timestamp;
115230

116231
/**
117232
* Constructs builder.
@@ -134,6 +249,9 @@ public Builder(SignatureInfo signatureInfo) {
134249
this.expiration = signatureInfo.expiration;
135250
this.canonicalizedExtensionHeaders = signatureInfo.canonicalizedExtensionHeaders;
136251
this.canonicalizedResource = signatureInfo.canonicalizedResource;
252+
this.signatureVersion = signatureInfo.signatureVersion;
253+
this.accountEmail = signatureInfo.accountEmail;
254+
this.timestamp = signatureInfo.timestamp;
137255
}
138256

139257
public Builder setContentMd5(String contentMd5) {
@@ -155,12 +273,37 @@ public Builder setCanonicalizedExtensionHeaders(
155273
return this;
156274
}
157275

276+
public Builder setSignatureVersion(Storage.SignUrlOption.SignatureVersion signatureVersion) {
277+
this.signatureVersion = signatureVersion;
278+
279+
return this;
280+
}
281+
282+
public Builder setAccountEmail(String accountEmail) {
283+
this.accountEmail = accountEmail;
284+
285+
return this;
286+
}
287+
288+
public Builder setTimestamp(long timestamp) {
289+
this.timestamp = timestamp;
290+
291+
return this;
292+
}
293+
158294
/** Creates an {@code SignatureInfo} object from this builder. */
159295
public SignatureInfo build() {
160296
checkArgument(httpVerb != null, "Required HTTP method");
161297
checkArgument(canonicalizedResource != null, "Required canonicalized resource");
162298
checkArgument(expiration >= 0, "Expiration must be greater than or equal to zero");
163299

300+
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)) {
301+
checkArgument(accountEmail != null, "Account email required to use V4 signing");
302+
checkArgument(timestamp > 0, "Timestamp required to use V4 signing");
303+
checkArgument(
304+
expiration <= 604800000, "Expiration can't be longer than 7 days to use V4 signing");
305+
}
306+
164307
return new SignatureInfo(this);
165308
}
166309
}

0 commit comments

Comments
 (0)