Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(storage) Add V4 signing support #4692

Merged
merged 42 commits into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d3b121c
Add support for V4 signing
JesseLovelace Nov 28, 2018
71846ac
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 8, 2019
ab931d8
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 18, 2019
d4fb299
(storage) WIP: Add V4 signing support
JesseLovelace Mar 18, 2019
4e30cdc
(storage) Add V4 signing support
JesseLovelace Mar 18, 2019
ddc786c
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 18, 2019
0d7f114
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 18, 2019
8e8ca08
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 20, 2019
d48e8c4
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 20, 2019
7d23539
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 20, 2019
d3acaf1
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 21, 2019
85a0dc2
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 21, 2019
c36cbc3
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 21, 2019
15737c5
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 21, 2019
69d4a17
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 21, 2019
964af50
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 21, 2019
9986c97
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 25, 2019
24bf576
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 25, 2019
7d18fde
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 25, 2019
c5fd070
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 25, 2019
98c31b9
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 25, 2019
e0d0f32
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 26, 2019
9709087
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 26, 2019
ed25ac4
Add V4 samples (#4753)
frankyn Mar 27, 2019
63c42ad
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 26, 2019
ed7e8c4
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Mar 27, 2019
836d7d5
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 27, 2019
022d5ba
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 27, 2019
5534b2f
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Mar 27, 2019
c104811
storage: fix v4 samples (#4754)
frankyn Mar 27, 2019
9fcb46f
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Apr 1, 2019
6d35976
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 1, 2019
54e722c
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 1, 2019
b0a992e
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Apr 1, 2019
7a836e4
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 1, 2019
0e152ec
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 1, 2019
6785005
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 1, 2019
8958a7d
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 1, 2019
b0d677d
Merge branch 'master' of github.com:googleapis/google-cloud-java into…
JesseLovelace Apr 3, 2019
d94d805
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 3, 2019
b249848
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 3, 2019
aa837fd
Merge branch 'v4support' of github.com:googleapis/google-cloud-java i…
JesseLovelace Apr 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,17 @@
public class CanonicalExtensionHeadersSerializer {

private static final char HEADER_SEPARATOR = ':';
private static final char HEADER_NAME_SEPARATOR = ';';

public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders) {
public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders, boolean isV4) {

StringBuilder serializedHeaders = new StringBuilder();

if (canonicalizedExtensionHeaders == null || canonicalizedExtensionHeaders.isEmpty()) {

return serializedHeaders;
}

// Make all custom header names lowercase.
Map<String, String> lowercaseHeaders = new HashMap<>();
for (String headerName : new ArrayList<>(canonicalizedExtensionHeaders.keySet())) {

String lowercaseHeaderName = headerName.toLowerCase();

// If present, remove the x-goog-encryption-key and x-goog-encryption-key-sha256 headers.
if ("x-goog-encryption-key".equals(lowercaseHeaderName)
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName)) {

continue;
}

lowercaseHeaders.put(lowercaseHeaderName, canonicalizedExtensionHeaders.get(headerName));
}
Map<String, String> lowercaseHeaders = getLowercaseHeaders(canonicalizedExtensionHeaders, isV4);

// Sort all custom headers by header name using a lexicographical sort by code point value.
List<String> sortedHeaderNames = new ArrayList<>(lowercaseHeaders.keySet());
Expand All @@ -81,4 +67,55 @@ public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders
// Concatenate all custom headers
return serializedHeaders;
}

public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders) {
return serialize(canonicalizedExtensionHeaders, false);
}

public StringBuilder serializeHeaderNames(
Map<String, String> canonicalizedExtensionHeaders, boolean isV4) {
StringBuilder serializedHeaders = new StringBuilder();

if (canonicalizedExtensionHeaders == null || canonicalizedExtensionHeaders.isEmpty()) {
return serializedHeaders;
}
Map<String, String> lowercaseHeaders = getLowercaseHeaders(canonicalizedExtensionHeaders, isV4);

List<String> sortedHeaderNames = new ArrayList<>(lowercaseHeaders.keySet());
Collections.sort(sortedHeaderNames);

for (String headerName : sortedHeaderNames) {
serializedHeaders.append(headerName).append(HEADER_NAME_SEPARATOR);
}

serializedHeaders.setLength(serializedHeaders.length() - 1); // remove trailing semicolon

return serializedHeaders;
}

public StringBuilder serializeHeaderNames(Map<String, String> canonicalizedExtentionHeaders) {
return serializeHeaderNames(canonicalizedExtentionHeaders, true);
}

