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 all 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,29 @@
public class CanonicalExtensionHeadersSerializer {

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

private final Storage.SignUrlOption.SignatureVersion signatureVersion;

public CanonicalExtensionHeadersSerializer(
Storage.SignUrlOption.SignatureVersion signatureVersion) {
this.signatureVersion = signatureVersion;
}

public CanonicalExtensionHeadersSerializer() {
Copy link
Member

Choose a reason for hiding this comment

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

I couldn't find where this method is being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not, I just didn't want to invalidate the constructor

// TODO switch this when V4 becomes default
this.signatureVersion = Storage.SignUrlOption.SignatureVersion.V2;
}

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

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);

// 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 +79,47 @@ public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders
// Concatenate all custom headers
return serializedHeaders;
}

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

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

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;
}

private Map<String, String> getLowercaseHeaders(
Map<String, String> canonicalizedExtensionHeaders) {
// 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 and we're V2, remove the x-goog-encryption-key and x-goog-encryption-key-sha256
// headers. (CSEK headers are allowed for V4)
if (Storage.SignUrlOption.SignatureVersion.V2.equals(signatureVersion)
&& ("x-goog-encryption-key".equals(lowercaseHeaderName)
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName))) {

continue;
}

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

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

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

import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.common.net.UrlEscapers;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;

/**
* Signature Info holds payload components of the string that requires signing.
Expand All @@ -31,30 +39,70 @@
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);

SimpleDateFormat yearMonthDayFormat = new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat exactDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");

yearMonthDayFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
exactDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

yearMonthDay = yearMonthDayFormat.format(date);
exactDate = exactDateFormat.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 @@ -67,19 +115,72 @@ public String constructUnsignedPayload() {
payload.append(contentType);
}
payload.append(COMPONENT_SEPARATOR);

payload.append(expiration).append(COMPONENT_SEPARATOR);

if (canonicalizedExtensionHeaders != null) {
payload.append(
new CanonicalExtensionHeadersSerializer().serialize(canonicalizedExtensionHeaders));
new CanonicalExtensionHeadersSerializer(Storage.SignUrlOption.SignatureVersion.V2)
.serialize(canonicalizedExtensionHeaders));
}

payload.append(canonicalizedResource);

return payload.toString();
}

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

payload.append(GOOG4_RSA_SHA256).append(COMPONENT_SEPARATOR);
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(Storage.SignUrlOption.SignatureVersion.V4);

canonicalRequest.append(httpVerb.name()).append(COMPONENT_SEPARATOR);
canonicalRequest.append(canonicalizedResource).append(COMPONENT_SEPARATOR);
canonicalRequest.append(constructV4QueryString()).append(COMPONENT_SEPARATOR);
canonicalRequest
.append(serializer.serialize(canonicalizedExtensionHeaders))
.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(Storage.SignUrlOption.SignatureVersion.V4)
.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,41 @@ 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 <= 604800, "Expiration can't be longer than 7 days to use V4 signing");
}

if (canonicalizedExtensionHeaders == null) {
canonicalizedExtensionHeaders = new HashMap<>();
}

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,23 @@ public static SignUrlOption withExtHeaders(Map<String, String> extHeaders) {
return new SignUrlOption(Option.EXT_HEADERS, extHeaders);
}

/**
* Use if signature version should be V2. This is the default if neither this or {@code
* withV4Signature()} is called.
*/
public static SignUrlOption withV2Signature() {
return new SignUrlOption(Option.SIGNATURE_VERSION, SignatureVersion.V2);
}

/**
* Use if signature version should be V4. Note that V4 Signed URLs can't have an expiration
* longer than 7 days. V2 will be the default if neither this or {@code withV2Signature()} is
* called.
*/
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 Expand Up @@ -2101,6 +2124,16 @@ Blob create(
* TimeUnit.DAYS);
* }</pre>
*
* <p>Example of creating a signed URL passing the {@link SignUrlOption#withV4Signature()} option,
* which enables V4 signing.
*
* <pre>{@code
* String bucketName = "my_unique_bucket";
* String blobName = "my_blob_name";
* URL signedUrl = storage.signUrl(BlobInfo.newBuilder(bucketName, blobName).build(),
* 7, TimeUnit.DAYS, Storage.SignUrlOption.withV4Signature());
* }</pre>
*
* <p>Example of creating a signed URL passing the {@link
* SignUrlOption#signWith(ServiceAccountSigner)} option, that will be used for signing the URL.
*
Expand Down
Loading