• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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