Skip to content

Commit 3184f3b

Browse files
committed
device names
1 parent 04719d4 commit 3184f3b

11 files changed

Lines changed: 117 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ key and app attestation**, pinning down the last unnecessarily moving parts.
2222
* Rename `NougatHybridAttestationChecker` -> `NougatHybridAttestationVerifier` (and introduce typealias, but marked as deprecated)
2323
* Rename `SoftwareAttestationChecker` -> `SoftwareAttestationVerifier` (and introduce typealias, but marked as deprecated)
2424
* Per-App StrongboxOverride
25+
* Ship a default OID to identify the attestation proof.
26+
* Transmit device names inside CSR on a best-effort basis.
2527
* Rework Trust Anchor Management:
2628
* Introduce `TrustedRoot` interface to represent trust anchors
2729
* `TrustedRoot.Certificate` for certificates

supreme/client/src/androidDeviceTest/kotlin/EndToEndTest.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
package at.asitplus.attestation.test
2-
3-
import android.os.StrictMode
41
import at.asitplus.attestation.supreme.*
52
import at.asitplus.signum.supreme.os.PlatformSigningProvider
63
import de.infix.testBalloon.framework.core.testSuite
@@ -19,17 +16,8 @@ val EndToEndTest by testSuite {
1916

2017

2118
test("endToEnd") {
22-
StrictMode.setThreadPolicy(
23-
StrictMode.ThreadPolicy.Builder()
24-
.permitAll()
25-
.build()
26-
)
27-
StrictMode.setVmPolicy(
28-
StrictMode.VmPolicy.Builder()
29-
.build()
30-
)
3119
PlatformSigningProvider.deleteSigningKey(alias)
32-
val client: AttestationClient = AttestationClient(HttpClient())
20+
val client = AttestationClient(HttpClient())
3321
val resp = client.getChallenge(Url(ENDPOINT_CHALLENGE))
3422
resp.isSuccess shouldBe true
3523
val attestationChallenge: AttestationChallenge = resp.getOrThrow()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package at.asitplus.attestation.supreme
2+
3+
import android.os.Build
4+
import at.asitplus.catchingUnwrapped
5+
6+
internal actual fun getDeviceName(): String = catchingUnwrapped {
7+
val manufacturer = Build.MANUFACTURER
8+
val model = Build.MODEL
9+
10+
val make = if (model.startsWith(manufacturer, ignoreCase = true)) {
11+
model
12+
} else {
13+
"$manufacturer $model"
14+
}
15+
"$make (${Build.DEVICE})"
16+
}.getOrElse { "Android Device" }

supreme/client/src/commonMain/kotlin/AttestationClient.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ suspend fun AttestationChallenge.createAttestationProof(
8080
* The alias to assign to the newly created signer. Must not exist!
8181
*/
8282
alias: String,
83-
): KmmResult<Pkcs10CertificationRequest> {
83+
): KmmResult<Pkcs10CertificationRequest> {
8484

8585

8686
val params = keyConstraints?.algorithmParameters
@@ -126,8 +126,13 @@ suspend fun AttestationChallenge.createAttestationProof(
126126
}
127127
}
128128
}.getOrThrow()
129-
130-
return signer.createCsr(this)
129+
val additionalAttributes = if (includeGenericDeviceName) listOf(
130+
Pkcs10CertificationRequestAttribute(
131+
WardenDefaults.OIDs.DEVICE_NAME,
132+
Asn1String.UTF8(getDeviceName()).encodeToTlv()
133+
)
134+
) else listOf()
135+
return signer.createCsr(this, additionalAttributes = additionalAttributes)
131136
}
132137

133138
/**
@@ -142,7 +147,7 @@ suspend fun Signer.Attestable<*>.createCsr(
142147
* The subject name, if required.
143148
* Usually, you'll want to use pass [AlternativeNames] into [additionalExtensions], not a subject name!
144149
* By default, the RDN used for this CSR will only contain [KnownOIDs.serialNumber] containing the nonce from the passed [challenge].
145-
* Hence, the valued passed to this parameter MUST NOT contain a [KnownOIDs.serialNumber].
150+
* Hence, the valued passed to this parameter containing a [KnownOIDs.serialNumber] will be overwritten.
146151
*/
147152
subjectName: List<RelativeDistinguishedName> = listOf(),
148153

