CertificateParser.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.internal.util;
26
27
import java.io.ByteArrayInputStream;
28
import java.io.InputStream;
29
import java.nio.ByteBuffer;
30
import java.security.MessageDigest;
31
import java.security.NoSuchAlgorithmException;
32
import java.security.cert.Certificate;
33
import java.security.cert.CertificateException;
34
import java.security.cert.CertificateFactory;
35
import java.security.cert.X509Certificate;
36
import java.util.Arrays;
37
import java.util.Base64;
38
import java.util.List;
39
import java.util.Optional;
40
41
public class CertificateParser {
42
  public static final String ID_FIDO_GEN_CE_AAGUID = "1.3.6.1.4.1.45724.1.1.4";
43
  private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
44
45
  private static final List<String> FIXSIG =
46
      Arrays.asList(
47
          "CN=Yubico U2F EE Serial 776137165",
48
          "CN=Yubico U2F EE Serial 1086591525",
49
          "CN=Yubico U2F EE Serial 1973679733",
50
          "CN=Yubico U2F EE Serial 13503277888",
51
          "CN=Yubico U2F EE Serial 13831167861",
52
          "CN=Yubico U2F EE Serial 14803321578");
53
54
  private static final int UNUSED_BITS_BYTE_INDEX_FROM_END = 257;
55
56
  public static X509Certificate parsePem(String pemEncodedCert) throws CertificateException {
57 1 1. parsePem : replaced return value with null for com/yubico/internal/util/CertificateParser::parsePem → KILLED
    return parseDer(
58
        pemEncodedCert
59
            .replaceAll("-----BEGIN CERTIFICATE-----", "")
60
            .replaceAll("-----END CERTIFICATE-----", "")
61
            .replaceAll("\n", ""));
62
  }
63
64
  public static X509Certificate parseDer(String base64DerEncodedCert) throws CertificateException {
65 1 1. parseDer : replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED
    return parseDer(BASE64_DECODER.decode(base64DerEncodedCert));
66
  }
67
68
  public static X509Certificate parseDer(byte[] derEncodedCert) throws CertificateException {
69 1 1. parseDer : replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED
    return parseDer(new ByteArrayInputStream(derEncodedCert));
70
  }
71
72
  public static X509Certificate parseDer(InputStream is) throws CertificateException {
73
    X509Certificate cert =
74
        (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
75
    // Some known certs have an incorrect "unused bits" value, which causes problems on newer
76
    // versions of BouncyCastle.
77 1 1. parseDer : negated conditional → SURVIVED
    if (FIXSIG.contains(cert.getSubjectX500Principal().getName())) {
78
      byte[] encoded = cert.getEncoded();
79
80 2 1. parseDer : changed conditional boundary → SURVIVED
2. parseDer : negated conditional → KILLED
      if (encoded.length >= UNUSED_BITS_BYTE_INDEX_FROM_END) {
81 1 1. parseDer : Replaced integer subtraction with addition → KILLED
        encoded[encoded.length - UNUSED_BITS_BYTE_INDEX_FROM_END] =
82
            0; // Fix the "unused bits" field (should always be 0).
83
      } else {
84
        throw new IllegalArgumentException(
85
            String.format(
86
                "Expected DER encoded cert to be at least %d bytes, was %d: %s",
87
                UNUSED_BITS_BYTE_INDEX_FROM_END, encoded.length, cert));
88
      }
89
90
      cert =
91
          (X509Certificate)
92
              CertificateFactory.getInstance("X.509")
93
                  .generateCertificate(new ByteArrayInputStream(encoded));
94
    }
95 1 1. parseDer : replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED
    return cert;
96
  }
97
98
  /**
99
   * Compute a Subject Key Identifier as defined as method (1) in RFC 5280 section 4.2.1.2.
100
   *
101
   * @throws NoSuchAlgorithmException if the SHA-1 hash algorithm is not available.
102
   * @see <a href="https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2">Internet X.509
103
   *     Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile,
104
   *     section 4.2.1.2. Subject Key Identifier</a>
105
   */
106
  public static byte[] computeSubjectKeyIdentifier(final Certificate cert)
107
      throws NoSuchAlgorithmException {
108
    final byte[] spki = cert.getPublicKey().getEncoded();
109
110
    // SubjectPublicKeyInfo  ::=  SEQUENCE  {
111
    //     algorithm            AlgorithmIdentifier,
112
    //     subjectPublicKey     BIT STRING  }
113
    final byte algLength = spki[2 + 1];
114
115
    // BIT STRING begins with one octet specifying number of unused bits at end;
116
    // this is not included in the content to hash for a Subject Key Identifier.
117 2 1. computeSubjectKeyIdentifier : Replaced integer addition with subtraction → KILLED
2. computeSubjectKeyIdentifier : Replaced integer addition with subtraction → KILLED
    final int spkBitsStart = 2 + 2 + 2 + algLength + 1;
118
119 1 1. computeSubjectKeyIdentifier : replaced return value with null for com/yubico/internal/util/CertificateParser::computeSubjectKeyIdentifier → KILLED
    return MessageDigest.getInstance("SHA-1")
120
        .digest(Arrays.copyOfRange(spki, spkBitsStart, spki.length));
121
  }
122
123
  /**
124
   * Parses an AAGUID into bytes. Refer to <a
125
   * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-packed-attestation-cert-requirements">Packed
126
   * Attestation Statement Certificate Requirements</a> on the W3C web site for details of the ASN.1
127
   * structure that this method parses.
128
   *
129
   * @param bytes the bytes making up value of the extension
130
   * @return the bytes of the AAGUID
131
   */
132
  private static byte[] parseAaguid(byte[] bytes) {
133
134 2 1. parseAaguid : negated conditional → NO_COVERAGE
2. parseAaguid : negated conditional → NO_COVERAGE
    if (bytes != null && bytes.length == 20) {
135
      ByteBuffer buffer = ByteBuffer.wrap(bytes);
136
137 1 1. parseAaguid : negated conditional → NO_COVERAGE
      if (buffer.get() == (byte) 0x04
138 1 1. parseAaguid : negated conditional → NO_COVERAGE
          && buffer.get() == (byte) 0x12
139 1 1. parseAaguid : negated conditional → NO_COVERAGE
          && buffer.get() == (byte) 0x04
140 1 1. parseAaguid : negated conditional → NO_COVERAGE
          && buffer.get() == (byte) 0x10) {
141
        byte[] aaguidBytes = new byte[16];
142
        buffer.get(aaguidBytes);
143
144 1 1. parseAaguid : replaced return value with null for com/yubico/internal/util/CertificateParser::parseAaguid → NO_COVERAGE
        return aaguidBytes;
145
      }
146
    }
147
148
    throw new IllegalArgumentException(
149
        "X.509 extension 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) is not valid.");
150
  }
151
152
  public static Optional<byte[]> parseFidoAaguidExtension(X509Certificate cert) {
153
    Optional<byte[]> result =
154
        Optional.ofNullable(cert.getExtensionValue(ID_FIDO_GEN_CE_AAGUID))
155
            .map(CertificateParser::parseAaguid);
156 1 1. parseFidoAaguidExtension : removed call to java/util/Optional::ifPresent → NO_COVERAGE
    result.ifPresent(
157
        aaguid -> {
158 1 1. lambda$parseFidoAaguidExtension$0 : negated conditional → NO_COVERAGE
          if (cert.getCriticalExtensionOIDs().contains(ID_FIDO_GEN_CE_AAGUID)) {
159
            throw new IllegalArgumentException(
160
                String.format(
161
                    "X.509 extension %s (id-fido-gen-ce-aaguid) must not be marked critical.",
162
                    ID_FIDO_GEN_CE_AAGUID));
163
          }
164
        });
165 1 1. parseFidoAaguidExtension : replaced return value with Optional.empty for com/yubico/internal/util/CertificateParser::parseFidoAaguidExtension → NO_COVERAGE
    return result;
166
  }
167
}

Mutations

57

1.1
Location : parsePem
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
replaced return value with null for com/yubico/internal/util/CertificateParser::parsePem → KILLED

65

1.1
Location : parseDer
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED

69

1.1
Location : parseDer
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED

77

1.1
Location : parseDer
Killed by : none
negated conditional → SURVIVED

80

1.1
Location : parseDer
Killed by : com.yubico.internal.util.CertificateParserTest.parsePemDoesNotReturnNull(com.yubico.internal.util.CertificateParserTest)
negated conditional → KILLED

2.2
Location : parseDer
Killed by : none
changed conditional boundary → SURVIVED

81

1.1
Location : parseDer
Killed by : com.yubico.internal.util.CertificateParserTest.parsePemDoesNotReturnNull(com.yubico.internal.util.CertificateParserTest)
Replaced integer subtraction with addition → KILLED

95

1.1
Location : parseDer
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED

117

1.1
Location : computeSubjectKeyIdentifier
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
Replaced integer addition with subtraction → KILLED

2.2
Location : computeSubjectKeyIdentifier
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
Replaced integer addition with subtraction → KILLED

119

1.1
Location : computeSubjectKeyIdentifier
Killed by : com.yubico.internal.util.CertificateParserTest.subjectPublicKeyIdentifierIsCorrect(com.yubico.internal.util.CertificateParserTest)
replaced return value with null for com/yubico/internal/util/CertificateParser::computeSubjectKeyIdentifier → KILLED

134

1.1
Location : parseAaguid
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : parseAaguid
Killed by : none
negated conditional → NO_COVERAGE

137

1.1
Location : parseAaguid
Killed by : none
negated conditional → NO_COVERAGE

138

1.1
Location : parseAaguid
Killed by : none
negated conditional → NO_COVERAGE

139

1.1
Location : parseAaguid
Killed by : none
negated conditional → NO_COVERAGE

140

1.1
Location : parseAaguid
Killed by : none
negated conditional → NO_COVERAGE

144

1.1
Location : parseAaguid
Killed by : none
replaced return value with null for com/yubico/internal/util/CertificateParser::parseAaguid → NO_COVERAGE

156

1.1
Location : parseFidoAaguidExtension
Killed by : none
removed call to java/util/Optional::ifPresent → NO_COVERAGE

158

1.1
Location : lambda$parseFidoAaguidExtension$0
Killed by : none
negated conditional → NO_COVERAGE

165

1.1
Location : parseFidoAaguidExtension
Killed by : none
replaced return value with Optional.empty for com/yubico/internal/util/CertificateParser::parseFidoAaguidExtension → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT 1.15.0