AuthenticatorData.java

1
// Copyright (c) 2018, Yubico AB
2
// All rights reserved.
3
//
4
// Redistribution and use in source and binary forms, with or without
5
// modification, are permitted provided that the following conditions are met:
6
//
7
// 1. Redistributions of source code must retain the above copyright notice, this
8
//    list of conditions and the following disclaimer.
9
//
10
// 2. Redistributions in binary form must reproduce the above copyright notice,
11
//    this list of conditions and the following disclaimer in the documentation
12
//    and/or other materials provided with the distribution.
13
//
14
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
25
package com.yubico.webauthn.data;
26
27
import com.fasterxml.jackson.annotation.JsonCreator;
28
import com.fasterxml.jackson.annotation.JsonIgnore;
29
import com.fasterxml.jackson.annotation.JsonProperty;
30
import com.fasterxml.jackson.core.JsonGenerator;
31
import com.fasterxml.jackson.databind.SerializerProvider;
32
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
33
import com.upokecenter.cbor.CBORException;
34
import com.upokecenter.cbor.CBORObject;
35
import com.yubico.internal.util.BinaryUtil;
36
import com.yubico.internal.util.ExceptionUtil;
37
import com.yubico.internal.util.JacksonCodecs;
38
import java.io.ByteArrayInputStream;
39
import java.io.IOException;
40
import java.util.Arrays;
41
import java.util.Optional;
42
import lombok.NonNull;
43
import lombok.Value;
44
45
/**
46
 * The authenticator data structure is a byte array of 37 bytes or more. This class presents the
47
 * authenticator data decoded as a high-level object.
48
 *
49
 * <p>The authenticator data structure encodes contextual bindings made by the authenticator. These
50
 * bindings are controlled by the authenticator itself, and derive their trust from the WebAuthn
51
 * Relying Party's assessment of the security properties of the authenticator. In one extreme case,
52
 * the authenticator may be embedded in the client, and its bindings may be no more trustworthy than
53
 * the client data. At the other extreme, the authenticator may be a discrete entity with
54
 * high-security hardware and software, connected to the client over a secure channel. In both
55
 * cases, the Relying Party receives the authenticator data in the same format, and uses its
56
 * knowledge of the authenticator to make trust decisions.
57
 *
58
 * @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-authenticator-data">§6.1.
59
 *     Authenticator Data</a>
60
 */
