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.net.MalformedURLException; | |
30 | import java.net.URL; | |
31 | import java.nio.ByteBuffer; | |
32 | import java.nio.charset.StandardCharsets; | |
33 | import java.security.MessageDigest; | |
34 | import java.security.NoSuchAlgorithmException; | |
35 | import java.security.cert.Certificate; | |
36 | import java.security.cert.CertificateException; | |
37 | import java.security.cert.CertificateFactory; | |
38 | import java.security.cert.X509Certificate; | |
39 | import java.util.ArrayList; | |
40 | import java.util.Arrays; | |
41 | import java.util.Base64; | |
42 | import java.util.Collection; | |
43 | import java.util.Collections; | |
44 | import java.util.List; | |
45 | import java.util.Optional; | |
46 | import lombok.Value; | |
47 | ||
48 | public class CertificateParser { | |
49 | public static final String ID_FIDO_GEN_CE_AAGUID = "1.3.6.1.4.1.45724.1.1.4"; | |
50 | public static final String OID_CRL_DISTRIBUTION_POINTS = "2.5.29.31"; | |
51 | private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); | |
52 | ||
53 | private static final List<String> FIXSIG = | |
54 | Arrays.asList( | |
55 | "CN=Yubico U2F EE Serial 776137165", | |
56 | "CN=Yubico U2F EE Serial 1086591525", | |
57 | "CN=Yubico U2F EE Serial 1973679733", | |
58 | "CN=Yubico U2F EE Serial 13503277888", | |
59 | "CN=Yubico U2F EE Serial 13831167861", | |
60 | "CN=Yubico U2F EE Serial 14803321578"); | |
61 | ||
62 | private static final int UNUSED_BITS_BYTE_INDEX_FROM_END = 257; | |
63 | ||
64 | public static X509Certificate parsePem(String pemEncodedCert) throws CertificateException { | |
65 |
1
1. parsePem : replaced return value with null for com/yubico/internal/util/CertificateParser::parsePem → KILLED |
return parseDer( |
66 | pemEncodedCert | |
67 | .replaceAll("-----BEGIN CERTIFICATE-----", "") | |
68 | .replaceAll("-----END CERTIFICATE-----", "") | |
69 | .replaceAll("\n", "")); | |
70 | } | |
71 | ||
72 | public static X509Certificate parseDer(String base64DerEncodedCert) throws CertificateException { | |
73 |
1
1. parseDer : replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED |
return parseDer(BASE64_DECODER.decode(base64DerEncodedCert)); |
74 | } | |
75 | ||
76 | public static X509Certificate parseDer(byte[] derEncodedCert) throws CertificateException { | |
77 |
1
1. parseDer : replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED |
return parseDer(new ByteArrayInputStream(derEncodedCert)); |
78 | } | |
79 | ||
80 | public static X509Certificate parseDer(InputStream is) throws CertificateException { | |
81 | X509Certificate cert = | |
82 | (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); | |
83 | // Some known certs have an incorrect "unused bits" value, which causes problems on newer | |
84 | // versions of BouncyCastle. | |
85 |
1
1. parseDer : negated conditional → SURVIVED |
if (FIXSIG.contains(cert.getSubjectX500Principal().getName())) { |
86 | byte[] encoded = cert.getEncoded(); | |
87 | ||
88 |
2
1. parseDer : changed conditional boundary → SURVIVED 2. parseDer : negated conditional → KILLED |
if (encoded.length >= UNUSED_BITS_BYTE_INDEX_FROM_END) { |
89 |
1
1. parseDer : Replaced integer subtraction with addition → KILLED |
encoded[encoded.length - UNUSED_BITS_BYTE_INDEX_FROM_END] = |
90 | 0; // Fix the "unused bits" field (should always be 0). | |
91 | } else { | |
92 | throw new IllegalArgumentException( | |
93 | String.format( | |
94 | "Expected DER encoded cert to be at least %d bytes, was %d: %s", | |
95 | UNUSED_BITS_BYTE_INDEX_FROM_END, encoded.length, cert)); | |
96 | } | |
97 | ||
98 | cert = | |
99 | (X509Certificate) | |
100 | CertificateFactory.getInstance("X.509") | |
101 | .generateCertificate(new ByteArrayInputStream(encoded)); | |
102 | } | |
103 |
1
1. parseDer : replaced return value with null for com/yubico/internal/util/CertificateParser::parseDer → KILLED |
return cert; |
104 | } | |
105 | ||
106 | /** | |
107 | * Compute a Subject Key Identifier as defined as method (1) in RFC 5280 section 4.2.1.2. | |
108 | * | |
109 | * @throws NoSuchAlgorithmException if the SHA-1 hash algorithm is not available. | |
110 | * @see <a href="https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2">Internet X.509 | |
111 | * Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile, | |
112 | * section 4.2.1.2. Subject Key Identifier</a> | |
113 | */ | |
114 | public static byte[] computeSubjectKeyIdentifier(final Certificate cert) | |
115 | throws NoSuchAlgorithmException { | |
116 | final byte[] spki = cert.getPublicKey().getEncoded(); | |
117 | ||
118 | // SubjectPublicKeyInfo ::= SEQUENCE { | |
119 | // algorithm AlgorithmIdentifier, | |
120 | // subjectPublicKey BIT STRING } | |
121 | final byte algLength = spki[2 + 1]; | |
122 | ||
123 | // BIT STRING begins with one octet specifying number of unused bits at end; | |
124 | // this is not included in the content to hash for a Subject Key Identifier. | |
125 |
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; |
126 | ||
127 |
1
1. computeSubjectKeyIdentifier : replaced return value with null for com/yubico/internal/util/CertificateParser::computeSubjectKeyIdentifier → KILLED |
return MessageDigest.getInstance("SHA-1") |
128 | .digest(Arrays.copyOfRange(spki, spkBitsStart, spki.length)); | |
129 | } | |
130 | ||
131 | /** | |
132 | * Parses an AAGUID into bytes. Refer to <a | |
133 | * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-packed-attestation-cert-requirements">Packed | |
134 | * Attestation Statement Certificate Requirements</a> on the W3C web site for details of the ASN.1 | |
135 | * structure that this method parses. | |
136 | * | |
137 | * @param bytes the bytes making up value of the extension | |
138 | * @return the bytes of the AAGUID | |
139 | */ | |
140 | private static byte[] parseAaguid(byte[] bytes) { | |
141 | ||
142 |
2
1. parseAaguid : negated conditional → NO_COVERAGE 2. parseAaguid : negated conditional → NO_COVERAGE |
if (bytes != null && bytes.length == 20) { |
143 | ByteBuffer buffer = ByteBuffer.wrap(bytes); | |
144 | ||
145 |
1
1. parseAaguid : negated conditional → NO_COVERAGE |
if (buffer.get() == (byte) 0x04 |
146 |
1
1. parseAaguid : negated conditional → NO_COVERAGE |
&& buffer.get() == (byte) 0x12 |
147 |
1
1. parseAaguid : negated conditional → NO_COVERAGE |
&& buffer.get() == (byte) 0x04 |
148 |
1
1. parseAaguid : negated conditional → NO_COVERAGE |
&& buffer.get() == (byte) 0x10) { |
149 | byte[] aaguidBytes = new byte[16]; | |
150 | buffer.get(aaguidBytes); | |
151 | ||
152 |
1
1. parseAaguid : replaced return value with null for com/yubico/internal/util/CertificateParser::parseAaguid → NO_COVERAGE |
return aaguidBytes; |
153 | } | |
154 | } | |
155 | ||
156 | throw new IllegalArgumentException( | |
157 | "X.509 extension 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) is not valid."); | |
158 | } | |
159 | ||
160 | public static Optional<byte[]> parseFidoAaguidExtension(X509Certificate cert) { | |
161 | Optional<byte[]> result = | |
162 | Optional.ofNullable(cert.getExtensionValue(ID_FIDO_GEN_CE_AAGUID)) | |
163 | .map(CertificateParser::parseAaguid); | |
164 |
1
1. parseFidoAaguidExtension : removed call to java/util/Optional::ifPresent → NO_COVERAGE |
result.ifPresent( |
165 | aaguid -> { | |
166 |
1
1. lambda$parseFidoAaguidExtension$0 : negated conditional → NO_COVERAGE |
if (cert.getCriticalExtensionOIDs().contains(ID_FIDO_GEN_CE_AAGUID)) { |
167 | throw new IllegalArgumentException( | |
168 | String.format( | |
169 | "X.509 extension %s (id-fido-gen-ce-aaguid) must not be marked critical.", | |
170 | ID_FIDO_GEN_CE_AAGUID)); | |
171 | } | |
172 | }); | |
173 |
1
1. parseFidoAaguidExtension : replaced return value with Optional.empty for com/yubico/internal/util/CertificateParser::parseFidoAaguidExtension → NO_COVERAGE |
return result; |
174 | } | |
175 | ||
176 | @Value | |
177 | public static class ParseCrlDistributionPointsExtensionResult { | |
178 | /** | |
179 | * The successfully parsed distribution point URLs. If the CRLDistributionPoints extension is | |
180 | * not present, this will be an empty list. | |
181 | */ | |
182 | Collection<URL> distributionPoints; | |
183 | ||
184 | /** | |
185 | * True if and only if the CRLDistributionPoints extension is present and contains anything that | |
186 | * is not a <code>distributionPoint [0] DistributionPointName</code> containing a <code> | |
187 | * fullName [0] GeneralNames</code> containing exactly one <code> | |
188 | * uniformResourceIdentifier [6] IA5String</code> | |
189 | */ | |
190 | boolean anyDistributionPointUnsupported; | |
191 | } | |
192 | ||
193 | public static ParseCrlDistributionPointsExtensionResult parseCrlDistributionPointsExtension( | |
194 | X509Certificate cert) { | |
195 | final byte[] crldpExtension = cert.getExtensionValue(OID_CRL_DISTRIBUTION_POINTS); | |
196 |
1
1. parseCrlDistributionPointsExtension : negated conditional → NO_COVERAGE |
if (crldpExtension != null) { |
197 | BinaryUtil.ParseDerResult<byte[]> octetString = | |
198 | BinaryUtil.parseDerOctetString(crldpExtension, 0); | |
199 | try { | |
200 | BinaryUtil.ParseDerResult<List<List<List<Optional<URL>>>>> distributionPoints = | |
201 | BinaryUtil.parseDerSequence( | |
202 | octetString.result, | |
203 | 0, | |
204 | (outerSequenceDer, distributionPointOffset) -> | |
205 |
1
1. lambda$parseCrlDistributionPointsExtension$3 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$3 → NO_COVERAGE |
BinaryUtil.parseDerSequence( |
206 | outerSequenceDer, | |
207 | distributionPointOffset, | |
208 | (innerSequenceDer, distributionPointChoiceOffset) -> { | |
209 | // DistributionPoint ::= SEQUENCE { | |
210 | // distributionPoint [0] DistributionPointName OPTIONAL, | |
211 | final BinaryUtil.ParseDerResult<Optional<Integer>> dpElementOffsets = | |
212 | BinaryUtil.parseDerTaggedOrSkip( | |
213 | innerSequenceDer, | |
214 | distributionPointChoiceOffset, | |
215 | (byte) 0, | |
216 | true, | |
217 | BinaryUtil.DerTagClass.CONTEXT_SPECIFIC); | |
218 |
1
1. lambda$parseCrlDistributionPointsExtension$2 : negated conditional → NO_COVERAGE |
if (dpElementOffsets.result.isPresent()) { |
219 | ||
220 | // DistributionPointName ::= CHOICE { | |
221 | // fullName [0] GeneralNames, | |
222 | final BinaryUtil.ParseDerResult<Optional<Integer>> | |
223 | dpNameElementOffsets = | |
224 | BinaryUtil.parseDerTaggedOrSkip( | |
225 | innerSequenceDer, | |
226 | dpElementOffsets.result.get(), | |
227 | (byte) 0, | |
228 | true, | |
229 | BinaryUtil.DerTagClass.CONTEXT_SPECIFIC); | |
230 | ||
231 |
1
1. lambda$parseCrlDistributionPointsExtension$2 : negated conditional → NO_COVERAGE |
if (dpNameElementOffsets.result.isPresent()) { |
232 |
1
1. lambda$parseCrlDistributionPointsExtension$2 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$2 → NO_COVERAGE |
return BinaryUtil.parseDerSequenceContents( |
233 | innerSequenceDer, | |
234 | dpNameElementOffsets.result.get(), | |
235 | dpNameElementOffsets.nextOffset, | |
236 | (generalNamesDer, generalNamesElementOffset) -> { | |
237 | // fullName [0] GeneralNames, | |
238 | // GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName | |
239 | // GeneralName ::= CHOICE { | |
240 | // uniformResourceIdentifier [6] IA5String, | |
241 | // | |
242 | // GeneralNames is defined in RFC 5280 appendix 2 which uses | |
243 | // IMPLICIT tagging | |
244 | // https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.2 | |
245 | // so the SEQUENCE tag in GeneralNames is implicit. | |
246 | // The IA5String tag is also implicit from the CHOICE tag. | |
247 | final BinaryUtil.ParseDerResult<Optional<Integer>> | |
248 | generalNameOffsets = | |
249 | BinaryUtil.parseDerTaggedOrSkip( | |
250 | generalNamesDer, | |
251 | generalNamesElementOffset, | |
252 | (byte) 6, | |
253 | false, | |
254 | BinaryUtil.DerTagClass.CONTEXT_SPECIFIC); | |
255 |
1
1. lambda$parseCrlDistributionPointsExtension$1 : negated conditional → NO_COVERAGE |
if (generalNameOffsets.result.isPresent()) { |
256 | String uriString = | |
257 | new String( | |
258 | Arrays.copyOfRange( | |
259 | generalNamesDer, | |
260 | generalNameOffsets.result.get(), | |
261 | generalNameOffsets.nextOffset), | |
262 | StandardCharsets.US_ASCII); | |
263 | try { | |
264 |
1
1. lambda$parseCrlDistributionPointsExtension$1 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$1 → NO_COVERAGE |
return new BinaryUtil.ParseDerResult<>( |
265 | Optional.of(new URL(uriString)), | |
266 | generalNameOffsets.nextOffset); | |
267 | } catch (MalformedURLException e) { | |
268 | throw new IllegalArgumentException( | |
269 | String.format( | |
270 | "Invalid URL in CRLDistributionPoints: %s", | |
271 | uriString), | |
272 | e); | |
273 | } | |
274 | } else { | |
275 |
1
1. lambda$parseCrlDistributionPointsExtension$1 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$1 → NO_COVERAGE |
return new BinaryUtil.ParseDerResult<>( |
276 | Optional.empty(), generalNameOffsets.nextOffset); | |
277 | } | |
278 | }); | |
279 | } | |
280 | } | |
281 | ||
282 | // Ignore all other forms of distribution points | |
283 |
1
1. lambda$parseCrlDistributionPointsExtension$2 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$2 → NO_COVERAGE |
return new BinaryUtil.ParseDerResult<>( |
284 | Collections.emptyList(), dpElementOffsets.nextOffset); | |
285 | })); | |
286 | ||
287 |
1
1. parseCrlDistributionPointsExtension : replaced return value with null for com/yubico/internal/util/CertificateParser::parseCrlDistributionPointsExtension → NO_COVERAGE |
return distributionPoints.result.stream() |
288 | .flatMap(Collection::stream) | |
289 | .flatMap(Collection::stream) | |
290 | .reduce( | |
291 | new ParseCrlDistributionPointsExtensionResult(new ArrayList<>(), false), | |
292 | (result, next) -> { | |
293 |
1
1. lambda$parseCrlDistributionPointsExtension$4 : negated conditional → NO_COVERAGE |
if (next.isPresent()) { |
294 | List<URL> dp = new ArrayList<>(result.distributionPoints); | |
295 | dp.add(next.get()); | |
296 |
1
1. lambda$parseCrlDistributionPointsExtension$4 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$4 → NO_COVERAGE |
return new ParseCrlDistributionPointsExtensionResult( |
297 | dp, result.anyDistributionPointUnsupported); | |
298 | } else { | |
299 |
1
1. lambda$parseCrlDistributionPointsExtension$4 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$4 → NO_COVERAGE |
return new ParseCrlDistributionPointsExtensionResult( |
300 | result.distributionPoints, true); | |
301 | } | |
302 | }, | |
303 | (resultA, resultB) -> { | |
304 | List<URL> dp = new ArrayList<>(resultA.distributionPoints); | |
305 | dp.addAll(resultB.distributionPoints); | |
306 |
1
1. lambda$parseCrlDistributionPointsExtension$5 : replaced return value with null for com/yubico/internal/util/CertificateParser::lambda$parseCrlDistributionPointsExtension$5 → NO_COVERAGE |
return new ParseCrlDistributionPointsExtensionResult( |
307 | dp, | |
308 |
1
1. lambda$parseCrlDistributionPointsExtension$5 : negated conditional → NO_COVERAGE |
resultA.anyDistributionPointUnsupported |
309 |
1
1. lambda$parseCrlDistributionPointsExtension$5 : negated conditional → NO_COVERAGE |
|| resultB.anyDistributionPointUnsupported); |
310 | }); | |
311 | ||
312 | } catch (IllegalArgumentException e) { | |
313 | throw new IllegalArgumentException( | |
314 | String.format( | |
315 | "X.509 extension %s (id-ce-cRLDistributionPoints) is incorrectly encoded.", | |
316 | OID_CRL_DISTRIBUTION_POINTS), | |
317 | e); | |
318 | } | |
319 | ||
320 | } else { | |
321 |
1
1. parseCrlDistributionPointsExtension : replaced return value with null for com/yubico/internal/util/CertificateParser::parseCrlDistributionPointsExtension → NO_COVERAGE |
return new ParseCrlDistributionPointsExtensionResult(Collections.emptySet(), false); |
322 | } | |
323 | } | |
324 | } | |
Mutations | ||
65 |
1.1 |
|
73 |
1.1 |
|
77 |
1.1 |
|
85 |
1.1 |
|
88 |
1.1 2.2 |
|
89 |
1.1 |
|
103 |
1.1 |
|
125 |
1.1 2.2 |
|
127 |
1.1 |
|
142 |
1.1 2.2 |
|
145 |
1.1 |
|
146 |
1.1 |
|
147 |
1.1 |
|
148 |
1.1 |
|
152 |
1.1 |
|
164 |
1.1 |
|
166 |
1.1 |
|
173 |
1.1 |
|
196 |
1.1 |
|
205 |
1.1 |
|
218 |
1.1 |
|
231 |
1.1 |
|
232 |
1.1 |
|
255 |
1.1 |
|
264 |
1.1 |
|
275 |
1.1 |
|
283 |
1.1 |
|
287 |
1.1 |
|
293 |
1.1 |
|
296 |
1.1 |
|
299 |
1.1 |
|
306 |
1.1 |
|
308 |
1.1 |
|
309 |
1.1 |
|
321 |
1.1 |