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; | |
26 | ||
27 | import com.google.common.primitives.Bytes; | |
28 | import com.upokecenter.cbor.CBORObject; | |
29 | import com.yubico.webauthn.data.ByteArray; | |
30 | import com.yubico.webauthn.data.COSEAlgorithmIdentifier; | |
31 | import java.io.IOException; | |
32 | import java.math.BigInteger; | |
33 | import java.security.KeyFactory; | |
34 | import java.security.NoSuchAlgorithmException; | |
35 | import java.security.PublicKey; | |
36 | import java.security.interfaces.ECPublicKey; | |
37 | import java.security.spec.InvalidKeySpecException; | |
38 | import java.security.spec.RSAPublicKeySpec; | |
39 | import java.security.spec.X509EncodedKeySpec; | |
40 | import java.util.Arrays; | |
41 | import java.util.HashMap; | |
42 | import java.util.Map; | |
43 | import java.util.stream.Stream; | |
44 | ||
45 | final class WebAuthnCodecs { | |
46 | ||
47 | private static final ByteArray EC_PUBLIC_KEY_OID = | |
48 | new ByteArray( | |
49 | new byte[] { | |
50 | 0x2A, (byte) 0x86, 0x48, (byte) 0xCE, 0x3D, 2, 1 | |
51 | }); // OID 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type) | |
52 | private static final ByteArray P256_CURVE_OID = | |
53 | new ByteArray( | |
54 | new byte[] { | |
55 | 0x2A, (byte) 0x86, 0x48, (byte) 0xCE, 0x3D, 3, 1, 7 // OID 1.2.840.10045.3.1.7 | |
56 | }); | |
57 | private static final ByteArray P384_CURVE_OID = | |
58 | new ByteArray(new byte[] {0x2B, (byte) 0x81, 0x04, 0, 34}); // OID 1.3.132.0.34 | |
59 | private static final ByteArray P512_CURVE_OID = | |
60 | new ByteArray(new byte[] {0x2B, (byte) 0x81, 0x04, 0, 35}); // OID 1.3.132.0.35 | |
61 | ||
62 | private static final ByteArray ED25519_ALG_ID = | |
63 | new ByteArray( | |
64 | new byte[] { | |
65 | // SEQUENCE (5 bytes) | |
66 | 0x30, | |
67 | 5, | |
68 | // OID (3 bytes) | |
69 | 0x06, | |
70 | 3, | |
71 | // OID 1.3.101.112 | |
72 | 0x2B, | |
73 | 101, | |
74 | 112 | |
75 | }); | |
76 | ||
77 | static ByteArray ecPublicKeyToRaw(ECPublicKey key) { | |
78 | ||
79 | final int fieldSizeBytes = | |
80 | Math.toIntExact( | |
81 |
1
1. ecPublicKeyToRaw : Replaced double division with multiplication → KILLED |
Math.round(Math.ceil(key.getParams().getCurve().getField().getFieldSize() / 8.0))); |
82 | byte[] x = key.getW().getAffineX().toByteArray(); | |
83 | byte[] y = key.getW().getAffineY().toByteArray(); | |
84 |
1
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
byte[] xPadding = new byte[Math.max(0, fieldSizeBytes - x.length)]; |
85 |
1
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
byte[] yPadding = new byte[Math.max(0, fieldSizeBytes - y.length)]; |
86 | ||
87 |
1
1. ecPublicKeyToRaw : removed call to java/util/Arrays::fill → SURVIVED |
Arrays.fill(xPadding, (byte) 0); |
88 |
1
1. ecPublicKeyToRaw : removed call to java/util/Arrays::fill → SURVIVED |
Arrays.fill(yPadding, (byte) 0); |
89 | ||
90 |
2
1. ecPublicKeyToRaw : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::ecPublicKeyToRaw → KILLED 2. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
return new ByteArray( |
91 | Bytes.concat( | |
92 | new byte[] {0x04}, | |
93 | xPadding, | |
94 |
1
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
Arrays.copyOfRange(x, Math.max(0, x.length - fieldSizeBytes), x.length), |
95 | yPadding, | |
96 | Arrays.copyOfRange(y, Math.max(0, y.length - fieldSizeBytes), y.length))); | |
97 | } | |
98 | ||
99 | static ByteArray rawEcKeyToCose(ByteArray key) { | |
100 | final byte[] keyBytes = key.getBytes(); | |
101 | final int len = keyBytes.length; | |
102 |
1
1. rawEcKeyToCose : Replaced integer subtraction with addition → KILLED |
final int lenSub1 = keyBytes.length - 1; |
103 |
7
1. rawEcKeyToCose : negated conditional → SURVIVED 2. rawEcKeyToCose : negated conditional → SURVIVED 3. rawEcKeyToCose : negated conditional → NO_COVERAGE 4. rawEcKeyToCose : negated conditional → SURVIVED 5. rawEcKeyToCose : negated conditional → KILLED 6. rawEcKeyToCose : negated conditional → KILLED 7. rawEcKeyToCose : negated conditional → KILLED |
if (!(len == 64 |
104 | || len == 96 | |
105 | || len == 132 | |
106 | || (keyBytes[0] == 0x04 && (lenSub1 == 64 || lenSub1 == 96 || lenSub1 == 132)))) { | |
107 | throw new IllegalArgumentException( | |
108 | String.format( | |
109 | "Raw key must be 64, 96 or 132 bytes long, or start with 0x04 and be 65, 97 or 133 bytes long; was %d bytes starting with %02x", | |
110 | keyBytes.length, keyBytes[0])); | |
111 | } | |
112 |
3
1. rawEcKeyToCose : negated conditional → KILLED 2. rawEcKeyToCose : negated conditional → KILLED 3. rawEcKeyToCose : negated conditional → KILLED |
final int start = (len == 64 || len == 96 || len == 132) ? 0 : 1; |
113 |
2
1. rawEcKeyToCose : Replaced integer division with multiplication → KILLED 2. rawEcKeyToCose : Replaced integer subtraction with addition → KILLED |
final int coordinateLength = (len - start) / 2; |
114 | ||
115 | final Map<Long, Object> coseKey = new HashMap<>(); | |
116 | coseKey.put(1L, 2L); // Key type: EC | |
117 | ||
118 | final COSEAlgorithmIdentifier coseAlg; | |
119 | final int coseCrv; | |
120 |
1
1. rawEcKeyToCose : Replaced integer subtraction with addition → KILLED |
switch (len - start) { |
121 | case 64: | |
122 | coseAlg = COSEAlgorithmIdentifier.ES256; | |
123 | coseCrv = 1; | |
124 | break; | |
125 | case 96: | |
126 | coseAlg = COSEAlgorithmIdentifier.ES384; | |
127 | coseCrv = 2; | |
128 | break; | |
129 | case 132: | |
130 | coseAlg = COSEAlgorithmIdentifier.ES512; | |
131 | coseCrv = 3; | |
132 | break; | |
133 | default: | |
134 | throw new RuntimeException( | |
135 | "Failed to determine COSE EC algorithm. This should not be possible, please file a bug report."); | |
136 | } | |
137 | coseKey.put(3L, coseAlg.getId()); | |
138 | coseKey.put(-1L, coseCrv); | |
139 | ||
140 |
1
1. rawEcKeyToCose : Replaced integer addition with subtraction → KILLED |
coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + coordinateLength)); // x |
141 | coseKey.put( | |
142 |
3
1. rawEcKeyToCose : Replaced integer addition with subtraction → KILLED 2. rawEcKeyToCose : Replaced integer addition with subtraction → KILLED 3. rawEcKeyToCose : Replaced integer multiplication with division → KILLED |
-3L, |
143 | Arrays.copyOfRange(keyBytes, start + coordinateLength, start + 2 * coordinateLength)); // y | |
144 | ||
145 |
1
1. rawEcKeyToCose : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::rawEcKeyToCose → KILLED |
return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes()); |
146 | } | |
147 | ||
148 | static PublicKey importCosePublicKey(ByteArray key) | |
149 | throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { | |
150 | CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes()); | |
151 | final int kty = cose.get(CBORObject.FromObject(1)).AsInt32(); | |
152 | switch (kty) { | |
153 | case 1: | |
154 |
1
1. importCosePublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCosePublicKey → KILLED |
return importCoseEdDsaPublicKey(cose); |
155 | case 2: | |
156 |
1
1. importCosePublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCosePublicKey → KILLED |
return importCoseEcdsaPublicKey(cose); |
157 | case 3: | |
158 |
1
1. importCosePublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCosePublicKey → KILLED |
return importCoseRsaPublicKey(cose); |
159 | default: | |
160 | throw new IllegalArgumentException("Unsupported key type: " + kty); | |
161 | } | |
162 | } | |
163 | ||
164 | private static PublicKey importCoseRsaPublicKey(CBORObject cose) | |
165 | throws NoSuchAlgorithmException, InvalidKeySpecException { | |
166 | RSAPublicKeySpec spec = | |
167 | new RSAPublicKeySpec( | |
168 | new BigInteger(1, cose.get(CBORObject.FromObject(-1)).GetByteString()), | |
169 | new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString())); | |
170 |
1
1. importCoseRsaPublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseRsaPublicKey → KILLED |
return KeyFactory.getInstance("RSA").generatePublic(spec); |
171 | } | |
172 | ||
173 | private static PublicKey importCoseEcdsaPublicKey(CBORObject cose) | |
174 | throws NoSuchAlgorithmException, InvalidKeySpecException { | |
175 | final int crv = cose.get(CBORObject.FromObject(-1)).AsInt32Value(); | |
176 | final ByteArray x = new ByteArray(cose.get(CBORObject.FromObject(-2)).GetByteString()); | |
177 | final ByteArray y = new ByteArray(cose.get(CBORObject.FromObject(-3)).GetByteString()); | |
178 | ||
179 | final ByteArray curveOid; | |
180 | switch (crv) { | |
181 | case 1: | |
182 | curveOid = P256_CURVE_OID; | |
183 | break; | |
184 | ||
185 | case 2: | |
186 | curveOid = P384_CURVE_OID; | |
187 | break; | |
188 | ||
189 | case 3: | |
190 | curveOid = P512_CURVE_OID; | |
191 | break; | |
192 | ||
193 | default: | |
194 | throw new IllegalArgumentException("Unknown COSE EC2 curve: " + crv); | |
195 | } | |
196 | ||
197 | final ByteArray algId = | |
198 | encodeDerSequence(encodeDerObjectId(EC_PUBLIC_KEY_OID), encodeDerObjectId(curveOid)); | |
199 | ||
200 | final ByteArray rawKey = | |
201 | encodeDerBitStringWithZeroUnused( | |
202 | new ByteArray(new byte[] {0x04}) // Raw EC public key with x and y | |
203 | .concat(x) | |
204 | .concat(y)); | |
205 | ||
206 | final ByteArray x509Key = encodeDerSequence(algId, rawKey); | |
207 | ||
208 | KeyFactory kFact = KeyFactory.getInstance("EC"); | |
209 |
1
1. importCoseEcdsaPublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseEcdsaPublicKey → KILLED |
return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); |
210 | } | |
211 | ||
212 | private static ByteArray encodeDerLength(final int length) { | |
213 |
2
1. encodeDerLength : changed conditional boundary → SURVIVED 2. encodeDerLength : negated conditional → KILLED |
if (length <= 127) { |
214 |
1
1. encodeDerLength : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::encodeDerLength → KILLED |
return new ByteArray(new byte[] {(byte) length}); |
215 |
2
1. encodeDerLength : changed conditional boundary → SURVIVED 2. encodeDerLength : negated conditional → KILLED |
} else if (length <= 0xffff) { |
216 |
2
1. encodeDerLength : negated conditional → SURVIVED 2. encodeDerLength : changed conditional boundary → SURVIVED |
if (length <= 255) { |
217 |
1
1. encodeDerLength : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::encodeDerLength → KILLED |
return new ByteArray(new byte[] {-127, (byte) length}); |
218 | } else { | |
219 |
3
1. encodeDerLength : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::encodeDerLength → NO_COVERAGE 2. encodeDerLength : Replaced Shift Right with Shift Left → NO_COVERAGE 3. encodeDerLength : Replaced bitwise AND with OR → NO_COVERAGE |
return new ByteArray(new byte[] {-126, (byte) (length >> 8), (byte) (length & 0x00ff)}); |
220 | } | |
221 | } else { | |
222 | throw new UnsupportedOperationException("Too long: " + length); | |
223 | } | |
224 | } | |
225 | ||
226 | private static ByteArray encodeDerObjectId(final ByteArray oid) { | |
227 |
1
1. encodeDerObjectId : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::encodeDerObjectId → KILLED |
return new ByteArray(new byte[] {0x06, (byte) oid.size()}).concat(oid); |
228 | } | |
229 | ||
230 | private static ByteArray encodeDerBitStringWithZeroUnused(final ByteArray content) { | |
231 |
1
1. encodeDerBitStringWithZeroUnused : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::encodeDerBitStringWithZeroUnused → KILLED |
return new ByteArray(new byte[] {0x03}) |
232 |
1
1. encodeDerBitStringWithZeroUnused : Replaced integer addition with subtraction → KILLED |
.concat(encodeDerLength(1 + content.size())) |
233 | .concat(new ByteArray(new byte[] {0})) | |
234 | .concat(content); | |
235 | } | |
236 | ||
237 | private static ByteArray encodeDerSequence(final ByteArray... items) { | |
238 | final ByteArray content = | |
239 |
1
1. lambda$encodeDerSequence$0 : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::lambda$encodeDerSequence$0 → NO_COVERAGE |
Stream.of(items).reduce(ByteArray::concat).orElseGet(() -> new ByteArray(new byte[0])); |
240 |
1
1. encodeDerSequence : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::encodeDerSequence → KILLED |
return new ByteArray(new byte[] {0x30}).concat(encodeDerLength(content.size())).concat(content); |
241 | } | |
242 | ||
243 | private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) | |
244 | throws InvalidKeySpecException, NoSuchAlgorithmException { | |
245 | final int curveId = cose.get(CBORObject.FromObject(-1)).AsInt32(); | |
246 | switch (curveId) { | |
247 | case 6: | |
248 |
1
1. importCoseEdDsaPublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseEdDsaPublicKey → KILLED |
return importCoseEd25519PublicKey(cose); |
249 | default: | |
250 | throw new IllegalArgumentException("Unsupported EdDSA curve: " + curveId); | |
251 | } | |
252 | } | |
253 | ||
254 | private static PublicKey importCoseEd25519PublicKey(CBORObject cose) | |
255 | throws InvalidKeySpecException, NoSuchAlgorithmException { | |
256 | final ByteArray rawKey = new ByteArray(cose.get(CBORObject.FromObject(-2)).GetByteString()); | |
257 | final ByteArray x509Key = | |
258 | encodeDerSequence(ED25519_ALG_ID, encodeDerBitStringWithZeroUnused(rawKey)); | |
259 | ||
260 | KeyFactory kFact = KeyFactory.getInstance("EdDSA"); | |
261 |
1
1. importCoseEd25519PublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseEd25519PublicKey → KILLED |
return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); |
262 | } | |
263 | ||
264 | static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) { | |
265 | switch (alg) { | |
266 | case EdDSA: | |
267 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "EDDSA"; |
268 | case ES256: | |
269 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA256withECDSA"; |
270 | case ES384: | |
271 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA384withECDSA"; |
272 | case ES512: | |
273 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA512withECDSA"; |
274 | case RS256: | |
275 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA256withRSA"; |
276 | case RS384: | |
277 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA384withRSA"; |
278 | case RS512: | |
279 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA512withRSA"; |
280 | case RS1: | |
281 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA1withRSA"; |
282 | default: | |
283 | throw new IllegalArgumentException("Unknown algorithm: " + alg); | |
284 | } | |
285 | } | |
286 | ||
287 | static String jwsAlgorithmNameToJavaAlgorithmName(String alg) { | |
288 |
1
1. jwsAlgorithmNameToJavaAlgorithmName : negated conditional → KILLED |
switch (alg) { |
289 | case "RS256": | |
290 |
1
1. jwsAlgorithmNameToJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::jwsAlgorithmNameToJavaAlgorithmName → KILLED |
return "SHA256withRSA"; |
291 | } | |
292 | throw new IllegalArgumentException("Unknown algorithm: " + alg); | |
293 | } | |
294 | } | |
Mutations | ||
81 |
1.1 |
|
84 |
1.1 |
|
85 |
1.1 |
|
87 |
1.1 |
|
88 |
1.1 |
|
90 |
1.1 2.2 |
|
94 |
1.1 |
|
102 |
1.1 |
|
103 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 |
|
112 |
1.1 2.2 3.3 |
|
113 |
1.1 2.2 |
|
120 |
1.1 |
|
140 |
1.1 |
|
142 |
1.1 2.2 3.3 |
|
145 |
1.1 |
|
154 |
1.1 |
|
156 |
1.1 |
|
158 |
1.1 |
|
170 |
1.1 |
|
209 |
1.1 |
|
213 |
1.1 2.2 |
|
214 |
1.1 |
|
215 |
1.1 2.2 |
|
216 |
1.1 2.2 |
|
217 |
1.1 |
|
219 |
1.1 2.2 3.3 |
|
227 |
1.1 |
|
231 |
1.1 |
|
232 |
1.1 |
|
239 |
1.1 |
|
240 |
1.1 |
|
248 |
1.1 |
|
261 |
1.1 |
|
267 |
1.1 |
|
269 |
1.1 |
|
271 |
1.1 |
|
273 |
1.1 |
|
275 |
1.1 |
|
277 |
1.1 |
|
279 |
1.1 |
|
281 |
1.1 |
|
288 |
1.1 |
|
290 |
1.1 |