Skip to content

Commit

Permalink
implement support for SharedKey signature (#186)
Browse files Browse the repository at this point in the history
* implement support for SharedKey signature

This is the recommended signature scheme for Azure, and the only scheme
that is supported by the Azurite emulator.

https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key

* Remove print statement

Co-authored-by: Ignasi Barrera <nacx@apache.org>

* Remove print statement

* simplify logic

---------

Co-authored-by: Ignasi Barrera <nacx@apache.org>
  • Loading branch information
larshagencognite and nacx committed Oct 20, 2023
1 parent eb1181d commit a2628f9
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 9 deletions.
4 changes: 4 additions & 0 deletions core/src/main/java/org/jclouds/http/HttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ public static String nullToEmpty(byte[] md5) {
return md5 != null ? base64().encode(md5) : "";
}

public static String nullOrZeroToEmpty(Long contentLength) {
return contentLength != null && contentLength > 0 ? contentLength.toString() : "";
}

public static String nullToEmpty(Collection<String> collection) {
return (collection == null || collection.isEmpty()) ? "" : collection.iterator().next();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public enum AuthType {
/** Includes both the API key and SAS credentials */
AZURE_KEY,
/** Azure AD credentials */
AZURE_AD;
AZURE_AD,
/** Uses the SharedKey scheme, rather than SharedKeyLite */
AZURE_SHARED_KEY;

@Override
public String toString() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
import javax.inject.Provider;
import javax.inject.Singleton;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Multiset;
import com.google.common.collect.TreeMultiset;
import org.jclouds.Constants;
import org.jclouds.azure.storage.config.AuthType;
import org.jclouds.azure.storage.util.storageurl.StorageUrlSupplier;
Expand All @@ -47,6 +55,8 @@
import org.jclouds.http.Uris;
import org.jclouds.http.Uris.UriBuilder;
import org.jclouds.http.internal.SignatureWire;
import org.jclouds.io.ContentMetadata;
import org.jclouds.io.Payload;
import org.jclouds.logging.Logger;
import org.jclouds.oauth.v2.filters.OAuthFilter;
import org.jclouds.util.Strings2;
Expand All @@ -56,13 +66,7 @@
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.io.ByteProcessor;
import com.google.common.net.HttpHeaders;

Expand All @@ -74,6 +78,9 @@
@Singleton
public class SharedKeyLiteAuthentication implements HttpRequestFilter {
private static final Collection<String> FIRST_HEADERS_TO_SIGN = ImmutableList.of(HttpHeaders.DATE);
private static final Collection<String> FIRST_HEADERS_TO_SIGN_FOR_SHARED_KEY =
ImmutableList.of(HttpHeaders.DATE, HttpHeaders.IF_MODIFIED_SINCE, HttpHeaders.IF_MATCH,
HttpHeaders.IF_NONE_MATCH, HttpHeaders.IF_UNMODIFIED_SINCE, HttpHeaders.RANGE);
private final SignatureWire signatureWire;
private final Supplier<Credentials> creds;
private final Provider<String> timeStampProvider;
Expand Down Expand Up @@ -114,6 +121,8 @@ public SharedKeyLiteAuthentication(SignatureWire signatureWire,
public HttpRequest filter(HttpRequest request) throws HttpException {
if (this.authType == AuthType.AZURE_AD) {
request = this.oAuthFilter.filter(request);
} else if (this.authType == AuthType.AZURE_SHARED_KEY){
request = this.isSAS ? filterSAS(request, this.credential) : filterSharedKey(request);
} else {
request = this.isSAS ? filterSAS(request, this.credential) : filterKey(request);
}
Expand Down Expand Up @@ -153,7 +162,22 @@ public HttpRequest filterKey(HttpRequest request) throws HttpException {
String signature = calculateSignature(createStringToSign(request));
return replaceAuthorizationHeader(request, signature);
}


/**
* this is a 'standard' filter method, applied when SharedKey authentication is used.
*/
public HttpRequest filterSharedKey(HttpRequest request) throws HttpException {
request = replaceDateHeader(request);
String signature = calculateSignature(createStringToSignForSharedKey(request));
return replaceAuthorizationHeaderForSharedKey(request, signature);
}

HttpRequest replaceAuthorizationHeaderForSharedKey(HttpRequest request, String signature) {
return request.toBuilder()
.replaceHeader(HttpHeaders.AUTHORIZATION, "SharedKey " + creds.get().identity + ":" + signature)
.build();
}

HttpRequest replaceAuthorizationHeader(HttpRequest request, String signature) {
return request.toBuilder()
.replaceHeader(HttpHeaders.AUTHORIZATION, "SharedKeyLite " + creds.get().identity + ":" + signature)
Expand Down Expand Up @@ -187,7 +211,21 @@ public String[] cutUri(URI uri) throws IllegalArgumentException {
throw new IllegalArgumentException("there is neither ContainerName nor BlobName in the URI path");
}
return result;
}
}

public String createStringToSignForSharedKey(HttpRequest request) {
utils.logRequest(signatureLog, request, ">>");
StringBuilder buffer = new StringBuilder();
// re-sign the request
appendMethod(request, buffer);
appendPayloadMetadataForSharedKey(request, buffer);
appendHttpHeadersForSharedKey(request, buffer);
appendCanonicalizedHeaders(request, buffer);
appendCanonicalizedResourceForSharedKey(request, buffer);
if (signatureWire.enabled())
signatureWire.output(buffer.toString());
return buffer.toString();
}

public String createStringToSign(HttpRequest request) {
utils.logRequest(signatureLog, request, ">>");
Expand All @@ -203,6 +241,26 @@ public String createStringToSign(HttpRequest request) {
return buffer.toString();
}

private void appendPayloadMetadataForSharedKey(HttpRequest request, StringBuilder buffer) {
Payload payload = request.getPayload();
if (payload == null) {
buffer.append("\n\n\n\n\n");
return;
}

ContentMetadata contentMetadata = payload.getContentMetadata();
buffer.append(Strings.nullToEmpty(contentMetadata.getContentEncoding()))
.append("\n");
buffer.append(Strings.nullToEmpty(contentMetadata.getContentLanguage()))
.append("\n");
buffer.append(HttpUtils.nullOrZeroToEmpty(contentMetadata.getContentLength()))
.append("\n");
buffer.append(HttpUtils.nullToEmpty(contentMetadata.getContentMD5()))
.append("\n");
buffer.append(Strings.nullToEmpty(contentMetadata.getContentType()))
.append("\n");
}

private void appendPayloadMetadata(HttpRequest request, StringBuilder buffer) {
buffer.append(
HttpUtils.nullToEmpty(request.getPayload() == null ? null : request.getPayload().getContentMetadata()
Expand Down Expand Up @@ -260,6 +318,11 @@ private void appendHttpHeaders(HttpRequest request, StringBuilder toSign) {
toSign.append(HttpUtils.nullToEmpty(request.getHeaders().get(header))).append("\n");
}

private void appendHttpHeadersForSharedKey(HttpRequest request, StringBuilder toSign) {
for (String header : FIRST_HEADERS_TO_SIGN_FOR_SHARED_KEY)
toSign.append(HttpUtils.nullToEmpty(request.getHeaders().get(header))).append("\n");
}

@VisibleForTesting
void appendCanonicalizedResource(HttpRequest request, StringBuilder toSign) {
// 1. Beginning with an empty string (""), append a forward slash (/), followed by the name of
Expand All @@ -268,6 +331,51 @@ void appendCanonicalizedResource(HttpRequest request, StringBuilder toSign) {
appendUriPath(request, toSign);
}

void appendCanonicalizedResourceForSharedKey(HttpRequest request, StringBuilder toSign) {
// 1. Beginning with an empty string (""), append a forward slash (/), followed by the name of
// the identity that owns the resource being accessed.
toSign.append("/").append(creds.get().identity);
// 2. Append the resource's encoded URI path
toSign.append(request.getEndpoint().getRawPath());
appendQueryParametersForSharedKey(request, toSign);
}

void appendQueryParametersForSharedKey(HttpRequest request, StringBuilder toSign) {
// 3. Append each query parameter as a new line
Map<String, Multiset<String>> sortedParams = Maps.newTreeMap();
if (request.getEndpoint().getQuery() != null) {
String[] params = request.getEndpoint().getQuery().split("&");
for (String param : params) {
String[] paramNameAndValue = param.split("=");
String key = paramNameAndValue[0];
String value = paramNameAndValue.length > 1 ? paramNameAndValue[1] : "";
if (sortedParams.containsKey(key)) {
sortedParams.get(key).add(value);
} else {
Multiset<String> values = TreeMultiset.create();
values.add(value);
sortedParams.put(key, values);
}
}
}

for (Entry<String, Multiset<String>> entry : sortedParams.entrySet()) {
String key = entry.getKey();
Multiset<String> values = entry.getValue();
toSign.append("\n");
toSign.append(key);
toSign.append(":");
boolean first = true;
for (String value : values) {
if (!first) {
toSign.append(",");
}
toSign.append(value);
first = false;
}
}
}

@VisibleForTesting
void appendUriPath(HttpRequest request, StringBuilder toSign) {
// 2. Append the resource's encoded URI path
Expand Down

0 comments on commit a2628f9

Please sign in to comment.