AndroidSafetynetAttestationStatementVerifier.java

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
Location : getAttestationType
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::getAttestationType → KILLED

47

1.1
Location : getX5cArray
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::getX5cArray → KILLED

55

1.1
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
negated conditional → KILLED

2.2
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
negated conditional → KILLED

63

1.1
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
negated conditional → KILLED

64

1.1
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced boolean return with true for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyAttestationSignature → KILLED

73

1.1
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED

79

1.1
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED

84

1.1
Location : verifyAttestationSignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced boolean return with false for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyAttestationSignature → KILLED

88

1.1
Location : parseJws
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::parseJws → KILLED

94

1.1
Location : getResponseBytes
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
negated conditional → KILLED

2.2
Location : getResponseBytes
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
negated conditional → KILLED

101

1.1
Location : getResponseBytes
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced return value with null for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::getResponseBytes → KILLED

123

1.1
Location : verifySignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
removed call to java/security/Signature::initVerify → KILLED

129

1.1
Location : verifySignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
removed call to java/security/Signature::update → KILLED

136

1.1
Location : verifySignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED

142

1.1
Location : verifySignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced boolean return with true for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifySignature → KILLED

2.2
Location : verifySignature
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced boolean return with false for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifySignature → KILLED

186

1.1
Location : getX5c
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced return value with Collections.emptyList for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier$JsonWebSignatureCustom::getX5c → KILLED

193

1.1
Location : verifyHostname
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
removed call to org/apache/hc/client5/http/ssl/DefaultHostnameVerifier::verify → KILLED

194

1.1
Location : verifyHostname
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced boolean return with false for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyHostname → KILLED

196

1.1
Location : verifyHostname
Killed by : com.yubico.webauthn.RelyingPartyV2RegistrationSpec
replaced boolean return with true for com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier::verifyHostname → KILLED

Active mutators

Tests examined


Report generated by PIT 1.15.0