@@ -176,3 +181,4 @@ suspend fun Signer.Attestable<*>.createCsr(
176181
*/
177182
val AttestationChallenge.attestationEndpointUrl: Url get() = Url(attestationEndpoint);
178183

184+
expect internal fun getDeviceName(): String

supreme/client/src/commonTest/kotlin/alibiTest.kt

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import at.asitplus.attestation.supreme.getDeviceName
2+
import at.asitplus.testballoon.invoke
3+
import de.infix.testBalloon.framework.core.testSuite
4+
import io.kotest.matchers.comparables.shouldBeGreaterThan
5+
6+
val deviceNameTest by testSuite {
7+
"should return something" {
8+
getDeviceName().length shouldBeGreaterThan 0
9+
println("Device name: ${getDeviceName()}")
10+
}
11+
12+
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package at.asitplus.attestation.supreme
2+
3+
import kotlinx.cinterop.*
4+
import platform.posix.uname
5+
import platform.posix.utsname
6+
7+
@OptIn(ExperimentalForeignApi::class)
8+
actual fun getDeviceName(): String = memScoped {
9+
val systemInfo = alloc<utsname>()
10+
if (uname(systemInfo.ptr) != 0) {
11+
return "iPhone"
12+
}
13+
// e.g. "iPhone15,3"
14+
systemInfo.machine.toKString()
15+
}

supreme/common/src/commonMain/kotlin/AttestationChallenge.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import at.asitplus.catching
55
import at.asitplus.signum.indispensable.Attestation
66
import at.asitplus.signum.indispensable.asn1.*
77
import at.asitplus.signum.indispensable.asn1.encoding.decodeToString
8+
import at.asitplus.signum.indispensable.asn1.encoding.decodeToUtf8String
89
import at.asitplus.signum.indispensable.io.ByteArrayBase64UrlSerializer
910
import at.asitplus.signum.indispensable.pki.AttributeTypeAndValue
11+
import at.asitplus.signum.indispensable.pki.Pkcs10CertificationRequest
1012
import at.asitplus.signum.indispensable.pki.TbsCertificationRequest
11-
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
1213
import kotlinx.datetime.TimeZone
1314
import kotlinx.datetime.serializers.TimeZoneSerializer
1415
import kotlinx.serialization.Serializable
@@ -62,6 +63,13 @@ private constructor(
6263
@Serializable(with = ObjectIdentifierStringSerializer::class)
6364
val proofOID: ObjectIdentifier,
6465

66+
/**
67+
* Whether to include a generic make and model (such as "Google Pixel 8", or "iPhone 16" with the attestation proof).
68+
* On its own this is **not the device's nickname and therefore cannot identify a person in its own**.
69+
* Defaults to `false`.
70+
*/
71+
val includeGenericDeviceName: Boolean = false,
72+
6573
/**
6674
* Indicates the wire format version
6775
*/
@@ -84,6 +92,7 @@ private constructor(
8492
nonce: ByteArray,
8593
attestationEndpoint: String,
8694
proofOID: ObjectIdentifier,
95+
includeGenericDeviceName: Boolean = false,
8796
keyConstraints: KeyConstraints? = null,
8897
) : this(
8998
issuedAt,
@@ -92,6 +101,7 @@ private constructor(
92101
nonce,
93102
attestationEndpoint,
94103
proofOID,
104+
includeGenericDeviceName,
95105
version = 1,
96106
keyConstraints,
97107
)
@@ -107,6 +117,7 @@ private constructor(
107117
nonce: ByteArray,
108118
attestationEndpoint: String,
109119
proofOID: ObjectIdentifier,
120+
includeGenericDeviceName: Boolean = false,
110121
keyConstrains: KeyConstraints? = null,
111122
) : this(
112123
issuedAt,
@@ -115,6 +126,7 @@ private constructor(
115126
nonce,
116127
attestationEndpoint,
117128
proofOID,
129+
includeGenericDeviceName,
118130
version = 1,
119131
keyConstrains
120132
)
@@ -185,3 +197,17 @@ val TbsCertificationRequest.challenge: KmmResult<ByteArray>
185197
else if (noncesRecovered.size != 1) throw Asn1StructuralException("More than one nonce present!")
186198
noncesRecovered.first().value.asPrimitive().decodeToString().hexToByteArray()
187199
}
200+
201+
/**
202+
* Tries to extract a device name from a TBS CSR's attribute with OID [WardenDefaults.OIDs.DEVICE_NAME].
203+
*/
204+
val TbsCertificationRequest.deviceName: String?
205+
get() = catching {
206+
attributes.find { it.oid == WardenDefaults.OIDs.DEVICE_NAME }?.value?.singleOrNull()?.asPrimitive()
207+
?.decodeToUtf8String()?.value
208+
}.getOrNull()
209+
210+
/**
211+
* @see TbsCertificationRequest.deviceName
212+
*/
213+
val Pkcs10CertificationRequest.deviceName: String? get() = tbsCsr.deviceName
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package at.asitplus.attestation.supreme
2+
3+
import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
4+
import kotlin.uuid.ExperimentalUuidApi
5+
import kotlin.uuid.Uuid
6+
7+
object WardenDefaults {
8+
@OptIn(ExperimentalUuidApi::class)
9+
object OIDs {
10+
val ATTESTATION_PROOF = ObjectIdentifier(Uuid.parse("3fe0e8a9-4a7a-4cd1-a3cc-93c228908116"))
11+
val DEVICE_NAME = ObjectIdentifier(Uuid.parse("792c51ff-6032-47a3-9c1c-2401be1b6a2f"))
12+
}
13+
}

supreme/verifier/src/commonMain/kotlin/AttestationValidator.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import kotlin.time.ExperimentalTime
2020

2121
/**
2222
* Verifies attestation statements and issues certificates on success.
23-
* Expects a preconfigured [Makoto] instance and an [attestationProofOID] to be used in a CSR to convey an attestation statement.
23+
* Expects a preconfigured [Makoto] instance defining which apps and devices are considered trustworthy.
2424
*
25+
* The [attestationProofOID] to be used in a CSR to convey an attestation statement. Can be overridden. It defaults to [WardenDefaults.OIDs.ATTESTATION_PROOF]
2526
* When [defaultKeyConstraints] is specified, all issued challenges will automatically convey this, unless overridden.
2627
* **Note that key constraints cannot be reliably enforced** due to technical client limitations. Not all platforms can restrict key usage and properties!
2728
*
@@ -30,7 +31,13 @@ import kotlin.time.ExperimentalTime
3031
*/
3132
class AttestationValidator(
3233
private val warden: Makoto,
33-
val attestationProofOID: ObjectIdentifier,
34+
val attestationProofOID: ObjectIdentifier = WardenDefaults.OIDs.ATTESTATION_PROOF,
35+
/**
36+
* Whether to include a generic make and model (such as "Google Pixel 8", or "iPhone 16" with the attestation proof).
37+
* On its own this is **not the device's nickname and therefore cannot identify a person in its own**.
38+
* Defaults to `true` as it is very useful technical, **non-personally-identifying data**.
39+
*/
40+
val includeGenericDeviceName: Boolean = true,
3441
val defaultKeyConstraints: KeyConstraints? = null,
3542
private val nonceGenerator: NonceGenerator = suspend { CryptoRand.nextBytes(ByteArray(64)) },
3643
private val challengeValidator: ChallengeValidator = InMemoryChallengeCache(warden.clock)
@@ -41,6 +48,10 @@ class AttestationValidator(
4148
* See [AndroidAttestationConfiguration]
4249
* for details.
4350
* @param iosAttestationConfiguration IOS AppAttest configuration. See [IosAttestationConfiguration] for details.
51+
* @param attestationProofOID specifies the OID be used in a CSR to convey an attestation statement. Can be overridden. It defaults to [WardenDefaults.OIDs.ATTESTATION_PROOF].
52+
* @param includeGenericDeviceName specifies Whether to include a generic make and model (such as "Google Pixel 8", or "iPhone 16" with the attestation proof).
53+
* On its own this is **not the device's nickname and therefore cannot identify a person in its own**.
54+
* Defaults to `true` as it is very useful technical, **non-personally-identifying data**.
4455
* @param clock a clock to set the time of verification (used for certificate validity checks)
4556
* @param verificationTimeOffset allows for fine-grained clock drift compensation (this duration is added to the certificate
4657
* @param defaultKeyConstraints allows for specifying key constraints to the client. Not all platforms can restrict key usage and properties!
@@ -51,7 +62,8 @@ class AttestationValidator(
5162
constructor(
5263
androidAttestationConfiguration: AndroidAttestationConfiguration,
5364
iosAttestationConfiguration: IosAttestationConfiguration,
54-
attestationProofOID: ObjectIdentifier,
65+
attestationProofOID: ObjectIdentifier = WardenDefaults.OIDs.ATTESTATION_PROOF,
66+
includeGenericDeviceName: Boolean = true,
5567
clock: Clock = Clock.System,
5668
verificationTimeOffset: Duration = Duration.ZERO,
5769
defaultKeyConstraints: KeyConstraints? = null,
@@ -60,6 +72,7 @@ class AttestationValidator(
6072
) : this(
6173
Makoto(androidAttestationConfiguration, iosAttestationConfiguration, clock, verificationTimeOffset),
6274
attestationProofOID,
75+
includeGenericDeviceName,
6376
defaultKeyConstraints,
6477
nonceGenerator,
6578
challengeValidator,
@@ -92,6 +105,7 @@ class AttestationValidator(
92105
nonceGenerator(),
93106
postEndpoint,
94107
attestationProofOID,
108+
includeGenericDeviceName,
95109
keyConstraints
96110
).also { challengeValidator.store(it) }
97111

0 commit comments

Comments
 (0)