Skip to content

Commit

Permalink
feat: Add support for explicit java.security.Provider to ServiceAccou…
Browse files Browse the repository at this point in the history
…ntCredentials

This enables the storage of service account credentials in external key
storage such as remote KeyVaults or HSMs.
  • Loading branch information
stian-svedenborg-gul-katt committed Mar 2, 2020
1 parent 0a57cd5 commit 2eb46f9
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 24 deletions.
Expand Up @@ -46,13 +46,15 @@
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.api.client.json.webtoken.JsonWebToken;
import com.google.api.client.util.Base64;
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Joiner;
import com.google.api.client.util.PemReader;
import com.google.api.client.util.PemReader.Section;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.SecurityUtils;
import com.google.api.client.util.StringUtils;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.Beta;
Expand All @@ -66,11 +68,11 @@
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
Expand Down Expand Up @@ -103,6 +105,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
private final URI tokenServerUri;
private final Collection<String> scopes;
private final String quotaProjectId;
private final Provider signingProvider;

private transient HttpTransportFactory transportFactory;

Expand All @@ -122,6 +125,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
* authority to the service account.
* @param projectId the project used for billing
* @param quotaProjectId The project used for quota and billing purposes. May be null.
* @param signingProvider Explicitly set the JCA provider to use during request signing. May be null.
*/
ServiceAccountCredentials(
String clientId,
Expand All @@ -133,7 +137,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
URI tokenServerUri,
String serviceAccountUser,
String projectId,
String quotaProjectId) {
String quotaProjectId,
Provider signingProvider) {
this.clientId = clientId;
this.clientEmail = Preconditions.checkNotNull(clientEmail);
this.privateKey = Preconditions.checkNotNull(privateKey);
Expand All @@ -143,6 +148,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
firstNonNull(
transportFactory,
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.signingProvider = signingProvider;
this.transportFactoryClassName = this.transportFactory.getClass().getName();
this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri;
this.serviceAccountUser = serviceAccountUser;
Expand Down Expand Up @@ -324,7 +330,8 @@ static ServiceAccountCredentials fromPkcs8(
tokenServerUri,
serviceAccountUser,
projectId,
quotaProject);
quotaProject,
null);
}

/** Helper to convert from a PKCS#8 String to an RSA private key */
Expand Down Expand Up @@ -512,7 +519,8 @@ public GoogleCredentials createScoped(Collection<String> newScopes) {
tokenServerUri,
serviceAccountUser,
projectId,
quotaProjectId);
quotaProjectId,
null);
}

@Override
Expand All @@ -527,7 +535,8 @@ public GoogleCredentials createDelegated(String user) {
tokenServerUri,
user,
projectId,
quotaProjectId);
quotaProjectId,
null);
}

public final String getClientId() {
Expand Down Expand Up @@ -570,7 +579,9 @@ public String getAccount() {
@Override
public byte[] sign(byte[] toSign) {
try {
Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM);
Signature signer = signingProvider == null
? Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM)
: Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM, signingProvider);
signer.initSign(getPrivateKey());
signer.update(toSign);
return signer.sign();
Expand Down Expand Up @@ -647,6 +658,19 @@ public boolean equals(Object obj) {
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
}

private String signJsonWebSignature(JsonFactory jsonFactory, JsonWebSignature.Header header, JsonWebToken.Payload payload) throws IOException {
String signedContentString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + "." + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload));
byte[] signedContentBytes = StringUtils.getBytesUtf8(signedContentString);
try {
byte[] signature = this.sign(signedContentBytes);
return signedContentString + "." + Base64.encodeBase64URLSafeString(signature);

} catch (Exception e) {
throw new IOException(
"Error signing service account access token request with private key.", e);
}
}

String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
throws IOException {
JsonWebSignature.Header header = new JsonWebSignature.Header();
Expand All @@ -667,14 +691,8 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String audienc
payload.setAudience(audience);
}

String assertion;
try {
assertion = JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
} catch (GeneralSecurityException e) {
throw new IOException(
"Error signing service account access token request with private key.", e);
}
return assertion;
String jsonWebSignature = signJsonWebSignature(jsonFactory, header, payload);
return jsonWebSignature;
}

