| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.security.identity.internal; |
| |
| import android.security.identity.ResultData; |
| import android.security.identity.IdentityCredentialStore; |
| |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.pm.FeatureInfo; |
| import android.os.SystemProperties; |
| import android.security.keystore.KeyProperties; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.math.BigInteger; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.KeyStore; |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PublicKey; |
| import java.security.PrivateKey; |
| import java.security.Signature; |
| import java.security.SignatureException; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.security.spec.ECGenParameterSpec; |
| import java.text.DecimalFormat; |
| import java.text.DecimalFormatSymbols; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Formatter; |
| import java.util.Map; |
| |
| import javax.crypto.KeyAgreement; |
| import javax.crypto.Mac; |
| import javax.crypto.SecretKey; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| import java.security.interfaces.ECPublicKey; |
| import java.security.spec.ECPoint; |
| |
| import org.bouncycastle.asn1.ASN1InputStream; |
| import org.bouncycastle.asn1.ASN1OctetString; |
| |
| import co.nstant.in.cbor.CborBuilder; |
| import co.nstant.in.cbor.CborDecoder; |
| import co.nstant.in.cbor.CborEncoder; |
| import co.nstant.in.cbor.CborException; |
| import co.nstant.in.cbor.builder.ArrayBuilder; |
| import co.nstant.in.cbor.builder.MapBuilder; |
| import co.nstant.in.cbor.model.AbstractFloat; |
| import co.nstant.in.cbor.model.Array; |
| import co.nstant.in.cbor.model.ByteString; |
| import co.nstant.in.cbor.model.DataItem; |
| import co.nstant.in.cbor.model.DoublePrecisionFloat; |
| import co.nstant.in.cbor.model.MajorType; |
| import co.nstant.in.cbor.model.NegativeInteger; |
| import co.nstant.in.cbor.model.SimpleValue; |
| import co.nstant.in.cbor.model.SimpleValueType; |
| import co.nstant.in.cbor.model.SpecialType; |
| import co.nstant.in.cbor.model.UnicodeString; |
| import co.nstant.in.cbor.model.UnsignedInteger; |
| |
| public class Util { |
| private static final String TAG = "Util"; |
| |
| public static byte[] canonicalizeCbor(byte[] encodedCbor) throws CborException { |
| ByteArrayInputStream bais = new ByteArrayInputStream(encodedCbor); |
| List<DataItem> dataItems = new CborDecoder(bais).decode(); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| for(DataItem dataItem : dataItems) { |
| CborEncoder encoder = new CborEncoder(baos); |
| encoder.encode(dataItem); |
| } |
| return baos.toByteArray(); |
| } |
| |
| |
| public static String cborPrettyPrint(byte[] encodedBytes) throws CborException { |
| StringBuilder sb = new StringBuilder(); |
| |
| ByteArrayInputStream bais = new ByteArrayInputStream(encodedBytes); |
| List<DataItem> dataItems = new CborDecoder(bais).decode(); |
| int count = 0; |
| for (DataItem dataItem : dataItems) { |
| if (count > 0) { |
| sb.append(",\n"); |
| } |
| cborPrettyPrintDataItem(sb, 0, dataItem); |
| count++; |
| } |
| |
| return sb.toString(); |
| } |
| |
| // Returns true iff all elements in |items| are not compound (e.g. an array or a map). |
| static boolean cborAreAllDataItemsNonCompound(List<DataItem> items) { |
| for (DataItem item : items) { |
| switch (item.getMajorType()) { |
| case ARRAY: |
| case MAP: |
| return false; |
| default: |
| // continue inspecting other data items |
| } |
| } |
| return true; |
| } |
| |
| public static void cborPrettyPrintDataItem(StringBuilder sb, int indent, DataItem dataItem) { |
| StringBuilder indentBuilder = new StringBuilder(); |
| for (int n = 0; n < indent; n++) { |
| indentBuilder.append(' '); |
| } |
| String indentString = indentBuilder.toString(); |
| |
| if (dataItem.hasTag()) { |
| sb.append(String.format("tag %d ", dataItem.getTag().getValue())); |
| } |
| |
| switch (dataItem.getMajorType()) { |
| case INVALID: |
| // TODO: throw |
| sb.append("<invalid>"); |
| break; |
| case UNSIGNED_INTEGER: { |
| // Major type 0: an unsigned integer. |
| BigInteger value = ((UnsignedInteger) dataItem).getValue(); |
| sb.append(value); |
| } |
| break; |
| case NEGATIVE_INTEGER: { |
| // Major type 1: a negative integer. |
| BigInteger value = ((NegativeInteger) dataItem).getValue(); |
| sb.append(value); |
| } |
| break; |
| case BYTE_STRING: { |
| // Major type 2: a byte string. |
| byte[] value = ((ByteString) dataItem).getBytes(); |
| sb.append("["); |
| int count = 0; |
| for (byte b : value) { |
| if (count > 0) { |
| sb.append(", "); |
| } |
| sb.append(String.format("0x%02x", b)); |
| count++; |
| } |
| sb.append("]"); |
| } |
| break; |
| case UNICODE_STRING: { |
| // Major type 3: string of Unicode characters that is encoded as UTF-8 [RFC3629]. |
| String value = ((UnicodeString) dataItem).getString(); |
| // TODO: escape ' in |value| |
| sb.append("'" + value + "'"); |
| } |
| break; |
| case ARRAY: { |
| // Major type 4: an array of data items. |
| List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); |
| if (items.size() == 0) { |
| sb.append("[]"); |
| } else if (cborAreAllDataItemsNonCompound(items)) { |
| // The case where everything fits on one line. |
| sb.append("["); |
| int count = 0; |
| for (DataItem item : items) { |
| cborPrettyPrintDataItem(sb, indent, item); |
| if (++count < items.size()) { |
| sb.append(", "); |
| } |
| } |
| sb.append("]"); |
| } else { |
| sb.append("[\n" + indentString); |
| int count = 0; |
| for (DataItem item : items) { |
| sb.append(" "); |
| cborPrettyPrintDataItem(sb, indent + 2, item); |
| if (++count < items.size()) { |
| sb.append(","); |
| } |
| sb.append("\n" + indentString); |
| } |
| sb.append("]"); |
| } |
| } |
| break; |
| case MAP: { |
| // Major type 5: a map of pairs of data items. |
| Collection<DataItem> keys = ((co.nstant.in.cbor.model.Map) dataItem).getKeys(); |
| if (keys.size() == 0) { |
| sb.append("{}"); |
| } else { |
| sb.append("{\n" + indentString); |
| int count = 0; |
| for (DataItem key : keys) { |
| sb.append(" "); |
| DataItem value = ((co.nstant.in.cbor.model.Map) dataItem).get(key); |
| cborPrettyPrintDataItem(sb, indent + 2, key); |
| sb.append(" : "); |
| cborPrettyPrintDataItem(sb, indent + 2, value); |
| if (++count < keys.size()) { |
| sb.append(","); |
| } |
| sb.append("\n" + indentString); |
| } |
| sb.append("}"); |
| } |
| } |
| break; |
| case TAG: |
| // Major type 6: optional semantic tagging of other major types |
| // |
| // We never encounter this one since it's automatically handled via the |
| // DataItem that is tagged. |
| throw new RuntimeException("Semantic tag data item not expected"); |
| |
| case SPECIAL: |
| // Major type 7: floating point numbers and simple data types that need no |
| // content, as well as the "break" stop code. |
| if (dataItem instanceof SimpleValue) { |
| switch (((SimpleValue) dataItem).getSimpleValueType()) { |
| case FALSE: |
| sb.append("false"); |
| break; |
| case TRUE: |
| sb.append("true"); |
| break; |
| case NULL: |
| sb.append("null"); |
| break; |
| case UNDEFINED: |
| sb.append("undefined"); |
| break; |
| case RESERVED: |
| sb.append("reserved"); |
| break; |
| case UNALLOCATED: |
| sb.append("unallocated"); |
| break; |
| } |
| } else if (dataItem instanceof DoublePrecisionFloat) { |
| DecimalFormat df = new DecimalFormat("0", |
| DecimalFormatSymbols.getInstance(Locale.ENGLISH)); |
| df.setMaximumFractionDigits(340); |
| sb.append(df.format(((DoublePrecisionFloat) dataItem).getValue())); |
| } else if (dataItem instanceof AbstractFloat) { |
| DecimalFormat df = new DecimalFormat("0", |
| DecimalFormatSymbols.getInstance(Locale.ENGLISH)); |
| df.setMaximumFractionDigits(340); |
| sb.append(df.format(((AbstractFloat) dataItem).getValue())); |
| } else { |
| sb.append("break"); |
| } |
| break; |
| } |
| } |
| |
| public static byte[] encodeCbor(List<DataItem> dataItems) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| CborEncoder encoder = new CborEncoder(baos); |
| try { |
| encoder.encode(dataItems); |
| } catch (CborException e) { |
| throw new RuntimeException("Error encoding data", e); |
| } |
| return baos.toByteArray(); |
| } |
| |
| public static byte[] coseBuildToBeSigned(byte[] encodedProtectedHeaders, |
| byte[] payload, |
| byte[] detachedContent) { |
| CborBuilder sigStructure = new CborBuilder(); |
| ArrayBuilder<CborBuilder> array = sigStructure.addArray(); |
| |
| array.add("Signature1"); |
| array.add(encodedProtectedHeaders); |
| |
| // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) |
| // so external_aad is the empty bstr |
| byte emptyExternalAad[] = new byte[0]; |
| array.add(emptyExternalAad); |
| |
| // Next field is the payload, independently of how it's transported (RFC |
| // 8152 section 4.4). Since our API specifies only one of |data| and |
| // |detachedContent| can be non-empty, it's simply just the non-empty one. |
| if (payload != null && payload.length > 0) { |
| array.add(payload); |
| } else { |
| array.add(detachedContent); |
| } |
| array.end(); |
| return encodeCbor(sigStructure.build()); |
| } |
| |
| private static final int COSE_LABEL_ALG = 1; |
| private static final int COSE_LABEL_X5CHAIN = 33; // temporary identifier |
| |
| // From "COSE Algorithms" registry |
| private static final int COSE_ALG_ECDSA_256 = -7; |
| private static final int COSE_ALG_HMAC_256_256 = 5; |
| |
| private static byte[] signatureDerToCose(byte[] signature) { |
| if (signature.length > 128) { |
| throw new RuntimeException("Unexpected length " + signature.length |
| + ", expected less than 128"); |
| } |
| if (signature[0] != 0x30) { |
| throw new RuntimeException("Unexpected first byte " + signature[0] |
| + ", expected 0x30"); |
| } |
| if ((signature[1] & 0x80) != 0x00) { |
| throw new RuntimeException("Unexpected second byte " + signature[1] |
| + ", bit 7 shouldn't be set"); |
| } |
| int rOffset = 2; |
| int rSize = signature[rOffset + 1]; |
| byte[] rBytes = stripLeadingZeroes( |
| Arrays.copyOfRange(signature,rOffset + 2, rOffset + rSize + 2)); |
| |
| int sOffset = rOffset + 2 + rSize; |
| int sSize = signature[sOffset + 1]; |
| byte[] sBytes = stripLeadingZeroes( |
| Arrays.copyOfRange(signature, sOffset + 2, sOffset + sSize + 2)); |
| |
| if (rBytes.length > 32) { |
| throw new RuntimeException("rBytes.length is " + rBytes.length + " which is > 32"); |
| } |
| if (sBytes.length > 32) { |
| throw new RuntimeException("sBytes.length is " + sBytes.length + " which is > 32"); |
| } |
| |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| for (int n = 0; n < 32 - rBytes.length; n++) { |
| baos.write(0x00); |
| } |
| baos.write(rBytes); |
| for (int n = 0; n < 32 - sBytes.length; n++) { |
| baos.write(0x00); |
| } |
| baos.write(sBytes); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| return baos.toByteArray(); |
| } |
| |
| // Adds leading 0x00 if the first encoded byte MSB is set. |
| private static byte[] encodePositiveBigInteger(BigInteger i) { |
| byte[] bytes = i.toByteArray(); |
| if ((bytes[0] & 0x80) != 0) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| baos.write(0x00); |
| baos.write(bytes); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| throw new RuntimeException("Failed writing data", e); |
| } |
| bytes = baos.toByteArray(); |
| } |
| return bytes; |
| } |
| |
| private static byte[] signatureCoseToDer(byte[] signature) { |
| if (signature.length != 64) { |
| throw new RuntimeException("signature.length is " + signature.length + ", expected 64"); |
| } |
| // r and s are always positive and may use all 256 bits so use the constructor which |
| // parses them as unsigned. |
| BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)); |
| BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)); |
| byte[] rBytes = encodePositiveBigInteger(r); |
| byte[] sBytes = encodePositiveBigInteger(s); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| baos.write(0x30); |
| baos.write(2 + rBytes.length + 2 + sBytes.length); |
| baos.write(0x02); |
| baos.write(rBytes.length); |
| baos.write(rBytes); |
| baos.write(0x02); |
| baos.write(sBytes.length); |
| baos.write(sBytes); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| return baos.toByteArray(); |
| } |
| |
| public static byte[] coseSign1Sign(PrivateKey key, |
| @Nullable byte[] data, |
| byte[] detachedContent, |
| @Nullable Collection<X509Certificate> certificateChain) |
| throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException { |
| |
| int dataLen = (data != null ? data.length : 0); |
| int detachedContentLen = (detachedContent != null ? detachedContent.length : 0); |
| if (dataLen > 0 && detachedContentLen > 0) { |
| throw new RuntimeException("data and detachedContent cannot both be non-empty"); |
| } |
| |
| CborBuilder protectedHeaders = new CborBuilder(); |
| MapBuilder<CborBuilder> protectedHeadersMap = protectedHeaders.addMap(); |
| protectedHeadersMap.put(COSE_LABEL_ALG, COSE_ALG_ECDSA_256); |
| byte[] protectedHeadersBytes = encodeCbor(protectedHeaders.build()); |
| |
| byte[] toBeSigned = coseBuildToBeSigned(protectedHeadersBytes, data, detachedContent); |
| |
| byte[] coseSignature = null; |
| try { |
| Signature s = Signature.getInstance("SHA256withECDSA"); |
| s.initSign(key); |
| s.update(toBeSigned); |
| byte[] derSignature = s.sign(); |
| coseSignature = signatureDerToCose(derSignature); |
| } catch (SignatureException e) { |
| throw new RuntimeException("Error signing data"); |
| } |
| |
| CborBuilder builder = new CborBuilder(); |
| ArrayBuilder<CborBuilder> array = builder.addArray(); |
| array.add(protectedHeadersBytes); |
| MapBuilder<ArrayBuilder<CborBuilder>> unprotectedHeaders = array.addMap(); |
| if (certificateChain != null && certificateChain.size() > 0) { |
| if (certificateChain.size() == 1) { |
| X509Certificate cert = certificateChain.iterator().next(); |
| unprotectedHeaders.put(COSE_LABEL_X5CHAIN, cert.getEncoded()); |
| } else { |
| ArrayBuilder<MapBuilder<ArrayBuilder<CborBuilder>>> x5chainsArray = |
| unprotectedHeaders.putArray(COSE_LABEL_X5CHAIN); |
| for (X509Certificate cert : certificateChain) { |
| x5chainsArray.add(cert.getEncoded()); |
| } |
| } |
| } |
| if (data == null || data.length == 0) { |
| array.add(new SimpleValue(SimpleValueType.NULL)); |
| } else { |
| array.add(data); |
| } |
| array.add(coseSignature); |
| |
| return encodeCbor(builder.build()); |
| } |
| |
| public static boolean coseSign1CheckSignature(byte[] signatureCose1, |
| byte[] detachedContent, |
| PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException { |
| ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1); |
| List<DataItem> dataItems = null; |
| try { |
| dataItems = new CborDecoder(bais).decode(); |
| } catch (CborException e) { |
| throw new RuntimeException("Given signature is not valid CBOR", e); |
| } |
| if (dataItems.size() != 1) { |
| throw new RuntimeException("Expected just one data item"); |
| } |
| DataItem dataItem = dataItems.get(0); |
| if (dataItem.getMajorType() != MajorType.ARRAY) { |
| throw new RuntimeException("Data item is not an array"); |
| } |
| List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); |
| if (items.size() < 4) { |
| throw new RuntimeException("Expected at least four items in COSE_Sign1 array"); |
| } |
| if (items.get(0).getMajorType() != MajorType.BYTE_STRING) { |
| throw new RuntimeException("Item 0 (protected headers) is not a byte-string"); |
| } |
| byte[] encodedProtectedHeaders = |
| ((co.nstant.in.cbor.model.ByteString) items.get(0)).getBytes(); |
| byte[] payload = new byte[0]; |
| if (items.get(2).getMajorType() == MajorType.SPECIAL) { |
| if (((co.nstant.in.cbor.model.Special) items.get(2)).getSpecialType() |
| != SpecialType.SIMPLE_VALUE) { |
| throw new RuntimeException("Item 2 (payload) is a special but not a simple value"); |
| } |
| SimpleValue simple = (co.nstant.in.cbor.model.SimpleValue) items.get(2); |
| if (simple.getSimpleValueType() != SimpleValueType.NULL) { |
| throw new RuntimeException("Item 2 (payload) is a simple but not the value null"); |
| } |
| } else if (items.get(2).getMajorType() == MajorType.BYTE_STRING) { |
| payload = ((co.nstant.in.cbor.model.ByteString) items.get(2)).getBytes(); |
| } else { |
| throw new RuntimeException("Item 2 (payload) is not nil or byte-string"); |
| } |
| if (items.get(3).getMajorType() != MajorType.BYTE_STRING) { |
| throw new RuntimeException("Item 3 (signature) is not a byte-string"); |
| } |
| byte[] coseSignature = ((co.nstant.in.cbor.model.ByteString) items.get(3)).getBytes(); |
| |
| byte[] derSignature = signatureCoseToDer(coseSignature); |
| |
| int dataLen = payload.length; |
| int detachedContentLen = (detachedContent != null ? detachedContent.length : 0); |
| if (dataLen > 0 && detachedContentLen > 0) { |
| throw new RuntimeException("data and detachedContent cannot both be non-empty"); |
| } |
| |
| byte[] toBeSigned = Util.coseBuildToBeSigned(encodedProtectedHeaders, |
| payload, detachedContent); |
| |
| try { |
| Signature verifier = Signature.getInstance("SHA256withECDSA"); |
| verifier.initVerify(publicKey); |
| verifier.update(toBeSigned); |
| return verifier.verify(derSignature); |
| } catch (SignatureException e) { |
| throw new RuntimeException("Error verifying signature"); |
| } |
| } |
| |
| // Returns the empty byte-array if no data is included in the structure. |
| // |
| // Throws RuntimeException if the given bytes aren't valid COSE_Sign1. |
| // |
| public static byte[] coseSign1GetData(byte[] signatureCose1) { |
| ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1); |
| List<DataItem> dataItems = null; |
| try { |
| dataItems = new CborDecoder(bais).decode(); |
| } catch (CborException e) { |
| throw new RuntimeException("Given signature is not valid CBOR", e); |
| } |
| if (dataItems.size() != 1) { |
| throw new RuntimeException("Expected just one data item"); |
| } |
| DataItem dataItem = dataItems.get(0); |
| if (dataItem.getMajorType() != MajorType.ARRAY) { |
| throw new RuntimeException("Data item is not an array"); |
| } |
| List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); |
| if (items.size() < 4) { |
| throw new RuntimeException("Expected at least four items in COSE_Sign1 array"); |
| } |
| byte[] payload = new byte[0]; |
| if (items.get(2).getMajorType() == MajorType.SPECIAL) { |
| if (((co.nstant.in.cbor.model.Special) items.get(2)).getSpecialType() |
| != SpecialType.SIMPLE_VALUE) { |
| throw new RuntimeException("Item 2 (payload) is a special but not a simple value"); |
| } |
| SimpleValue simple = (co.nstant.in.cbor.model.SimpleValue) items.get(2); |
| if (simple.getSimpleValueType() != SimpleValueType.NULL) { |
| throw new RuntimeException("Item 2 (payload) is a simple but not the value null"); |
| } |
| } else if (items.get(2).getMajorType() == MajorType.BYTE_STRING) { |
| payload = ((co.nstant.in.cbor.model.ByteString) items.get(2)).getBytes(); |
| } else { |
| throw new RuntimeException("Item 2 (payload) is not nil or byte-string"); |
| } |
| return payload; |
| } |
| |
| // Returns the empty collection if no x5chain is included in the structure. |
| // |
| // Throws RuntimeException if the given bytes aren't valid COSE_Sign1. |
| // |
| public static Collection<X509Certificate> coseSign1GetX5Chain(byte[] signatureCose1) |
| throws CertificateException { |
| ArrayList<X509Certificate> ret = new ArrayList<>(); |
| ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1); |
| List<DataItem> dataItems = null; |
| try { |
| dataItems = new CborDecoder(bais).decode(); |
| } catch (CborException e) { |
| throw new RuntimeException("Given signature is not valid CBOR", e); |
| } |
| if (dataItems.size() != 1) { |
| throw new RuntimeException("Expected just one data item"); |
| } |
| DataItem dataItem = dataItems.get(0); |
| if (dataItem.getMajorType() != MajorType.ARRAY) { |
| throw new RuntimeException("Data item is not an array"); |
| } |
| List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems(); |
| if (items.size() < 4) { |
| throw new RuntimeException("Expected at least four items in COSE_Sign1 array"); |
| } |
| if (items.get(1).getMajorType() != MajorType.MAP) { |
| throw new RuntimeException("Item 1 (unprocted headers) is not a map"); |
| } |
| co.nstant.in.cbor.model.Map map = (co.nstant.in.cbor.model.Map) items.get(1); |
| DataItem x5chainItem = map.get(new UnsignedInteger(COSE_LABEL_X5CHAIN)); |
| if (x5chainItem != null) { |
| CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
| if (x5chainItem instanceof ByteString) { |
| ByteArrayInputStream certBais = |
| new ByteArrayInputStream(((ByteString) x5chainItem).getBytes()); |
| ret.add((X509Certificate) factory.generateCertificate(certBais)); |
| } else if (x5chainItem instanceof Array) { |
| for (DataItem certItem : ((Array) x5chainItem).getDataItems()) { |
| if (!(certItem instanceof ByteString)) { |
| throw new RuntimeException( |
| "Unexpected type for array item in x5chain value"); |
| } |
| ByteArrayInputStream certBais = |
| new ByteArrayInputStream(((ByteString) certItem).getBytes()); |
| ret.add((X509Certificate) factory.generateCertificate(certBais)); |
| } |
| } else { |
| throw new RuntimeException("Unexpected type for x5chain value"); |
| } |
| } |
| return ret; |
| } |
| |
| public static byte[] coseBuildToBeMACed(byte[] encodedProtectedHeaders, |
| byte[] payload, |
| byte[] detachedContent) { |
| CborBuilder macStructure = new CborBuilder(); |
| ArrayBuilder<CborBuilder> array = macStructure.addArray(); |
| |
| array.add("MAC0"); |
| array.add(encodedProtectedHeaders); |
| |
| // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) |
| // so external_aad is the empty bstr |
| byte emptyExternalAad[] = new byte[0]; |
| array.add(emptyExternalAad); |
| |
| // Next field is the payload, independently of how it's transported (RFC |
| // 8152 section 4.4). Since our API specifies only one of |data| and |
| // |detachedContent| can be non-empty, it's simply just the non-empty one. |
| if (payload != null && payload.length > 0) { |
| array.add(payload); |
| } else { |
| array.add(detachedContent); |
| } |
| |
| return encodeCbor(macStructure.build()); |
| } |
| |
| public static byte[] coseMac0(SecretKey key, |
| @Nullable byte[] data, |
| byte[] detachedContent) |
| throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException { |
| |
| int dataLen = (data != null ? data.length : 0); |
| int detachedContentLen = (detachedContent != null ? detachedContent.length : 0); |
| if (dataLen > 0 && detachedContentLen > 0) { |
| throw new RuntimeException("data and detachedContent cannot both be non-empty"); |
| } |
| |
| CborBuilder protectedHeaders = new CborBuilder(); |
| MapBuilder<CborBuilder> protectedHeadersMap = protectedHeaders.addMap(); |
| protectedHeadersMap.put(COSE_LABEL_ALG, COSE_ALG_HMAC_256_256); |
| byte[] protectedHeadersBytes = encodeCbor(protectedHeaders.build()); |
| |
| byte[] toBeMACed = coseBuildToBeMACed(protectedHeadersBytes, data, detachedContent); |
| |
| byte[] mac = null; |
| Mac m = Mac.getInstance("HmacSHA256"); |
| m.init(key); |
| m.update(toBeMACed); |
| mac = m.doFinal(); |
| |
| CborBuilder builder = new CborBuilder(); |
| ArrayBuilder<CborBuilder> array = builder.addArray(); |
| array.add(protectedHeadersBytes); |
| MapBuilder<ArrayBuilder<CborBuilder>> unprotectedHeaders = array.addMap(); |
| if (data == null || data.length == 0) { |
| array.add(new SimpleValue(SimpleValueType.NULL)); |
| } else { |
| array.add(data); |
| } |
| array.add(mac); |
| |
| return encodeCbor(builder.build()); |
| } |
| |
| public static String replaceLine(String text, int lineNumber, String replacementLine) { |
| String[] lines = text.split("\n"); |
| int numLines = lines.length; |
| if (lineNumber < 0) { |
| lineNumber = numLines - (-lineNumber); |
| } |
| StringBuilder sb = new StringBuilder(); |
| for (int n = 0; n < numLines; n++) { |
| if (n == lineNumber) { |
| sb.append(replacementLine); |
| } else { |
| sb.append(lines[n]); |
| } |
| // Only add terminating newline if passed-in string ends in a newline. |
| if (n == numLines - 1) { |
| if (text.endsWith(("\n"))) { |
| sb.append('\n'); |
| } |
| } else { |
| sb.append('\n'); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| public static byte[] cborEncode(DataItem dataItem) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| new CborEncoder(baos).encode(dataItem); |
| } catch (CborException e) { |
| // This should never happen and we don't want cborEncode() to throw since that |
| // would complicate all callers. Log it instead. |
| e.printStackTrace(); |
| Log.e(TAG, "Error encoding DataItem"); |
| } |
| return baos.toByteArray(); |
| } |
| |
| public static byte[] cborEncodeBoolean(boolean value) { |
| return cborEncode(new CborBuilder().add(value).build().get(0)); |
| } |
| |
| public static byte[] cborEncodeString(@NonNull String value) { |
| return cborEncode(new CborBuilder().add(value).build().get(0)); |
| } |
| |
| public static byte[] cborEncodeBytestring(@NonNull byte[] value) { |
| return cborEncode(new CborBuilder().add(value).build().get(0)); |
| } |
| |
| public static byte[] cborEncodeInt(long value) { |
| return cborEncode(new CborBuilder().add(value).build().get(0)); |
| } |
| |
| static final int CBOR_SEMANTIC_TAG_ENCODED_CBOR = 24; |
| |
| public static DataItem cborToDataItem(byte[] data) { |
| ByteArrayInputStream bais = new ByteArrayInputStream(data); |
| try { |
| List<DataItem> dataItems = new CborDecoder(bais).decode(); |
| if (dataItems.size() != 1) { |
| throw new RuntimeException("Expected 1 item, found " + dataItems.size()); |
| } |
| return dataItems.get(0); |
| } catch (CborException e) { |
| throw new RuntimeException("Error decoding data", e); |
| } |
| } |
| |
| public static boolean cborDecodeBoolean(@NonNull byte[] data) { |
| return cborToDataItem(data) == SimpleValue.TRUE; |
| } |
| |
| public static String cborDecodeString(@NonNull byte[] data) { |
| return ((co.nstant.in.cbor.model.UnicodeString) cborToDataItem(data)).getString(); |
| } |
| |
| public static long cborDecodeInt(@NonNull byte[] data) { |
| return ((co.nstant.in.cbor.model.Number) cborToDataItem(data)).getValue().longValue(); |
| } |
| |
| public static byte[] cborDecodeBytestring(@NonNull byte[] data) { |
| return ((co.nstant.in.cbor.model.ByteString) cborToDataItem(data)).getBytes(); |
| } |
| |
| public static String getStringEntry(ResultData data, String namespaceName, String name) { |
| return Util.cborDecodeString(data.getEntry(namespaceName, name)); |
| } |
| |
| public static boolean getBooleanEntry(ResultData data, String namespaceName, String name) { |
| return Util.cborDecodeBoolean(data.getEntry(namespaceName, name)); |
| } |
| |
| public static long getIntegerEntry(ResultData data, String namespaceName, String name) { |
| return Util.cborDecodeInt(data.getEntry(namespaceName, name)); |
| } |
| |
| public static byte[] getBytestringEntry(ResultData data, String namespaceName, String name) { |
| return Util.cborDecodeBytestring(data.getEntry(namespaceName, name)); |
| } |
| |
| /* |
| Certificate: |
| Data: |
| Version: 3 (0x2) |
| Serial Number: 1 (0x1) |
| Signature Algorithm: ecdsa-with-SHA256 |
| Issuer: CN=fake |
| Validity |
| Not Before: Jan 1 00:00:00 1970 GMT |
| Not After : Jan 1 00:00:00 2048 GMT |
| Subject: CN=fake |
| Subject Public Key Info: |
| Public Key Algorithm: id-ecPublicKey |
| Public-Key: (256 bit) |
| 00000000 04 9b 60 70 8a 99 b6 bf e3 b8 17 02 9e 93 eb 48 |..`p...........H| |
| 00000010 23 b9 39 89 d1 00 bf a0 0f d0 2f bd 6b 11 bc d1 |#.9......./.k...| |
| 00000020 19 53 54 28 31 00 f5 49 db 31 fb 9f 7d 99 bf 23 |.ST(1..I.1..}..#| |
| 00000030 fb 92 04 6b 23 63 55 98 ad 24 d2 68 c4 83 bf 99 |...k#cU..$.h....| |
| 00000040 62 |b| |
| Signature Algorithm: ecdsa-with-SHA256 |
| 30:45:02:20:67:ad:d1:34:ed:a5:68:3f:5b:33:ee:b3:18:a2: |
| eb:03:61:74:0f:21:64:4a:a3:2e:82:b3:92:5c:21:0f:88:3f: |
| 02:21:00:b7:38:5c:9b:f2:9c:b1:27:86:37:44:df:eb:4a:b2: |
| 6c:11:9a:c1:ff:b2:80:95:ce:fc:5f:26:b4:20:6e:9b:0d |
| */ |
| |
| |
| public static @NonNull X509Certificate signPublicKeyWithPrivateKey(String keyToSignAlias, |
| String keyToSignWithAlias) { |
| |
| KeyStore ks = null; |
| try { |
| ks = KeyStore.getInstance("AndroidKeyStore"); |
| ks.load(null); |
| |
| /* First note that KeyStore.getCertificate() returns a self-signed X.509 certificate |
| * for the key in question. As per RFC 5280, section 4.1 an X.509 certificate has the |
| * following structure: |
| * |
| * Certificate ::= SEQUENCE { |
| * tbsCertificate TBSCertificate, |
| * signatureAlgorithm AlgorithmIdentifier, |
| * signatureValue BIT STRING } |
| * |
| * Conveniently, the X509Certificate class has a getTBSCertificate() method which |
| * returns the tbsCertificate blob. So all we need to do is just sign that and build |
| * signatureAlgorithm and signatureValue and combine it with tbsCertificate. We don't |
| * need a full-blown ASN.1/DER encoder to do this. |
| */ |
| X509Certificate selfSignedCert = (X509Certificate) ks.getCertificate(keyToSignAlias); |
| byte[] tbsCertificate = selfSignedCert.getTBSCertificate(); |
| |
| KeyStore.Entry keyToSignWithEntry = ks.getEntry(keyToSignWithAlias, null); |
| Signature s = Signature.getInstance("SHA256withECDSA"); |
| s.initSign(((KeyStore.PrivateKeyEntry) keyToSignWithEntry).getPrivateKey()); |
| s.update(tbsCertificate); |
| byte[] signatureValue = s.sign(); |
| |
| /* The DER encoding for a SEQUENCE of length 128-65536 - the length is updated below. |
| * |
| * We assume - and test for below - that the final length is always going to be in |
| * this range. This is a sound assumption given we're using 256-bit EC keys. |
| */ |
| byte[] sequence = new byte[]{ |
| 0x30, (byte) 0x82, 0x00, 0x00 |
| }; |
| |
| /* The DER encoding for the ECDSA with SHA-256 signature algorithm: |
| * |
| * SEQUENCE (1 elem) |
| * OBJECT IDENTIFIER 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA |
| * algorithm with SHA256) |
| */ |
| byte[] signatureAlgorithm = new byte[]{ |
| 0x30, 0x0a, 0x06, 0x08, 0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x04, 0x03, |
| 0x02 |
| }; |
| |
| /* The DER encoding for a BIT STRING with one element - the length is updated below. |
| * |
| * We assume the length of signatureValue is always going to be less than 128. This |
| * assumption works since we know ecdsaWithSHA256 signatures are always 69, 70, or |
| * 71 bytes long when DER encoded. |
| */ |
| byte[] bitStringForSignature = new byte[]{0x03, 0x00, 0x00}; |
| |
| // Calculate sequence length and set it in |sequence|. |
| int sequenceLength = tbsCertificate.length |
| + signatureAlgorithm.length |
| + bitStringForSignature.length |
| + signatureValue.length; |
| if (sequenceLength < 128 || sequenceLength > 65535) { |
| throw new Exception("Unexpected sequenceLength " + sequenceLength); |
| } |
| sequence[2] = (byte) (sequenceLength >> 8); |
| sequence[3] = (byte) (sequenceLength & 0xff); |
| |
| // Calculate signatureValue length and set it in |bitStringForSignature|. |
| int signatureValueLength = signatureValue.length + 1; |
| if (signatureValueLength >= 128) { |
| throw new Exception("Unexpected signatureValueLength " + signatureValueLength); |
| } |
| bitStringForSignature[1] = (byte) signatureValueLength; |
| |
| // Finally concatenate everything together. |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| baos.write(sequence); |
| baos.write(tbsCertificate); |
| baos.write(signatureAlgorithm); |
| baos.write(bitStringForSignature); |
| baos.write(signatureValue); |
| byte[] resultingCertBytes = baos.toByteArray(); |
| |
| CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
| ByteArrayInputStream bais = new ByteArrayInputStream(resultingCertBytes); |
| X509Certificate result = (X509Certificate) cf.generateCertificate(bais); |
| return result; |
| } catch (Exception e) { |
| throw new RuntimeException("Error signing public key with private key", e); |
| } |
| } |
| |
| public static byte[] buildDeviceAuthenticationCbor(String docType, |
| byte[] encodedSessionTranscript, |
| byte[] deviceNameSpacesBytes) { |
| ByteArrayOutputStream daBaos = new ByteArrayOutputStream(); |
| try { |
| ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript); |
| List<DataItem> dataItems = null; |
| dataItems = new CborDecoder(bais).decode(); |
| DataItem sessionTranscript = dataItems.get(0); |
| ByteString deviceNameSpacesBytesItem = new ByteString(deviceNameSpacesBytes); |
| deviceNameSpacesBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| new CborEncoder(daBaos).encode(new CborBuilder() |
| .addArray() |
| .add("DeviceAuthentication") |
| .add(sessionTranscript) |
| .add(docType) |
| .add(deviceNameSpacesBytesItem) |
| .end() |
| .build()); |
| } catch (CborException e) { |
| throw new RuntimeException("Error encoding DeviceAuthentication", e); |
| } |
| return daBaos.toByteArray(); |
| } |
| |
| public static byte[] buildReaderAuthenticationBytesCbor( |
| byte[] encodedSessionTranscript, |
| byte[] requestMessageBytes) { |
| |
| ByteArrayOutputStream daBaos = new ByteArrayOutputStream(); |
| try { |
| ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript); |
| List<DataItem> dataItems = null; |
| dataItems = new CborDecoder(bais).decode(); |
| DataItem sessionTranscript = dataItems.get(0); |
| ByteString requestMessageBytesItem = new ByteString(requestMessageBytes); |
| requestMessageBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| new CborEncoder(daBaos).encode(new CborBuilder() |
| .addArray() |
| .add("ReaderAuthentication") |
| .add(sessionTranscript) |
| .add(requestMessageBytesItem) |
| .end() |
| .build()); |
| } catch (CborException e) { |
| throw new RuntimeException("Error encoding ReaderAuthentication", e); |
| } |
| byte[] readerAuthentication = daBaos.toByteArray(); |
| return Util.prependSemanticTagForEncodedCbor(readerAuthentication); |
| } |
| |
| // Returns #6.24(bstr) of the given already encoded CBOR |
| // |
| public static @NonNull DataItem buildCborTaggedByteString(@NonNull byte[] encodedCbor) { |
| DataItem item = new ByteString(encodedCbor); |
| item.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| return item; |
| } |
| |
| public static byte[] prependSemanticTagForEncodedCbor(byte[] encodedCbor) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| new CborEncoder(baos).encode(buildCborTaggedByteString(encodedCbor)); |
| } catch (CborException e) { |
| throw new RuntimeException("Error encoding with semantic tag for CBOR encoding", e); |
| } |
| return baos.toByteArray(); |
| } |
| |
| public static byte[] concatArrays(byte[] a, byte[] b) { |
| byte[] ret = new byte[a.length + b.length]; |
| System.arraycopy(a, 0, ret, 0, a.length); |
| System.arraycopy(b, 0, ret, a.length, b.length); |
| return ret; |
| } |
| |
| public static SecretKey calcEMacKeyForReader(PublicKey authenticationPublicKey, |
| PrivateKey ephemeralReaderPrivateKey, |
| byte[] encodedSessionTranscript) { |
| try { |
| KeyAgreement ka = KeyAgreement.getInstance("ECDH"); |
| ka.init(ephemeralReaderPrivateKey); |
| ka.doPhase(authenticationPublicKey, true); |
| byte[] sharedSecret = ka.generateSecret(); |
| |
| byte[] sessionTranscriptBytes = |
| Util.prependSemanticTagForEncodedCbor(encodedSessionTranscript); |
| |
| byte[] salt = MessageDigest.getInstance("SHA-256").digest(sessionTranscriptBytes); |
| byte[] info = new byte[] {'E', 'M', 'a', 'c', 'K', 'e', 'y'}; |
| byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32); |
| SecretKey secretKey = new SecretKeySpec(derivedKey, ""); |
| return secretKey; |
| } catch (InvalidKeyException |
| | NoSuchAlgorithmException e) { |
| throw new RuntimeException("Error performing key agreement", e); |
| } |
| } |
| |
| /** |
| * Computes an HKDF. |
| * |
| * This is based on https://github.com/google/tink/blob/master/java/src/main/java/com/google |
| * /crypto/tink/subtle/Hkdf.java |
| * which is also Copyright (c) Google and also licensed under the Apache 2 license. |
| * |
| * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or |
| * "HMACSHA256". |
| * @param ikm the input keying material. |
| * @param salt optional salt. A possibly non-secret random value. If no salt is |
| * provided (i.e. if |
| * salt has length 0) then an array of 0s of the same size as the hash |
| * digest is used as salt. |
| * @param info optional context and application specific information. |
| * @param size The length of the generated pseudorandom string in bytes. The maximal |
| * size is |
| * 255.DigestSize, where DigestSize is the size of the underlying HMAC. |
| * @return size pseudorandom bytes. |
| */ |
| public static byte[] computeHkdf( |
| String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size) { |
| Mac mac = null; |
| try { |
| mac = Mac.getInstance(macAlgorithm); |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException("No such algorithm: " + macAlgorithm, e); |
| } |
| if (size > 255 * mac.getMacLength()) { |
| throw new RuntimeException("size too large"); |
| } |
| try { |
| if (salt == null || salt.length == 0) { |
| // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided |
| // then HKDF uses a salt that is an array of zeros of the same length as the hash |
| // digest. |
| mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm)); |
| } else { |
| mac.init(new SecretKeySpec(salt, macAlgorithm)); |
| } |
| byte[] prk = mac.doFinal(ikm); |
| byte[] result = new byte[size]; |
| int ctr = 1; |
| int pos = 0; |
| mac.init(new SecretKeySpec(prk, macAlgorithm)); |
| byte[] digest = new byte[0]; |
| while (true) { |
| mac.update(digest); |
| mac.update(info); |
| mac.update((byte) ctr); |
| digest = mac.doFinal(); |
| if (pos + digest.length < size) { |
| System.arraycopy(digest, 0, result, pos, digest.length); |
| pos += digest.length; |
| ctr++; |
| } else { |
| System.arraycopy(digest, 0, result, pos, size - pos); |
| break; |
| } |
| } |
| return result; |
| } catch (InvalidKeyException e) { |
| throw new RuntimeException("Error MACing", e); |
| } |
| } |
| |
| static byte[] stripLeadingZeroes(byte[] value) { |
| int n = 0; |
| while (n < value.length && value[n] == 0) { |
| n++; |
| } |
| int newLen = value.length - n; |
| byte[] ret = new byte[newLen]; |
| int m = 0; |
| while (n < value.length) { |
| ret[m++] = value[n++]; |
| } |
| return ret; |
| } |
| |
| public static void hexdump(String name, byte[] data) { |
| int n, m, o; |
| StringBuilder sb = new StringBuilder(); |
| Formatter fmt = new Formatter(sb); |
| for (n = 0; n < data.length; n += 16) { |
| fmt.format("%04x ", n); |
| for (m = 0; m < 16 && n + m < data.length; m++) { |
| fmt.format("%02x ", data[n + m]); |
| } |
| for (o = m; o < 16; o++) { |
| sb.append(" "); |
| } |
| sb.append(" "); |
| for (m = 0; m < 16 && n + m < data.length; m++) { |
| int c = data[n + m] & 0xff; |
| fmt.format("%c", Character.isISOControl(c) ? '.' : c); |
| } |
| sb.append("\n"); |
| } |
| sb.append("\n"); |
| Log.e(TAG, name + ": dumping " + data.length + " bytes\n" + fmt.toString()); |
| } |
| |
| // Convert EC P256 public key to DER format binary format |
| public static byte[] convertP256PublicKeyToDERFormat(ECPoint w) { |
| byte[] ret = new byte[64]; |
| |
| // Each coordinate may be encoded in 33*, 32, or fewer bytes. |
| // |
| // * : it can be 33 bytes because toByteArray() guarantees "The array will contain the |
| // minimum number of bytes required to represent this BigInteger, including at |
| // least one sign bit, which is (ceil((this.bitLength() + 1)/8))" which means that |
| // the MSB is always 0x00. This is taken care of by calling calling |
| // stripLeadingZeroes(). |
| // |
| // We need the encoding to be exactly 32 bytes since according to RFC 5480 section 2.2 |
| // and SEC 1: Elliptic Curve Cryptography section 2.3.3 the encoding is 0x04 | X | Y |
| // where X and Y are encoded in exactly 32 byte, big endian integer values each. |
| // |
| byte[] xBytes = stripLeadingZeroes(w.getAffineX().toByteArray()); |
| if (xBytes.length > 32) { |
| throw new RuntimeException("xBytes is " + xBytes.length + " which is unexpected"); |
| } |
| int numLeadingZeroBytes = 32 - xBytes.length; |
| for (int n = 0; n < numLeadingZeroBytes; n++) { |
| ret[n] = 0x00; |
| } |
| for (int n = 0; n < xBytes.length; n++) { |
| ret[numLeadingZeroBytes + n] = xBytes[n]; |
| } |
| |
| byte[] yBytes = stripLeadingZeroes(w.getAffineY().toByteArray()); |
| if (yBytes.length > 32) { |
| throw new RuntimeException("yBytes is " + yBytes.length + " which is unexpected"); |
| } |
| numLeadingZeroBytes = 32 - yBytes.length; |
| for (int n = 0; n < numLeadingZeroBytes; n++) { |
| ret[32 + n] = 0x00; |
| } |
| for (int n = 0; n < yBytes.length; n++) { |
| ret[32 + numLeadingZeroBytes + n] = yBytes[n]; |
| } |
| |
| return ret; |
| } |
| |
| // This returns a SessionTranscript which satisfy the requirement |
| // that the uncompressed X and Y coordinates of the public key for the |
| // mDL's ephemeral key-pair appear somewhere in the encoded |
| // DeviceEngagement. |
| public static byte[] buildSessionTranscript(KeyPair ephemeralKeyPair) { |
| // Make the coordinates appear in an already encoded bstr - this |
| // mimics how the mDL COSE_Key appear as encoded data inside the |
| // encoded DeviceEngagement |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| baos.write(new byte[]{42}); |
| |
| ECPoint w = ((ECPublicKey) ephemeralKeyPair.getPublic()).getW(); |
| baos.write(convertP256PublicKeyToDERFormat(w)); |
| |
| baos.write(new byte[]{43, 44}); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| byte[] blobWithCoords = baos.toByteArray(); |
| |
| baos = new ByteArrayOutputStream(); |
| try { |
| new CborEncoder(baos).encode(new CborBuilder() |
| .addArray() |
| .add(blobWithCoords) |
| .end() |
| .build()); |
| } catch (CborException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| ByteString encodedDeviceEngagementItem = new ByteString(baos.toByteArray()); |
| ByteString encodedEReaderKeyItem = new ByteString(cborEncodeString("doesn't matter")); |
| encodedDeviceEngagementItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| encodedEReaderKeyItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| |
| baos = new ByteArrayOutputStream(); |
| try { |
| new CborEncoder(baos).encode(new CborBuilder() |
| .addArray() |
| .add(encodedDeviceEngagementItem) |
| .add(encodedEReaderKeyItem) |
| .end() |
| .build()); |
| } catch (CborException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| return baos.toByteArray(); |
| } |
| |
| /* |
| * Helper function to create a CBOR data for requesting data items. The IntentToRetain |
| * value will be set to false for all elements. |
| * |
| * <p>The returned CBOR data conforms to the following CDDL schema:</p> |
| * |
| * <pre> |
| * ItemsRequest = { |
| * ? "docType" : DocType, |
| * "nameSpaces" : NameSpaces, |
| * ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide |
| * } |
| * |
| * NameSpaces = { |
| * + NameSpace => DataElements ; Requested data elements for each NameSpace |
| * } |
| * |
| * DataElements = { |
| * + DataElement => IntentToRetain |
| * } |
| * |
| * DocType = tstr |
| * |
| * DataElement = tstr |
| * IntentToRetain = bool |
| * NameSpace = tstr |
| * </pre> |
| * |
| * @param entriesToRequest The entries to request, organized as a map of namespace |
| * names with each value being a collection of data elements |
| * in the given namespace. |
| * @param docType The document type or {@code null} if there is no document |
| * type. |
| * @return CBOR data conforming to the CDDL mentioned above. |
| */ |
| public static @NonNull byte[] createItemsRequest( |
| @NonNull Map<String, Collection<String>> entriesToRequest, |
| @Nullable String docType) { |
| CborBuilder builder = new CborBuilder(); |
| MapBuilder<CborBuilder> mapBuilder = builder.addMap(); |
| if (docType != null) { |
| mapBuilder.put("docType", docType); |
| } |
| |
| MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces"); |
| for (String namespaceName : entriesToRequest.keySet()) { |
| Collection<String> entryNames = entriesToRequest.get(namespaceName); |
| MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder = |
| nsMapBuilder.putMap(namespaceName); |
| for (String entryName : entryNames) { |
| entryNameMapBuilder.put(entryName, false); |
| } |
| } |
| |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| CborEncoder encoder = new CborEncoder(baos); |
| try { |
| encoder.encode(builder.build()); |
| } catch (CborException e) { |
| throw new RuntimeException("Error encoding CBOR", e); |
| } |
| return baos.toByteArray(); |
| } |
| |
| public static KeyPair createEphemeralKeyPair() { |
| try { |
| KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC); |
| ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1"); |
| kpg.initialize(ecSpec); |
| KeyPair keyPair = kpg.generateKeyPair(); |
| return keyPair; |
| } catch (NoSuchAlgorithmException |
| | InvalidAlgorithmParameterException e) { |
| throw new RuntimeException("Error generating ephemeral key-pair", e); |
| } |
| } |
| |
| public static byte[] getPopSha256FromAuthKeyCert(X509Certificate cert) { |
| byte[] octetString = cert.getExtensionValue("1.3.6.1.4.1.11129.2.1.26"); |
| if (octetString == null) { |
| return null; |
| } |
| Util.hexdump("octetString", octetString); |
| |
| try { |
| ASN1InputStream asn1InputStream = new ASN1InputStream(octetString); |
| byte[] cborBytes = ((ASN1OctetString) asn1InputStream.readObject()).getOctets(); |
| Util.hexdump("cborBytes", cborBytes); |
| |
| ByteArrayInputStream bais = new ByteArrayInputStream(cborBytes); |
| List<DataItem> dataItems = new CborDecoder(bais).decode(); |
| if (dataItems.size() != 1) { |
| throw new RuntimeException("Expected 1 item, found " + dataItems.size()); |
| } |
| if (!(dataItems.get(0) instanceof co.nstant.in.cbor.model.Array)) { |
| throw new RuntimeException("Item is not a map"); |
| } |
| co.nstant.in.cbor.model.Array array = (co.nstant.in.cbor.model.Array) dataItems.get(0); |
| List<DataItem> items = array.getDataItems(); |
| if (items.size() < 2) { |
| throw new RuntimeException( |
| "Expected at least 2 array items, found " + items.size()); |
| } |
| if (!(items.get(0) instanceof UnicodeString)) { |
| throw new RuntimeException("First array item is not a string"); |
| } |
| String id = ((UnicodeString) items.get(0)).getString(); |
| if (!id.equals("ProofOfBinding")) { |
| throw new RuntimeException("Expected ProofOfBinding, got " + id); |
| } |
| if (!(items.get(1) instanceof ByteString)) { |
| throw new RuntimeException("Second array item is not a bytestring"); |
| } |
| byte[] popSha256 = ((ByteString) items.get(1)).getBytes(); |
| if (popSha256.length != 32) { |
| throw new RuntimeException( |
| "Expected bstr to be 32 bytes, it is " + popSha256.length); |
| } |
| return popSha256; |
| } catch (IOException e) { |
| throw new RuntimeException("Error decoding extension data", e); |
| } catch (CborException e) { |
| throw new RuntimeException("Error decoding data", e); |
| } |
| } |
| |
| } |