1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.rkpdapp.testutil; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import android.security.NetworkSecurityPolicy; 22 import android.util.Base64; 23 24 import com.google.protobuf.ByteString; 25 26 import java.io.IOException; 27 import java.io.PrintWriter; 28 import java.io.StringWriter; 29 import java.nio.charset.StandardCharsets; 30 31 import fi.iki.elonen.NanoHTTPD; 32 33 public class FakeRkpServer implements AutoCloseable { 34 private static final String EEK_RESPONSE_OK = 35 "g4KCAYOEQ6EBJqBYTaUBAgMmIAEhWCD3FIrbl/TMU+/SZBHE43UfZh+kcQxsz/oJRoB0h1TyrSJY" 36 + "IF5/W/bs5PYZzP8TN/0PociT2xgGdsRd5tdqd4bDLa+PWEAvl45C+74HLZVHhUeTQLAf1JtHpMRE" 37 + "qfKhB4cQx5/LEfS/n+g74Oc0TBX8e8N+MwX00TQ87QIEYHoV4HnTiv8khEOhASagWE2lAQIDJiAB" 38 + "IVggUYCsz4+WjOwPUOGpG7eQhjSL48OsZQJNtPYxDghGMjkiWCBU65Sd/ra05HM6JU4vH52dvfpm" 39 + "wRGL6ZaMQ+Qw9tp2q1hAmDj7NDpl23OYsSeiFXTyvgbnjSJO3fC/wgF0xLcpayQctdjSZvpE7/Uw" 40 + "LAR07ejGYNrOn1ZXJ3Qh096Tj+O4zYRDoQEmoFhxpgECAlggg5/4/RAcEp+SQcdbjeRO9BkTmscb" 41 + "bacOlfJkU12nHcEDOBggASFYIBakUhJjs4ZWUNjf8qCofbzZbqdoYOqMXPGT5ZcZDazeIlggib7M" 42 + "bD9esDk0r5e6ONEWHaHMHWTTjEhO+HKBGzs+Me5YQPrazy2rpTAMc8Xlq0mSWWBE+sTyM+UEsmwZ" 43 + "ZOkc42Q7NIYAZS313a+qAcmvg8lO+FqU6GWTUeMYHjmAp2lLM82CAoOEQ6EBJ6BYKqQBAQMnIAYh" 44 + "WCCZue7dXuRS9oXGTGLcPmGrV0h9dTcprXaAMtKzy2NY2VhAHiIIS6S3pMjXTgMO/rivFEynO2+l" 45 + "zdzaecYrZP6ZOa9254D6ZgCFDQeYKqyRXKclFEkGNHXKiid62eNaSesCA4RDoQEnoFgqpAEBAycg" 46 + "BiFYIOovhQ6eagxc973Z+igyv9pV6SCiUQPJA5MYzqAVKezRWECCa8ddpjZXt8dxEq0cwmqzLCMq" 47 + "3RQwy4IUtonF0x4xu7hQIUpJTbqRDG8zTYO8WCsuhNvFWQ+YYeLB6ony0K4EhEOhASegWE6lAQEC" 48 + "WCBvktEEbXHYp46I2NFWgV+W0XiD5jAbh+2/INFKO/5qLgM4GCAEIVggtl0cS5qDOp21FVk3oSb7" 49 + "D9/nnKwB1aTsyDopAIhYJTlYQICyn9Aynp1K/rAl8sLSImhGxiCwqugWrGShRYObzElUJX+rFgVT" 50 + "8L01k/PGu1lOXvneIQcUo7ako4uPgpaWugNYHQAAAYBINcxrASC0rWP9VTSO7LdABvcdkv7W2vh+" 51 + "onV0aW1lX3RvX3JlZnJlc2hfaG91cnMYSHgabnVtX2V4dHJhX2F0dGVzdGF0aW9uX2tleXMU"; 52 53 // Same as EEK_RESPONSE_OK, but the "num_extra_attestation_keys" value is 0, disabling RKP. 54 private static final String EEK_RESPONSE_RKP_DISABLED = 55 "g4KCAYOEQ6EBJqBYTaUBAgMmIAEhWCD3FIrbl/TMU+/SZBHE43UfZh+kcQxsz/oJRoB0h1TyrSJY" 56 + "IF5/W/bs5PYZzP8TN/0PociT2xgGdsRd5tdqd4bDLa+PWEAvl45C+74HLZVHhUeTQLAf1JtHpMRE" 57 + "qfKhB4cQx5/LEfS/n+g74Oc0TBX8e8N+MwX00TQ87QIEYHoV4HnTiv8khEOhASagWE2lAQIDJiAB" 58 + "IVggUYCsz4+WjOwPUOGpG7eQhjSL48OsZQJNtPYxDghGMjkiWCBU65Sd/ra05HM6JU4vH52dvfpm" 59 + "wRGL6ZaMQ+Qw9tp2q1hAmDj7NDpl23OYsSeiFXTyvgbnjSJO3fC/wgF0xLcpayQctdjSZvpE7/Uw" 60 + "LAR07ejGYNrOn1ZXJ3Qh096Tj+O4zYRDoQEmoFhxpgECAlggg5/4/RAcEp+SQcdbjeRO9BkTmscb" 61 + "bacOlfJkU12nHcEDOBggASFYIBakUhJjs4ZWUNjf8qCofbzZbqdoYOqMXPGT5ZcZDazeIlggib7M" 62 + "bD9esDk0r5e6ONEWHaHMHWTTjEhO+HKBGzs+Me5YQPrazy2rpTAMc8Xlq0mSWWBE+sTyM+UEsmwZ" 63 + "ZOkc42Q7NIYAZS313a+qAcmvg8lO+FqU6GWTUeMYHjmAp2lLM82CAoOEQ6EBJ6BYKqQBAQMnIAYh" 64 + "WCCZue7dXuRS9oXGTGLcPmGrV0h9dTcprXaAMtKzy2NY2VhAHiIIS6S3pMjXTgMO/rivFEynO2+l" 65 + "zdzaecYrZP6ZOa9254D6ZgCFDQeYKqyRXKclFEkGNHXKiid62eNaSesCA4RDoQEnoFgqpAEBAycg" 66 + "BiFYIOovhQ6eagxc973Z+igyv9pV6SCiUQPJA5MYzqAVKezRWECCa8ddpjZXt8dxEq0cwmqzLCMq" 67 + "3RQwy4IUtonF0x4xu7hQIUpJTbqRDG8zTYO8WCsuhNvFWQ+YYeLB6ony0K4EhEOhASegWE6lAQEC" 68 + "WCBvktEEbXHYp46I2NFWgV+W0XiD5jAbh+2/INFKO/5qLgM4GCAEIVggtl0cS5qDOp21FVk3oSb7" 69 + "D9/nnKwB1aTsyDopAIhYJTlYQICyn9Aynp1K/rAl8sLSImhGxiCwqugWrGShRYObzElUJX+rFgVT" 70 + "8L01k/PGu1lOXvneIQcUo7ako4uPgpaWugNYHQAAAYBINcxrASC0rWP9VTSO7LdABvcdkv7W2vh+" 71 + "onV0aW1lX3RvX3JlZnJlc2hfaG91cnMYSHgabnVtX2V4dHJhX2F0dGVzdGF0aW9uX2tleXMA"; 72 73 public enum Response { 74 // canned responses for :fetchEekChain 75 FETCH_EEK_OK(EEK_RESPONSE_OK), 76 FETCH_EEK_RKP_DISABLED(EEK_RESPONSE_RKP_DISABLED), 77 78 // canned responses for :signCertificates 79 SIGN_CERTS_OK_VALID_CBOR("gkCA"), 80 SIGN_CERTS_OK_INVALID_CBOR(200, "OK"), 81 SIGN_CERTS_DEVICE_UNREGISTERED(444, "Device Not Registered"), 82 SIGN_CERTS_USER_UNAUTHORIZED(403, "User not authorized"), 83 84 // canned responses for any request 85 INTERNAL_ERROR(500, "Internal Server Error"); 86 87 private final int mStatusCode; 88 private final String mDescription; 89 private final ByteString mBody; 90 private final String mMime; 91 92 // Text response (generally used to indicate an error) Response(int code, String description)93 Response(int code, String description) { 94 this(code, description, description.getBytes(StandardCharsets.UTF_8), "text/plain"); 95 } 96 97 // Standard OK CBOR response Response(String base64Body)98 Response(String base64Body) { 99 this(200, "OK", Base64.decode(base64Body, Base64.DEFAULT), "application/cbor"); 100 } 101 102 // Arbitrary response Response(int code, String description, byte[] body, String mime)103 Response(int code, String description, byte[] body, String mime) { 104 mStatusCode = code; 105 mDescription = code + " " + description; 106 mBody = ByteString.copyFrom(body); 107 mMime = mime; 108 } 109 toNanoResponse()110 NanoHTTPD.Response toNanoResponse() { 111 NanoHTTPD.Response.IStatus status = new NanoHTTPD.Response.IStatus() { 112 @Override 113 public String getDescription() { 114 return mDescription; 115 } 116 117 @Override 118 public int getRequestStatus() { 119 return mStatusCode; 120 } 121 }; 122 return NanoHTTPD.newFixedLengthResponse(status, mMime, mBody.newInput(), mBody.size()); 123 } 124 } 125 126 final NanoHTTPD mServer; 127 final boolean mCleartextPolicy; 128 129 // Interface allowing users to plug in completely custom handlers. 130 public interface RequestHandler { serve(NanoHTTPD.IHTTPSession session, int bodySize)131 NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession session, int bodySize) 132 throws IOException, NanoHTTPD.ResponseException; 133 } 134 135 // Create a test server that has a customer handler for all requests FakeRkpServer(RequestHandler handler)136 public FakeRkpServer(RequestHandler handler) throws IOException { 137 mCleartextPolicy = NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); 138 NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(true); 139 140 mServer = new NanoHTTPD("localhost", 0) { 141 @Override 142 public Response serve(IHTTPSession session) { 143 try { 144 return handler.serve(session, (int) ((HTTPSession) session).getBodySize()); 145 } catch (IOException | NanoHTTPD.ResponseException e) { 146 StringWriter stack = new StringWriter(); 147 e.printStackTrace(new PrintWriter(stack)); 148 assertWithMessage("Error handling request: " + stack).fail(); 149 } 150 return null; 151 } 152 }; 153 154 mServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); 155 } 156 157 // Create a test server that returns pre-defined responses for fetchEek and 158 // signCertificates FakeRkpServer(Response fetchEekResponse, Response signCertResponse)159 public FakeRkpServer(Response fetchEekResponse, Response signCertResponse) 160 throws IOException { 161 this((session, bodySize) -> { 162 session.getInputStream().readNBytes(bodySize); 163 if (session.getUri().contains(":fetchEekChain")) { 164 return fetchEekResponse.toNanoResponse(); 165 } else if (session.getUri().contains(":signCertificates")) { 166 return signCertResponse.toNanoResponse(); 167 } 168 assertWithMessage("Unexpected HTTP request: " + session.getUri()).fail(); 169 return null; 170 }); 171 } 172 173 @Override close()174 public void close() { 175 mServer.stop(); 176 NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(mCleartextPolicy); 177 } 178 getUrl()179 public String getUrl() { 180 return "http://localhost:" + mServer.getListeningPort() + "/"; 181 } 182 } 183