@VisibleForTesting
Expand All @@ -698,16 +716,10 @@ String createAssertionForIdToken(
payload.setAudience(audience);
}

try {
payload.set("target_audience", targetAudience);
payload.set("target_audience", targetAudience);

String assertion =
JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
return assertion;
} catch (GeneralSecurityException e) {
throw new IOException(
"Error signing service account access token request with private key.", e);
}
String assertion = signJsonWebSignature(jsonFactory, header, payload);
return assertion;
}

@SuppressWarnings("unused")
Expand Down Expand Up @@ -742,6 +754,7 @@ public static class Builder extends GoogleCredentials.Builder {
private Collection<String> scopes;
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private Provider signatureProvider;

protected Builder() {}

Expand All @@ -756,6 +769,7 @@ protected Builder(ServiceAccountCredentials credentials) {
this.serviceAccountUser = credentials.serviceAccountUser;
this.projectId = credentials.projectId;
this.quotaProjectId = credentials.quotaProjectId;
this.signatureProvider = credentials.signingProvider;
}

public Builder setClientId(String clientId) {
Expand Down Expand Up @@ -808,6 +822,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
return this;
}

public Builder setSignatureProvider(Provider signatureProvider) {
this.signatureProvider = signatureProvider;
return this;
}

public String getClientId() {
return clientId;
}
Expand Down Expand Up @@ -848,6 +867,10 @@ public String getQuotaProjectId() {
return quotaProjectId;
}

public Provider getSignatureProvider() {
return signatureProvider;
}

public ServiceAccountCredentials build() {
return new ServiceAccountCredentials(
clientId,
Expand All @@ -859,7 +882,8 @@ public ServiceAccountCredentials build() {
tokenServerUri,
serviceAccountUser,
projectId,
quotaProjectId);
quotaProjectId,
signatureProvider);
}
}
}
Expand Up @@ -49,6 +49,7 @@
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.client.util.Clock;
import com.google.api.client.util.Joiner;
import com.google.api.client.util.SecurityUtils;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.GoogleCredentialsTest.MockHttpTransportFactory;
Expand All @@ -61,6 +62,7 @@
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Arrays;
Expand Down Expand Up @@ -109,6 +111,15 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
+ "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0"
+ ".redacted";
private static final String QUOTA_PROJECT = "sample-quota-project-id";
private static Provider defaultRsaSignatureProvider;

static {
try {
defaultRsaSignatureProvider = SecurityUtils.getRsaKeyFactory().getProvider();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RSA keys not supported on this JVM", e);
}
}

@Test
public void createdScoped_clones() throws IOException {
Expand Down Expand Up @@ -200,6 +211,36 @@ public void createAssertion_correct() throws IOException {
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
}

@Test
public void createAssertionWithExplicitProvider_correct() throws IOException {
PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8);
List<String> scopes = Arrays.asList("scope1", "scope2");
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
.setClientId(CLIENT_ID)
.setClientEmail(CLIENT_EMAIL)
.setPrivateKey(privateKey)
.setPrivateKeyId(PRIVATE_KEY_ID)
.setScopes(scopes)
.setServiceAccountUser(USER)
.setProjectId(PROJECT_ID)
.setSignatureProvider(defaultRsaSignatureProvider)
.build();

JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
long currentTimeMillis = Clock.SYSTEM.currentTimeMillis();
String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis, null);

JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion);
JsonWebToken.Payload payload = signature.getPayload();
assertEquals(CLIENT_EMAIL, payload.getIssuer());
assertEquals(OAuth2Utils.TOKEN_SERVER_URI.toString(), payload.getAudience());
assertEquals(currentTimeMillis / 1000, (long) payload.getIssuedAtTimeSeconds());
assertEquals(currentTimeMillis / 1000 + 3600, (long) payload.getExpirationTimeSeconds());
assertEquals(USER, payload.getSubject());
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
}

@Test
public void createAssertionForIdToken_correct() throws IOException {

Expand Down

0 comments on commit 2eb46f9

Please sign in to comment.