#java.security.Providerの使い方メモ
java.securityにはProviderという仕組みがあって暗号化や署名の実装を追加できるのですが、たぶんごく一部の人達しか使わないので、説明しているサイトもあまりありません。
##まずは普通にJavaで署名して検証してみる
package io.github.tshibata.sample;
import java.security.*;
public class Sample {
static void test(byte[] data, KeyPair pair) throws Exception {
Signature sig = Signature.getInstance("SHA256withRSA");
// 署名するよ
sig.initSign(pair.getPrivate());
sig.update(data);
byte[] s = sig.sign();
// Base64で出力してみるよ
System.out.println(java.util.Base64.getEncoder().encodeToString(s));
// 検証するよ
sig.initVerify(pair.getPublic());
sig.update(data);
System.out.println(sig.verify(s));
}
public static void main(String[] args) throws Exception {
// 鍵作るよ
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
KeyPair pair = gen.generateKeyPair();
// "hello"を署名して検証するよ
test("hello".getBytes(), pair);
}
}
##Signatureのサブクラスを実装してみる
署名の場合はSignatureクラスが担当します。
package io.github.tshibata.sample;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.math.BigInteger;
public class MySignature extends Signature {
// echo | openssl dgst -sign private.pem | openssl rsautl -verify -inkey private.pem -raw | head --bytes=-32 | base64
private static byte[] padding = java.util.Base64.getDecoder().decode(
"AAH/////////////////////////////////////////////////////////////////////////" +
"////////////////////////////////////////////////////////////////////////////" +
"////////////////////////////////////////////////////////////////////////////" +
"////////////////////////////////////////////ADAxMA0GCWCGSAFlAwQCAQUABCA=");
private RSAPrivateKey privateKey;
private RSAPublicKey publicKey;
private MessageDigest messageDigest;
public MySignature() {
super("SHA256withRSA");
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exc) {
throw new UnsupportedOperationException(exc);
}
}
protected Object engineGetParameter(String param) throws InvalidParameterException {
throw new UnsupportedOperationException("engineGetParameter");
}
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
throw new UnsupportedOperationException("engineSetParameter");
}
protected void engineInitSign(PrivateKey key) throws InvalidKeyException {
messageDigest.reset();
privateKey = (RSAPrivateKey) key;
}
protected void engineInitVerify(PublicKey key) throws InvalidKeyException {
messageDigest.reset();
publicKey = (RSAPublicKey) key;
}
protected void engineUpdate(byte[] data, int offset, int length) throws SignatureException {
messageDigest.update(data, offset, length);
}
protected void engineUpdate(byte data) throws SignatureException {
messageDigest.update(data);
}
protected byte[] engineSign() throws SignatureException {
byte[] data = new byte[256];
System.arraycopy(padding, 0, data, 0, padding.length);
byte[] digest = messageDigest.digest();
System.arraycopy(digest, 0, data, padding.length, digest.length);
BigInteger m = new BigInteger(1, data);
BigInteger d = privateKey.getPrivateExponent();
BigInteger n = privateKey.getModulus();
BigInteger c = m.modPow(d, n);
byte[] signed = new byte[256];
for (int i = signed.length - 1; 0 <= i; i--) {
signed[i] = c.byteValue();
c = c.shiftRight(8);
}
return signed;
}
protected boolean engineVerify(byte[] signed) throws SignatureException {
BigInteger c = new BigInteger(1, signed);
BigInteger e = publicKey.getPublicExponent();
BigInteger n = publicKey.getModulus();
BigInteger m = c.modPow(e, n);
byte[] data = new byte[256];
System.arraycopy(padding, 0, data, 0, padding.length);
byte[] digest = messageDigest.digest();
System.arraycopy(digest, 0, data, padding.length, digest.length);
for (int i = data.length - 1; 0 <= i; i--) {
if (data[i] != m.byteValue()) {
return false;
}
m = m.shiftRight(8);
}
return true;
}
}
鍵の長さは2048bit(256byte)固定にしました。
ハッシュを作ってパディング付けてexponent乗してmodulusで割った余りを計算するだけです。
パディングはおまじないというか決まり事です。
echo | openssl dgst -sign private.pem | openssl rsautl -verify -inkey private.pem -raw | head --bytes=-32 | base64
ってやって作りました。
「exponent乗してmodulusで割った余り」ってがRSAのキモで、昔偉い学者さん達が見つけました。
でこれを使ってもらうために
package io.github.tshibata.sample;
import java.security.*;
public class MyProvider extends Provider {
public MyProvider() {
super("MyProvider", "1.0", "My provider");
put("Alg.Alias.Signature.OID.1.2.840.113549.1.1.11", "SHA256withRSA");
put("Signature.SHA256withRSA SupportedKeyClasses", "java.security.interfaces.RSAPublicKey|java.security.interfaces.RSAPrivateKey");
put("Signature.SHA256withRSA", "io.github.tshibata.sample.MySignature");
put("Alg.Alias.Signature.1.2.840.113549.1.1.11", "SHA256withRSA");
}
}
というProviderのサブクラスを作って
package io.github.tshibata.sample;
import java.security.*;
public class Sample {
static void test(byte[] data, KeyPair pair) throws Exception {
Signature sig = Signature.getInstance("SHA256withRSA");
// 署名するよ
sig.initSign(pair.getPrivate());
sig.update(data);
byte[] s = sig.sign();
// Base64で出力してみるよ
System.out.println(java.util.Base64.getEncoder().encodeToString(s));
// 検証するよ
sig.initVerify(pair.getPublic());
sig.update(data);
System.out.println(sig.verify(s));
}
public static void main(String[] args) throws Exception {
// 鍵作るよ
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
KeyPair pair = gen.generateKeyPair();
// "hello"を署名して検証するよ
test("hello".getBytes(), pair);
// MyProviderを使ってみるよ
Security.insertProviderAt(new MyProvider(), 1);
test("hello".getBytes(), pair);
}
}
という感じで署名や検証の前に登録しておきます。insertProviderAtの第2引数は優先順位で1にしておけば最優先で使われます。