tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ClientAuthCertificateManager.java (11832B)


      1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 package org.mozilla.gecko;
      7 
      8 import android.security.KeyChain;
      9 import android.security.KeyChainException;
     10 import android.util.Log;
     11 import java.security.InvalidKeyException;
     12 import java.security.NoSuchAlgorithmException;
     13 import java.security.PrivateKey;
     14 import java.security.PublicKey;
     15 import java.security.Signature;
     16 import java.security.SignatureException;
     17 import java.security.cert.CertificateEncodingException;
     18 import java.security.cert.X509Certificate;
     19 import java.security.interfaces.ECPublicKey;
     20 import java.security.interfaces.RSAPublicKey;
     21 import java.util.ArrayList;
     22 import java.util.Arrays;
     23 import javax.crypto.BadPaddingException;
     24 import javax.crypto.Cipher;
     25 import javax.crypto.IllegalBlockSizeException;
     26 import javax.crypto.NoSuchPaddingException;
     27 import org.mozilla.gecko.annotation.WrapForJNI;
     28 import org.mozilla.gecko.mozglue.JNIObject;
     29 
     30 // ClientAuthCertificateManager is a singleton that manages any client
     31 // authentication certificates that the user has made available by selecting
     32 // them via the dialog created by `KeyChain.choosePrivateKeyAlias` (see
     33 // `CertificatePicker` in `mozilla.components.feature.prompts.certificate`).
     34 // Once a certificate has been made available, this class can use its alias
     35 // to obtain the bytes of the certificate and issuer chain as well as create
     36 // signatures using the private key of the certificate.
     37 public class ClientAuthCertificateManager {
     38 
     39  // It would make sense to make the log tag "ClientAuthCertificateManager",
     40  // but that's 28 characters, and the limit is 23.
     41  private static final String LOGTAG = "ClientAuthCertManager";
     42  private static ClientAuthCertificateManager sClientAuthCertificateManager = null;
     43 
     44  // Internal list of cached client auth certificates.
     45  private final ArrayList<ClientAuthCertificate> mCertificates =
     46      new ArrayList<ClientAuthCertificate>();
     47 
     48  private ClientAuthCertificateManager() {}
     49 
     50  private static ClientAuthCertificateManager getSingleton() {
     51    synchronized (ClientAuthCertificateManager.class) {
     52      if (sClientAuthCertificateManager == null) {
     53        sClientAuthCertificateManager = new ClientAuthCertificateManager();
     54      }
     55      return sClientAuthCertificateManager;
     56    }
     57  }
     58 
     59  // Find the cached client auth certificate with the given alias, if any.
     60  private ClientAuthCertificate findCertificateByAlias(final String alias) {
     61    for (final ClientAuthCertificate certificate : mCertificates) {
     62      if (certificate.mAlias.equals(alias)) {
     63        return certificate;
     64      }
     65    }
     66    return null;
     67  }
     68 
     69  // Given the alias of a certificate, return its bytes, if available.
     70  // This also caches the certificate for later use.
     71  @WrapForJNI(calledFrom = "any")
     72  private static byte[] getCertificateFromAlias(final String alias) {
     73    // The certificate may have already been cached. If so, return it.
     74    final ClientAuthCertificateManager singleton = getSingleton();
     75    synchronized (singleton) {
     76      ClientAuthCertificate certificate = singleton.findCertificateByAlias(alias);
     77      if (certificate != null) {
     78        return certificate.getCertificateBytes();
     79      }
     80      // Otherwise, get the certificate chain corresponding to the alias, make a
     81      // ClientAuthCertificate out of that, cache it, and return it.
     82      final X509Certificate[] chain;
     83      try {
     84        chain = KeyChain.getCertificateChain(GeckoAppShell.getApplicationContext(), alias);
     85      } catch (final InterruptedException | KeyChainException e) {
     86        Log.e(LOGTAG, "getCertificateChain failed", e);
     87        return null;
     88      }
     89      if (chain == null || chain.length < 1) {
     90        return null;
     91      }
     92      try {
     93        certificate = new ClientAuthCertificate(alias, chain);
     94        singleton.mCertificates.add(certificate);
     95        return certificate.getCertificateBytes();
     96      } catch (final UnsuitableCertificateException e) {
     97        Log.e(LOGTAG, "unsuitable certificate", e);
     98      }
     99    }
    100    return null;
    101  }
    102 
    103  // List all known client authentication certificates.
    104  @WrapForJNI(calledFrom = "any")
    105  private static ClientAuthCertificate[] getClientAuthCertificates() {
    106    final ClientAuthCertificateManager singleton = ClientAuthCertificateManager.getSingleton();
    107    synchronized (singleton) {
    108      return singleton.mCertificates.toArray(new ClientAuthCertificate[0]);
    109    }
    110  }
    111 
    112  // Find the cached certificate with the given bytes, if any.
    113  private ClientAuthCertificate findCertificateByBytes(final byte[] certificateBytes) {
    114    for (final ClientAuthCertificate certificate : mCertificates) {
    115      if (Arrays.equals(certificate.getCertificateBytes(), certificateBytes)) {
    116        return certificate;
    117      }
    118    }
    119    return null;
    120  }
    121 
    122  // Given the bytes of a certificate previously returned by
    123  // `getClientAuthCertificates()`, returns the issuer certificate chain bytes.
    124  @WrapForJNI(calledFrom = "any")
    125  private static byte[][] getCertificateIssuersBytes(final byte[] certificateBytes) {
    126    final ClientAuthCertificateManager singleton = ClientAuthCertificateManager.getSingleton();
    127    synchronized (singleton) {
    128      final ClientAuthCertificate certificate = singleton.findCertificateByBytes(certificateBytes);
    129      if (certificate == null) {
    130        return null;
    131      }
    132      return certificate.getIssuersBytes();
    133    }
    134  }
    135 
    136  // Given the bytes of a certificate previously returned by
    137  // `getClientAuthCertificates()`, data to sign, and an algorithm, signs the
    138  // data using the algorithm. "NoneWithRSA" and "NoneWithECDSA" are supported,
    139  // as well as "raw", which corresponds to an RSA encryption operation with no
    140  // padding.
    141  @WrapForJNI(calledFrom = "any")
    142  private static byte[] sign(
    143      final byte[] certificateBytes, final byte[] data, final String algorithm) {
    144    final ClientAuthCertificateManager singleton = ClientAuthCertificateManager.getSingleton();
    145    synchronized (singleton) {
    146      final ClientAuthCertificate certificate = singleton.findCertificateByBytes(certificateBytes);
    147      if (certificate == null) {
    148        return null;
    149      }
    150      final PrivateKey key;
    151      try {
    152        key = KeyChain.getPrivateKey(GeckoAppShell.getApplicationContext(), certificate.mAlias);
    153      } catch (final InterruptedException | KeyChainException e) {
    154        Log.e(LOGTAG, "getPrivateKey failed", e);
    155        return null;
    156      }
    157      if (key == null) {
    158        Log.e(LOGTAG, "couldn't get private key");
    159        return null;
    160      }
    161 
    162      if (algorithm.equals("raw")) {
    163        final Cipher cipher;
    164        try {
    165          cipher = Cipher.getInstance("RSA/None/NoPadding");
    166        } catch (final NoSuchAlgorithmException | NoSuchPaddingException e) {
    167          Log.e(LOGTAG, "getInstance failed", e);
    168          return null;
    169        }
    170        try {
    171          cipher.init(Cipher.ENCRYPT_MODE, key);
    172        } catch (final InvalidKeyException e) {
    173          Log.e(LOGTAG, "init failed", e);
    174          return null;
    175        }
    176        try {
    177          return cipher.doFinal(data);
    178        } catch (final BadPaddingException | IllegalBlockSizeException e) {
    179          Log.e(LOGTAG, "doFinal failed", e);
    180          return null;
    181        }
    182      }
    183 
    184      if (!algorithm.equals("NoneWithRSA") && !algorithm.equals("NoneWithECDSA")) {
    185        Log.e(LOGTAG, "given unexpected algorithm " + algorithm);
    186        return null;
    187      }
    188 
    189      final Signature signature;
    190      try {
    191        signature = Signature.getInstance(algorithm);
    192      } catch (final NoSuchAlgorithmException e) {
    193        Log.e(LOGTAG, "getInstance failed", e);
    194        return null;
    195      }
    196      try {
    197        signature.initSign(key);
    198      } catch (final InvalidKeyException e) {
    199        Log.e(LOGTAG, "initSign failed", e);
    200        return null;
    201      }
    202      try {
    203        signature.update(data);
    204      } catch (final SignatureException e) {
    205        Log.e(LOGTAG, "update failed", e);
    206        return null;
    207      }
    208      try {
    209        return signature.sign();
    210      } catch (final SignatureException e) {
    211        Log.e(LOGTAG, "sign failed", e);
    212        return null;
    213      }
    214    }
    215  }
    216 
    217  // Helper exception class thrown upon encountering an unsuitable certificate,
    218  // where "unsuitable" means that the implementation couldn't gather the
    219  // information necessary for the rest of gecko to use it.
    220  private static class UnsuitableCertificateException extends Exception {
    221    public UnsuitableCertificateException(final String message) {
    222      super(message);
    223    }
    224  }
    225 
    226  // Helper class returned by
    227  // `ClientAuthCertificateManager.getClientAuthCertificates()`. Holds the bytes
    228  // of the certificate, the bytes of the issuer certificate chain, bytes
    229  // representing relevant data about the public key, and the type of key.
    230  // In particular, for RSA keys, the key parameter bytes represent the public
    231  // modulus of the key. For EC keys, the key parameter bytes represent the
    232  // encoded subject public key info, which importantly contains the OID
    233  // identifying the curve of the key.
    234  private static class ClientAuthCertificate extends JNIObject {
    235    private static final String LOGTAG = "ClientAuthCertificate";
    236    // Mirrors kIPCClientCertsObjectTypeRSAKey in nsNSSIOLayer.h
    237    private static int sRSAKey = 2;
    238    // Mirrors kIPCClientCertsObjectTypeECKey in nsNSSIOLayer.h
    239    private static int sECKey = 3;
    240 
    241    private String mAlias;
    242    private byte[] mCertificateBytes;
    243    private byte[][] mIssuersBytes;
    244    private byte[] mKeyParameters;
    245    private int mType;
    246 
    247    ClientAuthCertificate(final String alias, final X509Certificate[] x509Certificates)
    248        throws UnsuitableCertificateException {
    249      mAlias = alias;
    250      final ArrayList<byte[]> issuersBytes = new ArrayList<byte[]>();
    251      for (final X509Certificate certificate : x509Certificates) {
    252        if (mCertificateBytes == null) {
    253          try {
    254            mCertificateBytes = certificate.getEncoded();
    255          } catch (final CertificateEncodingException cee) {
    256            Log.e(LOGTAG, "getEncoded() failed", cee);
    257            throw new UnsuitableCertificateException("couldn't get certificate bytes");
    258          }
    259        } else {
    260          try {
    261            issuersBytes.add(certificate.getEncoded());
    262          } catch (final CertificateEncodingException cee) {
    263            Log.e(LOGTAG, "getEncoded() failed", cee);
    264            // This certificate may still be usable.
    265            break;
    266          }
    267        }
    268      }
    269      mIssuersBytes = issuersBytes.toArray(new byte[0][0]);
    270      final PublicKey publicKey = x509Certificates[0].getPublicKey();
    271      if (publicKey instanceof RSAPublicKey) {
    272        mKeyParameters = ((RSAPublicKey) publicKey).getModulus().toByteArray();
    273        mType = sRSAKey;
    274      } else if (publicKey instanceof ECPublicKey) {
    275        // getEncoded() actually returns the SPKI. This leaves it to osclientcerts
    276        // to decode into the OID identifying the curve.
    277        mKeyParameters = publicKey.getEncoded();
    278        mType = sECKey;
    279      } else {
    280        throw new UnsuitableCertificateException("unsupported key type");
    281      }
    282    }
    283 
    284    @WrapForJNI
    285    @Override // JNIObject
    286    protected native void disposeNative();
    287 
    288    @WrapForJNI(calledFrom = "any")
    289    public byte[] getCertificateBytes() {
    290      return mCertificateBytes;
    291    }
    292 
    293    @WrapForJNI(calledFrom = "any")
    294    public byte[][] getIssuersBytes() {
    295      return mIssuersBytes;
    296    }
    297 
    298    @WrapForJNI(calledFrom = "any")
    299    private byte[] getKeyParameters() {
    300      return mKeyParameters;
    301    }
    302 
    303    @WrapForJNI(calledFrom = "any")
    304    private int getType() {
    305      return mType;
    306    }
    307  }
    308 }