private Map<String, String> getLowercaseHeaders(
Map<String, String> canonicalizedExtensionHeaders, boolean isV4) {
// Make all custom header names lowercase.
Map<String, String> lowercaseHeaders = new HashMap<>();
for (String headerName : new ArrayList<>(canonicalizedExtensionHeaders.keySet())) {

String lowercaseHeaderName = headerName.toLowerCase();

// If present, remove the x-goog-encryption-key and x-goog-encryption-key-sha256 headers.
if ("x-goog-encryption-key".equals(lowercaseHeaderName)
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName)
|| (isV4 && "x-goog-encryption-algorithm".equals(lowercaseHeaderName))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment x-goog-encryption-key and x-goog-encryption-key-sha256 should be removed for both v2 and v4. There's an open question the GCS team if this is the case moving forward. (no-op for now).

How does knowing v4 help here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"x-goog-encryption-key" and "x-goog-encryption-key-sha256" are removed for both v2 and v4 here. "x-goog-encyrption-algorithm" is only removed for v4 here (C# does this as well) which is why there's a check

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking you to an internal bug: 128647687. This should be clarified when we get a response there.


continue;
}

lowercaseHeaders.put(lowercaseHeaderName, canonicalizedExtensionHeaders.get(headerName));
}

return lowercaseHeaders;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@

package com.google.cloud.storage;

import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.common.net.UrlEscapers;

import static com.google.common.base.Preconditions.checkArgument;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

/**
Expand All @@ -31,30 +38,64 @@
public class SignatureInfo {

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

private final HttpMethod httpVerb;
private final String contentMd5;
private final String contentType;
private final long expiration;
private final Map<String, String> canonicalizedExtensionHeaders;
private final URI canonicalizedResource;
private final Storage.SignUrlOption.SignatureVersion signatureVersion;
private final String accountEmail;
private final long timestamp;

private final String yearMonthDay;
private final String exactDate;

private SignatureInfo(Builder builder) {
this.httpVerb = builder.httpVerb;
this.contentMd5 = builder.contentMd5;
this.contentType = builder.contentType;
this.expiration = builder.expiration;
this.canonicalizedExtensionHeaders = builder.canonicalizedExtensionHeaders;
this.canonicalizedResource = builder.canonicalizedResource;
this.signatureVersion = builder.signatureVersion;
this.accountEmail = builder.accountEmail;
this.timestamp = builder.timestamp;

if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)
&& !builder.canonicalizedExtensionHeaders.containsKey("host")) {
canonicalizedExtensionHeaders =
new ImmutableMap.Builder<String, String>()
.putAll(builder.canonicalizedExtensionHeaders)
.put("host", "storage.googleapis.com")
.build();
} else {
canonicalizedExtensionHeaders = builder.canonicalizedExtensionHeaders;
}

Date date = new Date(timestamp);

yearMonthDay = new SimpleDateFormat("yyyyMMdd").format(date);
exactDate = new SimpleDateFormat("yyyyMMdd'T'hhmmss'Z'").format(date);
}

/**
* Constructs payload to be signed.
*
* @return paylod to sign
* @return payload to sign
* @see <a href="https://cloud.google.com/storage/docs/access-control#Signed-URLs">Signed URLs</a>
*/
public String constructUnsignedPayload() {
// TODO reverse order when V4 becomes default
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)) {
return constructV4UnsignedPayload();
}
return constructV2UnsignedPayload();
}

