信頼されない証明書を使ったHTTPSサーバーにBasic認証でPOST - Ruby, PHP, C#, Java, Groovy

信頼されないSSL証明書(自己証明書)を使ったサイトに対して、Basic認証を行い POST するサンプルを Ruby, PHP, C#, Java, Groovy で実装してみました。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20111002/


サンプルは、第1引数に URL、第2引数と第3引数に Basic 認証のユーザー名とパスワード、第4引数に POST するデータを指定するようにしています。

なお、POST データを & で区切ると Windows のコマンドプロンプトなどで実行するには不都合があるので ; で区切っている点に注意。

サンプルの実行例
> jruby basic_post_novalidate_certificate.rb https://localhost:8443/ user1 pass1 mode=test;id=123
hello

HTTPS サーバープログラム作成

まずは、クライアントからの処理を受け付けるサーバープログラムを作成し、起動しておきます。

サーバープログラムは HTTPSBasic認証・POST を処理する必要がありますので、今回は Sinatra + WEBrick で実現してみました。

実装が容易で証明書などの準備も不要なのでテスト用途にはお勧めな方法です。(証明書が未指定なら実行時に自動生成してくれる)


下記では ポート 8443 で実行、クライアントの検証を実施せず(:SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE)、ユーザー名が user1 ならBasic認証を通るように実装しています。

https_server.rb (Sinatra を使った HTTPS サーバープログラム)
require "rubygems"
require "sinatra/base"
require "webrick/https"
require "openssl"

class SampleApp < Sinatra::Base
    #Basic認証
    use Rack::Auth::Basic do |user, pass|
        user == 'user1'
    end

    #POST の処理
    post '/' do
        p params
        'hello'
    end
end

#WEBrick で SSL を使用するための処理
#(実行時に自己証明書が自動生成される)
Rack::Handler::WEBrick.run SampleApp, {
    :Port => 8443, 
    :SSLEnable => true, 
    #クライアントを検証しないための設定
    :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, 
    :SSLCertName => [
        ["CN", WEBrick::Utils::getservername]
    ]
}
サーバープログラム実行例
> jruby https_server.rb

今回はサーバープログラムを JRuby 1.6.4 (JavaSE 7) や Ruby 1.8.7 で実行しています。
Ruby 1.9.2(RubyInstaller for Windows)で実行した場合では、Java 系のクライアントからの接続に失敗するようなので注意が必要です。

Ruby による Web クライアント

Ruby では、use_ssl を true にした Net::HTTP オブジェクトの verify_mode に OpenSSL::SSL::VERIFY_NONE を設定すれば自己証明書を処理できるようになります。

basic_post_novalidate_certificate.rb
require 'net/https'
require 'uri'

url = URI.parse(ARGV[0])
user = ARGV[1]
pass = ARGV[2]
postData = ARGV[3]

https = Net::HTTP.new(url.host, url.port)
#SSLの有効化
https.use_ssl = true
#SSL証明書を検証しないための設定
https.verify_mode = OpenSSL::SSL::VERIFY_NONE

res = https.start do
    req = Net::HTTP::Post.new(url.path)
    #Basic認証
    req.basic_auth user, pass

    #POSTデータの設定
    req.body = postData

    #POST
    https.request(req)
end

#結果の出力
print res.body

PHP による Web クライアント

PHP で file_get_contents を使えば HTTPS を特に意識しなくてもよいので簡単に実装できます。(自己証明書を意識する必要も無いみたいです)

ただし、php_openssl の extension を有効化する等、file_get_contents で HTTPS を処理するための環境設定が必要かもしれません。(今回は php.ini で php_openssl の extension を有効化しました)

Basic認証に関しては URL でユーザー名とパスワードを指定する方法もありますが、今回は Authorization ヘッダーで指定する方法をとっています。

basic_post_novalidate_certificate.php
<?php
$url = $argv[1];
$user = $argv[2];
$pass = $argv[3];
$postData = $argv[4];

$options = array('http' => array(
    'method' => 'POST',
    'header' => 
        "Authorization: Basic " . base64_encode("$user:$pass") . "\r\n" . 
        "Content-Type: application/x-www-form-urlencoded\r\n",
    'content' => $postData
));

//POST
$res = file_get_contents($url, false, stream_context_create($options));