61
@Value
62
@JsonSerialize(using = AuthenticatorData.JsonSerializer.class)
63
public class AuthenticatorData {
64
65
  /**
66
   * The original raw byte array that this object is decoded from. This is a byte array of 37 bytes
67
   * or more.
68
   *
69
   * @see <a
70
   *     href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-authenticator-data">§6.1.
71
   *     Authenticator Data</a>
72
   */
73
  @NonNull private final ByteArray bytes;
74
75
  /** The flags bit field. */
76
  @NonNull private final transient AuthenticatorDataFlags flags;
77
78
  /**
79
   * Attested credential data, if present.
80
   *
81
   * <p>This member is present if and only if the {@link AuthenticatorDataFlags#AT} flag is set.
82
   *
83
   * @see #flags
84
   */
85
  @JsonIgnore private final transient AttestedCredentialData attestedCredentialData;
86
87
  @JsonIgnore private final transient CBORObject extensions;
88
89
  private static final int RP_ID_HASH_INDEX = 0;
90
  private static final int RP_ID_HASH_END = RP_ID_HASH_INDEX + 32;
91
92
  private static final int FLAGS_INDEX = RP_ID_HASH_END;
93
  private static final int FLAGS_END = FLAGS_INDEX + 1;
94
95
  private static final int COUNTER_INDEX = FLAGS_END;
96
  private static final int COUNTER_END = COUNTER_INDEX + 4;
97
98
  private static final int FIXED_LENGTH_PART_END_INDEX = COUNTER_END;
99
100
  /** Decode an {@link AuthenticatorData} object from a raw authenticator data byte array. */
101
  @JsonCreator
102 1 1. <init> : negated conditional → KILLED
  public AuthenticatorData(@NonNull ByteArray bytes) {
103 1 1. <init> : removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED
    ExceptionUtil.assertTrue(
104 2 1. <init> : negated conditional → KILLED
2. <init> : changed conditional boundary → KILLED
        bytes.size() >= FIXED_LENGTH_PART_END_INDEX,
105
        "%s byte array must be at least %d bytes, was %d: %s",
106
        AuthenticatorData.class.getSimpleName(),
107
        FIXED_LENGTH_PART_END_INDEX,
108
        bytes.size(),
109
        bytes.getBase64Url());
110
111
    this.bytes = bytes;
112
113
    final byte[] rawBytes = bytes.getBytes();
114
115
    this.flags = new AuthenticatorDataFlags(rawBytes[FLAGS_INDEX]);
116
117 1 1. <init> : negated conditional → KILLED
    if (flags.AT) {
118
      VariableLengthParseResult parseResult =
119
          parseAttestedCredentialData(
120
              flags, Arrays.copyOfRange(rawBytes, FIXED_LENGTH_PART_END_INDEX, rawBytes.length));
121
      attestedCredentialData = parseResult.getAttestedCredentialData();
122
      extensions = parseResult.getExtensions();
123 1 1. <init> : negated conditional → KILLED
    } else if (flags.ED) {
124
      attestedCredentialData = null;
125
      extensions =
126
          parseExtensions(
127
              Arrays.copyOfRange(rawBytes, FIXED_LENGTH_PART_END_INDEX, rawBytes.length));
128
    } else {
129
      attestedCredentialData = null;
130
      extensions = null;
131
    }
132
  }
133
134
  /** The SHA-256 hash of the RP ID the credential is scoped to. */
135
  @JsonProperty("rpIdHash")
136
  public ByteArray getRpIdHash() {
137 1 1. getRpIdHash : replaced return value with null for com/yubico/webauthn/data/AuthenticatorData::getRpIdHash → KILLED
    return new ByteArray(Arrays.copyOfRange(bytes.getBytes(), RP_ID_HASH_INDEX, RP_ID_HASH_END));
138
  }
139
140
  /** The 32-bit unsigned signature counter. */
141
  public long getSignatureCounter() {
142 1 1. getSignatureCounter : replaced long return with 0 for com/yubico/webauthn/data/AuthenticatorData::getSignatureCounter → KILLED
    return BinaryUtil.getUint32(Arrays.copyOfRange(bytes.getBytes(), COUNTER_INDEX, COUNTER_END));
143
  }
144
145
  private static VariableLengthParseResult parseAttestedCredentialData(
146
      AuthenticatorDataFlags flags, byte[] bytes) {
147
    final int AAGUID_INDEX = 0;
148
    final int AAGUID_END = AAGUID_INDEX + 16;
149
150
    final int CREDENTIAL_ID_LENGTH_INDEX = AAGUID_END;
151
    final int CREDENTIAL_ID_LENGTH_END = CREDENTIAL_ID_LENGTH_INDEX + 2;
152
153 3 1. parseAttestedCredentialData : removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED
2. parseAttestedCredentialData : changed conditional boundary → KILLED
3. parseAttestedCredentialData : negated conditional → KILLED
    ExceptionUtil.assertTrue(
154
        bytes.length >= CREDENTIAL_ID_LENGTH_END,
155
        "Attested credential data must contain at least %d bytes, was %d: %s",
156
        CREDENTIAL_ID_LENGTH_END,
157
        bytes.length,
158
        new ByteArray(bytes));
159
160
    byte[] credentialIdLengthBytes =
161
        Arrays.copyOfRange(bytes, CREDENTIAL_ID_LENGTH_INDEX, CREDENTIAL_ID_LENGTH_END);
162
163
    final int L;
164
    try {
165
      L = BinaryUtil.getUint16(credentialIdLengthBytes);
166
    } catch (IllegalArgumentException e) {
167
      throw new IllegalArgumentException(
168
          "Invalid credential ID length bytes: " + Arrays.asList(credentialIdLengthBytes), e);
169
    }
170
171
    final int CREDENTIAL_ID_INDEX = CREDENTIAL_ID_LENGTH_END;
172 1 1. parseAttestedCredentialData : Replaced integer addition with subtraction → KILLED
    final int CREDENTIAL_ID_END = CREDENTIAL_ID_INDEX + L;
173
174
    final int CREDENTIAL_PUBLIC_KEY_INDEX = CREDENTIAL_ID_END;
175
    final int CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END = bytes.length;
176
177 3 1. parseAttestedCredentialData : changed conditional boundary → SURVIVED
2. parseAttestedCredentialData : negated conditional → KILLED
3. parseAttestedCredentialData : removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED
    ExceptionUtil.assertTrue(
178
        bytes.length >= CREDENTIAL_ID_END,
179
        "Expected credential ID of length %d, but attested credential data and extension data is only %d bytes: %s",
180
        CREDENTIAL_ID_END,
181
        bytes.length,
182
        new ByteArray(bytes));
183
184
    ByteArrayInputStream indefiniteLengthBytes =
185
        new ByteArrayInputStream(
186
            Arrays.copyOfRange(
187
                bytes, CREDENTIAL_PUBLIC_KEY_INDEX, CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END));
188
189
    final CBORObject credentialPublicKey = CBORObject.Read(indefiniteLengthBytes);
190
    final CBORObject extensions;
191
192 2 1. parseAttestedCredentialData : changed conditional boundary → KILLED
2. parseAttestedCredentialData : negated conditional → KILLED
    if (indefiniteLengthBytes.available() > 0) {
193 1 1. parseAttestedCredentialData : negated conditional → KILLED
      if (flags.ED) {
194
        try {
195
          extensions = CBORObject.Read(indefiniteLengthBytes);
196
        } catch (CBORException e) {
197
          throw new IllegalArgumentException("Failed to parse extension data", e);
198
        }
199
      } else {
200
        throw new IllegalArgumentException(
201
            String.format(
202
                "Flags indicate no extension data, but %d bytes remain after attested credential data.",
203
                indefiniteLengthBytes.available()));
204
      }
205
    } else {
206 1 1. parseAttestedCredentialData : negated conditional → KILLED
      if (flags.ED) {
207
        throw new IllegalArgumentException(
208
            "Flags indicate there should be extension data, but no bytes remain after attested credential data.");
209
      } else {
210
        extensions = null;
211
      }
212
    }
213
214 1 1. parseAttestedCredentialData : replaced return value with null for com/yubico/webauthn/data/AuthenticatorData::parseAttestedCredentialData → KILLED
    return new VariableLengthParseResult(
215
        AttestedCredentialData.builder()
216
            .aaguid(new ByteArray(Arrays.copyOfRange(bytes, AAGUID_INDEX, AAGUID_END)))
217
            .credentialId(
218
                new ByteArray(Arrays.copyOfRange(bytes, CREDENTIAL_ID_INDEX, CREDENTIAL_ID_END)))
219
            .credentialPublicKey(new ByteArray(credentialPublicKey.EncodeToBytes()))
220
            .build(),
221
        extensions);
222
  }
223
224
  private static CBORObject parseExtensions(byte[] bytes) {
225
    try {
226 1 1. parseExtensions : replaced return value with null for com/yubico/webauthn/data/AuthenticatorData::parseExtensions → KILLED
      return CBORObject.DecodeFromBytes(bytes);
227
    } catch (CBORException e) {
228
      throw new IllegalArgumentException("Failed to parse extension data", e);
229
    }
230
  }
231
232
  @Value
233
  private static class VariableLengthParseResult {
234
    AttestedCredentialData attestedCredentialData;
235
    CBORObject extensions;
236
  }
237
238
  /**
239
   * Attested credential data, if present.
240
   *
241
   * <p>This member is present if and only if the {@link AuthenticatorDataFlags#AT} flag is set.
242
   *
243
   * @see #flags
244
   */
245
  public Optional<AttestedCredentialData> getAttestedCredentialData() {
246 1 1. getAttestedCredentialData : replaced return value with Optional.empty for com/yubico/webauthn/data/AuthenticatorData::getAttestedCredentialData → KILLED
    return Optional.ofNullable(attestedCredentialData);
247
  }
248
249
  /**
250
   * Extension-defined authenticator data, if present.
251
   *
252
   * <p>This member is present if and only if the {@link AuthenticatorDataFlags#ED} flag is set.
253
   *
254
   * <p>Changes to the returned value are not reflected in the {@link AuthenticatorData} object.
255
   *
256
   * @see #flags
257
   */
258
  public Optional<CBORObject> getExtensions() {
259 1 1. getExtensions : replaced return value with Optional.empty for com/yubico/webauthn/data/AuthenticatorData::getExtensions → KILLED
    return Optional.ofNullable(extensions).map(JacksonCodecs::deepCopy);
260
  }
261
262
  static class JsonSerializer
263
      extends com.fasterxml.jackson.databind.JsonSerializer<AuthenticatorData> {
264
    @Override
265
    public void serialize(
266
        AuthenticatorData value, JsonGenerator gen, SerializerProvider serializers)
267
        throws IOException {
268 1 1. serialize : removed call to com/fasterxml/jackson/core/JsonGenerator::writeString → KILLED
      gen.writeString(value.getBytes().getBase64Url());
269
    }
270
  }
271
}

