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 |