SoftwareWritableIdentityCredential.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.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;

import androidx.annotation.NonNull;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;

import co.nstant.in.cbor.CborBuilder;
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.UnicodeString;

class SoftwareWritableIdentityCredential extends WritableIdentityCredential {

    private static final String TAG = "SoftwareWritableIdentityCredential";

    private KeyPair mKeyPair = null;
    private Collection<X509Certificate> mCertificates = null;
    private String mDocType;
    private String mCredentialName;
    private Context mContext;

    SoftwareWritableIdentityCredential(Context context,
            @NonNull String credentialName,
            @NonNull String docType) throws AlreadyPersonalizedException {
        mContext = context;
        mDocType = docType;
        mCredentialName = credentialName;
        if (CredentialData.credentialAlreadyExists(context, credentialName)) {
            throw new AlreadyPersonalizedException("Credential with given name already exists");
        }
    }

    /**
     * Generates CredentialKey.
     *
     * If called a second time on the same object, does nothing and returns null.
     *
     * @param challenge The attestation challenge.
     * @return Attestation mCertificate chain or null if called a second time.
     * @throws AlreadyPersonalizedException     if this credential has already been personalized.
     * @throws CipherSuiteNotSupportedException if the cipher suite is not supported
     * @throws IdentityCredentialException      if unable to communicate with secure hardware.
     */
    private Collection<X509Certificate> ensureCredentialKey(byte[] challenge) {

        if (mKeyPair != null) {
            return null;
        }

        String aliasForCredential = CredentialData.getAliasFromCredentialName(mCredentialName);

        try {
            KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
            ks.load(null);
            if (ks.containsAlias(aliasForCredential)) {
                ks.deleteEntry(aliasForCredential);
            }

            // TODO: We most likely want to constrain the life of CredentialKey (through
            // setKeyValidityStart() and setKeyValidityEnd()) so it's limited to e.g. 5 years
            // or how long the credential might be valid. For US driving licenses it's typically
            // up to 5 years, where it expires on your birth day).
            //
            // This is likely something the issuer would want to specify.

            KeyPairGenerator kpg = KeyPairGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
                    aliasForCredential,
                    KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
                    .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512);

            // Attestation is only available in Nougat and onwards.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                if (challenge == null) {
                    challenge = new byte[0];
                }
                builder.setAttestationChallenge(challenge);
            }
            kpg.initialize(builder.build());
            mKeyPair = kpg.generateKeyPair();

            Certificate[] certificates = ks.getCertificateChain(aliasForCredential);
            mCertificates = new ArrayList<>();
            for (Certificate certificate : certificates) {
                mCertificates.add((X509Certificate) certificate);
            }
        } catch (InvalidAlgorithmParameterException
                | NoSuchAlgorithmException
                | NoSuchProviderException
                | CertificateException
                | KeyStoreException
                | IOException e) {
            throw new RuntimeException("Error creating CredentialKey", e);
        }
        return mCertificates;
    }

    @Override
    public @NonNull Collection<X509Certificate> getCredentialKeyCertificateChain(
            @NonNull byte[] challenge) {
        Collection<X509Certificate> certificates = ensureCredentialKey(challenge);
        if (certificates == null) {
            throw new RuntimeException(
                    "getCredentialKeyCertificateChain() must be called before personalize()");
        }
        return certificates;
    }

    // Returns COSE_Sign1 with payload set to ProofOfProvisioning
    static byte[] buildProofOfProvisioningWithSignature(String docType,
            PersonalizationData personalizationData,
            PrivateKey key) {

        CborBuilder accessControlProfileBuilder = new CborBuilder();
        ArrayBuilder<CborBuilder> arrayBuilder = accessControlProfileBuilder.addArray();
        for (AccessControlProfile profile : personalizationData.getAccessControlProfiles()) {
            arrayBuilder.add(Util.accessControlProfileToCbor(profile));
        }

        CborBuilder dataBuilder = new CborBuilder();
        MapBuilder<CborBuilder> dataMapBuilder = dataBuilder.addMap();
        for (PersonalizationData.NamespaceData namespaceData :
                personalizationData.getNamespaceDatas()) {
            dataMapBuilder.put(
                    new UnicodeString(namespaceData.getNamespaceName()),
                    Util.namespaceDataToCbor(namespaceData));
        }

        CborBuilder signedDataBuilder = new CborBuilder();
        signedDataBuilder.addArray()
                .add("ProofOfProvisioning")
                .add(docType)
                .add(accessControlProfileBuilder.build().get(0))
                .add(dataBuilder.build().get(0))
                .add(false);

        byte[] signatureBytes;
        try {
            ByteArrayOutputStream dtsBaos = new ByteArrayOutputStream();
            CborEncoder dtsEncoder = new CborEncoder(dtsBaos);
            dtsEncoder.encode(signedDataBuilder.build().get(0));
            byte[] dataToSign = dtsBaos.toByteArray();

            signatureBytes = Util.coseSign1Sign(key,
                    dataToSign,
                    null,
                    null);
        } catch (NoSuchAlgorithmException
                | InvalidKeyException
                | CertificateEncodingException
                | CborException e) {
            throw new RuntimeException("Error building ProofOfProvisioning", e);
        }
        return signatureBytes;
    }

    @NonNull
    @Override
    public byte[] personalize(@NonNull PersonalizationData personalizationData) {

        try {
            ensureCredentialKey(null);

            byte[] encodedBytes = buildProofOfProvisioningWithSignature(mDocType,
                    personalizationData,
                    mKeyPair.getPrivate());

            byte[] proofOfProvisioning = Util.coseSign1GetData(encodedBytes);
            byte[] proofOfProvisioningSha256 = MessageDigest.getInstance("SHA-256").digest(
                    proofOfProvisioning);

            CredentialData.createCredentialData(
                    mContext,
                    mDocType,
                    mCredentialName,
                    CredentialData.getAliasFromCredentialName(mCredentialName),
                    mCertificates,
                    personalizationData,
                    proofOfProvisioningSha256,
                    false);

            return encodedBytes;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Error digesting ProofOfProvisioning", e);
        }
    }
}