diff --git a/compute/pom.xml b/compute/pom.xml index ce278be55f4..becce02d9c5 100644 --- a/compute/pom.xml +++ b/compute/pom.xml @@ -34,6 +34,7 @@ error-reporting mailjet sendgrid + signed-metadata diff --git a/compute/signed-metadata/README.md b/compute/signed-metadata/README.md new file mode 100644 index 00000000000..de4f375efdd --- /dev/null +++ b/compute/signed-metadata/README.md @@ -0,0 +1,39 @@ +# Verify instance identity Java sample for Google Compute Engine + +This repository contains example code in Java that downloads and verifies JWT +provided by metadata endpoint, which is available on Compute Engine instances in +GCP. + +More about this verification can be read on official Google Cloud documentation +[Veryfying the Identity of +Instances](https://cloud.google.com/compute/docs/instances/verifying-instance-identity). + +## Running on Compute Engine + +To run the sample, you will need to do the following: + +1. Create a compute instance on the Google Cloud Platform Developer's Console +1. SSH into the instance you created +1. Update packages and install required packages + + `sudo apt-get update && sudo apt-get install git-core openjdk-8-jdk maven` + +1. Clone the repo + + `git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git` + +1. Navigate to `java-docs-samples/compute/signed-metadata/` + + `cd java-docs-samples/compute/signed-metadata/` + +1. Use maven to package the class as a jar + + `mvn clean package` + +1. Make sure that openjdk 8 is the selected java version + + `sudo update-alternatives --config java` + +1. Execute the jar file + + `java -jar target/compute-signed-metadata-1.0-SNAPSHOT-jar-with-dependencies.jar` diff --git a/compute/signed-metadata/pom.xml b/compute/signed-metadata/pom.xml new file mode 100644 index 00000000000..26c4b3f5182 --- /dev/null +++ b/compute/signed-metadata/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + com.google.cloud.example.jwt + compute-signed-metadata + 1.0-SNAPSHOT + compute-signed-metadata + + com.google.cloud + compute + 1.0.0 + .. + + + + com.auth0 + java-jwt + 3.2.0 + + + com.google.code.gson + gson + 2.8.1 + + + com.google.guava + guava + 23.0 + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-assembly-plugin + 3.0.0 + + + package + + single + + + + + + + com.example.compute.signedmetadata.App + + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.2 + + 1.8 + 1.8 + + + + + diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/App.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/App.java new file mode 100644 index 00000000000..63c42ed1f51 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/App.java @@ -0,0 +1,28 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata; + +public final class App { + + // AUDIENCE should be set according to your domain and needs. + // Please check https://cloud.google.com/compute/docs/instances/verifying-instance-identity + // for details. + private static final String AUDIENCE = "http://example.com"; + + public static void main(String[] args) { + // We get token from one instance and verify it trusted machine. + String token = new GCPInstance(AUDIENCE).provideToken(); + new VerifyingInstance(AUDIENCE).verifyToken(token); + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/GCPInstance.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/GCPInstance.java new file mode 100644 index 00000000000..712c3554cc9 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/GCPInstance.java @@ -0,0 +1,36 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata; + +import com.example.compute.signedmetadata.token.TokenDownloader; + +import java.io.IOException; +import java.net.URISyntaxException; + +class GCPInstance { + + private String audience; + + GCPInstance(String audience) { + this.audience = audience; + } + + String provideToken() { + try { + return new TokenDownloader().getTokenWithAudience(audience); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/VerifyingInstance.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/VerifyingInstance.java new file mode 100644 index 00000000000..d03a21cc990 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/VerifyingInstance.java @@ -0,0 +1,65 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata; + +import com.auth0.jwt.exceptions.AlgorithmMismatchException; +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.example.compute.signedmetadata.token.DecodedGoogleJWTWrapper; +import com.example.compute.signedmetadata.token.TokenVerifier; + +class VerifyingInstance { + + private String audience; + + VerifyingInstance(String audience) { + this.audience = audience; + } + + void verifyToken(String token) { + TokenVerifier gtv = new TokenVerifier(); + // JWTVerificationException is runtime exception, we don't need to catch it if we want to exit + // process in case of verification problem. However, to handle verification problems + // programmatically we can can JWTVerificationException or specific subclass. + // Following are examples how to handle verification failure. + try { + DecodedGoogleJWTWrapper decodedJWT = gtv.verifyWithAudience(audience, token); + System.out.println("Project id : " + decodedJWT.getProjectId()); + System.out.println("Project number : " + decodedJWT.getProjectNumber()); + // This are examples how to handle exceptions, which indicate verification failure. + } catch (AlgorithmMismatchException e) { + // We assume that downloaded certs are RSA256, this exception will happen if this changes. + throw e; + } catch (SignatureVerificationException e) { + // Could not verify signature of a token, possibly someone provided forged token. + throw e; + } catch (TokenExpiredException e) { + // We encountered old token, possibly replay attack. + throw e; + } catch (InvalidClaimException e) { + // Different Audience for token and for verification, possibly token for other verifier. + throw e; + } catch (JWTVerificationException e) { + // Some other problem during verification + // JWTVerificationException is super-class to: + // - SignatureVerificationException + // - TokenExpiredException + // - InvalidClaimException + throw e; + } + + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/DecodedGoogleJWTWrapper.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/DecodedGoogleJWTWrapper.java new file mode 100644 index 00000000000..b19d46461b5 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/DecodedGoogleJWTWrapper.java @@ -0,0 +1,73 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata.token; + +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.common.base.Suppliers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +public class DecodedGoogleJWTWrapper { + + private static final String KEY_PROJECT_ID = "project_id"; + private static final String KEY_PROJECT_NUMBER = "project_number"; + private static final String GOOGLE_METADATA_SPACE = "google"; + private static final String COMPUTE_ENGINE_METADATA_SUBSPACE = "compute_engine"; + + private DecodedJWT jwt; + private Supplier> computeEngineMetadata = Suppliers.memoize( + () -> { + Map googleMetadata = jwt.getClaims().get(GOOGLE_METADATA_SPACE).asMap(); + Object computeEngineObject = googleMetadata.get(COMPUTE_ENGINE_METADATA_SUBSPACE); + return castToMetadataMap(computeEngineObject); + }); + + DecodedGoogleJWTWrapper(DecodedJWT jwt) { + this.jwt = jwt; + } + + public String getProjectId() { + return getComputeEngineMetadata(KEY_PROJECT_ID); + } + + public String getProjectNumber() { + return getComputeEngineMetadata(KEY_PROJECT_NUMBER); + } + + private String getComputeEngineMetadata(String key) { + return computeEngineMetadata.get().get(key).toString(); + } + + // In Java we can only assure that an object is of class Map, we can check for key and value + // types of an object added to Map, but only if Map is not empty. + @SuppressWarnings({"rawtypes","unchecked"}) + private Map castToMetadataMap(Object object) { + if (object instanceof Map) { + Map map = (Map) object; + if (map.isEmpty()) { + // Map is empty, so we will create new map with desired types + return new HashMap<>(); + } + Set set = map.entrySet(); + Map.Entry someEntry = set.iterator().next(); + if (someEntry.getKey() instanceof String) { + return (Map) object; + } + } + throw new RuntimeException("We have not received a map of metadata"); + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/Downloader.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/Downloader.java new file mode 100644 index 00000000000..23debe286d7 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/Downloader.java @@ -0,0 +1,38 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata.token; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; + +class Downloader { + + String download(String urlString) throws IOException { + URL url = new URL(urlString); + return download(url.openConnection()); + } + + String download(URLConnection connection) throws IOException { + try(BufferedReader in = new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + in.lines().forEachOrdered(sb::append); + return sb.toString(); + } + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/GoogleRSAKeyProvider.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/GoogleRSAKeyProvider.java new file mode 100644 index 00000000000..27bc2ee309d --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/GoogleRSAKeyProvider.java @@ -0,0 +1,97 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata.token; + +import com.auth0.jwt.exceptions.AlgorithmMismatchException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.RSAKeyProvider; +import com.google.common.base.Suppliers; +import com.google.gson.Gson; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +class GoogleRSAKeyProvider implements RSAKeyProvider { + + private static final String GOOGLEAPIS_CERTS = "https://www.googleapis.com/oauth2/v1/certs"; + + private final Supplier> cachedSignedCertificates = Suppliers.memoizeWithExpiration( + this::getNewCertificate,1, TimeUnit.HOURS); + + @SuppressWarnings("unchecked") + private Map getNewCertificate() { + Gson gson = new Gson(); + String result; + try { + result = new Downloader().download(GOOGLEAPIS_CERTS); + } catch (IOException e) { + throw new JWTVerificationException("Could not download public Googleapis certs.",e); + } + return (Map) gson.fromJson(result, HashMap.class); + } + + @Override + public RSAPublicKey getPublicKeyById(String kid) { + // Received 'kid' value might be null if it wasn't defined in the Token's header + if (kid == null) { + throw new JWTVerificationException( + "Cannot verify without kid, we need to know which certificate should we use."); + } + String certificate = cachedSignedCertificates.get().get(kid); + return transformPEMCertificateToRSAKey(certificate); + } + + @Override + public RSAPrivateKey getPrivateKey() { + throw new UnsupportedOperationException("This class is used to decode certificates only."); + } + + @Override + public String getPrivateKeyId() { + throw new UnsupportedOperationException("This class is used to decode certificates only."); + } + + private RSAPublicKey transformPEMCertificateToRSAKey(String cert) { + try { + InputStream is = new ByteArrayInputStream(cert.getBytes()); + Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(is); + is.close(); + return safelyCastToRSAPublicKey(certificate.getPublicKey()); + } catch (CertificateException e) { + throw new JWTVerificationException("Could not extract RSA key from certificate String.",e); + } catch (IOException e) { + //Thrown when closing input stream. Built on in-memory array. From immutable String. + throw new RuntimeException(e); + } + } + + private RSAPublicKey safelyCastToRSAPublicKey(PublicKey publicKey) { + if (publicKey instanceof RSAPublicKey) { + return (RSAPublicKey) publicKey; + } else { + throw new AlgorithmMismatchException("We expected RSAPublicKey from certificate"); + } + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/TokenDownloader.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/TokenDownloader.java new file mode 100644 index 00000000000..6da78db0dc7 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/TokenDownloader.java @@ -0,0 +1,50 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata.token; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; + +public class TokenDownloader { + + private static final String HTTP_SCHEME = "http"; + private static final String NO_USER_INFO = null; + private static final String METADATA_HOST_ADDRESS = "metadata"; + private static final int UNSPECIFIED_PORT = -1; + private static final String METADATA_PATH = "/computeMetadata/v1/instance/" + + "service-accounts/default/identity"; + private static final String FRAGMENT = null; + + public String getTokenWithAudience(String audience) throws URISyntaxException, IOException { + String query = "audience=" + audience + "&format=full"; + URL website = new URI(HTTP_SCHEME, NO_USER_INFO, METADATA_HOST_ADDRESS, UNSPECIFIED_PORT, + METADATA_PATH, query, FRAGMENT).toURL(); + URLConnection connection = website.openConnection(); + HttpURLConnection httpConnection = safelyCastToHttpURLConnection(connection); + httpConnection.setRequestProperty("Metadata-Flavor", "Google"); + return new Downloader().download(httpConnection); + } + + private HttpURLConnection safelyCastToHttpURLConnection(URLConnection connection) { + if (connection instanceof HttpURLConnection) { + return (HttpURLConnection) connection; + } else { + throw new RuntimeException("We do not have Http connection, but we used http schema"); + } + } +} diff --git a/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/TokenVerifier.java b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/TokenVerifier.java new file mode 100644 index 00000000000..c196f9b5c86 --- /dev/null +++ b/compute/signed-metadata/src/main/java/com/example/compute/signedmetadata/token/TokenVerifier.java @@ -0,0 +1,30 @@ +// Copyright 2017 Google 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 com.example.compute.signedmetadata.token; + +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.JWT; + +public class TokenVerifier { + + private Algorithm algorithm = Algorithm.RSA256(new GoogleRSAKeyProvider()); + + public DecodedGoogleJWTWrapper verifyWithAudience(String audience, String token) { + JWTVerifier verifier = JWT.require(algorithm) + .withAudience(audience) + .build(); + return new DecodedGoogleJWTWrapper(verifier.verify(token)); + } +}