Skip to content

Commit 8cfaa4a

Browse files
agxpcopybara-github
authored andcommitted
Add JvmName for GoogleRevocationList to allow Java users
PiperOrigin-RevId: 803589779
1 parent a2c8b90 commit 8cfaa4a

File tree

2 files changed

+222
-0
lines changed

2 files changed

+222
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2025 Google LLC
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+
@file:JvmName("GoogleRevocationList")
17+
18+
package com.android.keyattestation.verifier
19+
20+
import com.google.gson.Gson
21+
import java.io.IOException
22+
import java.io.InputStream
23+
import java.io.InputStreamReader
24+
import java.net.HttpURLConnection
25+
import java.net.URI
26+
import java.net.URL
27+
28+
/**
29+
* Fetches Google's revocation status list from the web.
30+
*
31+
* This function will fail-closed if it cannot download or parse the revocation list, by throwing an
32+
* exception.
33+
*
34+
* @return A set of revoked serial numbers.
35+
*/
36+
fun getGoogleRevocationStatusFromWeb(): Set<String> =
37+
getRevocationStatusFromWeb(
38+
URI.create("https://android.googleapis.com/attestation/status").toURL()
39+
)
40+
41+
fun getRevocationStatusFromWeb(
42+
url: URL,
43+
connectionProvider: (URL) -> HttpURLConnection = {
44+
(it.openConnection() as? HttpURLConnection)
45+
?: throw IllegalArgumentException("Could not open HttpURLConnection to $it")
46+
},
47+
): Set<String> {
48+
val connection = connectionProvider(url)
49+
try {
50+
connection.requestMethod = "GET"
51+
connection.connect()
52+
53+
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
54+
throw IOException(
55+
"Failed to fetch revocation list from $url: HTTP ${connection.responseCode}"
56+
)
57+
}
58+
return parseAttestationStatus(connection.inputStream)
59+
} finally {
60+
connection.disconnect()
61+
}
62+
}
63+
64+
fun parseAttestationStatus(input: InputStream): Set<String> {
65+
data class StatusEntry(val status: String)
66+
data class StatusFile(val entries: Map<String, StatusEntry>)
67+
68+
val statusFile = Gson().fromJson(InputStreamReader(input), StatusFile::class.java)
69+
return statusFile.entries.filterValues { it.status == "REVOKED" }.keys
70+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2025 Google LLC
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.keyattestation.verifier
18+
19+
import com.google.common.truth.Truth.assertThat
20+
import java.io.IOException
21+
import java.io.InputStream
22+
import java.net.HttpURLConnection
23+
import java.net.URI
24+
import java.net.URL
25+
import kotlin.test.assertFailsWith
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.junit.runners.JUnit4
29+
30+
@RunWith(JUnit4::class)
31+
class GoogleRevocationListTest {
32+
@Test
33+
fun getGoogleRevocationStatusFromWeb_success() {
34+
val json =
35+
"""
36+
{
37+
"entries": {
38+
"abc": { "status": "REVOKED" },
39+
"def": { "status": "OK" }
40+
}
41+
}
42+
"""
43+
.trimIndent()
44+
val uri = URI("http://localhost")
45+
val result =
46+
getRevocationStatusFromWeb(uri.toURL()) {
47+
FakeHttpURLConnection(uri.toURL(), HttpURLConnection.HTTP_OK, json)
48+
}
49+
50+
assertThat(result).containsExactly("abc")
51+
}
52+
53+
@Test
54+
fun getRevocationStatusFromWeb_httpError_throwsIOException() {
55+
val uri = URI("http://localhost")
56+
assertFailsWith<IOException> {
57+
getRevocationStatusFromWeb(uri.toURL()) {
58+
FakeHttpURLConnection(uri.toURL(), HttpURLConnection.HTTP_NOT_FOUND)
59+
}
60+
}
61+
}
62+
63+
@Test
64+
fun parseAttestationStatus_emptyList() {
65+
val json = """{"entries": {}}"""
66+
val result = parseAttestationStatus(json.byteInputStream())
67+
assertThat(result).isEmpty()
68+
}
69+
70+
@Test
71+
fun parseAttestationStatus_revokedAndOkEntries() {
72+
val json =
73+
"""
74+
{
75+
"entries": {
76+
"abc": {
77+
"status": "REVOKED",
78+
"reason": "KEY_COMPROMISE"
79+
},
80+
"def": {
81+
"status": "OK"
82+
},
83+
"123": {
84+
"status": "REVOKED",
85+
"reason": "SUPERSEDED"
86+
}
87+
}
88+
}
89+
"""
90+
.trimIndent()
91+
val result = parseAttestationStatus(json.byteInputStream())
92+
assertThat(result).containsExactly("abc", "123")
93+
}
94+
95+
@Test
96+
fun parseAttestationStatus_onlyOkEntries() {
97+
val json =
98+
"""
99+
{
100+
"entries": {
101+
"def": {
102+
"status": "OK"
103+
},
104+
"456": {
105+
"status": "OK"
106+
}
107+
}
108+
}
109+
"""
110+
.trimIndent()
111+
val result = parseAttestationStatus(json.byteInputStream())
112+
assertThat(result).isEmpty()
113+
}
114+
115+
@Test
116+
fun parseAttestationStatus_onlyRevokedEntries() {
117+
val json =
118+
"""
119+
{
120+
"entries": {
121+
"abc": {
122+
"status": "REVOKED",
123+
"reason": "KEY_COMPROMISE"
124+
},
125+
"123": {
126+
"status": "REVOKED",
127+
"reason": "SUPERSEDED"
128+
}
129+
}
130+
}
131+
"""
132+
.trimIndent()
133+
val result = parseAttestationStatus(json.byteInputStream())
134+
assertThat(result).containsExactly("abc", "123")
135+
}
136+
}
137+
138+
private class FakeHttpURLConnection(
139+
url: URL,
140+
private val fakeResponseCode: Int,
141+
val responseBody: String = "",
142+
) : HttpURLConnection(url) {
143+
override fun connect() {}
144+
145+
override fun disconnect() {}
146+
147+
override fun getInputStream(): InputStream = responseBody.byteInputStream()
148+
149+
override fun getResponseCode() = fakeResponseCode
150+
151+
override fun usingProxy() = false
152+
}

0 commit comments

Comments
 (0)