Skip to content

Commit e91536c

Browse files
committed
polishing
1 parent 23f9f07 commit e91536c

11 files changed

Lines changed: 335 additions & 51 deletions

File tree

docs/docs/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This glossary centralises terms used across the documentation. Each entry is con
1515
- **Attestation Proof** — The authenticated transport sent to the server: either a signed CSR or a hash-bound unsigned TBS CSR carrying the attestation statement payload.
1616
- **Proof of Possession** — Evidence that the client can use the private key corresponding to a public key. Warden Supreme
1717
obtains this from the CSR signature in `DataAuthentication.Signature` mode. Hash mode deliberately omits it.
18-
- **Data Authentication** — The challenge-selected mechanism binding TBS CSR contents to the ceremony: a CSR signature,
18+
- **Data Authentication***(In the context of Warden Supreme!)* The challenge-selected mechanism binding TBS CSR contents to the ceremony: a CSR signature,
1919
or a digest incorporated into the platform attestation nonce.
2020
- **Challenge / Nonce** — A server‑generated, unpredictable byte string used exactly once to guarantee freshness and prevent replay. Android embeds it in the attestation extension; iOS mixes it into the attestation nonce via `clientDataHash` (see [Android Key Attestation]({{ links.android_key_attestation }}), [Attestation Object Validation Guide]({{ links.ios_attestation_validation }})).
2121
- **Binding** — Cryptographically tying data (e.g., public key bytes + challenge) into an attested statement so the verifier can trust their association.

docs/docs/integration/config.md

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,8 @@ Two integrated-flow properties control proof authentication and client-provided
142142
* `attestableAttributes` is optional. It defines the dedicated CSR attribute OID and an ordered list of values. `type`
143143
uses readable `PrimitiveType` names (`STRING`, `INT`, `BOOLEAN`, `BYTEARRAY`, etc.); `required` defaults to `true`.
144144

145-
For example:
146-
147-
```yaml
148-
dataAuthentication:
149-
type: hash
150-
algorithm: SHA256
151-
attestableAttributes:
152-
oid: 2.25.304198582559398858370235454530489176240
153-
attributes:
154-
- name: accountId
155-
type: STRING
156-
- name: riskScore
157-
type: INT
158-
required: false
159-
```
160-
161-
The example OID is the freely assignable UUID-based OID for
145+
The generated YAML and JSON examples below demonstrate hash authentication together with one required and one optional
146+
attestable attribute. Their example OID is the freely assignable UUID-based OID for
162147
`e4da8413-46ae-4f0f-88f5-c7325b5850b0`. Generate a fresh UUID-derived `2.25` OID for your own attribute instead of
163148
borrowing an enterprise subtree you do not control.
164149

@@ -173,7 +158,8 @@ expose the same (de)serialisation functions as `SupremeConfiguration`.
173158
??? example "YAML with Defaults for a Sample Android and iOS App"
174159
The below example shows every configuration property in YAML form.
175160
It uses a single Android app and a single iOS app. Android revocation checks use the default Google revocation list,
176-
as well as a custom file-based revocation list. All other properties show their default values.
161+
as well as a custom file-based revocation list. It also configures hash authentication and required and optional
162+
attestable attributes. All other properties show their default values.
177163
You can download the below example [here](../examples/supreme.yaml).
178164

179165
```yaml
@@ -183,7 +169,8 @@ expose the same (de)serialisation functions as `SupremeConfiguration`.
183169
??? example "JSON with Defaults for a Sample Android and iOS App"
184170
The below example shows every configuration property in JSON form.
185171
It uses a single Android app and a single iOS app. Android revocation checks use the default Google revocation list,
186-
as well as a custom file-based revocation list. All other properties show their default values.
172+
as well as a custom file-based revocation list. It also configures hash authentication and required and optional
173+
attestable attributes. All other properties show their default values.
187174
You can download the below example [here](../examples/supreme.json).
188175

189176
```json

