Skip to content

Commit f96ca0a

Browse files
authored
Merge pull request #118 from Yubico/apple-attestation
Implement Apple attestation statement format
2 parents afcb531 + 4b1841a commit f96ca0a

File tree

21 files changed

+932
-38
lines changed

21 files changed

+932
-38
lines changed

NEWS

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
== Version 1.9.0 (unreleased) ==
2+
3+
webauthn-server-attestation:
4+
5+
* Fixed that `SimpleAttestationResolver` would return empty transports when
6+
transports are unknown.
7+
8+
webauthn-server-core:
9+
10+
* Added support for the `"apple"` attestation statement format.
11+
12+
Other:
13+
14+
* Dependency versions moved to new meta-module `webauthn-server-parent`. Users
15+
should never need to depend on `webauthn-server-parent` directly.
16+
17+
118
== Version 1.8.0 ==
219

320
Changes:

build.gradle

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ plugins {
1818
import io.franzbecker.gradle.lombok.LombokPlugin
1919
import io.franzbecker.gradle.lombok.task.DelombokTask
2020

21+
rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family"
22+
2123
project.ext.isCiBuild = System.env.CI == 'true'
2224

2325
project.ext.publishEnabled = !isCiBuild &&
@@ -276,6 +278,66 @@ subprojects { project ->
276278
}
277279
}
278280

281+
// The root project has no sources, but the dependency platform also needs to be published as an artifact
282+
// See https://docs.gradle.org/current/userguide/java_platform_plugin.html
283+
// See https://github.com/Yubico/java-webauthn-server/issues/93#issuecomment-822806951
284+
if (publishEnabled) {
285+
apply plugin: 'maven-publish'
286+
apply plugin: 'signing'
287+
288+
publishing {
289+
publications {
290+
jars(MavenPublication) {
291+
from components.javaPlatform
292+
293+
pom {
294+
name = project.name
295+
description = project.description
296+
url = 'https://developers.yubico.com/java-webauthn-server/'
297+
298+
developers {
299+
developer {
300+
id = 'emil'
301+
name = 'Emil Lundberg'
302+
303+
}
304+
}
305+
306+
licenses {
307+
license {
308+
name = 'BSD-license'
309+
comments = 'Revised 2-clause BSD license'
310+
}
311+
}
312+
313+
scm {
314+
url = 'scm:git:git://github.com/Yubico/java-webauthn-server.git'
315+
connection = 'scm:git:git://github.com/Yubico/java-webauthn-server.git'
316+
developerConnection = 'scm:git:ssh://[email protected]/Yubico/java-webauthn-server.git'
317+
tag = 'HEAD'
318+
}
319+
}
320+
}
321+
}
322+
323+
repositories {
324+
maven {
325+
name = "sonatypeNexus"
326+
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
327+
credentials {
328+
username ossrhUsername
329+
password ossrhPassword
330+
}
331+
}
332+
}
333+
}
334+
335+
signing {
336+
useGpgCmd()
337+
sign publishing.publications.jars
338+
}
339+
}
340+
279341
task pitestMerge(type: com.yubico.gradle.pitest.tasks.PitestMergeTask)
280342

281343
coveralls {

webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.yubico.internal.util.CertificateParser;
3232
import com.yubico.internal.util.CollectionUtil;
3333
import com.yubico.internal.util.ExceptionUtil;
34+
import com.yubico.internal.util.OptionalUtil;
3435
import com.yubico.webauthn.attestation.Attestation;
3536
import com.yubico.webauthn.attestation.AttestationResolver;
3637
import com.yubico.webauthn.attestation.DeviceMatcher;
@@ -135,9 +136,11 @@ public Optional<Attestation> resolve(
135136
.vendorProperties(Optional.of(vendorProperties))
136137
.deviceProperties(Optional.ofNullable(deviceProperties))
137138
.transports(
138-
Optional.of(
139-
Transport.fromInt(
140-
getTransports(attestationCertificate) | metadataTransports)))
139+
OptionalUtil.zipWith(
140+
getTransports(attestationCertificate),
141+
Optional.of(metadataTransports).filter(t -> t != 0),
142+
(a, b) -> a | b)
143+
.map(Transport::fromInt))
141144
.build();
142145
});
143146
}
@@ -158,11 +161,11 @@ private boolean deviceMatches(
158161
}
159162
}
160163

