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 |
|
65 |
1.1 |
|
69 |
1.1 |
|
77 |
1.1 |
|
80 |
1.1 2.2 |
|
81 |
1.1 |
|
95 |
1.1 |
|
117 |
1.1 2.2 |
|
119 |
1.1 |
|
134 |
1.1 2.2 |
|
137 |
1.1 |
|
138 |
1.1 |
|
139 |
1.1 |
|
140 |
1.1 |
|
144 |
1.1 |
|
156 |
1.1 |
|
158 |
1.1 |
|
165 |
1.1 |