docs/docs/integration/migration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ Warden Supreme enforces unified flows and a unified data model. Migration mostly
2929
[Usage without Integrated Clients](raw.md) (Makoto/Roboto directly),
3030
and [Externalising Configuration](config.md) (canonical config loading).
3131

32-
## Integrated Client/Verifier API
32+
## Migrating to Warden Supreme 1.1+
33+
34+
!!! note
35+
This section is targets integrators coming from Warden Supreme 1.0.x.
3336

3437
The integrated transport is no longer unconditionally a signed CSR. New code uses `ToBeAuthenticatedData` end to end:
3538

docs/docs/integration/supreme.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ An attestation flow works as follows, in accordance with Figure 1:
6262
* If the attestation does not verify, the back-end records the reason for this failure.
6363
5. The back-end responds either with the full certificate chain (success) or a detailed error reason (failure).
6464

65-
### Choosing the authentication mode
65+
### Choosing an Authentication Mode
6666

6767
Use signature mode when the ceremony must prove that the client can operate the attested private key at that moment. This
68-
is the compatibility path and remains the default. If key use requires biometric or device authentication, creating the
68+
is the default. If key use requires biometric or device authentication, creating the
6969
CSR signature may display the configured authentication prompt.
7070

7171
Use hash mode when binding the request data and attested key is sufficient and an immediate private-key operation is
@@ -76,16 +76,15 @@ CSR public key.
7676
The modes are not interchangeable: the verifier rejects a signed CSR for a hash challenge and an unsigned TBS CSR for a
7777
signature challenge.
7878

79-
### Requesting client-provided attributes
79+
### Requesting Client-Provided Attributes
8080

8181
The verifier can place an `AttestableAttributes` schema in a challenge. It contains a dedicated attribute OID and an
8282
ordered list of `ToBeAttestedAttribute(name, type, required)` entries. The client callback receives that list and returns
8383
one `Primitive` per entry. Required values must be present; optional values can be `null`.
8484

8585
These values are produced by the app, not independently certified by Android or Apple. Successful verification means the
86-
expected app supplied them and the selected authentication mode bound them into this ceremony. Apply ordinary server-side
87-
authorisation and plausibility checks before treating their contents as business facts. Both signature and hash mode bind
88-
the same attribute sequence.
86+
expected app supplied them and the selected authentication mode bound them into this ceremony.
87+
Both signature and hash mode bind the same attribute sequence.
8988

9089
<figure>
9190
<picture>
@@ -391,8 +390,8 @@ This example assumes Ktor. Since this is an example environment, TLS is omitted
391390
3. It does nothing but issue challenges. In production, catch `InMemoryChallengeCache.ChallengeCacheFullException` here and return `429 Too Many Requests`; apply caller-aware rate limiting outside the verifier.
392391
4. The full URL to post the attestation proof to
393392
5. Endpoint expecting DER-encoded attestation proofs.
394-
6. `ToBeAuthenticatedData.decodeFromDer` distinguishes the complete-CSR and TBS-CSR ASN.1 structures. It does not accept a
395-
hash algorithm. The verifier obtains the expected mode and algorithm from the matched challenge and rejects a shape
393+
6. `ToBeAuthenticatedData.decodeFromDer` distinguishes the complete-CSR and TBS-CSR ASN.1 structures.
394+
The verifier obtains the expected mode and algorithm from the matched challenge and rejects a shape
396395
mismatch.
397396
7. Here, inside the `verifyAttestation` lambda, we already have a verified attestation according to the configured `makoto` instance.
398397
8. Signing a `TbsCertificate` automatically creates an X.509 certificate

docs/docs/overview.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,18 @@ Each challenge can require a signed proof of possession or hash-bind an unsigned
2121
application-provided attributes that are authenticated by either mode.
2222

