|
| 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