diff --git a/util/src/main/java/io/kubernetes/client/util/credentials/EKSAuthentication.java b/util/src/main/java/io/kubernetes/client/util/credentials/EKSAuthentication.java index 1146559227..3c7cbddc05 100644 --- a/util/src/main/java/io/kubernetes/client/util/credentials/EKSAuthentication.java +++ b/util/src/main/java/io/kubernetes/client/util/credentials/EKSAuthentication.java @@ -12,21 +12,23 @@ */ package io.kubernetes.client.util.credentials; -import com.amazonaws.auth.AWSSessionCredentials; +import com.amazonaws.DefaultRequest; +import com.amazonaws.auth.AWS4Signer; import com.amazonaws.auth.AWSSessionCredentialsProvider; +import com.amazonaws.http.HttpMethodName; +import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest; +import com.amazonaws.util.RuntimeHttpUtils; import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.util.eks.AWS4STSSigner; -import io.kubernetes.client.util.eks.AWS4SignerBase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; -import java.util.HashMap; +import java.util.Date; /** * EKS cluster authentication which generates a bearer token from AWS AK/SK. It doesn't require an "aws" @@ -55,45 +57,43 @@ public EKSAuthentication(AWSSessionCredentialsProvider provider, String region, expirySeconds = MAX_EXPIRY_SECONDS; } this.expirySeconds = expirySeconds; + this.stsEndpoint = URI.create("https://sts." + this.region + ".amazonaws.com"); } private static final int MAX_EXPIRY_SECONDS = 60 * 15; private final AWSSessionCredentialsProvider provider; private final String region; private final String clusterName; + private final URI stsEndpoint; private final int expirySeconds; @Override public void provide(ApiClient client) { - URI uri = URI.create("https://sts." + this.region + ".amazonaws.com/"); - AWSSessionCredentials cred = provider.getCredentials(); - try { - AWS4STSSigner signer = new AWS4STSSigner( - uri.toURL(), - "GET", - "sts", - this.region); - String token = "k8s-aws-v1." + Base64.getEncoder().withoutPadding().encodeToString(signer.computeSignature( - uri, - new HashMap() {{ - put("x-k8s-aws-id", clusterName); - - }}, - new HashMap() {{ - put("Action", "GetCallerIdentity"); - put("Version", "2011-06-15"); - }}, - expirySeconds, - AWS4SignerBase.EMPTY_BODY_SHA256, - cred.getAWSAccessKeyId(), - cred.getAWSSecretKey(), - cred.getSessionToken()).getBytes()); - client.setApiKeyPrefix("Bearer"); - client.setApiKey(token); - log.info("Generated BEARER token for ApiClient, expiring at {}", Instant.now().plus(expirySeconds, ChronoUnit.SECONDS)); - } catch (MalformedURLException | URISyntaxException e) { - throw new RuntimeException(e); - } + DefaultRequest defaultRequest = + new DefaultRequest<>(new GetCallerIdentityRequest(), "sts"); + defaultRequest.setResourcePath("/"); + defaultRequest.setEndpoint(stsEndpoint); + defaultRequest.setHttpMethod(HttpMethodName.GET); + defaultRequest.addParameter("Action", "GetCallerIdentity"); + defaultRequest.addParameter("Version", "2011-06-15"); + defaultRequest.addHeader("x-k8s-aws-id", clusterName); + AWS4Signer signer = new AWS4Signer(); + Date expirationTime = new Date(Clock.systemDefaultZone().millis() + 60 * 1000); + signer.setServiceName("sts"); + signer.presignRequest( + defaultRequest, + this.provider.getCredentials(), + expirationTime); + String encodedUrl = + Base64.getUrlEncoder() + .withoutPadding() + .encodeToString( RuntimeHttpUtils.convertRequestToUrl( + defaultRequest, true, false).toString() + .getBytes(StandardCharsets.UTF_8)); + String token = "k8s-aws-v1." + encodedUrl; + client.setApiKeyPrefix("Bearer"); + client.setApiKey(token); + log.info("Generated BEARER token for ApiClient, expiring at {}", Instant.now().plus(expirySeconds, ChronoUnit.SECONDS)); } } diff --git a/util/src/main/java/io/kubernetes/client/util/eks/AWS4STSSigner.java b/util/src/main/java/io/kubernetes/client/util/eks/AWS4STSSigner.java deleted file mode 100644 index 884422de18..0000000000 --- a/util/src/main/java/io/kubernetes/client/util/eks/AWS4STSSigner.java +++ /dev/null @@ -1,141 +0,0 @@ -/* -Copyright 2023 The Kubernetes Authors. -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 -http://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.kubernetes.client.util.eks; - -import org.apache.http.client.utils.URIBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Date; -import java.util.Map; - - -/** - * Sample AWS4 signer demonstrating how to sign requests to Amazon S3 using - * query string parameters. - */ -public class AWS4STSSigner extends AWS4SignerBase { - - private static final Logger log = LoggerFactory.getLogger(AWS4STSSigner.class); - - public AWS4STSSigner(URL endpointUrl, String httpMethod, - String serviceName, String regionName) { - super(endpointUrl, httpMethod, serviceName, regionName); - } - - /** - * Computes an AWS4 authorization for a request, suitable for embedding in - * query parameters. - * - * @param headers - * The request headers; 'Host' and 'X-Amz-Date' will be added to - * this set. - * @param queryParameters - * Any query parameters that will be added to the endpoint. The - * parameters should be specified in canonical format. - * @param bodyHash - * Precomputed SHA256 hash of the request body content; this - * value should also be set as the header 'X-Amz-Content-SHA256' - * for non-streaming uploads. - * @param awsAccessKey - * The user's AWS Access Key. - * @param awsSecretKey - * The user's AWS Secret Key. - * @return The computed authorization string for the request. This value - * needs to be set as the header 'Authorization' on the subsequent - * HTTP request. - */ - public String computeSignature(URI endpoint, - Map headers, - Map queryParameters, - int expiringInSeconds, - String bodyHash, - String awsAccessKey, - String awsSecretKey, - String sessionToken) throws URISyntaxException { - // first get the date and time for the subsequent request, and convert - // to ISO 8601 format - // for use in signature generation - Date now = new Date(); - String dateTimeStamp = dateTimeFormat.format(now); - - // make sure "Host" header is added - String hostHeader = endpointUrl.getHost(); - int port = endpointUrl.getPort(); - if ( port > -1 ) { - hostHeader.concat(":" + Integer.toString(port)); - } - headers.put("Host", hostHeader); - - // canonicalized headers need to be expressed in the query - // parameters processed in the signature - String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers); - String canonicalizedHeaders = getCanonicalizedHeaderString(headers); - - // we need scope as part of the query parameters - String dateStamp = dateStampFormat.format(now); - String scope = dateStamp + "/" + regionName + "/" + serviceName + "/" + TERMINATOR; - - // add the fixed authorization params required by Signature V4 - queryParameters.put("X-Amz-Algorithm", SCHEME + "-" + ALGORITHM); - queryParameters.put("X-Amz-Credential", awsAccessKey + "/" + scope); - - // x-amz-date is now added as a query parameter, but still need to be in ISO8601 basic form - queryParameters.put("X-Amz-Date", dateTimeStamp); - queryParameters.put("X-Amz-Expires", String.valueOf(expiringInSeconds)); - - queryParameters.put("X-Amz-Security-Token", sessionToken); - - queryParameters.put("X-Amz-SignedHeaders", canonicalizedHeaderNames); - - // build the expanded canonical query parameter string that will go into the - // signature computation - String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters); - - // express all the header and query parameter data as a canonical request string - String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod, - canonicalizedQueryParameters, canonicalizedHeaderNames, - canonicalizedHeaders, bodyHash); - log.debug("--------- Canonical request --------"); - log.debug(canonicalRequest); - log.debug("------------------------------------"); - - // construct the string to be signed - String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope, canonicalRequest); - log.debug("--------- String to sign -----------"); - log.debug(stringToSign); - log.debug("------------------------------------"); - - // compute the signing key - byte[] kSecret = (SCHEME + awsSecretKey).getBytes(); - byte[] kDate = sign(dateStamp, kSecret, "HmacSHA256"); - byte[] kRegion = sign(regionName, kDate, "HmacSHA256"); - byte[] kService = sign(serviceName, kRegion, "HmacSHA256"); - byte[] kSigning = sign(TERMINATOR, kService, "HmacSHA256"); - byte[] signature = sign(stringToSign, kSigning, "HmacSHA256"); - - // form up the authorization parameters for the caller to place in the query string - StringBuilder authString = new StringBuilder(); - URIBuilder builder = new URIBuilder(endpoint); - queryParameters.forEach((k, v)-> { - builder.addParameter(k, v); - }); - builder.addParameter("X-Amz-Signature", BinaryUtils.toHex(signature)); - authString.append(builder.build()); - return authString.toString(); - } - -} diff --git a/util/src/main/java/io/kubernetes/client/util/eks/AWS4SignerBase.java b/util/src/main/java/io/kubernetes/client/util/eks/AWS4SignerBase.java deleted file mode 100644 index 131a24c63f..0000000000 --- a/util/src/main/java/io/kubernetes/client/util/eks/AWS4SignerBase.java +++ /dev/null @@ -1,256 +0,0 @@ -/* -Copyright 2023 The Kubernetes Authors. -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 -http://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.kubernetes.client.util.eks; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.URL; -import java.security.MessageDigest; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.SimpleTimeZone; -import java.util.SortedMap; -import java.util.TreeMap; - - -/** - * Common methods and properties for all AWS4 signer variants - */ -public abstract class AWS4SignerBase { - - /** SHA256 hash of an empty request body **/ - public static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - public static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; - - public static final String SCHEME = "AWS4"; - public static final String ALGORITHM = "HMAC-SHA256"; - public static final String TERMINATOR = "aws4_request"; - - /** format strings for the date/time and date stamps required during signing **/ - public static final String ISO8601BasicFormat = "yyyyMMdd'T'HHmmss'Z'"; - public static final String DateStringFormat = "yyyyMMdd"; - - protected URL endpointUrl; - protected String httpMethod; - protected String serviceName; - protected String regionName; - - protected final SimpleDateFormat dateTimeFormat; - protected final SimpleDateFormat dateStampFormat; - - /** - * Create a new AWS V4 signer. - * - * @param endpointUrl - * The service endpoint, including the path to any resource. - * @param httpMethod - * The HTTP verb for the request, e.g. GET. - * @param serviceName - * The signing name of the service, e.g. 's3'. - * @param regionName - * The system name of the AWS region associated with the - * endpoint, e.g. us-east-1. - */ - public AWS4SignerBase(URL endpointUrl, String httpMethod, - String serviceName, String regionName) { - this.endpointUrl = endpointUrl; - this.httpMethod = httpMethod; - this.serviceName = serviceName; - this.regionName = regionName; - - dateTimeFormat = new SimpleDateFormat(ISO8601BasicFormat); - dateTimeFormat.setTimeZone(new SimpleTimeZone(0, "UTC")); - dateStampFormat = new SimpleDateFormat(DateStringFormat); - dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC")); - } - - /** - * Returns the canonical collection of header names that will be included in - * the signature. For AWS4, all header names must be included in the process - * in sorted canonicalized order. - */ - protected static String getCanonicalizeHeaderNames(Map headers) { - List sortedHeaders = new ArrayList(); - sortedHeaders.addAll(headers.keySet()); - Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER); - - StringBuilder buffer = new StringBuilder(); - for (String header : sortedHeaders) { - if (buffer.length() > 0) buffer.append(";"); - buffer.append(header.toLowerCase()); - } - - return buffer.toString(); - } - - /** - * Computes the canonical headers with values for the request. For AWS4, all - * headers must be included in the signing process. - */ - protected static String getCanonicalizedHeaderString(Map headers) { - if ( headers == null || headers.isEmpty() ) { - return ""; - } - - // step1: sort the headers by case-insensitive order - List sortedHeaders = new ArrayList(); - sortedHeaders.addAll(headers.keySet()); - Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER); - - // step2: form the canonical header:value entries in sorted order. - // Multiple white spaces in the values should be compressed to a single - // space. - StringBuilder buffer = new StringBuilder(); - for (String key : sortedHeaders) { - buffer.append(key.toLowerCase().replaceAll("\\s+", " ") + ":" + headers.get(key).replaceAll("\\s+", " ")); - buffer.append("\n"); - } - - return buffer.toString(); - } - - /** - * Returns the canonical request string to go into the signer process; this - consists of several canonical sub-parts. - * @return - */ - protected static String getCanonicalRequest(URL endpoint, - String httpMethod, - String queryParameters, - String canonicalizedHeaderNames, - String canonicalizedHeaders, - String bodyHash) { - String canonicalRequest = - httpMethod + "\n" + - getCanonicalizedResourcePath(endpoint) + "\n" + - queryParameters + "\n" + - canonicalizedHeaders + "\n" + - canonicalizedHeaderNames + "\n" + - bodyHash; - return canonicalRequest; - } - - /** - * Returns the canonicalized resource path for the service endpoint. - */ - protected static String getCanonicalizedResourcePath(URL endpoint) { - if ( endpoint == null ) { - return "/"; - } - String path = endpoint.getPath(); - if ( path == null || path.isEmpty() ) { - return "/"; - } - - String encodedPath = HttpUtils.urlEncode(path, true); - if (encodedPath.startsWith("/")) { - return encodedPath; - } else { - return "/".concat(encodedPath); - } - } - - /** - * Examines the specified query string parameters and returns a - * canonicalized form. - *

- * The canonicalized query string is formed by first sorting all the query - * string parameters, then URI encoding both the key and value and then - * joining them, in order, separating key value pairs with an '&'. - * - * @param parameters - * The query string parameters to be canonicalized. - * - * @return A canonicalized form for the specified query string parameters. - */ - public static String getCanonicalizedQueryString(Map parameters) { - if ( parameters == null || parameters.isEmpty() ) { - return ""; - } - - SortedMap sorted = new TreeMap(); - - Iterator> pairs = parameters.entrySet().iterator(); - while (pairs.hasNext()) { - Map.Entry pair = pairs.next(); - String key = pair.getKey(); - String value = pair.getValue(); - sorted.put(HttpUtils.urlEncode(key, false), HttpUtils.urlEncode(value, false)); - } - - StringBuilder builder = new StringBuilder(); - pairs = sorted.entrySet().iterator(); - while (pairs.hasNext()) { - Map.Entry pair = pairs.next(); - builder.append(pair.getKey()); - builder.append("="); - builder.append(pair.getValue()); - if (pairs.hasNext()) { - builder.append("&"); - } - } - - return builder.toString(); - } - - protected static String getStringToSign(String scheme, String algorithm, String dateTime, String scope, String canonicalRequest) { - String stringToSign = - scheme + "-" + algorithm + "\n" + - dateTime + "\n" + - scope + "\n" + - BinaryUtils.toHex(hash(canonicalRequest)); - return stringToSign; - } - - /** - * Hashes the string contents (assumed to be UTF-8) using the SHA-256 - * algorithm. - */ - public static byte[] hash(String text) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(text.getBytes("UTF-8")); - return md.digest(); - } catch (Exception e) { - throw new RuntimeException("Unable to compute hash while signing request: " + e.getMessage(), e); - } - } - - /** - * Hashes the byte array using the SHA-256 algorithm. - */ - public static byte[] hash(byte[] data) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(data); - return md.digest(); - } catch (Exception e) { - throw new RuntimeException("Unable to compute hash while signing request: " + e.getMessage(), e); - } - } - - protected static byte[] sign(String stringData, byte[] key, String algorithm) { - try { - byte[] data = stringData.getBytes("UTF-8"); - Mac mac = Mac.getInstance(algorithm); - mac.init(new SecretKeySpec(key, algorithm)); - return mac.doFinal(data); - } catch (Exception e) { - throw new RuntimeException("Unable to calculate a request signature: " + e.getMessage(), e); - } - } -} diff --git a/util/src/main/java/io/kubernetes/client/util/eks/BinaryUtils.java b/util/src/main/java/io/kubernetes/client/util/eks/BinaryUtils.java deleted file mode 100644 index 039ef20ad8..0000000000 --- a/util/src/main/java/io/kubernetes/client/util/eks/BinaryUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2023 The Kubernetes Authors. -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 -http://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.kubernetes.client.util.eks; - -import java.util.Locale; - -/** - * Utilities for encoding and decoding binary data to and from different forms. - */ -public class BinaryUtils { - - /** - * Converts byte data to a Hex-encoded string. - * - * @param data - * data to hex encode. - * - * @return hex-encoded string. - */ - public static String toHex(byte[] data) { - StringBuilder sb = new StringBuilder(data.length * 2); - for (int i = 0; i < data.length; i++) { - String hex = Integer.toHexString(data[i]); - if (hex.length() == 1) { - // Append leading zero. - sb.append("0"); - } else if (hex.length() == 8) { - // Remove ff prefix from negative numbers. - hex = hex.substring(6); - } - sb.append(hex); - } - return sb.toString().toLowerCase(Locale.getDefault()); - } - - /** - * Converts a Hex-encoded data string to the original byte data. - * - * @param hexData - * hex-encoded data to decode. - * @return decoded data from the hex string. - */ - public static byte[] fromHex(String hexData) { - byte[] result = new byte[(hexData.length() + 1) / 2]; - String hexNumber = null; - int stringOffset = 0; - int byteOffset = 0; - while (stringOffset < hexData.length()) { - hexNumber = hexData.substring(stringOffset, stringOffset + 2); - stringOffset += 2; - result[byteOffset++] = (byte) Integer.parseInt(hexNumber, 16); - } - return result; - } -} diff --git a/util/src/main/java/io/kubernetes/client/util/eks/HttpUtils.java b/util/src/main/java/io/kubernetes/client/util/eks/HttpUtils.java deleted file mode 100644 index 810bebb13d..0000000000 --- a/util/src/main/java/io/kubernetes/client/util/eks/HttpUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2023 The Kubernetes Authors. -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 -http://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.kubernetes.client.util.eks; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.Map; - -/** - * Various Http helper routines - */ -public class HttpUtils { - - private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); - /** - * Makes a http request to the specified endpoint - */ - public static String invokeHttpRequest(URL endpointUrl, - String httpMethod, - Map headers, - String requestBody) { - HttpURLConnection connection = createHttpConnection(endpointUrl, httpMethod, headers); - try { - if ( requestBody != null ) { - DataOutputStream wr = new DataOutputStream( - connection.getOutputStream()); - wr.writeBytes(requestBody); - wr.flush(); - wr.close(); - } - } catch (Exception e) { - throw new RuntimeException("Request failed. " + e.getMessage(), e); - } - return executeHttpRequest(connection); - } - - public static String executeHttpRequest(HttpURLConnection connection) { - try { - // Get Response - InputStream is; - try { - is = connection.getInputStream(); - } catch (IOException e) { - is = connection.getErrorStream(); - } - - BufferedReader rd = new BufferedReader(new InputStreamReader(is)); - String line; - StringBuffer response = new StringBuffer(); - while ((line = rd.readLine()) != null) { - response.append(line); - response.append('\r'); - } - rd.close(); - return response.toString(); - } catch (Exception e) { - throw new RuntimeException("Request failed. " + e.getMessage(), e); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - public static HttpURLConnection createHttpConnection(URL endpointUrl, - String httpMethod, - Map headers) { - try { - HttpURLConnection connection = (HttpURLConnection) endpointUrl.openConnection(); - connection.setRequestMethod(httpMethod); - - if ( headers != null ) { - log.debug("--------- Request headers ---------"); - for ( String headerKey : headers.keySet() ) { - log.debug("{}: {}", headerKey, headers.get(headerKey)); - connection.setRequestProperty(headerKey, headers.get(headerKey)); - } - } - - connection.setUseCaches(false); - connection.setDoInput(true); - connection.setDoOutput(true); - return connection; - } catch (Exception e) { - throw new RuntimeException("Cannot create connection. " + e.getMessage(), e); - } - } - - public static String urlEncode(String url, boolean keepPathSlash) { - String encoded; - try { - encoded = URLEncoder.encode(url, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding is not supported.", e); - } - if ( keepPathSlash ) { - encoded = encoded.replace("%2F", "/"); - } - return encoded; - } -}