Create attachments V3 endpoint for CDN2 on GCP

In preparation for resumable uploads, this creates a separate
attachment authorization endpoint that creates a signed URL for
accessing GCP Storage through Signal's CDN2. This should allow Signal
clients to do byte-level resume of media uploads.
This commit is contained in:
Ehren Kret
2020-03-18 09:47:30 -07:00
parent 2aca007a59
commit 41286650cc
12 changed files with 634 additions and 45 deletions

View File

@@ -0,0 +1,70 @@
package org.whispersystems.textsecuregcm.gcp;
import javax.annotation.Nonnull;
public class CanonicalRequest {
@Nonnull
private final String canonicalRequest;
@Nonnull
private final String resourcePath;
@Nonnull
private final String canonicalQuery;
@Nonnull
private final String activeDatetime;
@Nonnull
private final String credentialScope;
@Nonnull
private final String domain;
private final int maxSizeInBytes;
public CanonicalRequest(@Nonnull String canonicalRequest, @Nonnull String resourcePath, @Nonnull String canonicalQuery, @Nonnull String activeDatetime, @Nonnull String credentialScope, @Nonnull String domain, int maxSizeInBytes) {
this.canonicalRequest = canonicalRequest;
this.resourcePath = resourcePath;
this.canonicalQuery = canonicalQuery;
this.activeDatetime = activeDatetime;
this.credentialScope = credentialScope;
this.domain = domain;
this.maxSizeInBytes = maxSizeInBytes;
}
@Nonnull
String getCanonicalRequest() {
return canonicalRequest;
}
@Nonnull
public String getResourcePath() {
return resourcePath;
}
@Nonnull
public String getCanonicalQuery() {
return canonicalQuery;
}
@Nonnull
String getActiveDatetime() {
return activeDatetime;
}
@Nonnull
String getCredentialScope() {
return credentialScope;
}
@Nonnull
public String getDomain() {
return domain;
}
public int getMaxSizeInBytes() {
return maxSizeInBytes;
}
}

View File

@@ -0,0 +1,75 @@
package org.whispersystems.textsecuregcm.gcp;
import io.dropwizard.util.Strings;
import javax.annotation.Nonnull;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
public class CanonicalRequestGenerator {
private static final DateTimeFormatter SIMPLE_UTC_DATE = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(ZoneOffset.UTC);
private static final DateTimeFormatter SIMPLE_UTC_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(ZoneOffset.UTC);
@Nonnull
private final String domain;
@Nonnull
private final String email;
private final int maxSizeBytes;
@Nonnull
private final String pathPrefix;
public CanonicalRequestGenerator(@Nonnull String domain, @Nonnull String email, int maxSizeBytes, @Nonnull String pathPrefix) {
this.domain = domain;
this.email = email;
this.maxSizeBytes = maxSizeBytes;
this.pathPrefix = pathPrefix;
}
public CanonicalRequest createFor(@Nonnull final String key, @Nonnull final ZonedDateTime now) {
final StringBuilder result = new StringBuilder("POST\n");
final StringBuilder resourcePathBuilder = new StringBuilder();
if (!Strings.isNullOrEmpty(pathPrefix)) {
resourcePathBuilder.append(pathPrefix);
}
resourcePathBuilder.append('/').append(URLEncoder.encode(key, StandardCharsets.UTF_8));
final String resourcePath = resourcePathBuilder.toString();
result.append(resourcePath).append('\n');
final String activeDatetime = SIMPLE_UTC_DATE_TIME.format(now);
final String canonicalQuery = "X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=" + URLEncoder.encode(makeCredential(email, now), StandardCharsets.UTF_8) +
"&X-Goog-Date=" + URLEncoder.encode(activeDatetime, StandardCharsets.UTF_8) +
"&X-Goog-Expires=" + Duration.of(25, ChronoUnit.HOURS).toSeconds() +
"&X-Goog-SignedHeaders=host%3Bx-goog-content-length-range%3Bx-goog-resumable";
result.append(canonicalQuery).append('\n');
result.append("host:").append(domain).append('\n');
result.append("x-goog-content-length-range:1,").append(maxSizeBytes).append('\n');
result.append("x-goog-resumable:start\n");
result.append('\n');
result.append("host;x-goog-content-length-range;x-goog-resumable\n");
result.append("UNSIGNED-PAYLOAD");
return new CanonicalRequest(result.toString(), resourcePath, canonicalQuery, activeDatetime, makeCredentialScope(now), domain, maxSizeBytes);
}
private String makeCredentialScope(@Nonnull ZonedDateTime now) {
return SIMPLE_UTC_DATE.format(now) + "/auto/storage/goog4_request";
}
private String makeCredential(@Nonnull String email, @Nonnull ZonedDateTime now) {
return email + '/' + makeCredentialScope(now);
}
}

View File

@@ -0,0 +1,78 @@
package org.whispersystems.textsecuregcm.gcp;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.openssl.PEMReader;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
public class CanonicalRequestSigner {
@Nonnull
private final PrivateKey rsaSigningKey;
public CanonicalRequestSigner(@Nonnull String rsaSigningKey) throws IOException, InvalidKeyException {
this.rsaSigningKey = initializeRsaSigningKey(rsaSigningKey);
}
public String sign(@Nonnull CanonicalRequest canonicalRequest) {
return sign(makeStringToSign(canonicalRequest));
}
private String makeStringToSign(@Nonnull final CanonicalRequest canonicalRequest) {
final StringBuilder result = new StringBuilder("GOOG4-RSA-SHA256\n");
result.append(canonicalRequest.getActiveDatetime()).append('\n');
result.append(canonicalRequest.getCredentialScope()).append('\n');
final MessageDigest sha256;
try {
sha256 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
sha256.update(canonicalRequest.getCanonicalRequest().getBytes(StandardCharsets.UTF_8));
result.append(Hex.encodeHex(sha256.digest()));
return result.toString();
}
private String sign(@Nonnull String stringToSign) {
final byte[] signature;
try {
final Signature sha256rsa = Signature.getInstance("SHA256WITHRSA");
sha256rsa.initSign(rsaSigningKey);
sha256rsa.update(stringToSign.getBytes(StandardCharsets.UTF_8));
signature = sha256rsa.sign();
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new AssertionError(e);
}
return Hex.encodeHexString(signature);
}
private static PrivateKey initializeRsaSigningKey(String rsaSigningKey) throws IOException, InvalidKeyException {
final PEMReader pemReader = new PEMReader(new StringReader(rsaSigningKey));
final PrivateKey key = (PrivateKey) pemReader.readObject();
testKeyIsValidForSigning(key);
return key;
}
private static void testKeyIsValidForSigning(PrivateKey key) throws InvalidKeyException {
final Signature sha256rsa;
try {
sha256rsa = Signature.getInstance("SHA256WITHRSA");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
sha256rsa.initSign(key);
}
}