diff --git a/appengine/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java b/appengine/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java new file mode 100644 index 00000000000..8a632aa78d5 --- /dev/null +++ b/appengine/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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 com.example.appengine.appidentity; + +import com.google.appengine.api.appidentity.AppIdentityService; +import com.google.appengine.api.appidentity.AppIdentityServiceFactory; +import com.google.appengine.api.appidentity.PublicCertificate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Collection; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class SignForAppServlet extends HttpServlet { + private final AppIdentityService appIdentity; + + public SignForAppServlet() { + appIdentity = AppIdentityServiceFactory.getAppIdentityService(); + } + + // [START asserting_identity_to_other_services] + // Note that the algorithm used by AppIdentity.signForApp() and + // getPublicCertificatesForApp() is "SHA256withRSA" + + private byte[] signBlob(byte[] blob) { + AppIdentityService.SigningResult result = appIdentity.signForApp(blob); + return result.getSignature(); + } + + private byte[] getPublicCertificate() throws UnsupportedEncodingException { + Collection certs = appIdentity.getPublicCertificatesForApp(); + PublicCertificate publicCert = certs.iterator().next(); + return publicCert.getX509CertificateInPemFormat().getBytes("UTF-8"); + } + + private Certificate parsePublicCertificate(byte[] publicCert) + throws CertificateException, NoSuchAlgorithmException { + InputStream stream = new ByteArrayInputStream(publicCert); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return cf.generateCertificate(stream); + } + + private boolean verifySignature(byte[] blob, byte[] blobSignature, PublicKey pk) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(pk); + signature.update(blob); + return signature.verify(blobSignature); + } + + private String simulateIdentityAssertion() + throws CertificateException, UnsupportedEncodingException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + // Simulate the sending app. + String message = "abcdefg"; + byte[] blob = message.getBytes(); + byte[] blobSignature = signBlob(blob); + byte[] publicCert = getPublicCertificate(); + + // Simulate the receiving app, which gets the certificate, blob, and signature. + Certificate cert = parsePublicCertificate(publicCert); + PublicKey pk = cert.getPublicKey(); + boolean isValid = verifySignature(blob, blobSignature, pk); + + return String.format( + "isValid=%b for message: %s\n\tsignature: %s\n\tpublic cert: %s", + isValid, + message, + Arrays.toString(blobSignature), + Arrays.toString(publicCert)); + } + // [END asserting_identity_to_other_services] + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + try { + resp.getWriter().println(simulateIdentityAssertion()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/appengine/appidentity/src/main/webapp/WEB-INF/web.xml b/appengine/appidentity/src/main/webapp/WEB-INF/web.xml index 3ebbce0e290..3296a0799f4 100644 --- a/appengine/appidentity/src/main/webapp/WEB-INF/web.xml +++ b/appengine/appidentity/src/main/webapp/WEB-INF/web.xml @@ -7,6 +7,10 @@ appidentity com.example.appengine.appidentity.IdentityServlet + + signforapp + com.example.appengine.appidentity.SignForAppServlet + urlshortener com.example.appengine.appidentity.UrlShortenerServlet @@ -15,6 +19,10 @@ appidentity / + + signforapp + /sign + urlshortener /shorten diff --git a/appengine/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java b/appengine/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java index c0d32159cfe..53fdebf79bf 100644 --- a/appengine/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java +++ b/appengine/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,25 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.example.appengine.appidentity; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.google.appengine.tools.development.ApiProxyLocal; import com.google.appengine.tools.development.testing.LocalServiceTestHelper; -import com.google.apphosting.api.ApiProxy; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; @@ -62,6 +58,10 @@ public void setUp() throws Exception { servletUnderTest = new IdentityServlet(); } + @After public void tearDown() { + helper.tearDown(); + } + @Test public void doGet_defaultEnvironment_writesResponse() throws Exception { servletUnderTest.doGet(mockRequest, mockResponse); diff --git a/appengine/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java b/appengine/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java new file mode 100644 index 00000000000..c340def6b32 --- /dev/null +++ b/appengine/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java @@ -0,0 +1,69 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + *

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 com.example.appengine.appidentity; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Unit tests for {@link SignForAppServlet}. */ +@RunWith(JUnit4.class) +public class SignForAppServletTest { + + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private SignForAppServlet servletUnderTest; + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new SignForAppServlet(); + } + + @After public void tearDown() { + helper.tearDown(); + } + + @Test public void doGet_defaultEnvironment_successfullyVerifiesSignature() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + assertThat(responseWriter.toString()) + .named("SignForAppServlet response") + .contains("isValid=true for message: abcdefg"); + } +}