Skip to content

Commit

Permalink
Merge pull request #118 from Yubico/apple-attestation
Browse files Browse the repository at this point in the history
Implement Apple attestation statement format
  • Loading branch information
emlun authored Apr 26, 2021
2 parents afcb531 + 4b1841a commit f96ca0a
Show file tree
Hide file tree
Showing 21 changed files with 932 additions and 38 deletions.
17 changes: 17 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
== Version 1.9.0 (unreleased) ==

webauthn-server-attestation:

* Fixed that `SimpleAttestationResolver` would return empty transports when
transports are unknown.

webauthn-server-core:

* Added support for the `"apple"` attestation statement format.

Other:

* Dependency versions moved to new meta-module `webauthn-server-parent`. Users
should never need to depend on `webauthn-server-parent` directly.


== Version 1.8.0 ==

Changes:
Expand Down
62 changes: 62 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ plugins {
import io.franzbecker.gradle.lombok.LombokPlugin
import io.franzbecker.gradle.lombok.task.DelombokTask

rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family"

project.ext.isCiBuild = System.env.CI == 'true'

project.ext.publishEnabled = !isCiBuild &&
Expand Down Expand Up @@ -276,6 +278,66 @@ subprojects { project ->
}
}

// The root project has no sources, but the dependency platform also needs to be published as an artifact
// See https://docs.gradle.org/current/userguide/java_platform_plugin.html
// See https://github.com/Yubico/java-webauthn-server/issues/93#issuecomment-822806951
if (publishEnabled) {
apply plugin: 'maven-publish'
apply plugin: 'signing'

publishing {
publications {
jars(MavenPublication) {
from components.javaPlatform

pom {
name = project.name
description = project.description
url = 'https://developers.yubico.com/java-webauthn-server/'

developers {
developer {
id = 'emil'
name = 'Emil Lundberg'
email = '[email protected]'
}
}

licenses {
license {
name = 'BSD-license'
comments = 'Revised 2-clause BSD license'
}
}

scm {
url = 'scm:git:git://github.com/Yubico/java-webauthn-server.git'
connection = 'scm:git:git://github.com/Yubico/java-webauthn-server.git'
developerConnection = 'scm:git:ssh://[email protected]/Yubico/java-webauthn-server.git'
tag = 'HEAD'
}
}
}
}

repositories {
maven {
name = "sonatypeNexus"
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername
password ossrhPassword
}
}
}
}

signing {
useGpgCmd()
sign publishing.publications.jars
}
}

task pitestMerge(type: com.yubico.gradle.pitest.tasks.PitestMergeTask)

coveralls {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.yubico.internal.util.CertificateParser;
import com.yubico.internal.util.CollectionUtil;
import com.yubico.internal.util.ExceptionUtil;
import com.yubico.internal.util.OptionalUtil;
import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.attestation.AttestationResolver;
import com.yubico.webauthn.attestation.DeviceMatcher;
Expand Down Expand Up @@ -135,9 +136,11 @@ public Optional<Attestation> resolve(
.vendorProperties(Optional.of(vendorProperties))
.deviceProperties(Optional.ofNullable(deviceProperties))
.transports(
Optional.of(
Transport.fromInt(
getTransports(attestationCertificate) | metadataTransports)))
OptionalUtil.zipWith(
getTransports(attestationCertificate),
Optional.of(metadataTransports).filter(t -> t != 0),
(a, b) -> a | b)
.map(Transport::fromInt))
.build();
});
}
Expand All @@ -158,11 +161,11 @@ private boolean deviceMatches(
}
}

private static int getTransports(X509Certificate cert) {
private static Optional<Integer> getTransports(X509Certificate cert) {
byte[] extensionValue = cert.getExtensionValue(TRANSPORTS_EXT_OID);

if (extensionValue == null) {
return 0;
return Optional.empty();
}

ExceptionUtil.assure(
Expand All @@ -186,14 +189,14 @@ private static int getTransports(X509Certificate cert) {
}
}

return transports;
return Optional.of(transports);
}

@Override
public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) {
return Attestation.builder()
.trusted(false)
.transports(Optional.of(Transport.fromInt(getTransports(attestationCertificate))))
.transports(getTransports(attestationCertificate).map(Transport::fromInt))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,51 @@ class DeviceIdentificationSpec extends FunSpec with Matchers {
)
}
}