161-
private static int getTransports(X509Certificate cert) {
164+
private static Optional<Integer> getTransports(X509Certificate cert) {
162165
byte[] extensionValue = cert.getExtensionValue(TRANSPORTS_EXT_OID);
163166

164167
if (extensionValue == null) {
165-
return 0;
168+
return Optional.empty();
166169
}
167170

168171
ExceptionUtil.assure(
@@ -186,14 +189,14 @@ private static int getTransports(X509Certificate cert) {
186189
}
187190
}
188191

189-
return transports;
192+
return Optional.of(transports);
190193
}
191194

192195
@Override
193196
public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) {
194197
return Attestation.builder()
195198
.trusted(false)
196-
.transports(Optional.of(Transport.fromInt(getTransports(attestationCertificate))))
199+
.transports(getTransports(attestationCertificate).map(Transport::fromInt))
197200
.build();
198201
}
199202
}

webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,51 @@ class DeviceIdentificationSpec extends FunSpec with Matchers {
151151
)
152152
}
153153
}
154+
155+
describe("fails to identify") {
156+
def check(testData: RealExamples.Example): Unit = {
157+
val rp = RelyingParty
158+
.builder()
159+
.identity(testData.rp)
160+
.credentialRepository(Helpers.CredentialRepository.empty)
161+
.metadataService(new StandardMetadataService())
162+
.build()
163+
164+
val result = rp.finishRegistration(
165+
FinishRegistrationOptions
166+
.builder()
167+
.request(
168+
PublicKeyCredentialCreationOptions
169+
.builder()
170+
.rp(testData.rp)
171+
.user(testData.user)
172+
.challenge(testData.attestation.challenge)
173+
.pubKeyCredParams(
174+
List(PublicKeyCredentialParameters.ES256).asJava
175+
)
176+
.build()
177+
)
178+
.response(testData.attestation.credential)
179+
.build()
180+
);
181+
182+
result.isAttestationTrusted should be(false)
183+
result.getAttestationMetadata.isPresent should be(true)
184+
result.getAttestationMetadata.get.getDeviceProperties.isPresent should be(
185+
false
186+
)
187+
result.getAttestationMetadata.get.getVendorProperties.isPresent should be(
188+
false
189+
)
190+
result.getAttestationMetadata.get.getTransports.isPresent should be(
191+
false
192+
)
193+
}
194+
195+
it("an Apple iOS device.") {
196+
check(RealExamples.AppleAttestationIos)
197+
}
198+
}
154199
}
155200

156201
describe("The default AttestationResolver") {
@@ -217,4 +262,134 @@ class DeviceIdentificationSpec extends FunSpec with Matchers {
217262
}
218263
}
219264

