1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.security.identity.internal; 18 19 import android.security.identity.ResultData; 20 import android.security.identity.IdentityCredentialStore; 21 22 import android.content.Context; 23 import android.content.pm.PackageManager; 24 import android.content.pm.FeatureInfo; 25 import android.os.SystemProperties; 26 import android.security.keystore.KeyProperties; 27 import android.util.Log; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import java.io.ByteArrayInputStream; 33 import java.io.ByteArrayOutputStream; 34 import java.io.IOException; 35 import java.math.BigInteger; 36 import java.security.InvalidAlgorithmParameterException; 37 import java.security.InvalidKeyException; 38 import java.security.KeyStore; 39 import java.security.KeyPair; 40 import java.security.KeyPairGenerator; 41 import java.security.MessageDigest; 42 import java.security.NoSuchAlgorithmException; 43 import java.security.PublicKey; 44 import java.security.PrivateKey; 45 import java.security.Signature; 46 import java.security.SignatureException; 47 import java.security.cert.CertificateEncodingException; 48 import java.security.cert.CertificateException; 49 import java.security.cert.CertificateFactory; 50 import java.security.cert.X509Certificate; 51 import java.security.spec.ECGenParameterSpec; 52 import java.text.DecimalFormat; 53 import java.text.DecimalFormatSymbols; 54 import java.text.ParseException; 55 import java.util.ArrayList; 56 import java.util.Arrays; 57 import java.util.Collection; 58 import java.util.Date; 59 import java.util.List; 60 import java.util.Locale; 61 import java.util.Formatter; 62 import java.util.Map; 63 64 import javax.crypto.KeyAgreement; 65 import javax.crypto.Mac; 66 import javax.crypto.SecretKey; 67 import javax.crypto.spec.SecretKeySpec; 68 69 import java.security.interfaces.ECPublicKey; 70 import java.security.spec.ECPoint; 71 72 import org.bouncycastle.asn1.ASN1InputStream; 73 import org.bouncycastle.asn1.ASN1OctetString; 74 75 import co.nstant.in.cbor.CborBuilder; 76 import co.nstant.in.cbor.CborDecoder; 77 import co.nstant.in.cbor.CborEncoder; 78 import co.nstant.in.cbor.CborException; 79 import co.nstant.in.cbor.builder.ArrayBuilder; 80 import co.nstant.in.cbor.builder.MapBuilder; 81 import co.nstant.in.cbor.model.AbstractFloat; 82 import co.nstant.in.cbor.model.Array; 83 import co.nstant.in.cbor.model.ByteString; 84 import co.nstant.in.cbor.model.DataItem; 85 import co.nstant.in.cbor.model.DoublePrecisionFloat; 86 import co.nstant.in.cbor.model.MajorType; 87 import co.nstant.in.cbor.model.NegativeInteger; 88 import co.nstant.in.cbor.model.SimpleValue; 89 import co.nstant.in.cbor.model.SimpleValueType; 90 import co.nstant.in.cbor.model.SpecialType; 91 import co.nstant.in.cbor.model.UnicodeString; 92 import co.nstant.in.cbor.model.UnsignedInteger; 93 94 public class Util { 95 private static final String TAG = "Util"; 96 canonicalizeCbor(byte[] encodedCbor)97 public static byte[] canonicalizeCbor(byte[] encodedCbor) throws CborException { 98 ByteArrayInputStream bais = new ByteArrayInputStream(encodedCbor); 99 List<DataItem> dataItems = new CborDecoder(bais).decode(); 100 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 101 for(DataItem dataItem : dataItems) { 102 CborEncoder encoder = new CborEncoder(baos); 103 encoder.encode(dataItem); 104 } 105 return baos.toByteArray(); 106 } 107 108 cborPrettyPrint(byte[] encodedBytes)109 public static String cborPrettyPrint(byte[] encodedBytes) throws CborException { 110 StringBuilder sb = new StringBuilder(); 111 112 ByteArrayInputStream bais = new ByteArrayInputStream(encodedBytes); 113 List<DataItem> dataItems = new CborDecoder(bais).decode(); 114 int count = 0; 115 for (DataItem dataItem : dataItems) { 116 if (count > 0) { 117 sb.append(",\n"); 118 } 119 cborPrettyPrintDataItem(sb, 0, dataItem); 120 count++; 121 } 122 123 return sb.toString(); 124 } 125 126 // Returns true iff all elements in |items| are not compound (e.g. an array or a map). cborAreAllDataItemsNonCompound(List<DataItem> items)127 static boolean cborAreAllDataItemsNonCompound(List<DataItem> items) { 128 for (DataItem item : items) { 129 switch (item.getMajorType()) { 130 case ARRAY: 131 case MAP: 132 return false; 133 default: 134 // continue inspecting other data items 135 } 136 } 137 return true; 138 } 139 cborPrettyPrintDataItem(StringBuilder sb, int indent, DataItem dataItem)140 public static void cborPrettyPrintDataItem(StringBuilder sb, int indent, DataItem dataItem) { 141 StringBuilder indentBuilder = new StringBuilder(); 142 for (int n = 0; n < indent; n++) { 143 indentBuilder.append(' '); 144 } 145 String indentString = indentBuilder.toString(); 146 147 if (dataItem.hasTag()) { 148 sb.append(String.format("tag %d ", dataItem.getTag().getValue())); 149 } 150 151 switch (dataItem.getMajorType()) { 152 case INVALID: 153 // TODO: throw 154 sb.append("<invalid>"); 155 break; 156 case UNSIGNED_INTEGER: { 157 // Major type 0: an unsigned integer. 158 BigInteger value = ((UnsignedInteger) dataItem).getValue(); 159 sb.append(value); 160 } 161 break; 162 case NEGATIVE_INTEGER: { 163 // Major type 1: a negative integer. 164 BigInteger value = ((NegativeInteger) dataItem).getValue(); 165 sb.append(value); 166 } 167 break; 168 case BYTE_STRING: { 169 // Major type 2: a byte string. 170 byte[] value = ((ByteString) dataItem).getBytes(); 171 sb.append("["); 172 int count = 0; 173 for (byte b : value) { 174 if (count > 0) { 175 sb.append(", "); 176 } 177 sb.append(String.format("0x%02x", b)); 178 count++; 179 } 180 sb.append("]"); 181 } 182 break; 183 case UNICODE_STRING: { 184 // Major type 3: string of Unicode characters that is encoded as UTF-8 [RFC3629]. 185 String value = ((UnicodeString) dataItem).getString(); 186 // TODO: escape ' in |value| 187 sb.append("'" + value + "'"); 188 } 189 break; 190 case ARRAY: { 191 // Major type 4: an array of data items. 192 List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); 193 if (items.size() == 0) { 194 sb.append("[]"); 195 } else if (cborAreAllDataItemsNonCompound(items)) { 196 // The case where everything fits on one line. 197 sb.append("["); 198 int count = 0; 199 for (DataItem item : items) { 200 cborPrettyPrintDataItem(sb, indent, item); 201 if (++count < items.size()) { 202 sb.append(", "); 203 } 204 } 205 sb.append("]"); 206 } else { 207 sb.append("[\n" + indentString); 208 int count = 0; 209 for (DataItem item : items) { 210 sb.append(" "); 211 cborPrettyPrintDataItem(sb, indent + 2, item); 212 if (++count < items.size()) { 213 sb.append(","); 214 } 215 sb.append("\n" + indentString); 216 } 217 sb.append("]"); 218 } 219 } 220 break; 221 case MAP: { 222 // Major type 5: a map of pairs of data items. 223 Collection<DataItem> keys = ((co.nstant.in.cbor.model.Map) dataItem).getKeys(); 224 if (keys.size() == 0) { 225 sb.append("{}"); 226 } else { 227 sb.append("{\n" + indentString); 228 int count = 0; 229 for (DataItem key : keys) { 230 sb.append(" "); 231 DataItem value = ((co.nstant.in.cbor.model.Map) dataItem).get(key); 232 cborPrettyPrintDataItem(sb, indent + 2, key); 233 sb.append(" : "); 234 cborPrettyPrintDataItem(sb, indent + 2, value); 235 if (++count < keys.size()) { 236 sb.append(","); 237 } 238 sb.append("\n" + indentString); 239 } 240 sb.append("}"); 241 } 242 } 243 break; 244 case TAG: 245 // Major type 6: optional semantic tagging of other major types 246 // 247 // We never encounter this one since it's automatically handled via the 248 // DataItem that is tagged. 249 throw new RuntimeException("Semantic tag data item not expected"); 250 251 case SPECIAL: 252 // Major type 7: floating point numbers and simple data types that need no 253 // content, as well as the "break" stop code. 254 if (dataItem instanceof SimpleValue) { 255 switch (((SimpleValue) dataItem).getSimpleValueType()) { 256 case FALSE: 257 sb.append("false"); 258 break; 259 case TRUE: 260 sb.append("true"); 261 break; 262 case NULL: 263 sb.append("null"); 264 break; 265 case UNDEFINED: 266 sb.append("undefined"); 267 break; 268 case RESERVED: 269 sb.append("reserved"); 270 break; 271 case UNALLOCATED: 272 sb.append("unallocated"); 273 break; 274 } 275 } else if (dataItem instanceof DoublePrecisionFloat) { 276 DecimalFormat df = new DecimalFormat("0", 277 DecimalFormatSymbols.getInstance(Locale.ENGLISH)); 278 df.setMaximumFractionDigits(340); 279 sb.append(df.format(((DoublePrecisionFloat) dataItem).getValue())); 280 } else if (dataItem instanceof AbstractFloat) { 281 DecimalFormat df = new DecimalFormat("0", 282 DecimalFormatSymbols.getInstance(Locale.ENGLISH)); 283 df.setMaximumFractionDigits(340); 284 sb.append(df.format(((AbstractFloat) dataItem).getValue())); 285 } else { 286 sb.append("break"); 287 } 288 break; 289 } 290 } 291 encodeCbor(List<DataItem> dataItems)292 public static byte[] encodeCbor(List<DataItem> dataItems) { 293 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 294 CborEncoder encoder = new CborEncoder(baos); 295 try { 296 encoder.encode(dataItems); 297 } catch (CborException e) { 298 throw new RuntimeException("Error encoding data", e); 299 } 300 return baos.toByteArray(); 301 } 302 coseBuildToBeSigned(byte[] encodedProtectedHeaders, byte[] payload, byte[] detachedContent)303 public static byte[] coseBuildToBeSigned(byte[] encodedProtectedHeaders, 304 byte[] payload, 305 byte[] detachedContent) { 306 CborBuilder sigStructure = new CborBuilder(); 307 ArrayBuilder<CborBuilder> array = sigStructure.addArray(); 308 309 array.add("Signature1"); 310 array.add(encodedProtectedHeaders); 311 312 // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) 313 // so external_aad is the empty bstr 314 byte emptyExternalAad[] = new byte[0]; 315 array.add(emptyExternalAad); 316 317 // Next field is the payload, independently of how it's transported (RFC 318 // 8152 section 4.4). Since our API specifies only one of |data| and 319 // |detachedContent| can be non-empty, it's simply just the non-empty one. 320 if (payload != null && payload.length > 0) { 321 array.add(payload); 322 } else { 323 array.add(detachedContent); 324 } 325 array.end(); 326 return encodeCbor(sigStructure.build()); 327 } 328 329 private static final int COSE_LABEL_ALG = 1; 330 private static final int COSE_LABEL_X5CHAIN = 33; // temporary identifier 331 332 // From "COSE Algorithms" registry 333 private static final int COSE_ALG_ECDSA_256 = -7; 334 private static final int COSE_ALG_HMAC_256_256 = 5; 335 signatureDerToCose(byte[] signature)336 private static byte[] signatureDerToCose(byte[] signature) { 337 if (signature.length > 128) { 338 throw new RuntimeException("Unexpected length " + signature.length 339 + ", expected less than 128"); 340 } 341 if (signature[0] != 0x30) { 342 throw new RuntimeException("Unexpected first byte " + signature[0] 343 + ", expected 0x30"); 344 } 345 if ((signature[1] & 0x80) != 0x00) { 346 throw new RuntimeException("Unexpected second byte " + signature[1] 347 + ", bit 7 shouldn't be set"); 348 } 349 int rOffset = 2; 350 int rSize = signature[rOffset + 1]; 351 byte[] rBytes = stripLeadingZeroes( 352 Arrays.copyOfRange(signature,rOffset + 2, rOffset + rSize + 2)); 353 354 int sOffset = rOffset + 2 + rSize; 355 int sSize = signature[sOffset + 1]; 356 byte[] sBytes = stripLeadingZeroes( 357 Arrays.copyOfRange(signature, sOffset + 2, sOffset + sSize + 2)); 358 359 if (rBytes.length > 32) { 360 throw new RuntimeException("rBytes.length is " + rBytes.length + " which is > 32"); 361 } 362 if (sBytes.length > 32) { 363 throw new RuntimeException("sBytes.length is " + sBytes.length + " which is > 32"); 364 } 365 366 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 367 try { 368 for (int n = 0; n < 32 - rBytes.length; n++) { 369 baos.write(0x00); 370 } 371 baos.write(rBytes); 372 for (int n = 0; n < 32 - sBytes.length; n++) { 373 baos.write(0x00); 374 } 375 baos.write(sBytes); 376 } catch (IOException e) { 377 e.printStackTrace(); 378 return null; 379 } 380 return baos.toByteArray(); 381 } 382 383 // Adds leading 0x00 if the first encoded byte MSB is set. encodePositiveBigInteger(BigInteger i)384 private static byte[] encodePositiveBigInteger(BigInteger i) { 385 byte[] bytes = i.toByteArray(); 386 if ((bytes[0] & 0x80) != 0) { 387 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 388 try { 389 baos.write(0x00); 390 baos.write(bytes); 391 } catch (IOException e) { 392 e.printStackTrace(); 393 throw new RuntimeException("Failed writing data", e); 394 } 395 bytes = baos.toByteArray(); 396 } 397 return bytes; 398 } 399 signatureCoseToDer(byte[] signature)400 private static byte[] signatureCoseToDer(byte[] signature) { 401 if (signature.length != 64) { 402 throw new RuntimeException("signature.length is " + signature.length + ", expected 64"); 403 } 404 // r and s are always positive and may use all 256 bits so use the constructor which 405 // parses them as unsigned. 406 BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)); 407 BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)); 408 byte[] rBytes = encodePositiveBigInteger(r); 409 byte[] sBytes = encodePositiveBigInteger(s); 410 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 411 try { 412 baos.write(0x30); 413 baos.write(2 + rBytes.length + 2 + sBytes.length); 414 baos.write(0x02); 415 baos.write(rBytes.length); 416 baos.write(rBytes); 417 baos.write(0x02); 418 baos.write(sBytes.length); 419 baos.write(sBytes); 420 } catch (IOException e) { 421 e.printStackTrace(); 422 return null; 423 } 424 return baos.toByteArray(); 425 } 426 coseSign1Sign(PrivateKey key, @Nullable byte[] data, byte[] detachedContent, @Nullable Collection<X509Certificate> certificateChain)427 public static byte[] coseSign1Sign(PrivateKey key, 428 @Nullable byte[] data, 429 byte[] detachedContent, 430 @Nullable Collection<X509Certificate> certificateChain) 431 throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException { 432 433 int dataLen = (data != null ? data.length : 0); 434 int detachedContentLen = (detachedContent != null ? detachedContent.length : 0); 435 if (dataLen > 0 && detachedContentLen > 0) { 436 throw new RuntimeException("data and detachedContent cannot both be non-empty"); 437 } 438 439 CborBuilder protectedHeaders = new CborBuilder(); 440 MapBuilder<CborBuilder> protectedHeadersMap = protectedHeaders.addMap(); 441 protectedHeadersMap.put(COSE_LABEL_ALG, COSE_ALG_ECDSA_256); 442 byte[] protectedHeadersBytes = encodeCbor(protectedHeaders.build()); 443 444 byte[] toBeSigned = coseBuildToBeSigned(protectedHeadersBytes, data, detachedContent); 445 446 byte[] coseSignature = null; 447 try { 448 Signature s = Signature.getInstance("SHA256withECDSA"); 449 s.initSign(key); 450 s.update(toBeSigned); 451 byte[] derSignature = s.sign(); 452 coseSignature = signatureDerToCose(derSignature); 453 } catch (SignatureException e) { 454 throw new RuntimeException("Error signing data"); 455 } 456 457 CborBuilder builder = new CborBuilder(); 458 ArrayBuilder<CborBuilder> array = builder.addArray(); 459 array.add(protectedHeadersBytes); 460 MapBuilder<ArrayBuilder<CborBuilder>> unprotectedHeaders = array.addMap(); 461 if (certificateChain != null && certificateChain.size() > 0) { 462 if (certificateChain.size() == 1) { 463 X509Certificate cert = certificateChain.iterator().next(); 464 unprotectedHeaders.put(COSE_LABEL_X5CHAIN, cert.getEncoded()); 465 } else { 466 ArrayBuilder<MapBuilder<ArrayBuilder<CborBuilder>>> x5chainsArray = 467 unprotectedHeaders.putArray(COSE_LABEL_X5CHAIN); 468 for (X509Certificate cert : certificateChain) { 469 x5chainsArray.add(cert.getEncoded()); 470 } 471 } 472 } 473 if (data == null || data.length == 0) { 474 array.add(new SimpleValue(SimpleValueType.NULL)); 475 } else { 476 array.add(data); 477 } 478 array.add(coseSignature); 479 480 return encodeCbor(builder.build()); 481 } 482 coseSign1CheckSignature(byte[] signatureCose1, byte[] detachedContent, PublicKey publicKey)483 public static boolean coseSign1CheckSignature(byte[] signatureCose1, 484 byte[] detachedContent, 485 PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException { 486 ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1); 487 List<DataItem> dataItems = null; 488 try { 489 dataItems = new CborDecoder(bais).decode(); 490 } catch (CborException e) { 491 throw new RuntimeException("Given signature is not valid CBOR", e); 492 } 493 if (dataItems.size() != 1) { 494 throw new RuntimeException("Expected just one data item"); 495 } 496 DataItem dataItem = dataItems.get(0); 497 if (dataItem.getMajorType() != MajorType.ARRAY) { 498 throw new RuntimeException("Data item is not an array"); 499 } 500 List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); 501 if (items.size() < 4) { 502 throw new RuntimeException("Expected at least four items in COSE_Sign1 array"); 503 } 504 if (items.get(0).getMajorType() != MajorType.BYTE_STRING) { 505 throw new RuntimeException("Item 0 (protected headers) is not a byte-string"); 506 } 507 byte[] encodedProtectedHeaders = 508 ((co.nstant.in.cbor.model.ByteString) items.get(0)).getBytes(); 509 byte[] payload = new byte[0]; 510 if (items.get(2).getMajorType() == MajorType.SPECIAL) { 511 if (((co.nstant.in.cbor.model.Special) items.get(2)).getSpecialType() 512 != SpecialType.SIMPLE_VALUE) { 513 throw new RuntimeException("Item 2 (payload) is a special but not a simple value"); 514 } 515 SimpleValue simple = (co.nstant.in.cbor.model.SimpleValue) items.get(2); 516 if (simple.getSimpleValueType() != SimpleValueType.NULL) { 517 throw new RuntimeException("Item 2 (payload) is a simple but not the value null"); 518 } 519 } else if (items.get(2).getMajorType() == MajorType.BYTE_STRING) { 520 payload = ((co.nstant.in.cbor.model.ByteString) items.get(2)).getBytes(); 521 } else { 522 throw new RuntimeException("Item 2 (payload) is not nil or byte-string"); 523 } 524 if (items.get(3).getMajorType() != MajorType.BYTE_STRING) { 525 throw new RuntimeException("Item 3 (signature) is not a byte-string"); 526 } 527 byte[] coseSignature = ((co.nstant.in.cbor.model.ByteString) items.get(3)).getBytes(); 528 529 byte[] derSignature = signatureCoseToDer(coseSignature); 530 531 int dataLen = payload.length; 532 int detachedContentLen = (detachedContent != null ? detachedContent.length : 0); 533 if (dataLen > 0 && detachedContentLen > 0) { 534 throw new RuntimeException("data and detachedContent cannot both be non-empty"); 535 } 536 537 byte[] toBeSigned = Util.coseBuildToBeSigned(encodedProtectedHeaders, 538 payload, detachedContent); 539 540 try { 541 Signature verifier = Signature.getInstance("SHA256withECDSA"); 542 verifier.initVerify(publicKey); 543 verifier.update(toBeSigned); 544 return verifier.verify(derSignature); 545 } catch (SignatureException e) { 546 throw new RuntimeException("Error verifying signature"); 547 } 548 } 549 550 // Returns the empty byte-array if no data is included in the structure. 551 // 552 // Throws RuntimeException if the given bytes aren't valid COSE_Sign1. 553 // coseSign1GetData(byte[] signatureCose1)554 public static byte[] coseSign1GetData(byte[] signatureCose1) { 555 ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1); 556 List<DataItem> dataItems = null; 557 try { 558 dataItems = new CborDecoder(bais).decode(); 559 } catch (CborException e) { 560 throw new RuntimeException("Given signature is not valid CBOR", e); 561 } 562 if (dataItems.size() != 1) { 563 throw new RuntimeException("Expected just one data item"); 564 } 565 DataItem dataItem = dataItems.get(0); 566 if (dataItem.getMajorType() != MajorType.ARRAY) { 567 throw new RuntimeException("Data item is not an array"); 568 } 569 List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); 570 if (items.size() < 4) { 571 throw new RuntimeException("Expected at least four items in COSE_Sign1 array"); 572 } 573 byte[] payload = new byte[0]; 574 if (items.get(2).getMajorType() == MajorType.SPECIAL) { 575 if (((co.nstant.in.cbor.model.Special) items.get(2)).getSpecialType() 576 != SpecialType.SIMPLE_VALUE) { 577 throw new RuntimeException("Item 2 (payload) is a special but not a simple value"); 578 } 579 SimpleValue simple = (co.nstant.in.cbor.model.SimpleValue) items.get(2); 580 if (simple.getSimpleValueType() != SimpleValueType.NULL) { 581 throw new RuntimeException("Item 2 (payload) is a simple but not the value null"); 582 } 583 } else if (items.get(2).getMajorType() == MajorType.BYTE_STRING) { 584 payload = ((co.nstant.in.cbor.model.ByteString) items.get(2)).getBytes(); 585 } else { 586 throw new RuntimeException("Item 2 (payload) is not nil or byte-string"); 587 } 588 return payload; 589 } 590 591 // Returns the empty collection if no x5chain is included in the structure. 592 // 593 // Throws RuntimeException if the given bytes aren't valid COSE_Sign1. 594 // coseSign1GetX5Chain(byte[] signatureCose1)595 public static Collection<X509Certificate> coseSign1GetX5Chain(byte[] signatureCose1) 596 throws CertificateException { 597 ArrayList<X509Certificate> ret = new ArrayList<>(); 598 ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1); 599 List<DataItem> dataItems = null; 600 try { 601 dataItems = new CborDecoder(bais).decode(); 602 } catch (CborException e) { 603 throw new RuntimeException("Given signature is not valid CBOR", e); 604 } 605 if (dataItems.size() != 1) { 606 throw new RuntimeException("Expected just one data item"); 607 } 608 DataItem dataItem = dataItems.get(0); 609 if (dataItem.getMajorType() != MajorType.ARRAY) { 610 throw new RuntimeException("Data item is not an array"); 611 } 612 List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); 613 if (items.size() < 4) { 614 throw new RuntimeException("Expected at least four items in COSE_Sign1 array"); 615 } 616 if (items.get(1).getMajorType() != MajorType.MAP) { 617 throw new RuntimeException("Item 1 (unprocted headers) is not a map"); 618 } 619 co.nstant.in.cbor.model.Map map = (co.nstant.in.cbor.model.Map) items.get(1); 620 DataItem x5chainItem = map.get(new UnsignedInteger(COSE_LABEL_X5CHAIN)); 621 if (x5chainItem != null) { 622 CertificateFactory factory = CertificateFactory.getInstance("X.509"); 623 if (x5chainItem instanceof ByteString) { 624 ByteArrayInputStream certBais = 625 new ByteArrayInputStream(((ByteString) x5chainItem).getBytes()); 626 ret.add((X509Certificate) factory.generateCertificate(certBais)); 627 } else if (x5chainItem instanceof Array) { 628 for (DataItem certItem : ((Array) x5chainItem).getDataItems()) { 629 if (!(certItem instanceof ByteString)) { 630 throw new RuntimeException( 631 "Unexpected type for array item in x5chain value"); 632 } 633 ByteArrayInputStream certBais = 634 new ByteArrayInputStream(((ByteString) certItem).getBytes()); 635 ret.add((X509Certificate) factory.generateCertificate(certBais)); 636 } 637 } else { 638 throw new RuntimeException("Unexpected type for x5chain value"); 639 } 640 } 641 return ret; 642 } 643 coseBuildToBeMACed(byte[] encodedProtectedHeaders, byte[] payload, byte[] detachedContent)644 public static byte[] coseBuildToBeMACed(byte[] encodedProtectedHeaders, 645 byte[] payload, 646 byte[] detachedContent) { 647 CborBuilder macStructure = new CborBuilder(); 648 ArrayBuilder<CborBuilder> array = macStructure.addArray(); 649 650 array.add("MAC0"); 651 array.add(encodedProtectedHeaders); 652 653 // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) 654 // so external_aad is the empty bstr 655 byte emptyExternalAad[] = new byte[0]; 656 array.add(emptyExternalAad); 657 658 // Next field is the payload, independently of how it's transported (RFC 659 // 8152 section 4.4). Since our API specifies only one of |data| and 660 // |detachedContent| can be non-empty, it's simply just the non-empty one. 661 if (payload != null && payload.length > 0) { 662 array.add(payload); 663 } else { 664 array.add(detachedContent); 665 } 666 667 return encodeCbor(macStructure.build()); 668 } 669 coseMac0(SecretKey key, @Nullable byte[] data, byte[] detachedContent)670 public static byte[] coseMac0(SecretKey key, 671 @Nullable byte[] data, 672 byte[] detachedContent) 673 throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException { 674 675 int dataLen = (data != null ? data.length : 0); 676 int detachedContentLen = (detachedContent != null ? detachedContent.length : 0); 677 if (dataLen > 0 && detachedContentLen > 0) { 678 throw new RuntimeException("data and detachedContent cannot both be non-empty"); 679 } 680 681 CborBuilder protectedHeaders = new CborBuilder(); 682 MapBuilder<CborBuilder> protectedHeadersMap = protectedHeaders.addMap(); 683 protectedHeadersMap.put(COSE_LABEL_ALG, COSE_ALG_HMAC_256_256); 684 byte[] protectedHeadersBytes = encodeCbor(protectedHeaders.build()); 685 686 byte[] toBeMACed = coseBuildToBeMACed(protectedHeadersBytes, data, detachedContent); 687 688 byte[] mac = null; 689 Mac m = Mac.getInstance("HmacSHA256"); 690 m.init(key); 691 m.update(toBeMACed); 692 mac = m.doFinal(); 693 694 CborBuilder builder = new CborBuilder(); 695 ArrayBuilder<CborBuilder> array = builder.addArray(); 696 array.add(protectedHeadersBytes); 697 MapBuilder<ArrayBuilder<CborBuilder>> unprotectedHeaders = array.addMap(); 698 if (data == null || data.length == 0) { 699 array.add(new SimpleValue(SimpleValueType.NULL)); 700 } else { 701 array.add(data); 702 } 703 array.add(mac); 704 705 return encodeCbor(builder.build()); 706 } 707 replaceLine(String text, int lineNumber, String replacementLine)708 public static String replaceLine(String text, int lineNumber, String replacementLine) { 709 String[] lines = text.split("\n"); 710 int numLines = lines.length; 711 if (lineNumber < 0) { 712 lineNumber = numLines - (-lineNumber); 713 } 714 StringBuilder sb = new StringBuilder(); 715 for (int n = 0; n < numLines; n++) { 716 if (n == lineNumber) { 717 sb.append(replacementLine); 718 } else { 719 sb.append(lines[n]); 720 } 721 // Only add terminating newline if passed-in string ends in a newline. 722 if (n == numLines - 1) { 723 if (text.endsWith(("\n"))) { 724 sb.append('\n'); 725 } 726 } else { 727 sb.append('\n'); 728 } 729 } 730 return sb.toString(); 731 } 732 cborEncode(DataItem dataItem)733 public static byte[] cborEncode(DataItem dataItem) { 734 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 735 try { 736 new CborEncoder(baos).encode(dataItem); 737 } catch (CborException e) { 738 // This should never happen and we don't want cborEncode() to throw since that 739 // would complicate all callers. Log it instead. 740 e.printStackTrace(); 741 Log.e(TAG, "Error encoding DataItem"); 742 } 743 return baos.toByteArray(); 744 } 745 cborEncodeBoolean(boolean value)746 public static byte[] cborEncodeBoolean(boolean value) { 747 return cborEncode(new CborBuilder().add(value).build().get(0)); 748 } 749 cborEncodeString(@onNull String value)750 public static byte[] cborEncodeString(@NonNull String value) { 751 return cborEncode(new CborBuilder().add(value).build().get(0)); 752 } 753 cborEncodeBytestring(@onNull byte[] value)754 public static byte[] cborEncodeBytestring(@NonNull byte[] value) { 755 return cborEncode(new CborBuilder().add(value).build().get(0)); 756 } 757 cborEncodeInt(long value)758 public static byte[] cborEncodeInt(long value) { 759 return cborEncode(new CborBuilder().add(value).build().get(0)); 760 } 761 762 static final int CBOR_SEMANTIC_TAG_ENCODED_CBOR = 24; 763 cborToDataItem(byte[] data)764 public static DataItem cborToDataItem(byte[] data) { 765 ByteArrayInputStream bais = new ByteArrayInputStream(data); 766 try { 767 List<DataItem> dataItems = new CborDecoder(bais).decode(); 768 if (dataItems.size() != 1) { 769 throw new RuntimeException("Expected 1 item, found " + dataItems.size()); 770 } 771 return dataItems.get(0); 772 } catch (CborException e) { 773 throw new RuntimeException("Error decoding data", e); 774 } 775 } 776 cborDecodeBoolean(@onNull byte[] data)777 public static boolean cborDecodeBoolean(@NonNull byte[] data) { 778 return cborToDataItem(data) == SimpleValue.TRUE; 779 } 780 cborDecodeString(@onNull byte[] data)781 public static String cborDecodeString(@NonNull byte[] data) { 782 return ((co.nstant.in.cbor.model.UnicodeString) cborToDataItem(data)).getString(); 783 } 784 cborDecodeInt(@onNull byte[] data)785 public static long cborDecodeInt(@NonNull byte[] data) { 786 return ((co.nstant.in.cbor.model.Number) cborToDataItem(data)).getValue().longValue(); 787 } 788 cborDecodeBytestring(@onNull byte[] data)789 public static byte[] cborDecodeBytestring(@NonNull byte[] data) { 790 return ((co.nstant.in.cbor.model.ByteString) cborToDataItem(data)).getBytes(); 791 } 792 getStringEntry(ResultData data, String namespaceName, String name)793 public static String getStringEntry(ResultData data, String namespaceName, String name) { 794 return Util.cborDecodeString(data.getEntry(namespaceName, name)); 795 } 796 getBooleanEntry(ResultData data, String namespaceName, String name)797 public static boolean getBooleanEntry(ResultData data, String namespaceName, String name) { 798 return Util.cborDecodeBoolean(data.getEntry(namespaceName, name)); 799 } 800 getIntegerEntry(ResultData data, String namespaceName, String name)801 public static long getIntegerEntry(ResultData data, String namespaceName, String name) { 802 return Util.cborDecodeInt(data.getEntry(namespaceName, name)); 803 } 804 getBytestringEntry(ResultData data, String namespaceName, String name)805 public static byte[] getBytestringEntry(ResultData data, String namespaceName, String name) { 806 return Util.cborDecodeBytestring(data.getEntry(namespaceName, name)); 807 } 808 809 /* 810 Certificate: 811 Data: 812 Version: 3 (0x2) 813 Serial Number: 1 (0x1) 814 Signature Algorithm: ecdsa-with-SHA256 815 Issuer: CN=fake 816 Validity 817 Not Before: Jan 1 00:00:00 1970 GMT 818 Not After : Jan 1 00:00:00 2048 GMT 819 Subject: CN=fake 820 Subject Public Key Info: 821 Public Key Algorithm: id-ecPublicKey 822 Public-Key: (256 bit) 823 00000000 04 9b 60 70 8a 99 b6 bf e3 b8 17 02 9e 93 eb 48 |..`p...........H| 824 00000010 23 b9 39 89 d1 00 bf a0 0f d0 2f bd 6b 11 bc d1 |#.9......./.k...| 825 00000020 19 53 54 28 31 00 f5 49 db 31 fb 9f 7d 99 bf 23 |.ST(1..I.1..}..#| 826 00000030 fb 92 04 6b 23 63 55 98 ad 24 d2 68 c4 83 bf 99 |...k#cU..$.h....| 827 00000040 62 |b| 828 Signature Algorithm: ecdsa-with-SHA256 829 30:45:02:20:67:ad:d1:34:ed:a5:68:3f:5b:33:ee:b3:18:a2: 830 eb:03:61:74:0f:21:64:4a:a3:2e:82:b3:92:5c:21:0f:88:3f: 831 02:21:00:b7:38:5c:9b:f2:9c:b1:27:86:37:44:df:eb:4a:b2: 832 6c:11:9a:c1:ff:b2:80:95:ce:fc:5f:26:b4:20:6e:9b:0d 833 */ 834 835 signPublicKeyWithPrivateKey(String keyToSignAlias, String keyToSignWithAlias)836 public static @NonNull X509Certificate signPublicKeyWithPrivateKey(String keyToSignAlias, 837 String keyToSignWithAlias) { 838 839 KeyStore ks = null; 840 try { 841 ks = KeyStore.getInstance("AndroidKeyStore"); 842 ks.load(null); 843 844 /* First note that KeyStore.getCertificate() returns a self-signed X.509 certificate 845 * for the key in question. As per RFC 5280, section 4.1 an X.509 certificate has the 846 * following structure: 847 * 848 * Certificate ::= SEQUENCE { 849 * tbsCertificate TBSCertificate, 850 * signatureAlgorithm AlgorithmIdentifier, 851 * signatureValue BIT STRING } 852 * 853 * Conveniently, the X509Certificate class has a getTBSCertificate() method which 854 * returns the tbsCertificate blob. So all we need to do is just sign that and build 855 * signatureAlgorithm and signatureValue and combine it with tbsCertificate. We don't 856 * need a full-blown ASN.1/DER encoder to do this. 857 */ 858 X509Certificate selfSignedCert = (X509Certificate) ks.getCertificate(keyToSignAlias); 859 byte[] tbsCertificate = selfSignedCert.getTBSCertificate(); 860 861 KeyStore.Entry keyToSignWithEntry = ks.getEntry(keyToSignWithAlias, null); 862 Signature s = Signature.getInstance("SHA256withECDSA"); 863 s.initSign(((KeyStore.PrivateKeyEntry) keyToSignWithEntry).getPrivateKey()); 864 s.update(tbsCertificate); 865 byte[] signatureValue = s.sign(); 866 867 /* The DER encoding for a SEQUENCE of length 128-65536 - the length is updated below. 868 * 869 * We assume - and test for below - that the final length is always going to be in 870 * this range. This is a sound assumption given we're using 256-bit EC keys. 871 */ 872 byte[] sequence = new byte[]{ 873 0x30, (byte) 0x82, 0x00, 0x00 874 }; 875 876 /* The DER encoding for the ECDSA with SHA-256 signature algorithm: 877 * 878 * SEQUENCE (1 elem) 879 * OBJECT IDENTIFIER 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA 880 * algorithm with SHA256) 881 */ 882 byte[] signatureAlgorithm = new byte[]{ 883 0x30, 0x0a, 0x06, 0x08, 0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x04, 0x03, 884 0x02 885 }; 886 887 /* The DER encoding for a BIT STRING with one element - the length is updated below. 888 * 889 * We assume the length of signatureValue is always going to be less than 128. This 890 * assumption works since we know ecdsaWithSHA256 signatures are always 69, 70, or 891 * 71 bytes long when DER encoded. 892 */ 893 byte[] bitStringForSignature = new byte[]{0x03, 0x00, 0x00}; 894 895 // Calculate sequence length and set it in |sequence|. 896 int sequenceLength = tbsCertificate.length 897 + signatureAlgorithm.length 898 + bitStringForSignature.length 899 + signatureValue.length; 900 if (sequenceLength < 128 || sequenceLength > 65535) { 901 throw new Exception("Unexpected sequenceLength " + sequenceLength); 902 } 903 sequence[2] = (byte) (sequenceLength >> 8); 904 sequence[3] = (byte) (sequenceLength & 0xff); 905 906 // Calculate signatureValue length and set it in |bitStringForSignature|. 907 int signatureValueLength = signatureValue.length + 1; 908 if (signatureValueLength >= 128) { 909 throw new Exception("Unexpected signatureValueLength " + signatureValueLength); 910 } 911 bitStringForSignature[1] = (byte) signatureValueLength; 912 913 // Finally concatenate everything together. 914 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 915 baos.write(sequence); 916 baos.write(tbsCertificate); 917 baos.write(signatureAlgorithm); 918 baos.write(bitStringForSignature); 919 baos.write(signatureValue); 920 byte[] resultingCertBytes = baos.toByteArray(); 921 922 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 923 ByteArrayInputStream bais = new ByteArrayInputStream(resultingCertBytes); 924 X509Certificate result = (X509Certificate) cf.generateCertificate(bais); 925 return result; 926 } catch (Exception e) { 927 throw new RuntimeException("Error signing public key with private key", e); 928 } 929 } 930 buildDeviceAuthenticationCbor(String docType, byte[] encodedSessionTranscript, byte[] deviceNameSpacesBytes)931 public static byte[] buildDeviceAuthenticationCbor(String docType, 932 byte[] encodedSessionTranscript, 933 byte[] deviceNameSpacesBytes) { 934 ByteArrayOutputStream daBaos = new ByteArrayOutputStream(); 935 try { 936 ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript); 937 List<DataItem> dataItems = null; 938 dataItems = new CborDecoder(bais).decode(); 939 DataItem sessionTranscript = dataItems.get(0); 940 ByteString deviceNameSpacesBytesItem = new ByteString(deviceNameSpacesBytes); 941 deviceNameSpacesBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); 942 new CborEncoder(daBaos).encode(new CborBuilder() 943 .addArray() 944 .add("DeviceAuthentication") 945 .add(sessionTranscript) 946 .add(docType) 947 .add(deviceNameSpacesBytesItem) 948 .end() 949 .build()); 950 } catch (CborException e) { 951 throw new RuntimeException("Error encoding DeviceAuthentication", e); 952 } 953 return daBaos.toByteArray(); 954 } 955 buildReaderAuthenticationBytesCbor( byte[] encodedSessionTranscript, byte[] requestMessageBytes)956 public static byte[] buildReaderAuthenticationBytesCbor( 957 byte[] encodedSessionTranscript, 958 byte[] requestMessageBytes) { 959 960 ByteArrayOutputStream daBaos = new ByteArrayOutputStream(); 961 try { 962 ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript); 963 List<DataItem> dataItems = null; 964 dataItems = new CborDecoder(bais).decode(); 965 DataItem sessionTranscript = dataItems.get(0); 966 ByteString requestMessageBytesItem = new ByteString(requestMessageBytes); 967 requestMessageBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); 968 new CborEncoder(daBaos).encode(new CborBuilder() 969 .addArray() 970 .add("ReaderAuthentication") 971 .add(sessionTranscript) 972 .add(requestMessageBytesItem) 973 .end() 974 .build()); 975 } catch (CborException e) { 976 throw new RuntimeException("Error encoding ReaderAuthentication", e); 977 } 978 byte[] readerAuthentication = daBaos.toByteArray(); 979 return Util.prependSemanticTagForEncodedCbor(readerAuthentication); 980 } 981 982 // Returns #6.24(bstr) of the given already encoded CBOR 983 // buildCborTaggedByteString(@onNull byte[] encodedCbor)984 public static @NonNull DataItem buildCborTaggedByteString(@NonNull byte[] encodedCbor) { 985 DataItem item = new ByteString(encodedCbor); 986 item.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); 987 return item; 988 } 989 prependSemanticTagForEncodedCbor(byte[] encodedCbor)990 public static byte[] prependSemanticTagForEncodedCbor(byte[] encodedCbor) { 991 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 992 try { 993 new CborEncoder(baos).encode(buildCborTaggedByteString(encodedCbor)); 994 } catch (CborException e) { 995 throw new RuntimeException("Error encoding with semantic tag for CBOR encoding", e); 996 } 997 return baos.toByteArray(); 998 } 999 concatArrays(byte[] a, byte[] b)1000 public static byte[] concatArrays(byte[] a, byte[] b) { 1001 byte[] ret = new byte[a.length + b.length]; 1002 System.arraycopy(a, 0, ret, 0, a.length); 1003 System.arraycopy(b, 0, ret, a.length, b.length); 1004 return ret; 1005 } 1006 calcEMacKeyForReader(PublicKey authenticationPublicKey, PrivateKey ephemeralReaderPrivateKey, byte[] encodedSessionTranscript)1007 public static SecretKey calcEMacKeyForReader(PublicKey authenticationPublicKey, 1008 PrivateKey ephemeralReaderPrivateKey, 1009 byte[] encodedSessionTranscript) { 1010 try { 1011 KeyAgreement ka = KeyAgreement.getInstance("ECDH"); 1012 ka.init(ephemeralReaderPrivateKey); 1013 ka.doPhase(authenticationPublicKey, true); 1014 byte[] sharedSecret = ka.generateSecret(); 1015 1016 byte[] sessionTranscriptBytes = 1017 Util.prependSemanticTagForEncodedCbor(encodedSessionTranscript); 1018 1019 byte[] salt = MessageDigest.getInstance("SHA-256").digest(sessionTranscriptBytes); 1020 byte[] info = new byte[] {'E', 'M', 'a', 'c', 'K', 'e', 'y'}; 1021 byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32); 1022 SecretKey secretKey = new SecretKeySpec(derivedKey, ""); 1023 return secretKey; 1024 } catch (InvalidKeyException 1025 | NoSuchAlgorithmException e) { 1026 throw new RuntimeException("Error performing key agreement", e); 1027 } 1028 } 1029 1030 /** 1031 * Computes an HKDF. 1032 * 1033 * This is based on https://github.com/google/tink/blob/master/java/src/main/java/com/google 1034 * /crypto/tink/subtle/Hkdf.java 1035 * which is also Copyright (c) Google and also licensed under the Apache 2 license. 1036 * 1037 * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or 1038 * "HMACSHA256". 1039 * @param ikm the input keying material. 1040 * @param salt optional salt. A possibly non-secret random value. If no salt is 1041 * provided (i.e. if 1042 * salt has length 0) then an array of 0s of the same size as the hash 1043 * digest is used as salt. 1044 * @param info optional context and application specific information. 1045 * @param size The length of the generated pseudorandom string in bytes. The maximal 1046 * size is 1047 * 255.DigestSize, where DigestSize is the size of the underlying HMAC. 1048 * @return size pseudorandom bytes. 1049 */ computeHkdf( String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size)1050 public static byte[] computeHkdf( 1051 String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size) { 1052 Mac mac = null; 1053 try { 1054 mac = Mac.getInstance(macAlgorithm); 1055 } catch (NoSuchAlgorithmException e) { 1056 throw new RuntimeException("No such algorithm: " + macAlgorithm, e); 1057 } 1058 if (size > 255 * mac.getMacLength()) { 1059 throw new RuntimeException("size too large"); 1060 } 1061 try { 1062 if (salt == null || salt.length == 0) { 1063 // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided 1064 // then HKDF uses a salt that is an array of zeros of the same length as the hash 1065 // digest. 1066 mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm)); 1067 } else { 1068 mac.init(new SecretKeySpec(salt, macAlgorithm)); 1069 } 1070 byte[] prk = mac.doFinal(ikm); 1071 byte[] result = new byte[size]; 1072 int ctr = 1; 1073 int pos = 0; 1074 mac.init(new SecretKeySpec(prk, macAlgorithm)); 1075 byte[] digest = new byte[0]; 1076 while (true) { 1077 mac.update(digest); 1078 mac.update(info); 1079 mac.update((byte) ctr); 1080 digest = mac.doFinal(); 1081 if (pos + digest.length < size) { 1082 System.arraycopy(digest, 0, result, pos, digest.length); 1083 pos += digest.length; 1084 ctr++; 1085 } else { 1086 System.arraycopy(digest, 0, result, pos, size - pos); 1087 break; 1088 } 1089 } 1090 return result; 1091 } catch (InvalidKeyException e) { 1092 throw new RuntimeException("Error MACing", e); 1093 } 1094 } 1095 stripLeadingZeroes(byte[] value)1096 static byte[] stripLeadingZeroes(byte[] value) { 1097 int n = 0; 1098 while (n < value.length && value[n] == 0) { 1099 n++; 1100 } 1101 int newLen = value.length - n; 1102 byte[] ret = new byte[newLen]; 1103 int m = 0; 1104 while (n < value.length) { 1105 ret[m++] = value[n++]; 1106 } 1107 return ret; 1108 } 1109 hexdump(String name, byte[] data)1110 public static void hexdump(String name, byte[] data) { 1111 int n, m, o; 1112 StringBuilder sb = new StringBuilder(); 1113 Formatter fmt = new Formatter(sb); 1114 for (n = 0; n < data.length; n += 16) { 1115 fmt.format("%04x ", n); 1116 for (m = 0; m < 16 && n + m < data.length; m++) { 1117 fmt.format("%02x ", data[n + m]); 1118 } 1119 for (o = m; o < 16; o++) { 1120 sb.append(" "); 1121 } 1122 sb.append(" "); 1123 for (m = 0; m < 16 && n + m < data.length; m++) { 1124 int c = data[n + m] & 0xff; 1125 fmt.format("%c", Character.isISOControl(c) ? '.' : c); 1126 } 1127 sb.append("\n"); 1128 } 1129 sb.append("\n"); 1130 Log.e(TAG, name + ": dumping " + data.length + " bytes\n" + fmt.toString()); 1131 } 1132 1133 // Convert EC P256 public key to DER format binary format convertP256PublicKeyToDERFormat(ECPoint w)1134 public static byte[] convertP256PublicKeyToDERFormat(ECPoint w) { 1135 byte[] ret = new byte[64]; 1136 1137 // Each coordinate may be encoded in 33*, 32, or fewer bytes. 1138 // 1139 // * : it can be 33 bytes because toByteArray() guarantees "The array will contain the 1140 // minimum number of bytes required to represent this BigInteger, including at 1141 // least one sign bit, which is (ceil((this.bitLength() + 1)/8))" which means that 1142 // the MSB is always 0x00. This is taken care of by calling calling 1143 // stripLeadingZeroes(). 1144 // 1145 // We need the encoding to be exactly 32 bytes since according to RFC 5480 section 2.2 1146 // and SEC 1: Elliptic Curve Cryptography section 2.3.3 the encoding is 0x04 | X | Y 1147 // where X and Y are encoded in exactly 32 byte, big endian integer values each. 1148 // 1149 byte[] xBytes = stripLeadingZeroes(w.getAffineX().toByteArray()); 1150 if (xBytes.length > 32) { 1151 throw new RuntimeException("xBytes is " + xBytes.length + " which is unexpected"); 1152 } 1153 int numLeadingZeroBytes = 32 - xBytes.length; 1154 for (int n = 0; n < numLeadingZeroBytes; n++) { 1155 ret[n] = 0x00; 1156 } 1157 for (int n = 0; n < xBytes.length; n++) { 1158 ret[numLeadingZeroBytes + n] = xBytes[n]; 1159 } 1160 1161 byte[] yBytes = stripLeadingZeroes(w.getAffineY().toByteArray()); 1162 if (yBytes.length > 32) { 1163 throw new RuntimeException("yBytes is " + yBytes.length + " which is unexpected"); 1164 } 1165 numLeadingZeroBytes = 32 - yBytes.length; 1166 for (int n = 0; n < numLeadingZeroBytes; n++) { 1167 ret[32 + n] = 0x00; 1168 } 1169 for (int n = 0; n < yBytes.length; n++) { 1170 ret[32 + numLeadingZeroBytes + n] = yBytes[n]; 1171 } 1172 1173 return ret; 1174 } 1175 1176 // This returns a SessionTranscript which satisfy the requirement 1177 // that the uncompressed X and Y coordinates of the public key for the 1178 // mDL's ephemeral key-pair appear somewhere in the encoded 1179 // DeviceEngagement. buildSessionTranscript(KeyPair ephemeralKeyPair)1180 public static byte[] buildSessionTranscript(KeyPair ephemeralKeyPair) { 1181 // Make the coordinates appear in an already encoded bstr - this 1182 // mimics how the mDL COSE_Key appear as encoded data inside the 1183 // encoded DeviceEngagement 1184 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1185 try { 1186 baos.write(new byte[]{42}); 1187 1188 ECPoint w = ((ECPublicKey) ephemeralKeyPair.getPublic()).getW(); 1189 baos.write(convertP256PublicKeyToDERFormat(w)); 1190 1191 baos.write(new byte[]{43, 44}); 1192 } catch (IOException e) { 1193 e.printStackTrace(); 1194 return null; 1195 } 1196 byte[] blobWithCoords = baos.toByteArray(); 1197 1198 baos = new ByteArrayOutputStream(); 1199 try { 1200 new CborEncoder(baos).encode(new CborBuilder() 1201 .addArray() 1202 .add(blobWithCoords) 1203 .end() 1204 .build()); 1205 } catch (CborException e) { 1206 e.printStackTrace(); 1207 return null; 1208 } 1209 ByteString encodedDeviceEngagementItem = new ByteString(baos.toByteArray()); 1210 ByteString encodedEReaderKeyItem = new ByteString(cborEncodeString("doesn't matter")); 1211 encodedDeviceEngagementItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); 1212 encodedEReaderKeyItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); 1213 1214 baos = new ByteArrayOutputStream(); 1215 try { 1216 new CborEncoder(baos).encode(new CborBuilder() 1217 .addArray() 1218 .add(encodedDeviceEngagementItem) 1219 .add(encodedEReaderKeyItem) 1220 .end() 1221 .build()); 1222 } catch (CborException e) { 1223 e.printStackTrace(); 1224 return null; 1225 } 1226 return baos.toByteArray(); 1227 } 1228 1229 /* 1230 * Helper function to create a CBOR data for requesting data items. The IntentToRetain 1231 * value will be set to false for all elements. 1232 * 1233 * <p>The returned CBOR data conforms to the following CDDL schema:</p> 1234 * 1235 * <pre> 1236 * ItemsRequest = { 1237 * ? "docType" : DocType, 1238 * "nameSpaces" : NameSpaces, 1239 * ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide 1240 * } 1241 * 1242 * NameSpaces = { 1243 * + NameSpace => DataElements ; Requested data elements for each NameSpace 1244 * } 1245 * 1246 * DataElements = { 1247 * + DataElement => IntentToRetain 1248 * } 1249 * 1250 * DocType = tstr 1251 * 1252 * DataElement = tstr 1253 * IntentToRetain = bool 1254 * NameSpace = tstr 1255 * </pre> 1256 * 1257 * @param entriesToRequest The entries to request, organized as a map of namespace 1258 * names with each value being a collection of data elements 1259 * in the given namespace. 1260 * @param docType The document type or {@code null} if there is no document 1261 * type. 1262 * @return CBOR data conforming to the CDDL mentioned above. 1263 */ createItemsRequest( @onNull Map<String, Collection<String>> entriesToRequest, @Nullable String docType)1264 public static @NonNull byte[] createItemsRequest( 1265 @NonNull Map<String, Collection<String>> entriesToRequest, 1266 @Nullable String docType) { 1267 CborBuilder builder = new CborBuilder(); 1268 MapBuilder<CborBuilder> mapBuilder = builder.addMap(); 1269 if (docType != null) { 1270 mapBuilder.put("docType", docType); 1271 } 1272 1273 MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces"); 1274 for (String namespaceName : entriesToRequest.keySet()) { 1275 Collection<String> entryNames = entriesToRequest.get(namespaceName); 1276 MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder = 1277 nsMapBuilder.putMap(namespaceName); 1278 for (String entryName : entryNames) { 1279 entryNameMapBuilder.put(entryName, false); 1280 } 1281 } 1282 1283 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1284 CborEncoder encoder = new CborEncoder(baos); 1285 try { 1286 encoder.encode(builder.build()); 1287 } catch (CborException e) { 1288 throw new RuntimeException("Error encoding CBOR", e); 1289 } 1290 return baos.toByteArray(); 1291 } 1292 createEphemeralKeyPair()1293 public static KeyPair createEphemeralKeyPair() { 1294 try { 1295 KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC); 1296 ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1"); 1297 kpg.initialize(ecSpec); 1298 KeyPair keyPair = kpg.generateKeyPair(); 1299 return keyPair; 1300 } catch (NoSuchAlgorithmException 1301 | InvalidAlgorithmParameterException e) { 1302 throw new RuntimeException("Error generating ephemeral key-pair", e); 1303 } 1304 } 1305 getPopSha256FromAuthKeyCert(X509Certificate cert)1306 public static byte[] getPopSha256FromAuthKeyCert(X509Certificate cert) { 1307 byte[] octetString = cert.getExtensionValue("1.3.6.1.4.1.11129.2.1.26"); 1308 if (octetString == null) { 1309 return null; 1310 } 1311 Util.hexdump("octetString", octetString); 1312 1313 try { 1314 ASN1InputStream asn1InputStream = new ASN1InputStream(octetString); 1315 byte[] cborBytes = ((ASN1OctetString) asn1InputStream.readObject()).getOctets(); 1316 Util.hexdump("cborBytes", cborBytes); 1317 1318 ByteArrayInputStream bais = new ByteArrayInputStream(cborBytes); 1319 List<DataItem> dataItems = new CborDecoder(bais).decode(); 1320 if (dataItems.size() != 1) { 1321 throw new RuntimeException("Expected 1 item, found " + dataItems.size()); 1322 } 1323 if (!(dataItems.get(0) instanceof co.nstant.in.cbor.model.Array)) { 1324 throw new RuntimeException("Item is not a map"); 1325 } 1326 co.nstant.in.cbor.model.Array array = (co.nstant.in.cbor.model.Array) dataItems.get(0); 1327 List<DataItem> items = array.getDataItems(); 1328 if (items.size() < 2) { 1329 throw new RuntimeException( 1330 "Expected at least 2 array items, found " + items.size()); 1331 } 1332 if (!(items.get(0) instanceof UnicodeString)) { 1333 throw new RuntimeException("First array item is not a string"); 1334 } 1335 String id = ((UnicodeString) items.get(0)).getString(); 1336 if (!id.equals("ProofOfBinding")) { 1337 throw new RuntimeException("Expected ProofOfBinding, got " + id); 1338 } 1339 if (!(items.get(1) instanceof ByteString)) { 1340 throw new RuntimeException("Second array item is not a bytestring"); 1341 } 1342 byte[] popSha256 = ((ByteString) items.get(1)).getBytes(); 1343 if (popSha256.length != 32) { 1344 throw new RuntimeException( 1345 "Expected bstr to be 32 bytes, it is " + popSha256.length); 1346 } 1347 return popSha256; 1348 } catch (IOException e) { 1349 throw new RuntimeException("Error decoding extension data", e); 1350 } catch (CborException e) { 1351 throw new RuntimeException("Error decoding data", e); 1352 } 1353 } 1354 1355 } 1356