describe("fails to identify") {
def check(testData: RealExamples.Example): Unit = {
val rp = RelyingParty
.builder()
.identity(testData.rp)
.credentialRepository(Helpers.CredentialRepository.empty)
.metadataService(new StandardMetadataService())
.build()

val result = rp.finishRegistration(
FinishRegistrationOptions
.builder()
.request(
PublicKeyCredentialCreationOptions
.builder()
.rp(testData.rp)
.user(testData.user)
.challenge(testData.attestation.challenge)
.pubKeyCredParams(
List(PublicKeyCredentialParameters.ES256).asJava
)
.build()
)
.response(testData.attestation.credential)
.build()
);

result.isAttestationTrusted should be(false)
result.getAttestationMetadata.isPresent should be(true)
result.getAttestationMetadata.get.getDeviceProperties.isPresent should be(
false
)
result.getAttestationMetadata.get.getVendorProperties.isPresent should be(
false
)
result.getAttestationMetadata.get.getTransports.isPresent should be(
false
)
}

it("an Apple iOS device.") {
check(RealExamples.AppleAttestationIos)
}
}
}

describe("The default AttestationResolver") {
Expand Down Expand Up @@ -217,4 +262,134 @@ class DeviceIdentificationSpec extends FunSpec with Matchers {
}
}

describe(
"A StandardMetadataService configured with an Apple root certificate"
) {
// Apple WebAuthn Root CA cert downloaded from https://www.apple.com/certificateauthority/private/ on 2021-04-12
// https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
val mds = metadataService("""{
| "identifier": "98cf2729-e2b9-4633-8b6a-b295cda99ccf",
| "version": 1,
| "vendorInfo": {
| "name": "Apple Inc. (Metadata file by Yubico)"
| },
| "trustedCertificates": [
| "-----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-----"
| ],
| "devices": [
| {
| "displayName": "Apple device",
| "selectors": [
| {
| "type": "x509Extension",
| "parameters": {
| "key": "1.2.840.113635.100.8.2"
| }
| }
| ]
| }
| ]
|}""".stripMargin)

describe("successfully identifies") {
def check(
expectedName: String,
testData: RealExamples.Example,
): Unit = {
val rp = RelyingParty
.builder()
.identity(testData.rp)
.credentialRepository(Helpers.CredentialRepository.empty)
.metadataService(mds)
.build()

val result = rp.finishRegistration(
FinishRegistrationOptions
.builder()
.request(
PublicKeyCredentialCreationOptions
.builder()
.rp(testData.rp)
.user(testData.user)
.challenge(testData.attestation.challenge)
.pubKeyCredParams(
List(PublicKeyCredentialParameters.ES256).asJava
)
.build()
)
.response(testData.attestation.credential)
.build()
)

result.isAttestationTrusted should be(true)
result.getAttestationMetadata.isPresent should be(true)
result.getAttestationMetadata.get.getDeviceProperties.isPresent should be(
true
)
result.getAttestationMetadata.get.getDeviceProperties
.get()
.get("displayName") should equal(expectedName)
result.getAttestationMetadata.get.getTransports.isPresent should be(
false
)
}

it("an Apple iOS device.") {
check(
"Apple device",
RealExamples.AppleAttestationIos,
)
}

it("an Apple MacOS device.") {
check(
"Apple device",
RealExamples.AppleAttestationMacos,
)
}
}

describe("fails to identify") {
def check(testData: RealExamples.Example): Unit = {
val rp = RelyingParty
.builder()
.identity(testData.rp)
.credentialRepository(Helpers.CredentialRepository.empty)
.metadataService(mds)
.build()

val result = rp.finishRegistration(
FinishRegistrationOptions
.builder()
.request(
PublicKeyCredentialCreationOptions
.builder()
.rp(testData.rp)
.user(testData.user)
.challenge(testData.attestation.challenge)
.pubKeyCredParams(
List(PublicKeyCredentialParameters.ES256).asJava
)
.build()
)
.response(testData.attestation.credential)
.build()
)

result.isAttestationTrusted should be(false)
result.getAttestationMetadata.isPresent should be(true)
result.getAttestationMetadata.get.getVendorProperties.isPresent should be(
false
)
result.getAttestationMetadata.get.getDeviceProperties.isPresent should be(
false
)
}

it("a YubiKey 5 NFC.") {
check(RealExamples.YubiKey5)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public boolean verifyAttestationSignature(

ByteArray signedData =
attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash);
ByteArray hashSignedData = Crypto.hash(signedData);
ByteArray hashSignedData = Crypto.sha256(signedData);
ByteArray nonceByteArray = ByteArray.fromBase64(payload.get("nonce").textValue());
ExceptionUtil.assure(
hashSignedData.equals(nonceByteArray),
Expand Down
Loading

0 comments on commit f96ca0a

Please sign in to comment.