Util.java

/*
 * 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 androidx.security.identity;

import android.icu.text.DateFormat;
import android.icu.text.SimpleDateFormat;
import android.icu.util.Calendar;
import android.icu.util.GregorianCalendar;
import android.icu.util.TimeZone;
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.InvalidKeyException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
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.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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

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.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.MajorType;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.Number;
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;

class Util {
    private static final String TAG = "Util";

    // Not called.
    private Util() {}

    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();
    }

    static byte[] cborEncodeBoolean(boolean value) {
        return cborEncode(new CborBuilder().add(value).build().get(0));
    }

    static byte[] cborEncodeString(@NonNull String value) {
        return cborEncode(new CborBuilder().add(value).build().get(0));
    }

    static byte[] cborEncodeLong(long value) {
        return cborEncode(new CborBuilder().add(value).build().get(0));
    }

    static byte[] cborEncodeBytestring(@NonNull byte[] value) {
        return cborEncode(new CborBuilder().add(value).build().get(0));
    }

    static byte[] cborEncodeCalendar(@NonNull Calendar calendar) {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
        if (calendar.isSet(Calendar.MILLISECOND) && calendar.get(Calendar.MILLISECOND) != 0) {
            df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
        }
        df.setTimeZone(calendar.getTimeZone());
        Date val = calendar.getTime();
        String dateString = df.format(val);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            new CborEncoder(baos).encode(new CborBuilder()
                    .addTag(0)
                    .add(dateString)
                    .build());
        } catch (CborException e) {
            // Should never happen and we don't want to complicate callers by throwing.
            e.printStackTrace();
            Log.e(TAG, "Error encoding Calendar");
        }
        byte[] data = baos.toByteArray();
        return data;
    }

    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);
        }
    }

    static boolean cborDecodeBoolean(@NonNull byte[] data) {
        SimpleValue simple = (SimpleValue) cborToDataItem(data);
        return simple.getSimpleValueType() == SimpleValueType.TRUE;
    }

    static String cborDecodeString(@NonNull byte[] data) {
        return ((co.nstant.in.cbor.model.UnicodeString) cborToDataItem(data)).getString();
    }

    static long cborDecodeLong(@NonNull byte[] data) {
        return ((co.nstant.in.cbor.model.Number) cborToDataItem(data)).getValue().longValue();
    }

    static byte[] cborDecodeBytestring(@NonNull byte[] data) {
        return ((co.nstant.in.cbor.model.ByteString) cborToDataItem(data)).getBytes();
    }

    static Calendar cborDecodeCalendar(@NonNull byte[] data) {
        DataItem di = cborToDataItem(data);
        if (!(di instanceof co.nstant.in.cbor.model.UnicodeString)) {
            throw new RuntimeException("Passed in data is not a Unicode-string");
        }
        if (!di.hasTag() || di.getTag().getValue() != 0) {
            throw new RuntimeException("Passed in data is not tagged with tag 0");
        }
        String dateString = ((co.nstant.in.cbor.model.UnicodeString) di).getString();

        // Manually parse the timezone
        TimeZone parsedTz = TimeZone.getTimeZone("UTC");
        if (!dateString.endsWith("Z")) {
            String timeZoneSubstr = dateString.substring(dateString.length() - 6);
            parsedTz = TimeZone.getTimeZone("GMT" + timeZoneSubstr);
        }

        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
        df.setTimeZone(parsedTz);
        Date date = null;
        try {
            date = df.parse(dateString);
        } catch (ParseException e) {
            // Try again, this time without the milliseconds
            df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
            df.setTimeZone(parsedTz);
            try {
                date = df.parse(dateString);
            } catch (ParseException e2) {
                throw new RuntimeException("Error parsing string", e2);
            }
        }

        Calendar c = new GregorianCalendar();
        c.clear();
        c.setTimeZone(df.getTimeZone());
        c.setTime(date);
        return c;
    }

    static DataItem namespaceDataToCbor(PersonalizationData.NamespaceData entryNamespace) {
        CborBuilder entryBuilder = new CborBuilder();
        ArrayBuilder<CborBuilder> entryArrayBuilder = entryBuilder.addArray();
        for (String entryName : entryNamespace.getEntryNames()) {
            byte[] entryValue = entryNamespace.getEntryValue(entryName);
            Collection<AccessControlProfileId> accessControlProfileIds =
                    entryNamespace.getAccessControlProfileIds(
                            entryName);

            CborBuilder accessControlProfileIdsBuilder = new CborBuilder();
            ArrayBuilder<CborBuilder> accessControlProfileIdsArrayBuilder =
                    accessControlProfileIdsBuilder.addArray();
            for (AccessControlProfileId id : accessControlProfileIds) {
                accessControlProfileIdsArrayBuilder.add(id.getId());
            }

            MapBuilder<ArrayBuilder<CborBuilder>> entryMapBuilder = entryArrayBuilder.addMap();
            entryMapBuilder.put("name", entryName);
            entryMapBuilder.put(new UnicodeString("accessControlProfiles"),
                    accessControlProfileIdsBuilder.build().get(0));
            entryMapBuilder.put(new UnicodeString("value"), cborToDataItem(entryValue));
        }
        return entryBuilder.build().get(0);
    }

    public static PersonalizationData.NamespaceData namespaceDataFromCbor(String namespaceName,
            DataItem dataItem) {
        if (!(dataItem instanceof Array)) {
            throw new RuntimeException("Item is not an Array");
        }
        Array array = (Array) dataItem;

        PersonalizationData.Builder builder = new PersonalizationData.Builder();

        for (DataItem item : array.getDataItems()) {
            if (!(item instanceof co.nstant.in.cbor.model.Map)) {
                throw new RuntimeException("Item is not a map");
            }
            co.nstant.in.cbor.model.Map map = (co.nstant.in.cbor.model.Map) item;

            String name = ((UnicodeString) map.get(new UnicodeString("name"))).getString();

            Collection<AccessControlProfileId> accessControlProfileIds = new ArrayList<>();
            co.nstant.in.cbor.model.Array accessControlProfileArray =
                    (co.nstant.in.cbor.model.Array) map.get(
                            new UnicodeString("accessControlProfiles"));
            for (DataItem acpIdItem : accessControlProfileArray.getDataItems()) {
                accessControlProfileIds.add(
                        new AccessControlProfileId(((Number) acpIdItem).getValue().intValue()));
            }

            DataItem cborValue = map.get(new UnicodeString("value"));
            byte[] data = cborEncode(cborValue);
            builder.putEntry(namespaceName, name, accessControlProfileIds, data);
        }

        return builder.build().getNamespaceData(namespaceName);
    }

    public static AccessControlProfile accessControlProfileFromCbor(DataItem item) {
        if (!(item instanceof co.nstant.in.cbor.model.Map)) {
            throw new RuntimeException("Item is not a map");
        }
        Map map = (Map) item;

        int accessControlProfileId = ((Number) map.get(
                new UnicodeString("id"))).getValue().intValue();
        AccessControlProfile.Builder builder = new AccessControlProfile.Builder(
                new AccessControlProfileId(accessControlProfileId));

        item = map.get(new UnicodeString("readerCertificate"));
        if (item != null) {
            byte[] rcBytes = ((ByteString) item).getBytes();
            CertificateFactory certFactory = null;
            try {
                certFactory = CertificateFactory.getInstance("X.509");
                builder.setReaderCertificate((X509Certificate) certFactory.generateCertificate(
                        new ByteArrayInputStream(rcBytes)));
            } catch (CertificateException e) {
                throw new RuntimeException("Error decoding readerCertificate", e);
            }
        }

        builder.setUserAuthenticationRequired(false);
        item = map.get(new UnicodeString("capabilityType"));
        if (item != null) {
            // TODO: deal with -1 as per entryNamespaceToCbor()
            builder.setUserAuthenticationRequired(true);
            item = map.get(new UnicodeString("timeout"));
            builder.setUserAuthenticationTimeout(
                    item == null ? 0 : ((Number) item).getValue().intValue());
        }
        return builder.build();
    }

    static DataItem accessControlProfileToCbor(AccessControlProfile accessControlProfile) {
        CborBuilder cborBuilder = new CborBuilder();
        MapBuilder<CborBuilder> mapBuilder = cborBuilder.addMap();

        mapBuilder.put("id", accessControlProfile.getAccessControlProfileId().getId());
        X509Certificate readerCertificate = accessControlProfile.getReaderCertificate();
        if (readerCertificate != null) {
            try {
                mapBuilder.put("readerCertificate", readerCertificate.getEncoded());
            } catch (CertificateEncodingException e) {
                throw new RuntimeException("Error encoding reader mCertificate", e);
            }
        }
        if (accessControlProfile.isUserAuthenticationRequired()) {
            mapBuilder.put("capabilityType", 1); // TODO: what value to put here?
            long timeout = accessControlProfile.getUserAuthenticationTimeout();
            if (timeout != 0) {
                mapBuilder.put("timeout", timeout);
            }
        }
        return cborBuilder.build().get(0);
    }

    static int[] integerCollectionToArray(Collection<Integer> collection) {
        int[] result = new int[collection.size()];
        int n = 0;
        for (int item : collection) {
            result[n++] = item;
        }
        return result;
    }

    /*
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
     */


    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 RuntimeException("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 RuntimeException(
                        "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);
        }
    }

    /**
     * Helper function to check if a given certificate chain is valid.
     *
     * NOTE NOTE NOTE: We only check that the certificates in the chain sign each other. We
     * <em>specifically</em> don't check that each certificate is also a CA certificate.
     *
     * @param certificateChain the chain to validate.
     * @return <code>true</code> if valid, <code>false</code> otherwise.
     */
    public static boolean validateCertificateChain(Collection<X509Certificate> certificateChain) {
        // First check that each certificate signs the previous one...
        X509Certificate prevCertificate = null;
        for (X509Certificate certificate : certificateChain) {
            if (prevCertificate != null) {
                // We're not the leaf certificate...
                //
                // Check the previous certificate was signed by this one.
                try {
                    prevCertificate.verify(certificate.getPublicKey());
                } catch (CertificateException
                        | InvalidKeyException
                        | NoSuchAlgorithmException
                        | NoSuchProviderException
                        | SignatureException e) {
                    return false;
                }
            } else {
                // we're the leaf certificate so we're not signing anything nor
                // do we need to be e.g. a CA certificate.
            }
            prevCertificate = certificate;
        }
        return true;
    }

    /**
     * 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.
     */
    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);
        }
    }


    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");
        }
        BigInteger r = new BigInteger(Arrays.copyOfRange(signature, 0, 32));
        BigInteger s = new BigInteger(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(Signature s,
            @Nullable byte[] data,
            byte[] detachedContent,
            @Nullable Collection<X509Certificate> certificateChain)
            throws 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 {
            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 byte[] coseSign1Sign(PrivateKey key,
            @Nullable byte[] data,
            byte[] additionalData,
            @Nullable Collection<X509Certificate> certificateChain)
            throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException {

        Signature s = Signature.getInstance("SHA256withECDSA");
        s.initSign(key);
        return coseSign1Sign(s, data, additionalData, certificateChain);
    }

    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");
        }
    }

    // Brute-force but good enough since users will only pass relatively small amounts of data.
    static boolean hasSubByteArray(byte[] haystack, byte[] needle) {
        int n = 0;
        while (needle.length + n <= haystack.length) {
            boolean found = true;
            for (int m = 0; m < needle.length; m++) {
                if (needle[m] != haystack[n + m]) {
                    found = false;
                    break;
                }
            }
            if (found) {
                return true;
            }
            n++;
        }
        return false;
    }

    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;
    }

    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;
    }

    static final int CBOR_SEMANTIC_TAG_ENCODED_CBOR = 24;

    static byte[] prependSemanticTagForEncodedCbor(byte[] encodedCbor) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ByteString taggedBytestring = new ByteString(encodedCbor);
            taggedBytestring.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
            new CborEncoder(baos).encode(taggedBytestring);
        } catch (CborException e) {
            throw new RuntimeException("Error encoding with semantic tag for CBOR encoding", e);
        }
        return baos.toByteArray();
    }

    static byte[] buildDeviceAuthenticationCbor(String docType,
            byte[] encodedSessionTranscript,
            byte[] encodedDeviceNameSpaces) {
        ByteArrayOutputStream daBaos = new ByteArrayOutputStream();
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript);
            DataItem sessionTranscript = new CborDecoder(bais).decode().get(0);
            ByteString deviceNameSpacesBytesItem = new ByteString(encodedDeviceNameSpaces);
            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();
    }

    static byte[] buildReaderAuthenticationCbor(
            byte[] sessionTranscriptBytes,
            byte[] requestMessageBytes) {

        ByteArrayOutputStream daBaos = new ByteArrayOutputStream();
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(sessionTranscriptBytes);
            DataItem sessionTranscript = new CborDecoder(bais).decode().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);
        }
        return daBaos.toByteArray();
    }

    // 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;
    }

}