Mutations

102

1.1
Location : <init>
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
negated conditional → KILLED

103

1.1
Location : <init>
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED

104

1.1
Location : <init>
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
negated conditional → KILLED

2.2
Location : <init>
Killed by : com.yubico.webauthn.data.AttestationObjectSpec
changed conditional boundary → KILLED

117

1.1
Location : <init>
Killed by : com.yubico.webauthn.data.AttestationObjectSpec
negated conditional → KILLED

123

1.1
Location : <init>
Killed by : com.yubico.webauthn.data.AttestationObjectSpec
negated conditional → KILLED

137

1.1
Location : getRpIdHash
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
replaced return value with null for com/yubico/webauthn/data/AuthenticatorData::getRpIdHash → KILLED

142

1.1
Location : getSignatureCounter
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
replaced long return with 0 for com/yubico/webauthn/data/AuthenticatorData::getSignatureCounter → KILLED

153

1.1
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED

2.2
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
changed conditional boundary → KILLED

3.3
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
negated conditional → KILLED

172

1.1
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
Replaced integer addition with subtraction → KILLED

177

1.1
Location : parseAttestedCredentialData
Killed by : none
changed conditional boundary → SURVIVED

2.2
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
negated conditional → KILLED

3.3
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
removed call to com/yubico/internal/util/ExceptionUtil::assertTrue → KILLED

192

1.1
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
changed conditional boundary → KILLED

2.2
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
negated conditional → KILLED

193

1.1
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
negated conditional → KILLED

206

1.1
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
negated conditional → KILLED

214

1.1
Location : parseAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorAttestationResponseSpec
replaced return value with null for com/yubico/webauthn/data/AuthenticatorData::parseAttestedCredentialData → KILLED

226

1.1
Location : parseExtensions
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
replaced return value with null for com/yubico/webauthn/data/AuthenticatorData::parseExtensions → KILLED

246

1.1
Location : getAttestedCredentialData
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
replaced return value with Optional.empty for com/yubico/webauthn/data/AuthenticatorData::getAttestedCredentialData → KILLED

259

1.1
Location : getExtensions
Killed by : com.yubico.webauthn.data.AuthenticatorDataSpec
replaced return value with Optional.empty for com/yubico/webauthn/data/AuthenticatorData::getExtensions → KILLED

268

1.1
Location : serialize
Killed by : com.yubico.webauthn.data.JsonIoSpec
removed call to com/fasterxml/jackson/core/JsonGenerator::writeString → KILLED

Active mutators

Tests examined


Report generated by PIT 1.15.0