diff --git a/api/src/main/java/io/minio/Digest.java b/api/src/main/java/io/minio/Digest.java index 7108bf826..baa101015 100644 --- a/api/src/main/java/io/minio/Digest.java +++ b/api/src/main/java/io/minio/Digest.java @@ -28,7 +28,7 @@ import java.util.Locale; /** Various global static functions used. */ -class Digest { +public class Digest { /** Private constructor. */ private Digest() {} diff --git a/api/src/main/java/io/minio/MinioClient.java b/api/src/main/java/io/minio/MinioClient.java index 838b4e8de..0e05e644e 100644 --- a/api/src/main/java/io/minio/MinioClient.java +++ b/api/src/main/java/io/minio/MinioClient.java @@ -1034,7 +1034,13 @@ protected Response execute( Credentials creds = (provider == null) ? null : provider.fetch(); Request request = createRequest(url, method, headerMap, body, length, creds); if (creds != null) { - request = Signer.signV4(request, region, creds.accessKey(), creds.secretKey()); + request = + Signer.signV4S3( + request, + region, + creds.accessKey(), + creds.secretKey(), + request.header("x-amz-content-sha256")); } if (this.traceStream != null) { diff --git a/api/src/main/java/io/minio/Signer.java b/api/src/main/java/io/minio/Signer.java index e7154cd2c..9c02c262e 100644 --- a/api/src/main/java/io/minio/Signer.java +++ b/api/src/main/java/io/minio/Signer.java @@ -37,7 +37,7 @@ import okhttp3.Request; /** Amazon AWS S3 signature V4 signer. */ -class Signer { +public class Signer { // // Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258 // @@ -115,7 +115,7 @@ class Signer { * @param secretKey Secret Key string. * @param prevSignature Previous signature of chunk upload. */ - public Signer( + private Signer( Request request, String contentSha256, ZonedDateTime date, @@ -132,8 +132,14 @@ public Signer( this.prevSignature = prevSignature; } - private void setScope() { - this.scope = this.date.format(Time.SIGNER_DATE_FORMAT) + "/" + this.region + "/s3/aws4_request"; + private void setScope(String serviceName) { + this.scope = + this.date.format(Time.SIGNER_DATE_FORMAT) + + "/" + + this.region + + "/" + + serviceName + + "/aws4_request"; } private void setCanonicalHeaders() { @@ -240,7 +246,8 @@ private void setChunkStringToSign() throws NoSuchAlgorithmException { + this.contentSha256; } - private void setSigningKey() throws NoSuchAlgorithmException, InvalidKeyException { + private void setSigningKey(String serviceName) + throws NoSuchAlgorithmException, InvalidKeyException { String aws4SecretKey = "AWS4" + this.secretKey; byte[] dateKey = @@ -250,7 +257,8 @@ private void setSigningKey() throws NoSuchAlgorithmException, InvalidKeyExceptio byte[] dateRegionKey = sumHmac(dateKey, this.region.getBytes(StandardCharsets.UTF_8)); - byte[] dateRegionServiceKey = sumHmac(dateRegionKey, "s3".getBytes(StandardCharsets.UTF_8)); + byte[] dateRegionServiceKey = + sumHmac(dateRegionKey, serviceName.getBytes(StandardCharsets.UTF_8)); this.signingKey = sumHmac(dateRegionServiceKey, "aws4_request".getBytes(StandardCharsets.UTF_8)); @@ -278,9 +286,9 @@ public static String getChunkSignature( String chunkSha256, ZonedDateTime date, String region, String secretKey, String prevSignature) throws NoSuchAlgorithmException, InvalidKeyException { Signer signer = new Signer(null, chunkSha256, date, region, null, secretKey, prevSignature); - signer.setScope(); + signer.setScope("s3"); signer.setChunkStringToSign(); - signer.setSigningKey(); + signer.setSigningKey("s3"); signer.setSignature(); return signer.signature; @@ -293,32 +301,51 @@ public static String getChunkSeedSignature(Request request, String region, Strin ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT); Signer signer = new Signer(request, contentSha256, date, region, null, secretKey, null); - signer.setScope(); + signer.setScope("s3"); signer.setCanonicalRequest(); signer.setStringToSign(); - signer.setSigningKey(); + signer.setSigningKey("s3"); signer.setSignature(); return signer.signature; } /** Returns signed request object for given request, region, access key and secret key. */ - public static Request signV4(Request request, String region, String accessKey, String secretKey) + private static Request signV4( + String serviceName, + Request request, + String region, + String accessKey, + String secretKey, + String contentSha256) throws NoSuchAlgorithmException, InvalidKeyException { - String contentSha256 = request.header("x-amz-content-sha256"); ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT); Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null); - signer.setScope(); + signer.setScope(serviceName); signer.setCanonicalRequest(); signer.setStringToSign(); - signer.setSigningKey(); + signer.setSigningKey(serviceName); signer.setSignature(); signer.setAuthorization(); return request.newBuilder().header("Authorization", signer.authorization).build(); } + /** Returns signed request of given request for S3 service. */ + public static Request signV4S3( + Request request, String region, String accessKey, String secretKey, String contentSha256) + throws NoSuchAlgorithmException, InvalidKeyException { + return signV4("s3", request, region, accessKey, secretKey, contentSha256); + } + + /** Returns signed request of given request for STS service. */ + public static Request signV4Sts( + Request request, String region, String accessKey, String secretKey, String contentSha256) + throws NoSuchAlgorithmException, InvalidKeyException { + return signV4("sts", request, region, accessKey, secretKey, contentSha256); + } + private void setPresignCanonicalRequest(int expires) throws NoSuchAlgorithmException { this.canonicalHeaders = new TreeMap<>(); this.canonicalHeaders.put("host", this.request.headers().get("Host")); @@ -367,10 +394,10 @@ public static HttpUrl presignV4( ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT); Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null); - signer.setScope(); + signer.setScope("s3"); signer.setPresignCanonicalRequest(expires); signer.setStringToSign(); - signer.setSigningKey(); + signer.setSigningKey("s3"); signer.setSignature(); return signer @@ -397,7 +424,7 @@ public static String postPresignV4( throws NoSuchAlgorithmException, InvalidKeyException { Signer signer = new Signer(null, null, date, region, null, secretKey, null); signer.stringToSign = stringToSign; - signer.setSigningKey(); + signer.setSigningKey("s3"); signer.setSignature(); return signer.signature; diff --git a/api/src/main/java/io/minio/credentials/AssumeRoleBaseProvider.java b/api/src/main/java/io/minio/credentials/AssumeRoleBaseProvider.java new file mode 100644 index 000000000..13f53a0fe --- /dev/null +++ b/api/src/main/java/io/minio/credentials/AssumeRoleBaseProvider.java @@ -0,0 +1,96 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.minio.credentials; + +import io.minio.errors.XmlParserException; +import java.io.IOException; +import java.util.Arrays; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; + +/** Base class to AssumeRole based providers. */ +public abstract class AssumeRoleBaseProvider implements Provider { + private final OkHttpClient httpClient; + private Credentials credentials; + + public AssumeRoleBaseProvider(OkHttpClient customHttpClient) { + // HTTP/1.1 is only supported in default client because of HTTP/2 in OkHttpClient cause 5 + // minutes timeout on program exit. + this.httpClient = + (customHttpClient != null) + ? customHttpClient + : new OkHttpClient().newBuilder().protocols(Arrays.asList(Protocol.HTTP_1_1)).build(); + } + + @Override + public synchronized Credentials fetch() { + if (credentials != null && !credentials.isExpired()) { + return credentials; + } + + try (Response response = httpClient.newCall(getRequest()).execute()) { + if (!response.isSuccessful()) { + throw new IllegalStateException( + "STS service failed with HTTP status code " + response.code()); + } + + credentials = parseResponse(response); + return credentials; + } catch (XmlParserException | IOException e) { + throw new IllegalStateException("Unable to parse STS response", e); + } + } + + protected HttpUrl.Builder newUrlBuilder( + HttpUrl url, + String action, + int durationSeconds, + String policy, + String roleArn, + String roleSessionName) { + HttpUrl.Builder urlBuilder = + url.newBuilder() + .addQueryParameter("Action", action) + .addQueryParameter("Version", "2011-06-15"); + + if (durationSeconds > 0) { + urlBuilder.addQueryParameter("DurationSeconds", String.valueOf(durationSeconds)); + } + + if (policy != null) { + urlBuilder.addQueryParameter("Policy", policy); + } + + if (roleArn != null) { + urlBuilder.addQueryParameter("RoleArn", roleArn); + } + + if (roleSessionName != null) { + urlBuilder.addQueryParameter("RoleSessionName", roleSessionName); + } + + return urlBuilder; + } + + protected abstract Request getRequest(); + + protected abstract Credentials parseResponse(Response response) + throws XmlParserException, IOException; +} diff --git a/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java b/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java new file mode 100644 index 000000000..314143b1a --- /dev/null +++ b/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java @@ -0,0 +1,149 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.minio.credentials; + +import io.minio.Digest; +import io.minio.Signer; +import io.minio.Time; +import io.minio.Xml; +import io.minio.errors.XmlParserException; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Path; +import org.simpleframework.xml.Root; + +/** + * Credential provider using AssumeRole + * API. + */ +public class AssumeRoleProvider extends AssumeRoleBaseProvider { + public static final int DEFAULT_DURATION_SECONDS = (int) TimeUnit.HOURS.toSeconds(1); + private final String accessKey; + private final String secretKey; + private final String region; + private final String contentSha256; + private final Request request; + + public AssumeRoleProvider( + @Nonnull String stsEndpoint, + @Nonnull String accessKey, + @Nonnull String secretKey, + @Nullable Integer durationSeconds, + @Nullable String policy, + @Nullable String region, + @Nullable String roleArn, + @Nullable String roleSessionName, + @Nullable String externalId, + @Nullable OkHttpClient customHttpClient) + throws NoSuchAlgorithmException { + super(customHttpClient); + stsEndpoint = Objects.requireNonNull(stsEndpoint, "STS endpoint cannot be empty"); + HttpUrl url = Objects.requireNonNull(HttpUrl.parse(stsEndpoint), "Invalid STS endpoint"); + accessKey = Objects.requireNonNull(accessKey, "Access key must not be null"); + if (accessKey.isEmpty()) { + throw new IllegalArgumentException("Access key must not be empty"); + } + this.accessKey = accessKey; + this.secretKey = Objects.requireNonNull(secretKey, "Secret key must not be null"); + this.region = (region != null) ? region : ""; + + if (externalId != null && (externalId.length() < 2 || externalId.length() > 1224)) { + throw new IllegalArgumentException("Length of ExternalId must be in between 2 and 1224"); + } + + durationSeconds = + (durationSeconds != null && durationSeconds > DEFAULT_DURATION_SECONDS) + ? durationSeconds + : DEFAULT_DURATION_SECONDS; + + String host = url.host() + ":" + url.port(); + // ignore port when port and service matches i.e HTTP -> 80, HTTPS -> 443 + if ((url.scheme().equals("http") && url.port() == 80) + || (url.scheme().equals("https") && url.port() == 443)) { + host = url.host(); + } + + HttpUrl.Builder urlBuilder = + newUrlBuilder(url, "AssumeRole", durationSeconds, policy, roleArn, roleSessionName); + if (externalId != null) { + urlBuilder.addQueryParameter("ExternalId", externalId); + } + + String data = urlBuilder.build().encodedQuery(); + this.contentSha256 = Digest.sha256Hash(data); + this.request = + new Request.Builder() + .url(url) + .header("Host", host) + .method( + "POST", + RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), data)) + .build(); + } + + @Override + protected Request getRequest() { + try { + return Signer.signV4Sts( + this.request + .newBuilder() + .header("x-amz-date", ZonedDateTime.now().format(Time.AMZ_DATE_FORMAT)) + .build(), + region, + accessKey, + secretKey, + contentSha256); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException("Signature calculation failed", e); + } + } + + @Override + protected Credentials parseResponse(Response response) throws XmlParserException, IOException { + AssumeRoleResponse result = + Xml.unmarshal(AssumeRoleResponse.class, response.body().charStream()); + return result.credentials(); + } + + /** Object representation of response XML of AssumeRole API. */ + @Root(name = "AssumeRoleResponse", strict = false) + @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/") + public static class AssumeRoleResponse { + @Path(value = "AssumeRoleResult") + @Element(name = "Credentials") + private Credentials credentials; + + public Credentials credentials() { + return credentials; + } + } +} diff --git a/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java b/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java index a390d30fe..5f96f55e0 100644 --- a/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java +++ b/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java @@ -16,10 +16,19 @@ package io.minio.credentials; +import io.minio.Xml; +import io.minio.errors.XmlParserException; +import java.io.IOException; import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; +import okhttp3.Response; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Path; +import org.simpleframework.xml.Root; /** * Credential provider using supplier; - private final HttpUrl stsEndpoint; - private final Integer durationSeconds; - private final String policy; - private final String roleArn; - private final String roleSessionName; - private final OkHttpClient httpClient; - private Credentials credentials; + protected final HttpUrl stsEndpoint; + protected final Integer durationSeconds; + protected final String policy; + protected final String roleArn; + protected final String roleSessionName; public WebIdentityClientGrantsProvider( @Nonnull Supplier supplier, @@ -58,6 +48,7 @@ public WebIdentityClientGrantsProvider( @Nullable String roleArn, @Nullable String roleSessionName, @Nullable OkHttpClient customHttpClient) { + super(customHttpClient); this.supplier = Objects.requireNonNull(supplier, "JWT token supplier must not be null"); stsEndpoint = Objects.requireNonNull(stsEndpoint, "STS endpoint cannot be empty"); this.stsEndpoint = Objects.requireNonNull(HttpUrl.parse(stsEndpoint), "Invalid STS endpoint"); @@ -65,10 +56,9 @@ public WebIdentityClientGrantsProvider( this.policy = policy; this.roleArn = roleArn; this.roleSessionName = roleSessionName; - this.httpClient = (customHttpClient != null) ? customHttpClient : new OkHttpClient(); } - private int getDurationSeconds(int expiry) { + protected int getDurationSeconds(int expiry) { if (durationSeconds != null && durationSeconds > 0) { expiry = durationSeconds; } @@ -85,93 +75,11 @@ private int getDurationSeconds(int expiry) { } @Override - public synchronized Credentials fetch() { - if (credentials != null && !credentials.isExpired()) { - return credentials; - } - + protected Request getRequest() { Jwt jwt = supplier.get(); - - HttpUrl.Builder urlBuilder = - stsEndpoint.newBuilder().addQueryParameter("Version", "2011-06-15"); - - int durationSeconds = getDurationSeconds(jwt.expiry()); - if (durationSeconds > 0) { - urlBuilder.addQueryParameter("DurationSeconds", String.valueOf(durationSeconds)); - } - - if (policy != null) { - urlBuilder.addQueryParameter("Policy", policy); - } - - if (isWebIdentity()) { - urlBuilder - .addQueryParameter("Action", "AssumeRoleWithWebIdentity") - .addQueryParameter("WebIdentityToken", jwt.token()); - if (roleArn != null) { - urlBuilder - .addQueryParameter("RoleArn", roleArn) - .addQueryParameter( - "RoleSessionName", - (roleSessionName != null) - ? roleSessionName - : String.valueOf(System.currentTimeMillis())); - } - } else { - urlBuilder - .addQueryParameter("Action", "AssumeRoleWithClientGrants") - .addQueryParameter("Token", jwt.token()); - } - - Request request = - new Request.Builder().url(urlBuilder.build()).method("POST", EMPTY_BODY).build(); - try (Response response = httpClient.newCall(request).execute()) { - if (!response.isSuccessful()) { - throw new IllegalStateException( - "STS service failed with HTTP status code " + response.code()); - } - - if (isWebIdentity()) { - WebIdentityResponse result = - Xml.unmarshal(WebIdentityResponse.class, response.body().charStream()); - credentials = result.credentials(); - } else { - ClientGrantsResponse result = - Xml.unmarshal(ClientGrantsResponse.class, response.body().charStream()); - credentials = result.credentials(); - } - - return credentials; - } catch (XmlParserException | IOException e) { - throw new IllegalStateException("Unable to parse STS response", e); - } - } - - protected abstract boolean isWebIdentity(); - - /** Object representation of response XML of AssumeRoleWithWebIdentity API. */ - @Root(name = "AssumeRoleWithWebIdentityResponse", strict = false) - @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/") - public static class WebIdentityResponse { - @Path(value = "AssumeRoleWithWebIdentityResult") - @Element(name = "Credentials") - private Credentials credentials; - - public Credentials credentials() { - return credentials; - } + HttpUrl.Builder urlBuilder = newUrlBuilder(jwt); + return new Request.Builder().url(urlBuilder.build()).method("POST", EMPTY_BODY).build(); } - /** Object representation of response XML of AssumeRoleWithClientGrants API. */ - @Root(name = "AssumeRoleWithClientGrantsResponse", strict = false) - @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/") - public static class ClientGrantsResponse { - @Path(value = "AssumeRoleWithClientGrantsResult") - @Element(name = "Credentials") - private Credentials credentials; - - public Credentials credentials() { - return credentials; - } - } + protected abstract HttpUrl.Builder newUrlBuilder(Jwt jwt); } diff --git a/api/src/main/java/io/minio/credentials/WebIdentityProvider.java b/api/src/main/java/io/minio/credentials/WebIdentityProvider.java index c6588cab7..00d45d362 100644 --- a/api/src/main/java/io/minio/credentials/WebIdentityProvider.java +++ b/api/src/main/java/io/minio/credentials/WebIdentityProvider.java @@ -16,10 +16,19 @@ package io.minio.credentials; +import io.minio.Xml; +import io.minio.errors.XmlParserException; +import java.io.IOException; import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; +import okhttp3.Response; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Path; +import org.simpleframework.xml.Root; /** * Credential provider using