| 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.internal.util.BinaryUtil; | |
| 30 | import com.yubico.webauthn.data.ByteArray; | |
| 31 | import com.yubico.webauthn.data.COSEAlgorithmIdentifier; | |
| 32 | import java.io.IOException; | |
| 33 | import java.math.BigInteger; | |
| 34 | import java.security.KeyFactory; | |
| 35 | import java.security.NoSuchAlgorithmException; | |
| 36 | import java.security.PublicKey; | |
| 37 | import java.security.interfaces.ECPublicKey; | |
| 38 | import java.security.spec.InvalidKeySpecException; | |
| 39 | import java.security.spec.RSAPublicKeySpec; | |
| 40 | import java.security.spec.X509EncodedKeySpec; | |
| 41 | import java.util.Arrays; | |
| 42 | import java.util.HashMap; | |
| 43 | import java.util.Map; | |
| 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 | 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 final ByteArray ED448_ALG_ID = | |
| 78 | new ByteArray( | |
| 79 | new byte[] { | |
| 80 | // SEQUENCE (5 bytes) | |
| 81 | 0x30, | |
| 82 | 5, | |
| 83 | // OID (3 bytes) | |
| 84 | 0x06, | |
| 85 | 3, | |
| 86 | // OID 1.3.101.113 | |
| 87 | 0x2B, | |
| 88 | 101, | |
| 89 | 113 | |
| 90 | }); | |
| 91 | ||
| 92 | // See: https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves | |
| 93 | static final int COSE_CRV_P256 = 1; | |
| 94 | static final int COSE_CRV_P384 = 2; | |
| 95 | static final int COSE_CRV_P521 = 3; | |
| 96 | static final int COSE_CRV_ED25519 = 6; | |
| 97 | static final int COSE_CRV_ED448 = 7; | |
| 98 | ||
| 99 | static ByteArray ecPublicKeyToRaw(ECPublicKey key) { | |
| 100 | ||
| 101 | final int fieldSizeBytes = | |
| 102 | Math.toIntExact( | |
| 103 |
1
1. ecPublicKeyToRaw : Replaced double division with multiplication → KILLED |
Math.round(Math.ceil(key.getParams().getCurve().getField().getFieldSize() / 8.0))); |
| 104 | byte[] x = key.getW().getAffineX().toByteArray(); | |
| 105 | byte[] y = key.getW().getAffineY().toByteArray(); | |
| 106 |
1
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
byte[] xPadding = new byte[Math.max(0, fieldSizeBytes - x.length)]; |
| 107 |
1
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
byte[] yPadding = new byte[Math.max(0, fieldSizeBytes - y.length)]; |
| 108 | ||
| 109 |
1
1. ecPublicKeyToRaw : removed call to java/util/Arrays::fill → SURVIVED |
Arrays.fill(xPadding, (byte) 0); |
| 110 |
1
1. ecPublicKeyToRaw : removed call to java/util/Arrays::fill → SURVIVED |
Arrays.fill(yPadding, (byte) 0); |
| 111 | ||
| 112 |
2
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED 2. ecPublicKeyToRaw : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::ecPublicKeyToRaw → KILLED |
return new ByteArray( |
| 113 | Bytes.concat( | |
| 114 | new byte[] {0x04}, | |
| 115 | xPadding, | |
| 116 |
1
1. ecPublicKeyToRaw : Replaced integer subtraction with addition → KILLED |
Arrays.copyOfRange(x, Math.max(0, x.length - fieldSizeBytes), x.length), |
| 117 | yPadding, | |
| 118 | Arrays.copyOfRange(y, Math.max(0, y.length - fieldSizeBytes), y.length))); | |
| 119 | } | |
| 120 | ||
| 121 | static ByteArray rawEcKeyToCose(ByteArray key) { | |
| 122 | final byte[] keyBytes = key.getBytes(); | |
| 123 | final int len = keyBytes.length; | |
| 124 |
1
1. rawEcKeyToCose : Replaced integer subtraction with addition → KILLED |
final int lenSub1 = keyBytes.length - 1; |
| 125 |
7
1. rawEcKeyToCose : negated conditional → SURVIVED 2. rawEcKeyToCose : negated conditional → SURVIVED 3. rawEcKeyToCose : negated conditional → SURVIVED 4. rawEcKeyToCose : negated conditional → NO_COVERAGE 5. rawEcKeyToCose : negated conditional → KILLED 6. rawEcKeyToCose : negated conditional → KILLED 7. rawEcKeyToCose : negated conditional → KILLED |
if (!(len == 64 |
| 126 | || len == 96 | |
| 127 | || len == 132 | |
| 128 | || (keyBytes[0] == 0x04 && (lenSub1 == 64 || lenSub1 == 96 || lenSub1 == 132)))) { | |
| 129 | throw new IllegalArgumentException( | |
| 130 | String.format( | |
| 131 | "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", | |
| 132 | keyBytes.length, keyBytes[0])); | |
| 133 | } | |
| 134 |
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; |
| 135 |
2
1. rawEcKeyToCose : Replaced integer subtraction with addition → KILLED 2. rawEcKeyToCose : Replaced integer division with multiplication → KILLED |
final int coordinateLength = (len - start) / 2; |
| 136 | ||
| 137 | final Map<Long, Object> coseKey = new HashMap<>(); | |
| 138 | coseKey.put(1L, 2L); // Key type: EC | |
| 139 | ||
| 140 | final COSEAlgorithmIdentifier coseAlg; | |
| 141 | final int coseCrv; | |
| 142 |
1
1. rawEcKeyToCose : Replaced integer subtraction with addition → KILLED |
switch (len - start) { |
| 143 | case 64: | |
| 144 | coseAlg = COSEAlgorithmIdentifier.ES256; | |
| 145 | coseCrv = COSE_CRV_P256; | |
| 146 | break; | |
| 147 | case 96: | |
| 148 | coseAlg = COSEAlgorithmIdentifier.ES384; | |
| 149 | coseCrv = COSE_CRV_P384; | |
| 150 | break; | |
| 151 | case 132: | |
| 152 | coseAlg = COSEAlgorithmIdentifier.ES512; | |
| 153 | coseCrv = COSE_CRV_P521; | |
| 154 | break; | |
| 155 | default: | |
| 156 | throw new RuntimeException( | |
| 157 | "Failed to determine COSE EC algorithm. This should not be possible, please file a bug report."); | |
| 158 | } | |
| 159 | coseKey.put(3L, coseAlg.getId()); | |
| 160 | coseKey.put(-1L, coseCrv); | |
| 161 | ||
| 162 |
1
1. rawEcKeyToCose : Replaced integer addition with subtraction → KILLED |
coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + coordinateLength)); // x |
| 163 | coseKey.put( | |
| 164 |
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, |
| 165 | Arrays.copyOfRange(keyBytes, start + coordinateLength, start + 2 * coordinateLength)); // y | |
| 166 | ||
| 167 |
1
1. rawEcKeyToCose : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::rawEcKeyToCose → KILLED |
return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes()); |
| 168 | } | |
| 169 | ||
| 170 | static PublicKey importCosePublicKey(ByteArray key) | |
| 171 | throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { | |
| 172 | CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes()); | |
| 173 | final int kty = cose.get(CBORObject.FromObject(1)).AsInt32(); | |
| 174 | switch (kty) { | |
| 175 | case 1: | |
| 176 |
1
1. importCosePublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCosePublicKey → KILLED |
return importCoseEdDsaPublicKey(cose); |
| 177 | case 2: | |
| 178 |
1
1. importCosePublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCosePublicKey → KILLED |
return importCoseEcdsaPublicKey(cose); |
| 179 | case 3: | |
| 180 |
1
1. importCosePublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCosePublicKey → KILLED |
return importCoseRsaPublicKey(cose); |
| 181 | default: | |
| 182 | throw new IllegalArgumentException("Unsupported key type: " + kty); | |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | private static PublicKey importCoseRsaPublicKey(CBORObject cose) | |
| 187 | throws NoSuchAlgorithmException, InvalidKeySpecException { | |
| 188 | RSAPublicKeySpec spec = | |
| 189 | new RSAPublicKeySpec( | |
| 190 | new BigInteger(1, cose.get(CBORObject.FromObject(-1)).GetByteString()), | |
| 191 | new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString())); | |
| 192 |
1
1. importCoseRsaPublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseRsaPublicKey → KILLED |
return KeyFactory.getInstance("RSA").generatePublic(spec); |
| 193 | } | |
| 194 | ||
| 195 | private static PublicKey importCoseEcdsaPublicKey(CBORObject cose) | |
| 196 | throws NoSuchAlgorithmException, InvalidKeySpecException { | |
| 197 | final int crv = cose.get(CBORObject.FromObject(-1)).AsInt32Value(); | |
| 198 | final byte[] x = cose.get(CBORObject.FromObject(-2)).GetByteString(); | |
| 199 | final byte[] y = cose.get(CBORObject.FromObject(-3)).GetByteString(); | |
| 200 | ||
| 201 | final byte[] curveOid; | |
| 202 | switch (crv) { | |
| 203 | case COSE_CRV_P256: | |
| 204 | curveOid = P256_CURVE_OID.getBytes(); | |
| 205 | break; | |
| 206 | ||
| 207 | case COSE_CRV_P384: | |
| 208 | curveOid = P384_CURVE_OID.getBytes(); | |
| 209 | break; | |
| 210 | ||
| 211 | case COSE_CRV_P521: | |
| 212 | curveOid = P512_CURVE_OID.getBytes(); | |
| 213 | break; | |
| 214 | ||
| 215 | default: | |
| 216 | throw new IllegalArgumentException("Unknown COSE EC2 curve: " + crv); | |
| 217 | } | |
| 218 | ||
| 219 | final byte[] algId = | |
| 220 | BinaryUtil.encodeDerSequence( | |
| 221 | BinaryUtil.encodeDerObjectId(EC_PUBLIC_KEY_OID.getBytes()), | |
| 222 | BinaryUtil.encodeDerObjectId(curveOid)); | |
| 223 | ||
| 224 | final byte[] rawKey = | |
| 225 | BinaryUtil.encodeDerBitStringWithZeroUnused( | |
| 226 | BinaryUtil.concat( | |
| 227 | new byte[] {0x04}, // Raw EC public key with x and y | |
| 228 | x, | |
| 229 | y)); | |
| 230 | ||
| 231 | final byte[] x509Key = BinaryUtil.encodeDerSequence(algId, rawKey); | |
| 232 | ||
| 233 | KeyFactory kFact = KeyFactory.getInstance("EC"); | |
| 234 |
1
1. importCoseEcdsaPublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseEcdsaPublicKey → KILLED |
return kFact.generatePublic(new X509EncodedKeySpec(x509Key)); |
| 235 | } | |
| 236 | ||
| 237 | private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) | |
| 238 | throws InvalidKeySpecException, NoSuchAlgorithmException { | |
| 239 | final int alg = cose.get(CBORObject.FromObject(3)).AsInt32(); | |
| 240 | final int curveId = cose.get(CBORObject.FromObject(-1)).AsInt32(); | |
| 241 | final ByteArray algorithmOid = coseCurveToEddsaAlgorithmOid(curveId); | |
| 242 | final byte[] rawKey = cose.get(CBORObject.FromObject(-2)).GetByteString(); | |
| 243 | final byte[] x509Key = | |
| 244 | BinaryUtil.encodeDerSequence( | |
| 245 | algorithmOid.getBytes(), BinaryUtil.encodeDerBitStringWithZeroUnused(rawKey)); | |
| 246 | ||
| 247 | KeyFactory kFact = | |
| 248 | KeyFactory.getInstance( | |
| 249 | getJavaAlgorithmName( | |
| 250 | COSEAlgorithmIdentifier.fromId(alg) | |
| 251 |
1
1. lambda$importCoseEdDsaPublicKey$0 : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::lambda$importCoseEdDsaPublicKey$0 → NO_COVERAGE |
.orElseThrow(() -> new IllegalArgumentException("Unknown algorithm: " + alg)))); |
| 252 |
1
1. importCoseEdDsaPublicKey : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::importCoseEdDsaPublicKey → KILLED |
return kFact.generatePublic(new X509EncodedKeySpec(x509Key)); |
| 253 | } | |
| 254 | ||
| 255 | private static ByteArray coseCurveToEddsaAlgorithmOid(int curveId) { | |
| 256 | switch (curveId) { | |
| 257 | case COSE_CRV_ED25519: | |
| 258 |
1
1. coseCurveToEddsaAlgorithmOid : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::coseCurveToEddsaAlgorithmOid → KILLED |
return ED25519_ALG_ID; |
| 259 | case COSE_CRV_ED448: | |
| 260 |
1
1. coseCurveToEddsaAlgorithmOid : replaced return value with null for com/yubico/webauthn/WebAuthnCodecs::coseCurveToEddsaAlgorithmOid → KILLED |
return ED448_ALG_ID; |
| 261 | default: | |
| 262 | throw new IllegalArgumentException("Unsupported EdDSA curve: " + curveId); | |
| 263 | } | |
| 264 | } | |
| 265 | ||
| 266 | static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) { | |
| 267 | switch (alg) { | |
| 268 | case EdDSA: | |
| 269 | case Ed25519: | |
| 270 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "Ed25519"; |
| 271 | case Ed448: | |
| 272 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "Ed448"; |
| 273 | case ES256: | |
| 274 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA256withECDSA"; |
| 275 | case ES384: | |
| 276 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA384withECDSA"; |
| 277 | case ES512: | |
| 278 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA512withECDSA"; |
| 279 | case RS256: | |
| 280 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA256withRSA"; |
| 281 | case RS384: | |
| 282 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA384withRSA"; |
| 283 | case RS512: | |
| 284 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA512withRSA"; |
| 285 | case RS1: | |
| 286 |
1
1. getJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::getJavaAlgorithmName → KILLED |
return "SHA1withRSA"; |
| 287 | default: | |
| 288 | throw new IllegalArgumentException("Unknown algorithm: " + alg); | |
| 289 | } | |
| 290 | } | |
| 291 | ||
| 292 | static String jwsAlgorithmNameToJavaAlgorithmName(String alg) { | |
| 293 |
1
1. jwsAlgorithmNameToJavaAlgorithmName : negated conditional → KILLED |
switch (alg) { |
| 294 | case "RS256": | |
| 295 |
1
1. jwsAlgorithmNameToJavaAlgorithmName : replaced return value with "" for com/yubico/webauthn/WebAuthnCodecs::jwsAlgorithmNameToJavaAlgorithmName → KILLED |
return "SHA256withRSA"; |
| 296 | } | |
| 297 | throw new IllegalArgumentException("Unknown algorithm: " + alg); | |
| 298 | } | |
| 299 | } | |
Mutations | ||
| 103 |
1.1 |
|
| 106 |
1.1 |
|
| 107 |
1.1 |
|
| 109 |
1.1 |
|
| 110 |
1.1 |
|
| 112 |
1.1 2.2 |
|
| 116 |
1.1 |
|
| 124 |
1.1 |
|
| 125 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 |
|
| 134 |
1.1 2.2 3.3 |
|
| 135 |
1.1 2.2 |
|
| 142 |
1.1 |
|
| 162 |
1.1 |
|
| 164 |
1.1 2.2 3.3 |
|
| 167 |
1.1 |
|
| 176 |
1.1 |
|
| 178 |
1.1 |
|
| 180 |
1.1 |
|
| 192 |
1.1 |
|
| 234 |
1.1 |
|
| 251 |
1.1 |
|
| 252 |
1.1 |
|
| 258 |
1.1 |
|
| 260 |
1.1 |
|
| 270 |
1.1 |
|
| 272 |
1.1 |
|
| 274 |
1.1 |
|
| 276 |
1.1 |
|
| 278 |
1.1 |
|
| 280 |
1.1 |
|
| 282 |
1.1 |
|
| 284 |
1.1 |
|
| 286 |
1.1 |
|
| 293 |
1.1 |
|
| 295 |
1.1 |