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 }