//結果の出力
echo $res;
?>

C# による Web クライアント

C# では ServicePointManager.ServerCertificateValidationCallback に true を返すコールバックを設定するだけで自己証明書を処理できるようになります。
Basic認証も POST も WebClient クラスを使えば簡単に実装できます。

BasicPostNovalidateCertificate.cs
using System;
using System.Net;

class BasicPostNovalidateCertificate
{
    public static void Main(string[] args)
    {
        var url = args[0];
        var user = args[1];
        var pass = args[2];
        var postData = args[3];

        //SSL 証明書を検証しないようにする設定
        //(証明書を何でも受け入れるようにする)
        ServicePointManager.ServerCertificateValidationCallback = 
            (sender, cert, chain, errors) => true;

        using (WebClient wc = new WebClient())
        {
            //Basic認証
            wc.Credentials = new NetworkCredential(user, pass);

            //POST
            var res = wc.UploadString(url, "POST", postData);

            //結果の出力
            Console.Write(res);
        }
    }
}

なお、上記を実行すると結果は一応返って来ますがサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。(解決策は不明。サーバーの実行環境が JRubyRuby の違いに関わらず発生する)

Java による Web クライアント

Java では SSLContext を何もしない X509TrustManager で初期化し、どんなホスト名でも受け入れる HostnameVerifier を設定します。

  • JavaSE 7
BasicPostNovalidateCertificate.java
import java.io.*;
import java.net.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.*;

public class BasicPostNovalidateCertificate {
    public static void main(String[] args) throws Exception {

        URL url = new URL(args[0]);
        final String user = args[1];
        final String pass = args[2];
        String postData = args[3];

        //Basic認証
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, pass.toCharArray());
            }
        });

        // ホスト名を検証しないようにする設定
        //(openConnection する前に設定しておく必要あり)
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            public boolean verify(String host, SSLSession ses) {
                return true;
            }
        });

        HttpsURLConnection con = (HttpsURLConnection)url.openConnection();
        con.setDoOutput(true);
        con.setRequestMethod("POST");

        //SSL 証明書を検証しないための設定
        SSLContext sslctx = SSLContext.getInstance("SSL");
        sslctx.init(null, new X509TrustManager[] {
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
                }
                public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
                }
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            }
        }, new SecureRandom());

        con.setSSLSocketFactory(sslctx.getSocketFactory());

        //POSTデータの出力
        OutputStream os = con.getOutputStream();
        PrintStream ps = new PrintStream(os);
        ps.print(postData);
        ps.close();

        //結果の出力
        InputStream is = con.getInputStream();
        BufferedInputStream bis = new BufferedInputStream(is);

        int len = 0;
        byte[] buf = new byte[1024];

        while ((len = bis.read(buf, 0, buf.length)) > -1) {
            System.out.write(buf, 0, len);
        }

        bis.close();
    }
}

なお、Java でも C# と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。

Groovy による Web クライアント

Groovy は基本的に Java 版の内容をそのまま実装しました。

  • Groovy 1.8.2(JavaSE 7)
basic_post_novalidate_certificate.groovy
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.*

def url = new URL(args[0])
def user = args[1]
def pass = args[2]
def postData = args[3]

//Basic認証(getPasswordAuthentication() をオーバーライド)
Authenticator.default = {
    new PasswordAuthentication(user, pass.toCharArray())
} as Authenticator

// ホスト名を検証しないようにする設定
//(注)openConnection する前に設定しておく必要あり
HttpsURLConnection.defaultHostnameVerifier = {
    host, session -> true
} as HostnameVerifier

def con = url.openConnection()
con.doOutput = true
con.requestMethod = "POST"

//SSL 証明書を検証しないための設定
def sslctx = SSLContext.getInstance("SSL")
//X509TrustManager インターフェースの実装
def tmanager = [
    checkClientTrusted: {chain, authType -> },
    checkServerTrusted: {chain, authType -> },
    getAcceptedIssuers: {null}
] as X509TrustManager

sslctx.init(null as KeyManager[], [tmanager] as X509TrustManager[], new SecureRandom())

con.SSLSocketFactory = sslctx.socketFactory

//POSTデータの出力
con.outputStream.withWriter {
    it.print postData
}

//結果の出力
print con.inputStream.text

なお、Java と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。