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