private String constructV2UnsignedPayload() {
StringBuilder payload = new StringBuilder();

payload.append(httpVerb.name()).append(COMPONENT_SEPARATOR);
Expand All @@ -80,6 +121,66 @@ public String constructUnsignedPayload() {
return payload.toString();
}

private String constructV4UnsignedPayload() {
StringBuilder payload = new StringBuilder();

payload.append(GOOG4_RSA_SHA256).append(COMPONENT_SEPARATOR);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the new lines between each append.

payload.append(exactDate).append(COMPONENT_SEPARATOR);

payload.append(yearMonthDay).append(SCOPE).append(COMPONENT_SEPARATOR);

payload.append(constructV4CanonicalRequestHash());

return payload.toString();
}

private String constructV4CanonicalRequestHash() {
StringBuilder canonicalRequest = new StringBuilder();

CanonicalExtensionHeadersSerializer serializer = new CanonicalExtensionHeadersSerializer();

canonicalRequest.append(httpVerb.name()).append(COMPONENT_SEPARATOR);

canonicalRequest.append(canonicalizedResource).append(COMPONENT_SEPARATOR);

canonicalRequest.append(constructV4QueryString()).append(COMPONENT_SEPARATOR);

canonicalRequest
.append(serializer.serialize(canonicalizedExtensionHeaders, true))
.append(COMPONENT_SEPARATOR);

canonicalRequest
.append(serializer.serializeHeaderNames(canonicalizedExtensionHeaders))
.append(COMPONENT_SEPARATOR);

canonicalRequest.append("UNSIGNED-PAYLOAD");

return Hashing.sha256()
.hashString(canonicalRequest.toString(), StandardCharsets.UTF_8)
.toString();
}

public String constructV4QueryString() {
StringBuilder signedHeaders =
new CanonicalExtensionHeadersSerializer()
.serializeHeaderNames(canonicalizedExtensionHeaders);

StringBuilder queryString = new StringBuilder();
queryString.append("X-Goog-Algorithm=").append(GOOG4_RSA_SHA256).append("&");
queryString.append(
"X-Goog-Credential="
+ UrlEscapers.urlFormParameterEscaper()
.escape(accountEmail + "/" + yearMonthDay + SCOPE)
+ "&");
queryString.append("X-Goog-Date=" + exactDate + "&");
queryString.append("X-Goog-Expires=" + expiration + "&");
queryString.append(
"X-Goog-SignedHeaders="
+ UrlEscapers.urlFormParameterEscaper().escape(signedHeaders.toString()));
return queryString.toString();
}

public HttpMethod getHttpVerb() {
return httpVerb;
}
Expand All @@ -104,6 +205,18 @@ public URI getCanonicalizedResource() {
return canonicalizedResource;
}

public Storage.SignUrlOption.SignatureVersion getSignatureVersion() {
return signatureVersion;
}

public long getTimestamp() {
return timestamp;
}

public String getAccountEmail() {
return accountEmail;
}

public static final class Builder {

private final HttpMethod httpVerb;
Expand All @@ -112,6 +225,9 @@ public static final class Builder {
private final long expiration;
private Map<String, String> canonicalizedExtensionHeaders;
private final URI canonicalizedResource;
private Storage.SignUrlOption.SignatureVersion signatureVersion;
private String accountEmail;
private long timestamp;

/**
* Constructs builder.
Expand All @@ -134,6 +250,9 @@ public Builder(SignatureInfo signatureInfo) {
this.expiration = signatureInfo.expiration;
this.canonicalizedExtensionHeaders = signatureInfo.canonicalizedExtensionHeaders;
this.canonicalizedResource = signatureInfo.canonicalizedResource;
this.signatureVersion = signatureInfo.signatureVersion;
this.accountEmail = signatureInfo.accountEmail;
this.timestamp = signatureInfo.timestamp;
}

public Builder setContentMd5(String contentMd5) {
Expand All @@ -155,12 +274,37 @@ public Builder setCanonicalizedExtensionHeaders(
return this;
}

public Builder setSignatureVersion(Storage.SignUrlOption.SignatureVersion signatureVersion) {
this.signatureVersion = signatureVersion;

return this;
}

public Builder setAccountEmail(String accountEmail) {
this.accountEmail = accountEmail;

return this;
}

public Builder setTimestamp(long timestamp) {
this.timestamp = timestamp;

return this;
}

/** Creates an {@code SignatureInfo} object from this builder. */
public SignatureInfo build() {
checkArgument(httpVerb != null, "Required HTTP method");
checkArgument(canonicalizedResource != null, "Required canonicalized resource");
checkArgument(expiration >= 0, "Expiration must be greater than or equal to zero");

if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)) {
checkArgument(accountEmail != null, "Account email required to use V4 signing");
checkArgument(timestamp > 0, "Timestamp required to use V4 signing");
checkArgument(
expiration <= 604800000, "Expiration can't be longer than 7 days to use V4 signing");
Copy link
Member

@frankyn frankyn Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 days is inclusive of maximum (604800). This is longer.
expiration > 604800.

}

return new SignatureInfo(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -886,9 +886,15 @@ enum Option {
MD5,
EXT_HEADERS,
SERVICE_ACCOUNT_CRED,
SIGNATURE_VERSION,
HOST_NAME
}

enum SignatureVersion {
V2,
V4
}

private SignUrlOption(Option option, Object value) {
this.option = option;
this.value = value;
Expand Down Expand Up @@ -937,6 +943,14 @@ public static SignUrlOption withExtHeaders(Map<String, String> extHeaders) {
return new SignUrlOption(Option.EXT_HEADERS, extHeaders);
}

public static SignUrlOption withV2Signature() {
return new SignUrlOption(Option.SIGNATURE_VERSION, SignatureVersion.V2);
}

public static SignUrlOption withV4Signature() {
return new SignUrlOption(Option.SIGNATURE_VERSION, SignatureVersion.V4);
}

/**
* Provides a service account signer to sign the URL. If not provided an attempt will be made to
* get it from the environment.
Expand Down
Loading