Skip to content

Commit

Permalink
Add signing capabilities to AppEngine and service account classes (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
mziccard committed Oct 30, 2016
1 parent 0cf8387 commit 1993157
Show file tree
Hide file tree
Showing 17 changed files with 413 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.appengine.api.appidentity.AppIdentityService;
import com.google.appengine.api.appidentity.AppIdentityService.GetAccessTokenResult;
import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.base.MoreObjects;
Expand All @@ -51,7 +52,7 @@
*
* <p>Fetches access tokens from the App Identity service.
*/
public class AppEngineCredentials extends GoogleCredentials {
public class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {

private static final long serialVersionUID = -2627708355455064660L;

Expand Down Expand Up @@ -97,6 +98,16 @@ public GoogleCredentials createScoped(Collection<String> scopes) {
return new AppEngineCredentials(scopes, appIdentityService);
}

@Override
public String getAccount() {
return appIdentityService.getServiceAccountName();
}

@Override
public byte[] sign(byte[] toSign) {
return appIdentityService.signForApp(toSign).getSignature();
}

@Override
public int hashCode() {
return Objects.hash(scopes, scopesRequired, appIdentityServiceClassName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

package com.google.auth.appengine;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
Expand Down Expand Up @@ -65,6 +66,7 @@ public class AppEngineCredentialsTest extends BaseSerializationTest {
private static final Collection<String> SCOPES =
Collections.unmodifiableCollection(Arrays.asList("scope1", "scope2"));
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
private static final String EXPECTED_ACCOUNT = "serviceAccount";

@Test
public void constructor_usesAppIdentityService() throws IOException {
Expand Down Expand Up @@ -93,7 +95,24 @@ public void refreshAccessToken_sameAs() throws IOException {
assertEquals(appIdentity.getExpiration(), accessToken.getExpirationTime());
}

@Test
@Test
public void getAccount_sameAs() throws IOException {
MockAppIdentityService appIdentity = new MockAppIdentityService();
appIdentity.setServiceAccountName(EXPECTED_ACCOUNT);
AppEngineCredentials credentials = new AppEngineCredentials(SCOPES, appIdentity);
assertEquals(EXPECTED_ACCOUNT, credentials.getAccount());
}

@Test
public void sign_sameAs() throws IOException {
byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
MockAppIdentityService appIdentity = new MockAppIdentityService();
appIdentity.setSignature(expectedSignature);
AppEngineCredentials credentials = new AppEngineCredentials(SCOPES, appIdentity);
assertArrayEquals(expectedSignature, credentials.sign(expectedSignature));
}

@Test
public void createScoped_clonesWithScopes() throws IOException {
final String expectedAccessToken = "ExpectedAccessToken";
final Collection<String> emptyScopes = Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class MockAppIdentityService implements AppIdentityService {
private int getAccessTokenCallCount = 0;
private String accessTokenText = null;
private Date expiration = null;
private String serviceAccountName = null;
private SigningResult signingResult = null;

public MockAppIdentityService() {
}
Expand All @@ -72,7 +74,11 @@ public void setExpiration(Date expiration) {

@Override
public SigningResult signForApp(byte[] signBlob) {
return null;
return signingResult;
}

public void setSignature(byte[] signature) {
this.signingResult = new SigningResult("keyName", signature);
}

@Override
Expand Down Expand Up @@ -102,7 +108,11 @@ public GetAccessTokenResult getAccessTokenUncached(Iterable<String> scopes) {

@Override
public String getServiceAccountName() {
return null;
return serviceAccountName;
}

public void setServiceAccountName(String serviceAccountName) {
this.serviceAccountName = serviceAccountName;
}

@Override
Expand Down
82 changes: 82 additions & 0 deletions credentials/java/com/google/auth/ServiceAccountSigner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.auth;

import java.util.Objects;

/**
* Interface for a service account signer. A signer for a service account is capable of signing
* bytes using the private key associated with its service account.
*/
public interface ServiceAccountSigner {

class SigningException extends RuntimeException {

private static final long serialVersionUID = -6503954300538947223L;

public SigningException(String message, Exception cause) {
super(message, cause);
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof SigningException)) {
return false;
}
SigningException other = (SigningException) obj;
return Objects.equals(getCause(), other.getCause())
&& Objects.equals(getMessage(), other.getMessage());
}

@Override
public int hashCode() {
return Objects.hash(getMessage(), getCause());
}
}

/**
* Returns the service account associated with the signer.
*/
String getAccount();

/**
* Signs the provided bytes using the private key associated with the service account.
*
* @param toSign bytes to sign
* @return signed bytes
* @throws SigningException if the attempt to sign the provided bytes failed
*/
byte[] sign(byte[] toSign);
}
88 changes: 88 additions & 0 deletions credentials/javatests/com/google/auth/SigningExceptionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.auth;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import com.google.auth.ServiceAccountSigner.SigningException;

import org.junit.Test;

import java.io.IOException;

public class SigningExceptionTest {

private static final String EXPECTED_MESSAGE = "message";
private static final RuntimeException EXPECTED_CAUSE = new RuntimeException();

@Test
public void constructor() {
SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
assertEquals(EXPECTED_MESSAGE, signingException.getMessage());
assertSame(EXPECTED_CAUSE, signingException.getCause());
}

@Test
public void equals_true() throws IOException {
SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
SigningException otherSigningException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
assertTrue(signingException.equals(otherSigningException));
assertTrue(otherSigningException.equals(signingException));
}

@Test
public void equals_false_message() throws IOException {
SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
SigningException otherSigningException = new SigningException("otherMessage", EXPECTED_CAUSE);
assertFalse(signingException.equals(otherSigningException));
assertFalse(otherSigningException.equals(signingException));
}

@Test
public void equals_false_cause() throws IOException {
SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
SigningException otherSigningException =
new SigningException("otherMessage", new RuntimeException());
assertFalse(signingException.equals(otherSigningException));
assertFalse(otherSigningException.equals(signingException));
}

@Test
public void hashCode_equals() throws IOException {
SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
SigningException otherSigningException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
assertEquals(signingException.hashCode(), otherSigningException.hashCode());
}
}
9 changes: 9 additions & 0 deletions credentials/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<build>
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>javatests</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
Expand All @@ -37,4 +38,12 @@
</plugins>
</build>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

package com.google.auth.oauth2;

import com.google.auth.ServiceAccountSigner;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
Expand All @@ -48,7 +49,7 @@
*
* <p>Instances of this class use reflection to access AppIdentityService in AppEngine SDK.
*/
class AppEngineCredentials extends GoogleCredentials {
class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {

private static final long serialVersionUID = -493219027336622194L;

Expand All @@ -58,10 +59,15 @@ class AppEngineCredentials extends GoogleCredentials {
"com.google.appengine.api.appidentity.AppIdentityService";
static final String GET_ACCESS_TOKEN_RESULT_CLASS =
"com.google.appengine.api.appidentity.AppIdentityService$GetAccessTokenResult";
static final String SIGNING_RESULT_CLASS =
"com.google.appengine.api.appidentity.AppIdentityService$SigningResult";
private static final String GET_APP_IDENTITY_SERVICE_METHOD = "getAppIdentityService";
private static final String GET_ACCESS_TOKEN_RESULT_METHOD = "getAccessToken";
private static final String GET_ACCESS_TOKEN_METHOD = "getAccessToken";
private static final String GET_EXPIRATION_TIME_METHOD = "getExpirationTime";
private static final String GET_SERVICE_ACCOUNT_NAME_METHOD = "getServiceAccountName";
private static final String SIGN_FOR_APP_METHOD = "signForApp";
private static final String GET_SIGNATURE_METHOD = "getSignature";

private final Collection<String> scopes;
private final boolean scopesRequired;
Expand All @@ -70,6 +76,9 @@ class AppEngineCredentials extends GoogleCredentials {
private transient Method getAccessToken;
private transient Method getAccessTokenResult;
private transient Method getExpirationTime;
private transient Method signForApp;
private transient Method getSignature;
private transient String account;

AppEngineCredentials(Collection<String> scopes) throws IOException {
this.scopes = scopes == null ? ImmutableSet.<String>of() : ImmutableList.copyOf(scopes);
Expand Down Expand Up @@ -97,6 +106,11 @@ private void init() throws IOException {
serviceClass.getMethod(GET_ACCESS_TOKEN_RESULT_METHOD, Iterable.class);
this.getAccessToken = tokenResultClass.getMethod(GET_ACCESS_TOKEN_METHOD);
this.getExpirationTime = tokenResultClass.getMethod(GET_EXPIRATION_TIME_METHOD);
this.account = (String) serviceClass.getMethod(GET_SERVICE_ACCOUNT_NAME_METHOD)
.invoke(appIdentityService);
this.signForApp = serviceClass.getMethod(SIGN_FOR_APP_METHOD, byte[].class);
Class<?> signingResultClass = forName(SIGNING_RESULT_CLASS);
this.getSignature = signingResultClass.getMethod(GET_SIGNATURE_METHOD);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException
| InvocationTargetException ex) {
throw new IOException(
Expand Down Expand Up @@ -133,6 +147,21 @@ public GoogleCredentials createScoped(Collection<String> scopes) {
return new AppEngineCredentials(scopes, this);
}

@Override
public String getAccount() {
return account;
}

@Override
public byte[] sign(byte[] toSign) {
try {
Object signingResult = signForApp.invoke(appIdentityService, toSign);
return (byte[]) getSignature.invoke(signingResult);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new SigningException("Failed to sign the provided bytes", ex);
}
}

@Override
public int hashCode() {
return Objects.hash(scopes, scopesRequired);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
public class CloudShellCredentials extends GoogleCredentials {

private static final long serialVersionUID = -2133257318957488451L;
private final static int ACCESS_TOKEN_INDEX = 2;
private final static int READ_TIMEOUT_MS = 5000;
private static final int ACCESS_TOKEN_INDEX = 2;
private static final int READ_TIMEOUT_MS = 5000;

/**
* The Cloud Shell back authorization channel uses serialized
Expand Down
Loading

0 comments on commit 1993157

Please sign in to comment.