1 /*
2 * Copyright (C) 2025 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 package com.android.devicediagnostics.commands
17
18 import com.android.devicediagnostics.AttestationResult
19 import com.android.devicediagnostics.checkAttestation
20 import com.android.devicediagnostics.getVerifiedBootState
21 import com.android.devicediagnostics.isDeviceLocked
22 import com.google.android.attestation.AuthorizationList
23 import com.google.android.attestation.ParsedAttestationRecord
24 import com.google.common.io.CharStreams
25 import java.io.InputStream
26 import java.io.InputStreamReader
27 import java.nio.charset.StandardCharsets
28 import java.nio.file.Files
29 import java.nio.file.Paths
30 import java.util.Optional
31 import kotlin.system.exitProcess
32 import org.bouncycastle.util.encoders.Base64
33 import org.json.JSONArray
34 import org.json.JSONObject
35
36 private class Tokenizer(private val args: Array<String>) {
37 private var cursor = 0
38
nextnull39 fun next(): String? {
40 if (cursor >= args.size) return null
41 return args[cursor++]
42 }
43
morenull44 fun more(): Boolean {
45 return cursor < args.size
46 }
47 }
48
49 class AttestationCli {
50 companion object {
51 @JvmStatic
mainnull52 fun main(args: Array<String>) {
53 try {
54 doMain(Tokenizer(args))
55 } catch (e: Exception) {
56 System.err.println("Error: $e")
57 exitProcess(1)
58 }
59 }
60 }
61 }
62
doMainnull63 private fun doMain(args: Tokenizer) {
64 var challenge: ByteArray? = null
65
66 var path = args.next()
67 if (path == "--challenge") {
68 val challengeString = args.next()
69 if (challengeString == null) {
70 throw IllegalArgumentException("Expected challenge string")
71 }
72
73 challenge = challengeString.toByteArray(StandardCharsets.UTF_8)
74 path = args.next()
75 }
76
77 var stream: InputStream?
78 if (path != null) {
79 stream = Files.newInputStream(Paths.get(path))
80 } else {
81 stream = System.`in`
82 }
83
84 val text = CharStreams.toString(InputStreamReader(stream, Charsets.UTF_8))
85 val root = JSONObject(text)
86 if (!root.has("attestation")) {
87 throw IllegalArgumentException("No attestation record was found")
88 }
89 var attestation = root.getJSONObject("attestation")
90 if (!attestation.has("certificates")) {
91 throw IllegalArgumentException("No attestation record was found")
92 }
93
94 val encodedData = attestation.getString("certificates")
95 if (encodedData.isEmpty()) {
96 throw IllegalArgumentException("No attestation record was found")
97 }
98
99 val decodedData = java.util.Base64.getMimeDecoder().decode(encodedData)
100 val result = checkAttestation(decodedData, null)
101 val output = JSONObject()
102 output.put("certificate", result.second.toString().lowercase())
103 output.put("record", recordToJson(result.first))
104 output.put("trustworthy", getTrustworthiness(result.first, result.second, challenge))
105 println(output.toString(2))
106 }
107
recordToJsonnull108 private fun recordToJson(record: ParsedAttestationRecord?): JSONObject? {
109 if (record == null) {
110 return null
111 }
112
113 val root = JSONObject()
114 root.put("bootloader_locked", isDeviceLocked(record))
115 root.put("verified_boot", getVerifiedBootState(record))
116 root.put("security_level", record.attestationSecurityLevel.name)
117 root.put("keymaster_version", record.keymasterVersion.toString())
118 root.put("keymaster_security_level", record.keymasterSecurityLevel.name)
119
120 var attrs = authorizationListToJson(record.teeEnforced)
121 if (attrs != null) {
122 attrs.put("source", "hardware")
123 } else {
124 attrs = authorizationListToJson(record.softwareEnforced)
125 if (attrs != null) {
126 attrs.put("source", "software")
127 }
128 }
129 root.put("attributes", attrs)
130
131 return root
132 }
133
authorizationListToJsonnull134 private fun authorizationListToJson(list: AuthorizationList?): JSONObject? {
135 if (list == null) {
136 return null
137 }
138
139 val root = JSONObject()
140 putOptionalInt(root, "os_version", list.osVersion)
141 putOptionalString(root, "brand", list.attestationIdBrand)
142 putOptionalString(root, "device", list.attestationIdDevice)
143 putOptionalString(root, "product", list.attestationIdProduct)
144 putOptionalString(root, "serial", list.attestationIdSerial)
145 putOptionalString(root, "meid", list.attestationIdMeid)
146 putOptionalString(root, "manufacturer", list.attestationIdManufacturer)
147 putOptionalString(root, "model", list.attestationIdModel)
148 putOptionalInt(root, "vendor_patch_level", list.vendorPatchLevel)
149 putOptionalInt(root, "boot_patch_level", list.bootPatchLevel)
150
151 val imeis = JSONArray()
152 putOptionalString(imeis, list.attestationIdImei)
153 putOptionalString(imeis, list.attestationIdSecondImei)
154 if (imeis.length() > 0) {
155 root.put("imeis", imeis)
156 }
157
158 if (root.length() == 0) {
159 return null
160 }
161 return root
162 }
163
getTrustworthinessnull164 private fun getTrustworthiness(
165 record: ParsedAttestationRecord?,
166 result: AttestationResult,
167 challenge: ByteArray?,
168 ): String {
169 if (record == null || result != AttestationResult.VERIFIED) {
170 return "unverified certificate"
171 }
172 if (challenge != null && !record.attestationChallenge.contentEquals(challenge)) {
173 return "bad challenge"
174 }
175 if (!getVerifiedBootState(record)) {
176 return "verified boot disabled"
177 }
178 if (!isDeviceLocked(record)) {
179 return "bootloader unlocked"
180 }
181 if (challenge == null) {
182 return "no challenge"
183 }
184 return "yes"
185 }
186
putOptionalStringnull187 private fun putOptionalString(list: JSONArray, value: Optional<ByteArray>?) {
188 if (value == null || !value.isPresent) {
189 return
190 }
191 list.put(value.get().toString(Charsets.UTF_8))
192 }
193
putOptionalStringnull194 private fun putOptionalString(root: JSONObject, key: String, value: Optional<ByteArray>?) {
195 if (value == null || !value.isPresent) {
196 return
197 }
198 root.put(key, value.get().toString(Charsets.UTF_8))
199 }
200
putOptionalIntnull201 private fun putOptionalInt(root: JSONObject, key: String, value: Optional<Int>?) {
202 if (value == null || !value.isPresent) {
203 return
204 }
205 root.put(key, value.get())
206 }
207