2323
The platform libraries underneath Warden Supreme answer whether an Android or Apple attestation artefact is valid.
24-
Warden Supreme supplies the substantially larger system around that decision: client-side hardware-key creation, a
25-
versioned challenge and proof protocol, replay protection, cross-platform policy, key and application-data binding,
26-
certificate issuance, external configuration, stable error handling, and production-tested platform workarounds.
24+
Warden Supreme supplies the substantially larger system around that decision that makes the difference between verifying
25+
an attestation artefact and a production-grade, ready-to-deploy solution:
26+
27+
* Client-side hardware-key creation
28+
* Versioned challenge and proof protocol
29+
* Replay protection
30+
* Cross-platform policy
31+
* Key and application-data binding
32+
* Certificate issuance
33+
* Externalisable configuration
34+
* Stable error handling
35+
* **Production-prove, battle-tested platform workarounds**
2736

2837
!!! tip inline end "Familiar with attestation?"
2938
**[Jump to the integration guide!](integration/supreme.md)**

docs/docs/testing.md

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,6 @@ Since the test trust anchor is trusted only on T and Q, these generated proofs n
5050

5151
This is the [Test Clients and Staging Pattern](#test-clients-and-staging-pattern) applied exhaustively: one generated proof per property (or combination) you care about, valid and invalid variants alike.
5252

53-
For the integrated flow, keep a matrix covering both `DataAuthentication` modes:
54-
55-
- Signature mode returns a signed CSR and rejects an unsigned TBS CSR; hash mode does the inverse.
56-
- Signature mode verifies proof of possession. Hash mode verifies the reconstructed hash input and still rejects a public
57-
key that differs from the attested key.
58-
- Requested attributes cover required present/missing, optional present/omitted, and required-plus-optional combinations
59-
under both modes.
60-
- Tampering with the subject, extension request, ordinary attributes, or requested values fails binding verification.
61-
- Duplicate attribute/extension OIDs, malformed extension requests, and non-canonical attribute ordering are rejected as
62-
`CONTENT` before certificate issuance.
63-
64-
The repository's Android-emulator end-to-end suite exercises the real client/server transport for these combinations;
65-
focused verifier and client tests cover callbacks, mode selection, key matching, hash matching, and attribute-provider
66-
execution. Keep the older signed-flow scenario as a regression test for compatibility.
67-
6853
<span id="attestation-security-level"></span>
6954
!!! warning "Generated chains must match the claimed security level"
7055
Starting with Warden Supreme 1.0.2, the verifier now **derives the attestation security level from the certificate chain when asserting `SecurityLevel.STRONGBOX`** and requires it to match the level advertised in the attestation extension (`keymasterSecurityLevel`).

supreme/common/src/jvmTest/kotlin/DataAuthenticationSerializerTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ import kotlinx.serialization.json.Json
66
import net.mamoe.yamlkt.Yaml
77

88
val DataAuthenticationSerializerTest by matrixSuite {
9+
data(
10+
"concrete Hash serializer",
11+
listOf(Digest.SHA256, Digest.SHA384, Digest.SHA512),
12+
) test { digest ->
13+
val value = DataAuthentication.Hash(digest)
14+
val serializer = DataAuthentication.Hash.serializer()
15+
val expectedJson = "{\"type\":\"hash\",\"algorithm\":\"${digest.name}\"}"
16+
17+
Json.encodeToString(serializer, value) shouldBe expectedJson
18+
Json.decodeFromString(serializer, expectedJson) shouldBe value
19+
val yaml = Yaml.encodeToString(serializer, value)
20+
Yaml.decodeFromString(serializer, yaml) shouldBe value
21+
}
22+
923
data(
1024
"variants",
1125
listOf(
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
@file:OptIn(kotlin.time.ExperimentalTime::class)
2+
3+
package at.asitplus.attestation.supreme
4+
5+
import at.asitplus.attestation.android.TrustedRoot
6+
import at.asitplus.signum.indispensable.Digest
7+
import at.asitplus.signum.indispensable.asn1.Asn1String
8+
import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
9+
import at.asitplus.signum.indispensable.asn1.encoding.Asn1
10+
import at.asitplus.signum.indispensable.pki.AttributeTypeAndValue
11+
import at.asitplus.signum.indispensable.pki.Pkcs10CertificationRequestAttribute
12+
import at.asitplus.signum.indispensable.pki.RelativeDistinguishedName
13+
import at.asitplus.signum.indispensable.pki.X509CertificateExtension
14+
import at.asitplus.signum.indispensable.toCryptoPublicKey
15+
import at.asitplus.signum.supreme.hash.digest
16+
import at.asitplus.testballoon.matrix.matrixSuite
17+
import io.kotest.matchers.shouldBe
18+
import io.kotest.matchers.types.shouldBeInstanceOf
19+
20+
private val bindingExtensionRequestOid = ObjectIdentifier("1.2.840.113549.1.9.14")
21+
private val bindingAttributeOid = ObjectIdentifier("2.25.206039601192291490934634330976917168161")
22+
private val bindingExtensionOid = ObjectIdentifier("2.5.29.17")
23+
private val bindingDeviceNameOid = ObjectIdentifier("2.25.206039601192291490934634330976917168163")
24+
25+
private data class HashBindingFixture(
26+
val verifier: AttestationVerifier,
27+
val proof: ToBeAuthenticatedData.Hashed,
28+
)
29+
30+
private suspend fun hashBindingFixture(
31+
expectedAlgorithm: Digest = Digest.SHA256,
32+
attestedAlgorithm: Digest = expectedAlgorithm,
33+
genericDeviceNameOid: ObjectIdentifier? = null,
34+
attributes: List<Pkcs10CertificationRequestAttribute> = emptyList(),
35+
extensions: List<X509CertificateExtension> = emptyList(),
36+
mutate: (AttestationHashInput) -> AttestationHashInput = { it },
37+
): HashBindingFixture {
38+
val nonce = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8)
39+
val challengeTemplate = AttestationChallenge(
40+
issuedAt = fixedClock.now(),
41+
validity = kotlin.time.Duration.ZERO,
42+
nonce = nonce,
43+
attestationEndpoint = attestationEndpoint,
44+
proofOID = WardenDefaults.OIDs.ATTESTATION_PROOF,
45+
genericDeviceNameOID = genericDeviceNameOid,
46+
)
47+
val hashInput = AttestationHashInput(
48+
subjectName = listOf(RelativeDistinguishedName(challengeTemplate.getRdnSerialNumber())),
49+
extensions = extensions,
50+
attributes = attributes,
51+
)
52+
val fake = createFakeAndroidAttestation(
53+
challenge = attestedAlgorithm.digest(hashInput.encodeToDer()),
54+
packageName = fakeAndroidPackage,
55+
signatureDigest = fakeAndroidSignerDigest,
56+
)
57+
val verifier = AttestationVerifier(
58+
makoto = fixedMakoto(
59+
androidConfigForFake(
60+
packageName = fakeAndroidPackage,
61+
signatureDigest = fakeAndroidSignerDigest,
62+
trustedRoots = setOf(TrustedRoot.Certificate(fake.rootCertificate)),
63+
)
64+
),
65+
genericDeviceNameOID = genericDeviceNameOid,
66+
nonceGenerator = suspend { nonce },
67+
)
68+
val challenge = verifier.issueChallenge(
69+
attestationEndpoint,
70+
dataAuth = DataAuthentication.Hash(expectedAlgorithm),
71+
)
72+
val proof = Pkcs10CertificationRequestAttribute(
73+
challenge.proofOID,
74+
Asn1String.UTF8(fake.attestationJson()).encodeToTlv(),
75+
)
76+
return HashBindingFixture(
77+
verifier,
78+
ToBeAuthenticatedData.Hashed(
79+
mutate(hashInput).toTbsCsr(fake.leafKeyPair.public.toCryptoPublicKey().getOrThrow(), proof)
80+
),
81+
)
82+
}
83+
84+
private suspend fun HashBindingFixture.verify() =
85+
verifier.verifyAttestation(proof, certificateIssuer = { emptyList() })
86+
87+
private suspend fun HashBindingFixture.verifyRejected() =
88+
verify().shouldBeInstanceOf<AttestationResponse.Failure>().also {
89+
it.kind shouldBe AttestationResponse.Failure.Type.CONTENT
90+
}
91+
92+
val AttestationVerifierHashBindingTest by matrixSuite {
93+
test("subject mutation after hashed attestation is rejected") {
94+
hashBindingFixture { input ->
95+
AttestationHashInput(
96+
version = input.version,
97+
subjectName = input.subjectName + RelativeDistinguishedName(
98+
AttributeTypeAndValue.CommonName(Asn1String.UTF8("mutated"))
99+
),
100+
attributes = input.attributes,
101+
)
102+
}.verifyRejected()
103+
}
104+
105+
test("Subject Alternative Name extension mutation after hashed attestation is rejected") {
106+
val original = X509CertificateExtension(bindingExtensionOid, false, Asn1.OctetString(byteArrayOf(1)))
107+
hashBindingFixture(extensions = listOf(original)) { input ->
108+
AttestationHashInput(
109+
version = input.version,
110+
subjectName = input.subjectName,
111+
extensions = listOf(
112+
X509CertificateExtension(bindingExtensionOid, false, Asn1.OctetString(byteArrayOf(2)))
113+
),
114+
attributes = input.attributes.filterNot { it.oid == bindingExtensionRequestOid },
115+
)
116+
}.verifyRejected()
117+
}
118+
119+
test("arbitrary attribute mutation after hashed attestation is rejected") {
120+
hashBindingFixture { input ->
121+
AttestationHashInput(
122+
version = input.version,
123+
subjectName = input.subjectName,
124+
attributes = input.attributes + Pkcs10CertificationRequestAttribute(
125+
bindingAttributeOid,
126+
Asn1String.UTF8("added later").encodeToTlv(),
127+
),
128+
)
129+
}.verifyRejected()
130+
}
131+
132+
test("SHA-256 challenge rejects a proof attested with SHA-384") {
133+
hashBindingFixture(
134+
expectedAlgorithm = Digest.SHA256,
135+
attestedAlgorithm = Digest.SHA384,
136+
).verifyRejected()
137+
}
138+
139+
test("hash binding succeeds with device name present") {
140+
hashBindingFixture(
141+
genericDeviceNameOid = bindingDeviceNameOid,
142+
attributes = listOf(
143+
Pkcs10CertificationRequestAttribute(
144+
bindingDeviceNameOid,
145+
Asn1String.UTF8("Example Device").encodeToTlv(),
146+
)
147+
),
148+
).verify().shouldBeInstanceOf<AttestationResponse.Success>()
149+
}
150+
151+
test("hash binding succeeds with device name absent") {
152+
hashBindingFixture(
153+
genericDeviceNameOid = bindingDeviceNameOid,
154+
).verify().shouldBeInstanceOf<AttestationResponse.Success>()
155+
}
156+
157+
test("device name mutation after hashed attestation is rejected") {
158+
hashBindingFixture(
159+
genericDeviceNameOid = bindingDeviceNameOid,
160+
attributes = listOf(
161+
Pkcs10CertificationRequestAttribute(
162+
bindingDeviceNameOid,
163+
Asn1String.UTF8("Original Device").encodeToTlv(),
164+
)
165+
),
166+
) { input ->
167+
AttestationHashInput(
168+
version = input.version,
169+
subjectName = input.subjectName,
170+
attributes = input.attributes.map {
171+
if (it.oid == bindingDeviceNameOid) Pkcs10CertificationRequestAttribute(
172+
bindingDeviceNameOid,
173+
Asn1String.UTF8("Modified Device").encodeToTlv(),
174+
) else it
175+
}
176+
)
177+
}.verifyRejected()
178+
}
179+
}

0 commit comments

Comments
 (0)