Skip to content

Commit

Permalink
Update PR fabric8io#663
Browse files Browse the repository at this point in the history
* Simplified some methods
* Added a unit test
* Minor tweaks
* Only one entry point `createAuthConfig()`
  • Loading branch information
rhuss committed Dec 19, 2016
1 parent 31f3437 commit 80d6d62
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 173 deletions.
2 changes: 1 addition & 1 deletion doc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* **0.18.2**
- Better log message when waiting for URL (#640)
- Update to jnr-unixsocket 0.15
- Extended authentication for AWS ECR
- Extended authentication for AWS ECR (#637)

* **0.18.1** (2016-11-17)
- Renamed `basedir` and `exportBasedir` in an `<assembly>` configuration to `targetDir` and `exportTargetDir` since this better reflects the purpose, i.e. the target in the Docker image to which the assembly is copied. The old name is still recognized but deprecated.
Expand Down
12 changes: 9 additions & 3 deletions src/main/asciidoc/inc/_authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ password.
[[extended-authentication]]
== Extended Authentication

Some docker registries require additional steps to authenticate. link:https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_GetStarted.html[Amazon ECR] requires using an IAM access key to obtain temporary docker login credentials. The `docker:push` and `docker:pull` goals automatically execute this exchange for any registry of the form _awsAccountId_*.dkr.ecr.*_awsRegion_*.amazonaws.com*, unless the `skipExtendedAuth` configuration (`docker.skip.extendedAuth` property) is set true.

You can use any IAM access key with the necessary permissions in any of the locations mentioned above except `~/.docker/config.json`. Use the IAM *Access key ID* as the username and the *Secret access key* as the password.
Some docker registries require additional steps to authenticate.
link:https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_GetStarted.html[Amazon ECR] requires using an IAM
access key to obtain temporary docker login credentials. The `docker:push` and `docker:pull`
goals automatically execute this exchange for any registry of the form
_awsAccountId_*.dkr.ecr.*_awsRegion_*.amazonaws.com*, unless the `skipExtendedAuth`
configuration (`docker.skip.extendedAuth` property) is set to `true`.

You can use any IAM access key with the necessary permissions in any of the locations mentioned above
except `~/.docker/config.json`. Use the IAM *Access key ID* as the username and the *Secret access key* as the password.
12 changes: 6 additions & 6 deletions src/main/java/io/fabric8/maven/docker/access/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public class AuthConfig {

public AuthConfig(Map<String,String> params) {
this(params.get("username"),
params.get("password"),
params.get("email"),
params.get("auth"));
params.get("password"),
params.get("email"),
params.get("auth"));
}

public AuthConfig(String username, String password, String email, String auth) {
Expand All @@ -42,11 +42,11 @@ public AuthConfig(String username, String password, String email, String auth) {
/**
* Constructor which takes an base64 encoded credentials in the form 'user:password'
*
* @param credentialsDockerEncoded the docker encoded user and password
* @param credentialsEncoded the encoded user and password
* @param email the email to use for authentication
*/
public AuthConfig(String credentialsDockerEncoded, String email) {
String credentials = new String(Base64.decodeBase64(credentialsDockerEncoded));
public AuthConfig(String credentialsEncoded, String email) {
String credentials = new String(Base64.decodeBase64(credentialsEncoded));
String[] parsedCreds = credentials.split(":",2);
username = parsedCreds[0];
password = parsedCreds[1];
Expand Down
56 changes: 28 additions & 28 deletions src/main/java/io/fabric8/maven/docker/access/ecr/AwsSigner4.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,15 @@
import org.apache.http.client.utils.URLEncodedUtils;

import io.fabric8.maven.docker.access.AuthConfig;
import org.apache.maven.shared.utils.StringUtils;

/**
* AwsSigner4 implementation that signs requests with the AWS4 signing protocol.
* AwsSigner4 implementation that signs requests with the AWS4 signing protocol. Refere to the AWS docs for mor details.
*
* @author chas
* @since 2016-12-9
*/
public class AwsSigner4 {

private static final Comparator<NameValuePair> PAIR_NAME_COMPARATOR = new Comparator<NameValuePair>() {
@Override
public int compare(NameValuePair l, NameValuePair r) {
return l.getName().compareToIgnoreCase(r.getName());
}
};
class AwsSigner4 {

// a-f must be lower case
final private static char[] HEXITS = "0123456789abcdef".toCharArray();
Expand All @@ -46,7 +40,7 @@ public int compare(NameValuePair l, NameValuePair r) {
* @param region The aws region.
* @param service The aws service.
*/
public AwsSigner4(String region, String service) {
AwsSigner4(String region, String service) {
this.region = region;
this.service = service;
}
Expand All @@ -58,19 +52,21 @@ public AwsSigner4(String region, String service) {
* @param credentials The credentials to use when signing.
* @param signingTime The invocation time to use;
*/
public void sign(HttpRequest request, AuthConfig credentials, Date signingTime) {
void sign(HttpRequest request, AuthConfig credentials, Date signingTime) {
AwsSigner4Request sr = new AwsSigner4Request(region, service, request, signingTime);
if(!request.containsHeader("X-Amz-Date")) {
request.addHeader("X-Amz-Date", sr.getSigningDateTime());
}
request.addHeader("Authorization", task4(sr, credentials));
}

// ======================================================================================================

/**
* Task 1.
* <a href="https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html">Create a Canonical Request</a>
*/
String task1(AwsSigner4Request sr) {
private String task1(AwsSigner4Request sr) {
StringBuilder sb = new StringBuilder(sr.getMethod()).append('\n')
.append(sr.getUri().getRawPath()).append('\n')
.append(getCanonicalQuery(sr.getUri())).append('\n')
Expand All @@ -85,7 +81,7 @@ String task1(AwsSigner4Request sr) {
* Task 2.
* <a href="https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html">Create a String to Sign for Signature Version 4</a>
*/
String task2(AwsSigner4Request sr) {
private String task2(AwsSigner4Request sr) {
StringBuilder sb = new StringBuilder("AWS4-HMAC-SHA256\n")
.append(sr.getSigningDateTime()).append('\n')
.append(sr.getScope()).append('\n');
Expand All @@ -97,24 +93,23 @@ String task2(AwsSigner4Request sr) {
* Task 3.
* <a href="https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html">Calculate the Signature for AWS Signature Version 4</a>
*/
final byte[] task3(AwsSigner4Request sr, AuthConfig credentials) {
private byte[] task3(AwsSigner4Request sr, AuthConfig credentials) {
return hmacSha256(getSigningKey(sr, credentials), task2(sr));
}

static byte[] getSigningKey(AwsSigner4Request sr, AuthConfig credentials) {
private static byte[] getSigningKey(AwsSigner4Request sr, AuthConfig credentials) {
byte[] kSecret = ("AWS4" + credentials.getPassword()).getBytes(StandardCharsets.UTF_8);
byte[] kDate = hmacSha256(kSecret, sr.getSigningDate());
byte[] kRegion = hmacSha256(kDate, sr.getRegion());
byte[] kService = hmacSha256(kRegion, sr.getService());
byte[] signingKey = hmacSha256(kService, "aws4_request");
return signingKey;
return hmacSha256(kService, "aws4_request");
}

/**
* Task 4.
* <a href="https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html">Add the Signing Information to the Request</a>
*/
String task4(AwsSigner4Request sr, AuthConfig credentials) {
private String task4(AwsSigner4Request sr, AuthConfig credentials) {
StringBuilder sb = new StringBuilder("AWS4-HMAC-SHA256 Credential=")
.append(credentials.getUsername() ).append( '/' ).append( sr.getScope() )
.append(", SignedHeaders=").append(sr.getSignedHeaders())
Expand All @@ -125,23 +120,28 @@ String task4(AwsSigner4Request sr, AuthConfig credentials) {

private String getCanonicalQuery(URI uri) {
String query = uri.getQuery();
if(query == null || query.isEmpty()) {
if (StringUtils.isBlank(query)) {
return "";
}
List<NameValuePair> params = URLEncodedUtils.parse(query, StandardCharsets.UTF_8);
Collections.sort(params, PAIR_NAME_COMPARATOR);
Collections.sort(params, new Comparator<NameValuePair>() {
@Override
public int compare(NameValuePair l, NameValuePair r) {
return l.getName().compareToIgnoreCase(r.getName());
}
});
return URLEncodedUtils.format(params, StandardCharsets.UTF_8);
}

static void hexEncode(StringBuilder dst, byte[] src) {
for ( int i = 0; i < src.length; ++i ) {
int v = src[i] & 0xFF;
private static void hexEncode(StringBuilder dst, byte[] src) {
for (byte aSrc : src) {
int v = aSrc & 0xFF;
dst.append(HEXITS[v >>> 4]);
dst.append(HEXITS[v & 0x0F]);
}
}

static byte[] hmacSha256(byte[] key, String value) {
private static byte[] hmacSha256(byte[] key, String value) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
Expand All @@ -156,14 +156,14 @@ private static byte[] sha256(String string) {
return sha256(string.getBytes(StandardCharsets.UTF_8));
}

private static byte[] sha256(byte[] bytes) {
try {
private static byte[] sha256(byte[] bytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(bytes);
return md.digest();
}
catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e.getMessage(), e);
catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.*;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
Expand Down Expand Up @@ -58,11 +55,10 @@ public class AwsSigner4Request {
method = request.getRequestLine().getMethod();
uri = getUri(request);

StringBuilder canonical = new StringBuilder();
StringBuilder signed = new StringBuilder();
canonicalizeHeaders(request, canonical, signed);
canonicalHeaders = canonical.toString();
signedHeaders = signed.toString();

String[] headers = canonicalizeHeaders(request);
canonicalHeaders = headers[0];
signedHeaders = headers[1];
}

public String getRegion() {
Expand Down Expand Up @@ -149,68 +145,37 @@ private static URI createUri(String authority, String uri) {
}
}

private static void canonicalizeHeaders(HttpRequest request, StringBuilder canonical, StringBuilder signed) {
Map<String, StringBuilder> unique = new TreeMap<>();
private static String[] canonicalizeHeaders(HttpRequest request) {
Map<String, List<String>> unique = new TreeMap<>();
for (Header header : request.getAllHeaders()) {
String key = header.getName().toLowerCase(Locale.US);
if (key.equals("connection")) {
continue;
}
StringBuilder builder = unique.get(key);
if (builder != null) {
if (builder.length() > 0) {
builder.append(',');
}
} else {
builder = new StringBuilder();
unique.put(key, builder);
}
String value = header.getValue();
if (value != null) {
builder.append(value);
List<String> values = unique.get(key);
if (values == null) {
values = new ArrayList<>();
unique.put(key, values);
}
values.add(value);
}
}

for (Map.Entry<String, StringBuilder> header : unique.entrySet()) {
if (signed.length() > 0) {
signed.append(';');
}
signed.append(header.getKey());

squeezeWhite(canonical, header.getKey());
StringBuilder canonical = new StringBuilder();
for (Map.Entry<String, List<String>> header : unique.entrySet()) {
// HTTP Header names never contain white-space
canonical.append(header.getKey());
canonical.append(':');
squeezeWhite(canonical, header.getValue().toString());
canonical.append(StringUtils.join(header.getValue(), ",").replaceAll("\\s+", " ").trim());
canonical.append('\n');
}
}

private static void squeezeWhite(StringBuilder dst, String src) {
int l = src.length();
while (l > 0) {
char ch = src.charAt(--l);
if (!Character.isWhitespace(ch)) {
break;
}
}

boolean wasWhite = true;
for (int i = 0; i <= l; ++i) {
char ch = src.charAt(i);
boolean isWhite = Character.isWhitespace(ch);
if (isWhite) {
if (wasWhite) {
continue;
}
dst.append(' ');
} else {
dst.append(ch);
}
wasWhite = isWhite;
}
return new String[] { canonical.toString(), StringUtils.join(unique.keySet(),";") };
}

byte[] getBytes() {
if(request instanceof HttpEntityEnclosingRequestBase) {
if (request instanceof HttpEntityEnclosingRequestBase) {
try {
HttpEntity entity = ((HttpEntityEnclosingRequestBase)request).getEntity();
return EntityUtils.toByteArray(entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ public class EcrExtendedAuth {
Pattern.compile("^(\\d{12})\\.dkr\\.ecr\\.([a-z\\-0-9]+)\\.amazonaws\\.com$");

private final Logger logger;
private final Matcher matcher;
private final boolean isValid;

private final boolean isAwsRegistry;
private final String accountId;
private final String region;

/**
* Initialize an extended authentication for ecr registry.
Expand All @@ -45,17 +47,25 @@ public class EcrExtendedAuth {
*/
public EcrExtendedAuth(Logger logger, String registry) {
this.logger = logger;
matcher = AWS_REGISTRY.matcher(registry);
isValid = matcher.matches();
logger.debug("registry = %s, isValid= %b", registry, isValid);
Matcher matcher = AWS_REGISTRY.matcher(registry);
isAwsRegistry = matcher.matches();
if (isAwsRegistry) {
accountId = matcher.group(1);
region = matcher.group(2);
} else {
accountId = null;
region = null;
}

logger.debug("registry = %s, isValid= %b", registry, isAwsRegistry);
}

/**
* Is the registry an ecr registry?
* @return true, if the registry matches the ecr pattern
*/
public boolean isValidRegistry() {
return isValid;
public boolean isAwsRegistry() {
return isAwsRegistry;
}

/**
Expand Down Expand Up @@ -86,12 +96,12 @@ CloseableHttpClient createClient() {
return HttpClients.createDefault();
}

JSONObject executeRequest(CloseableHttpClient client, HttpPost request) throws IOException, MojoExecutionException {
private JSONObject executeRequest(CloseableHttpClient client, HttpPost request) throws IOException, MojoExecutionException {
try {
CloseableHttpResponse response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
logger.debug("Response status %d", statusCode);
if(statusCode != HttpStatus.SC_OK) {
if (statusCode != HttpStatus.SC_OK) {
throw new MojoExecutionException("AWS authentication failure");
}

Expand All @@ -105,11 +115,9 @@ JSONObject executeRequest(CloseableHttpClient client, HttpPost request) throws I
}

HttpPost createSignedRequest(AuthConfig localCredentials, Date time) {
String accountId = matcher.group(1);
String region = matcher.group(2);
String host = "ecr." + region + ".amazonaws.com";

logger.debug("GetAuthorizationToken from %s", host);
logger.debug("Get ECR AuthorizationToken from %s", host);

HttpPost request = new HttpPost("https://" + host + '/');
request.setHeader("host", host);
Expand Down
Loading

0 comments on commit 80d6d62

Please sign in to comment.