| 1 | package com.yubico.webauthn; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.databind.JsonNode; | |
| 4 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 5 | import com.fasterxml.jackson.databind.node.ArrayNode; | |
| 6 | import com.fasterxml.jackson.databind.node.JsonNodeFactory; | |
| 7 | import com.yubico.internal.util.CertificateParser; | |
| 8 | import com.yubico.internal.util.ExceptionUtil; | |
| 9 | import com.yubico.internal.util.JacksonCodecs; | |
| 10 | import com.yubico.webauthn.data.AttestationObject; | |
| 11 | import com.yubico.webauthn.data.AttestationType; | |
| 12 | import com.yubico.webauthn.data.ByteArray; | |
| 13 | import com.yubico.webauthn.data.exception.Base64UrlException; | |
| 14 | import java.io.IOException; | |
| 15 | import java.nio.charset.StandardCharsets; | |
| 16 | import java.security.InvalidKeyException; | |
| 17 | import java.security.NoSuchAlgorithmException; | |
| 18 | import java.security.Signature; | |
| 19 | import java.security.SignatureException; | |
| 20 | import java.security.cert.CertificateException; | |
| 21 | import java.security.cert.X509Certificate; | |
| 22 | import java.util.ArrayList; | |
| 23 | import java.util.List; | |
| 24 | import javax.net.ssl.SSLException; | |
| 25 | import lombok.Value; | |
| 26 | import lombok.extern.slf4j.Slf4j; | |
| 27 | import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; | |
| 28 | ||
| 29 | @Slf4j | |
| 30 | class AndroidSafetynetAttestationStatementVerifier | |
| 31 | implements AttestationStatementVerifier, X5cAttestationStatementVerifier { | |
| 32 | ||
| 33 | private static final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier(); | |
| 34 | ||
| 35 | @Override | |
| 36 | public AttestationType getAttestationType(AttestationObject attestation) { | |
| 37 |
1
1. getAttestationType : replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::getAttestationType → KILLED |
return AttestationType.BASIC; |
| 38 | } | |
| 39 | ||
| 40 | @Override | |
| 41 | public JsonNode getX5cArray(AttestationObject attestationObject) { | |
| 42 | JsonNodeFactory jsonFactory = JsonNodeFactory.instance; | |
| 43 | ArrayNode array = jsonFactory.arrayNode(); | |
| 44 | for (JsonNode cert : parseJws(attestationObject).getHeader().get("x5c")) { | |
| 45 | array.add(jsonFactory.binaryNode(ByteArray.fromBase64(cert.textValue()).getBytes())); | |
| 46 | } | |
| 47 |
1
1. getX5cArray : replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::getX5cArray → KILLED |
return array; |
| 48 | } | |
| 49 | ||
| 50 | @Override | |
| 51 | public boolean verifyAttestationSignature( | |
| 52 | AttestationObject attestationObject, ByteArray clientDataJsonHash) { | |
| 53 | final JsonNode ver = attestationObject.getAttestationStatement().get("ver"); | |
| 54 | ||
| 55 |
2
1. verifyAttestationSignature : negated conditional → KILLED 2. verifyAttestationSignature : negated conditional → KILLED |
if (ver == null || !ver.isTextual()) { |
| 56 | throw new IllegalArgumentException( | |
| 57 | "Property \"ver\" of android-safetynet attestation statement must be a string, was: " | |
| 58 | + ver); | |
| 59 | } | |
| 60 | ||
| 61 | JsonWebSignatureCustom jws = parseJws(attestationObject); | |
| 62 | ||
| 63 |
1
1. verifyAttestationSignature : negated conditional → KILLED |
if (!verifySignature(jws)) { |
| 64 |
1
1. verifyAttestationSignature : replaced boolean return with true for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyAttestationSignature → KILLED |
return false; |
| 65 | } | |
| 66 | ||
| 67 | JsonNode payload = jws.getPayload(); | |
| 68 | ||
| 69 | ByteArray signedData = | |
| 70 | attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); | |
| 71 | ByteArray hashSignedData = Crypto.sha256(signedData); | |
| 72 | ByteArray nonceByteArray = ByteArray.fromBase64(payload.get("nonce").textValue()); | |
| 73 |
1
1. verifyAttestationSignature : removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED |
ExceptionUtil.assertTrue( |
| 74 | hashSignedData.equals(nonceByteArray), | |
| 75 | "Nonce does not equal authenticator data + client data. Expected nonce: %s, was nonce: %s", | |
| 76 | hashSignedData.getBase64Url(), | |
| 77 | nonceByteArray.getBase64Url()); | |
| 78 | ||
| 79 |
1
1. verifyAttestationSignature : removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED |
ExceptionUtil.assertTrue( |
| 80 | payload.get("ctsProfileMatch").booleanValue(), | |
| 81 | "Expected ctsProfileMatch to be true, was: %s", | |
| 82 | payload.get("ctsProfileMatch")); | |
| 83 | ||
| 84 |
1
1. verifyAttestationSignature : replaced boolean return with false for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyAttestationSignature → KILLED |
return true; |
| 85 | } | |
| 86 | ||
| 87 | private static JsonWebSignatureCustom parseJws(AttestationObject attestationObject) { | |
| 88 |
1
1. parseJws : replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::parseJws → KILLED |
return new JsonWebSignatureCustom( |
| 89 | new String(getResponseBytes(attestationObject).getBytes(), StandardCharsets.UTF_8)); | |
| 90 | } | |
| 91 | ||
| 92 | private static ByteArray getResponseBytes(AttestationObject attestationObject) { | |
| 93 | final JsonNode response = attestationObject.getAttestationStatement().get("response"); | |
| 94 |
2
1. getResponseBytes : negated conditional → KILLED 2. getResponseBytes : negated conditional → KILLED |
if (response == null || !response.isBinary()) { |
| 95 | throw new IllegalArgumentException( | |
| 96 | "Property \"response\" of android-safetynet attestation statement must be a binary value, was: " | |
| 97 | + response); | |
| 98 | } | |
| 99 | ||
| 100 | try { | |
| 101 |
1
1. getResponseBytes : replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::getResponseBytes → KILLED |
return new ByteArray(response.binaryValue()); |
| 102 | } catch (IOException ioe) { | |
| 103 | throw ExceptionUtil.wrapAndLog( | |
| 104 | log, "response.isBinary() was true but response.binaryValue failed: " + response, ioe); | |
| 105 | } | |
| 106 | } | |
| 107 | ||
| 108 | private boolean verifySignature(JsonWebSignatureCustom jws) { | |
| 109 | // Verify the signature of the JWS and retrieve the signature certificate. | |
| 110 | X509Certificate attestationCertificate = jws.getX5c().get(0); | |
| 111 | ||
| 112 | String signatureAlgorithmName = | |
| 113 | WebAuthnCodecs.jwsAlgorithmNameToJavaAlgorithmName(jws.getAlgorithm()); | |
| 114 | ||
| 115 | Signature signatureVerifier; | |
| 116 | try { | |
| 117 | signatureVerifier = Signature.getInstance(signatureAlgorithmName); | |
| 118 | } catch (NoSuchAlgorithmException e) { | |
| 119 | throw ExceptionUtil.wrapAndLog( | |
| 120 | log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); | |
| 121 | } | |
| 122 | try { | |
| 123 |
1
1. verifySignature : removed call to java/security/Signature::initVerify → KILLED |
signatureVerifier.initVerify(attestationCertificate.getPublicKey()); |
| 124 | } catch (InvalidKeyException e) { | |
| 125 | throw ExceptionUtil.wrapAndLog( | |
| 126 | log, "Attestation key is invalid: " + attestationCertificate, e); | |
| 127 | } | |
| 128 | try { | |
| 129 |
1
1. verifySignature : removed call to java/security/Signature::update → KILLED |
signatureVerifier.update(jws.getSignedBytes().getBytes()); |
| 130 | } catch (SignatureException e) { | |
| 131 | throw ExceptionUtil.wrapAndLog( | |
| 132 | log, "Signature object in invalid state: " + signatureVerifier, e); | |
| 133 | } | |
| 134 | ||
| 135 | // Verify the hostname of the certificate. | |
| 136 |
1
1. verifySignature : removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED |
ExceptionUtil.assertTrue( |
| 137 | verifyHostname(attestationCertificate), | |
| 138 | "Certificate isn't issued for the hostname attest.android.com: %s", | |
| 139 | attestationCertificate); | |
| 140 | ||
| 141 | try { | |
| 142 |
2
1. verifySignature : replaced boolean return with true for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifySignature → KILLED 2. verifySignature : replaced boolean return with false for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifySignature → KILLED |
return signatureVerifier.verify(jws.getSignature().getBytes()); |
| 143 | } catch (SignatureException e) { | |
| 144 | throw ExceptionUtil.wrapAndLog(log, "Failed to verify signature of JWS: " + jws, e); | |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | @Value | |
| 149 | private static class JsonWebSignatureCustom { | |
| 150 | public final JsonNode header; | |
| 151 | public final JsonNode payload; | |
| 152 | public final ByteArray signedBytes; | |
| 153 | public final ByteArray signature; | |
| 154 | public final List<X509Certificate> x5c; | |
| 155 | public final String algorithm; | |
| 156 | ||
| 157 | JsonWebSignatureCustom(String jwsCompact) { | |
| 158 | String[] parts = jwsCompact.split("\\."); | |
| 159 | ObjectMapper json = JacksonCodecs.json(); | |
| 160 | ||
| 161 | try { | |
| 162 | final ByteArray header = ByteArray.fromBase64Url(parts[0]); | |
| 163 | final ByteArray payload = ByteArray.fromBase64Url(parts[1]); | |
| 164 | ||
| 165 | this.header = json.readTree(header.getBytes()); | |
| 166 | this.payload = json.readTree(payload.getBytes()); | |
| 167 | this.signedBytes = | |
| 168 | new ByteArray((parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8)); | |
| 169 | this.signature = ByteArray.fromBase64Url(parts[2]); | |
| 170 | this.x5c = getX5c(this.header); | |
| 171 | this.algorithm = this.header.get("alg").textValue(); | |
| 172 | } catch (IOException | Base64UrlException e) { | |
| 173 | throw ExceptionUtil.wrapAndLog(log, "Failed to parse JWS: " + jwsCompact, e); | |
| 174 | } catch (CertificateException e) { | |
| 175 | throw ExceptionUtil.wrapAndLog( | |
| 176 | log, "Failed to parse attestation certificates in JWS header: " + jwsCompact, e); | |
| 177 | } | |
| 178 | } | |
| 179 | ||
| 180 | private static List<X509Certificate> getX5c(JsonNode header) | |
| 181 | throws IOException, CertificateException { | |
| 182 | List<X509Certificate> result = new ArrayList<>(); | |
| 183 | for (JsonNode jsonNode : header.get("x5c")) { | |
| 184 | result.add(CertificateParser.parseDer(jsonNode.binaryValue())); | |
| 185 | } | |
| 186 |
1
1. getX5c : replaced return value with Collections.emptyList for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier$JsonWebSignatureCustom::getX5c → KILLED |
return result; |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /** Verifies that the certificate matches the hostname "attest.android.com". */ | |
| 191 | private static boolean verifyHostname(X509Certificate leafCert) { | |
| 192 | try { | |
| 193 |
1
1. verifyHostname : removed call to org/apache/hc/client5/http/ssl/DefaultHostnameVerifier::verify → KILLED |
HOSTNAME_VERIFIER.verify("attest.android.com", leafCert); |
| 194 |
1
1. verifyHostname : replaced boolean return with false for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyHostname → KILLED |
return true; |
| 195 | } catch (SSLException e) { | |
| 196 |
1
1. verifyHostname : replaced boolean return with true for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyHostname → KILLED |
return false; |
| 197 | } | |
| 198 | } | |
| 199 | } | |
Mutations | ||
| 37 |
1.1 |
|
| 47 |
1.1 |
|
| 55 |
1.1 2.2 |
|
| 63 |
1.1 |
|
| 64 |
1.1 |
|
| 73 |
1.1 |
|
| 79 |
1.1 |
|
| 84 |
1.1 |
|
| 88 |
1.1 |
|
| 94 |
1.1 2.2 |
|
| 101 |
1.1 |
|
| 123 |
1.1 |
|
| 129 |
1.1 |
|
| 136 |
1.1 |
|
| 142 |
1.1 2.2 |
|
| 186 |
1.1 |
|
| 193 |
1.1 |
|
| 194 |
1.1 |
|
| 196 |
1.1 |