265+
describe(
266+
"A StandardMetadataService configured with an Apple root certificate"
267+
) {
268+
// Apple WebAuthn Root CA cert downloaded from https://www.apple.com/certificateauthority/private/ on 2021-04-12
269+
// https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
270+
val mds = metadataService("""{
271+
| "identifier": "98cf2729-e2b9-4633-8b6a-b295cda99ccf",
272+
| "version": 1,
273+
| "vendorInfo": {
274+
| "name": "Apple Inc. (Metadata file by Yubico)"
275+
| },
276+
| "trustedCertificates": [
277+
| "-----BEGIN CERTIFICATE-----\nMIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w\nHQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ\nbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx\nNTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG\nA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k\nxu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/\npcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk\n2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA\nMGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3\njAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B\n1bWeT0vT\n-----END CERTIFICATE-----"
278+
| ],
279+
| "devices": [
280+
| {
281+
| "displayName": "Apple device",
282+
| "selectors": [
283+
| {
284+
| "type": "x509Extension",
285+
| "parameters": {
286+
| "key": "1.2.840.113635.100.8.2"
287+
| }
288+
| }
289+
| ]
290+
| }
291+
| ]
292+
|}""".stripMargin)
293+
294+
describe("successfully identifies") {
295+
def check(
296+
expectedName: String,
297+
testData: RealExamples.Example,
298+
): Unit = {
299+
val rp = RelyingParty
300+
.builder()
301+
.identity(testData.rp)
302+
.credentialRepository(Helpers.CredentialRepository.empty)
303+
.metadataService(mds)
304+
.build()
305+
306+
val result = rp.finishRegistration(
307+
FinishRegistrationOptions
308+
.builder()
309+
.request(
310+
PublicKeyCredentialCreationOptions
311+
.builder()
312+
.rp(testData.rp)
313+
.user(testData.user)
314+
.challenge(testData.attestation.challenge)
315+
.pubKeyCredParams(
316+
List(PublicKeyCredentialParameters.ES256).asJava
317+
)
318+
.build()
319+
)
320+
.response(testData.attestation.credential)
321+
.build()
322+
)
323+
324+
result.isAttestationTrusted should be(true)
325+
result.getAttestationMetadata.isPresent should be(true)
326+
result.getAttestationMetadata.get.getDeviceProperties.isPresent should be(
327+
true
328+
)
329+
result.getAttestationMetadata.get.getDeviceProperties
330+
.get()
331+
.get("displayName") should equal(expectedName)
332+
result.getAttestationMetadata.get.getTransports.isPresent should be(
333+
false
334+
)
335+
}
336+
337+
it("an Apple iOS device.") {
338+
check(
339+
"Apple device",
340+
RealExamples.AppleAttestationIos,
341+
)
342+
}
343+
344+
it("an Apple MacOS device.") {
345+
check(
346+
"Apple device",
347+
RealExamples.AppleAttestationMacos,
348+
)
349+
}
350+
}
351+
352+
describe("fails to identify") {
353+
def check(testData: RealExamples.Example): Unit = {
354+
val rp = RelyingParty
355+
.builder()
356+
.identity(testData.rp)
357+
.credentialRepository(Helpers.CredentialRepository.empty)
358+
.metadataService(mds)
359+
.build()
360+
361+
val result = rp.finishRegistration(
362+
FinishRegistrationOptions
363+
.builder()
364+
.request(
365+
PublicKeyCredentialCreationOptions
366+
.builder()
367+
.rp(testData.rp)
368+
.user(testData.user)
369+
.challenge(testData.attestation.challenge)
370+
.pubKeyCredParams(
371+
List(PublicKeyCredentialParameters.ES256).asJava
372+
)
373+
.build()
374+
)
375+
.response(testData.attestation.credential)
376+
.build()
377+
)
378+
379+
result.isAttestationTrusted should be(false)
380+
result.getAttestationMetadata.isPresent should be(true)
381+
result.getAttestationMetadata.get.getVendorProperties.isPresent should be(
382+
false
383+
)
384+
result.getAttestationMetadata.get.getDeviceProperties.isPresent should be(
385+
false
386+
)
387+
}
388+
389+
it("a YubiKey 5 NFC.") {
390+
check(RealExamples.YubiKey5)
391+
}
392+
}
393+
}
394+
220395
}

webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public boolean verifyAttestationSignature(
6868

6969
ByteArray signedData =
7070
attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash);
71-
ByteArray hashSignedData = Crypto.hash(signedData);
71+
ByteArray hashSignedData = Crypto.sha256(signedData);
7272
ByteArray nonceByteArray = ByteArray.fromBase64(payload.get("nonce").textValue());
7373
ExceptionUtil.assure(
7474
hashSignedData.equals(nonceByteArray),

0 commit comments

Comments
 (0)