-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathVmConfigVerifier.java
More file actions
235 lines (204 loc) · 10.2 KB
/
VmConfigVerifier.java
File metadata and controls
235 lines (204 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package com.uid2.shared.secure.gcp;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.gax.paging.Page;
import com.google.api.services.compute.Compute;
import com.google.api.services.compute.model.AttachedDisk;
import com.google.api.services.compute.model.Disk;
import com.google.api.services.compute.model.Instance;
import com.google.api.services.compute.model.Metadata;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.audit.AuditLog;
import com.google.cloud.logging.LogEntry;
import com.google.cloud.logging.Logging;
import com.google.cloud.logging.LoggingOptions;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.uid2.shared.Utils;
import com.uid2.shared.secure.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
public class VmConfigVerifier {
private static final Logger LOGGER = LoggerFactory.getLogger(VmConfigVerifier.class);
private static final String ENCLAVE_PARAM_PREFIX = "UID2_ENCLAVE_";
private final GoogleCredentials credentials;
public static final boolean VALIDATE_AUDITLOGS = true;
public static final boolean VALIDATE_VMCONFIG = true;
private final Set<String> enclaveParams;
private final Set<String> allowedMethodsFromInstanceAuditLogs =
new HashSet<String>(Collections.singletonList("v1.compute.instances.insert"));
private final Set<String> forbiddenMetadataKeys =
new HashSet<String>(Arrays.asList(
"startup-script",
"startup-script-url",
"shutdown-script",
"shutdown-script-url",
"sysprep-specialize-script-ps1",
"sysprep-specialize-script-cmd",
"sysprep-specialize-script-bat",
"sysprep-specialize-script-url",
"windows-startup-script-ps1",
"windows-startup-script-cmd",
"windows-startup-script-bat",
"windows-startup-script-url",
"windows-shutdown-script-cmd"));
private final Compute computeApi;
private final Logging loggingApi;
public VmConfigVerifier(GoogleCredentials credentials, Set<String> enclaveParams) throws Exception {
this.credentials = credentials;
if (this.credentials != null) {
LOGGER.info("Using Using Google Service Account: " + credentials.toString());
final HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
final GsonFactory jsonFactory = GsonFactory.getDefaultInstance();
final HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);
computeApi = new Compute.Builder(httpTransport, jsonFactory, requestInitializer)
.setApplicationName("UID-Operator/2.0")
.build();
loggingApi = LoggingOptions.newBuilder()
.setCredentials(credentials)
.build()
.getService();
} else {
computeApi = null;
loggingApi = null;
}
this.enclaveParams = enclaveParams;
if (this.enclaveParams != null) {
for (String enclaveParam : this.enclaveParams) {
LOGGER.info("Allowed Enclave Parameter: " + normalizeEnclaveParam(enclaveParam));
}
}
}
public VmConfigId getVmConfigId(InstanceDocument id) {
try {
LOGGER.debug("Issuing instance get request...");
Instance instance = computeApi.instances()
.get(id.getProjectId(), id.getZone(), id.getInstanceId())
.execute();
StringBuilder str = new StringBuilder();
for (AttachedDisk disk : instance.getDisks()) {
if (!disk.getAutoDelete()) return VmConfigId.failure("!disk.autodelete", id.getProjectId());
if (!disk.getBoot()) return VmConfigId.failure("!disk.getboot", id.getProjectId());
String diskSourceUrl = disk.getSource();
String imageUrl = getDiskSourceImage(diskSourceUrl);
str.append(getSha256Base64Encoded(imageUrl));
}
Metadata metadata = instance.getMetadata();
for (Metadata.Items metadataItem : metadata.getItems()) {
if (metadataItem.getKey().equals("user-data")) {
String cloudInitConfig = metadataItem.getValue();
String templatizedConfig = templatizeVmConfig(cloudInitConfig);
str.append(getSha256Base64Encoded(templatizedConfig));
} else if (forbiddenMetadataKeys.contains(metadataItem.getKey())) {
LOGGER.debug("{} attestation got forbidden metadata key: {}", Protocol.GCP_VMID, metadataItem.getKey());
return VmConfigId.failure("forbidden metadata key: " + metadataItem.getKey(), id.getProjectId());
}
}
String badAuditLog = findUnauthorizedAuditLog(id);
if (badAuditLog != null) {
LOGGER.debug("attestation failed because of audit log: {}", badAuditLog);
return VmConfigId.failure("bad audit log: " + badAuditLog, id.getProjectId());
}
// str is a concatenation of disk hashes and cloud-init hashes
// configId is the SHA-256 output of str.toString()
return VmConfigId.success(getSha256Base64Encoded(str.toString()), id.getProjectId());
} catch (Exception e) {
LOGGER.error("getVmConfigId error " + e.getMessage(), e);
return VmConfigId.failure(e.getMessage(), id.getProjectId());
}
}
public String templatizeVmConfig(String cloudInitConfig) {
// return original value if no enclave parameter is specified
if (this.enclaveParams == null) return cloudInitConfig;
// If enclave param is `api_token`, we will look for the following line in the cloudInitConfig:
// Environment="UID2_ENCLAVE_API_TOKEN=token_value"
// and replace it with dummy value to templatize the cloud-init config
// Environment="UID2_ENCLAVE_API_TOKEN=dummy"
//
// This is done so that the core don't need to approve different cloud-init that differs only in
// the allowed enclave parameter values.
for (String enclaveParam : this.enclaveParams) {
String subRegex = String.format("^([ \t]*Environment=.%s)=.+?\"$", normalizeEnclaveParam(enclaveParam));
Pattern pattern = Pattern.compile(subRegex, Pattern.MULTILINE );
cloudInitConfig = pattern.matcher(cloudInitConfig).replaceAll("$1=dummy\"");
}
return cloudInitConfig;
}
private String getAuditLogFilter(InstanceDocument id) {
return String.format("resource.type=gce_instance" +
" AND (" +
" logName=projects/%s/logs/cloudaudit.googleapis.com%%2Factivity" +
" OR logName=projects/%s/logs/cloudaudit.googleapis.com%%2Fdata_access" +
" )" +
" AND protoPayload.\"@type\"=\"type.googleapis.com/google.cloud.audit.AuditLog\"" +
" AND resource.labels.instance_id=%s",
id.getProjectId(),
id.getProjectId(),
id.getInstanceId());
}
/**
* Find the first unauthorized audit log and its reason.
* @param id the instance document
* @return reason the log is unauthorized, *null* if all passed or skipped.
* @throws InvalidProtocolBufferException
*/
private String findUnauthorizedAuditLog(InstanceDocument id) throws InvalidProtocolBufferException {
if (!VALIDATE_AUDITLOGS) {
LOGGER.error("Skip AuditLogs validation (VALIDATE_AUDITLOGS off)...");
return null;
}
LOGGER.debug("Searching AuditLogs...");
String logFilter = getAuditLogFilter(id);
Page<LogEntry> entries = loggingApi.listLogEntries(Logging.EntryListOption.filter(logFilter));
do {
for (LogEntry logEntry : entries.iterateAll()) {
Any data = (Any)logEntry.getPayload().getData();
AuditLog auditLog = AuditLog.parseFrom(data.getValue());
if (!validateAuditLog(auditLog)) {
return auditLog.getMethodName();
}
}
entries = entries.getNextPage();
} while (entries != null);
return null;
}
private boolean validateAuditLog(AuditLog auditLog) {
LOGGER.debug("Validating AuditLog for operation: " + auditLog.getMethodName());
if (allowedMethodsFromInstanceAuditLogs.contains(auditLog.getMethodName())) {
return true;
} else {
LOGGER.warn("{} attestation receives unauthorized method: {}", Protocol.GCP_VMID, auditLog.getMethodName());
return false;
}
}
private String getDiskSourceImage(String diskSourceUrl) throws IOException {
String[] splits = diskSourceUrl.split("/");
String projectId = splits[6];
String zone = splits[8];
String diskId = splits[10];
LOGGER.debug("Issuing disk get request for " + diskId + "...");
Disk disk = computeApi.disks().get(projectId, zone, diskId).execute();
return disk.getSourceImage();
}
private String getSha256Base64Encoded(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
// input should contain only US-ASCII chars
md.update(input.getBytes(StandardCharsets.US_ASCII));
return Utils.toBase64String(md.digest());
}
private static String normalizeEnclaveParam(String name) {
return ENCLAVE_PARAM_PREFIX + name.toUpperCase();
}
}