Androidでオレオレ証明書SSL
2011年2月27日日曜日10:09:52
オレオレ証明な鯖とSSL通信したい。ググって出てくる「Androidでオレオレ証明のサーバへSSLアクセスする: 雪羽の発火後忘失」は素通しなので、事前に用意したルート証明書でチェックしたい。ちなみにリンク切れてる「Customizing SSL in HttpClient」の今の正しいリンク先は「SSL/TLS customization」。
HttpURLConnectionを使う方法とApache HttpClientを使う方法の二通りがなんとか実装できたのでサンプルコード書いとく。ここに書くにあたって例外処理とか全部消してるのでそのままだと動かない。
どう考えてもApache HttpClientの方が使いやすいので使いにくい方から。
HttpURLConnection
Apache Harmonyのコードが大変参考になる。今回はclasslib/modules/x-net/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.javaを参考に。若干コピペが混じってるのでApacheライセンス。
どうやらCertPathValidatorがネガティブキャッシュ持ってるぽい(未検証。よくわからない。)ので一々作り直してる。
ちょっと不安定で、わりとよく途中で切れる。Basic認証する時はjava.net.Authenticatorを使うんだけど、これがなんともアレな感じで、あとたぶんDigest認証使えない(?)。
Apache HttpClient
ステータスコードが401でも例外投げてくれないから自力で投げてる。HttpResponseInterceptor=>CredentialsProviderの順で呼ばれるのでgetCredentials()内でaddResponseInterceptor()すると動かない。
# 2011/02/28: ルート証明書以外弾くように変更。エントリの題名を少し変更。(なんか変だったので戻した。)
HttpURLConnectionを使う方法とApache HttpClientを使う方法の二通りがなんとか実装できたのでサンプルコード書いとく。ここに書くにあたって例外処理とか全部消してるのでそのままだと動かない。
どう考えてもApache HttpClientの方が使いやすいので使いにくい方から。
HttpURLConnection
Apache Harmonyのコードが大変参考になる。今回はclasslib/modules/x-net/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.javaを参考に。若干コピペが混じってるのでApacheライセンス。
どうやらCertPathValidatorがネガティブキャッシュ持ってるぽい(未検証。よくわからない。)ので一々作り直してる。
public class MyX509TrustManager implements X509TrustManager { public static X509Certificate readPem(File file) { FileInputStream stream = new FileInputStream(file); CertPath cp; try { CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC"); cp = cf.generateCertPath(stream, "PEM"); } finally { stream.close(); } List<? extends Certificate> certs = cp.getCertificates(); if (certs.size() < 1) { throw new CertificateException("Certificate list is empty"); } else if (certs.size() > 1) { throw new CertificateException("Intermediate certificate is not allowed"); } if (certs.get(0) instanceof X509Certificate) { X509Certificate cert = (X509Certificate)certs.get(0); cert.checkValidity(); return cert; } else { throw new CertificateException("Certificate is not X509Certificate"); } } private File certdir = null; private CertPathValidator validator = null; private CertificateFactory factory = null; private HashSet<TrustAnchor> trusted = null; private PKIXParameters params = null; private Exception err = null; public MyX509TrustManager(File certdir) { this.certdir = certdir; reload(); } public void reload() { err = null; try { // prevent negative cache validator = CertPathValidator.getInstance("PKIX"); factory = CertificateFactory.getInstance("X509"); } catch (NoSuchAlgorithmException ex) { err = ex; return; } catch (CertificateException ex) { err = ex; return; } trusted = new HashSet<TrustAnchor>(); if (!certdir.exists()) return; for(File file : certdir.listFiles()) { if (file.isDirectory()) continue; try { X509Certificate cert = readPem(file); trusted.add(new TrustAnchor(cert, null)); } catch (Exception ex) { } } try { params = new PKIXParameters(trusted); params.setRevocationEnabled(false); } catch (InvalidAlgorithmParameterException ex) { err = ex; } } public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { throw new CertificateException("Client not trusted"); } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (chain == null || chain.length == 0) { throw new CertificateException("Empty chain"); } if (authType == null || authType.length() == 0) { throw new CertificateException("Empty auth type"); } if (err != null) { throw new CertificateException(err); } try { validator.validate(factory.generateCertPath(Arrays.asList(chain)), params); } catch (InvalidAlgorithmParameterException ex) { throw new CertificateException(ex); } catch (CertPathValidatorException ex) { throw new CertificateException(ex); } } public X509Certificate[] getAcceptedIssuers() { if (params == null) { return new X509Certificate[0]; } Set<TrustAnchor> anchors = params.getTrustAnchors(); X509Certificate[] certs = new X509Certificate[anchors.size()]; int i = 0; for(TrustAnchor anchor : anchors) { certs[i++] = anchor.getTrustedCert(); } return certs; } }
X509TrustManager[] managers = new X509TrustManager[] { new MyX509TrustManager(new File("/foo/bar/baz")) }; SSLContext context = SSLContext.getInstance("TLS"); context.init(null, managers, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); URLConnection conn = new URL("https://hoge.fuga.piyo").openConnection(); InputStream stream = conn.getInputStream(); // ほげる
ちょっと不安定で、わりとよく途中で切れる。Basic認証する時はjava.net.Authenticatorを使うんだけど、これがなんともアレな感じで、あとたぶんDigest認証使えない(?)。
Apache HttpClient
ステータスコードが401でも例外投げてくれないから自力で投げてる。HttpResponseInterceptor=>CredentialsProviderの順で呼ばれるのでgetCredentials()内でaddResponseInterceptor()すると動かない。
public class MyHttpClient implements HttpClient, CredentialsProvider, HttpResponseInterceptor { private DefaultHttpClient client = null; private String username = null; private String password = null; private boolean stop_auth = false; public MyHttpClient(KeyStore certstore) { HttpParams params = new BasicHttpParams(); SSLSocketFactory sf = new SSLSocketFactory(certstore); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); schemeRegistry.register(new Scheme("https", sf, 443)); ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, schemeRegistry); client = new DefaultHttpClient(ccm, params); client.setCredentialsProvider(this); client.addResponseInterceptor(this); } public void clear() { } public Credentials getCredentials(AuthScope authscope) { if ((username != null && username.length() > 0) || (password != null && password.length() > 0)) { stop_auth = true; return new UsernamePasswordCredentials(username == null ? "" : username, password == null ? "" : password); } else { throw new RuntimeException("Authentication required"); } } public void setCredentials(AuthScope authscope, Credentials credentials) { } public void process(HttpResponse response, HttpContext context) throws HttpException, IOException { AuthenticationHandler handler = client.getTargetAuthenticationHandler(); if (stop_auth && handler.isAuthenticationRequested(response, context)) { throw new ClientProtocolException("Authentication failed"); } } public HttpResponse execute(HttpUriRequest request) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(request); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public HttpResponse execute(HttpUriRequest request, HttpContext context) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(request, context); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public HttpResponse execute(HttpHost target, HttpRequest request) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(target, request); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> handler) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(request, handler); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(target, request, context); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> handler, HttpContext context) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(request, handler, context); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public <T> T execute(HttpHost host, HttpRequest request, ResponseHandler<? extends T> handler) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(host, request, handler); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public <T> T execute(HttpHost host, HttpRequest request, ResponseHandler<? extends T> handler, HttpContext context) throws IOException, ClientProtocolException { stop_auth = false; try { return client.execute(host, request, handler, context); } catch (RuntimeException ex) { throw new ClientProtocolException(ex.getMessage()); } } public ClientConnectionManager getConnectionManager() { return client.getConnectionManager(); } public HttpParams getParams() { return client.getParams(); } }
KeyStore certstore = KeyStore.getInstance(KeyStore.getDefaultType()); certstore.load(null, null); // 初期化しないとたぶん無視される。 for(File file : new File("/foo/bar/baz").listFiles()) { if (file.isDirectory()) continue; try { X509Certificate cert = readPem(file); // HttpURLConnectionのサンプル参照。 certstore.setCertificateEntry(file.getName(), cert); } catch (Exception ex) { } } MyHttpClient client = new MyHttpClient(certstore); HttpResponse res = client.execute(new HttpGet("https://hoge.fuga.piyo")); HttpEntity entity = res.getEntity(); InputStream stream = entity.getContent(); // ほげる
# 2011/02/28: ルート証明書以外弾くように変更。
引用機能の使い方: