- Published on
OCSPとPKI
- Authors
- Name
- Kikusan
OCSPを使う必要性があったので、学習したことをダンプします。
参考
OCSPはX.509公開鍵証明書の失効状態を取得するための通信プロトコルであり、
CRLから失効済みの証明書を探すより簡単に当該証明書の状態を取得できる。
googleのCRL. これを調べるよりOCSPレスポンダに問い合わせるほうが簡単に済む。
ハンズオン
一部BouncyCastleのライブラリを使用して行う。
Javadocはホームページのリンクから飛べる。 OCSP: https://www.bouncycastle.org/docs/pkixdocs1.8on/index.html
本やpdfでサンプルも公開してくれている。 https://www.bouncycastle.org/fips-java/BCFipsIn100.pdf
ライブラリはこちら。
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- ついでにこれも https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
定数はこちら
public class Const {
public static final String FILE_NAME = "selfSign.keystore";
public static final String KEY_ALIAS = "mykeys";
public static final String KEY_PASSWORD = "password";
public static final String KEY_TYPE = "JKS";
public static final String KEY_ALGORITHM = "RSA";
public static final String SIGN_ALGORITHM = "SHA256withRSA";
public static final String DOMAIN_NAME = "cn=mycn";
}
KeyPair/自己証明書 作成
public class Certs {
public void generate(String filename) throws Exception {
KeyPair keyPair = generateKeyPair();
Certificate cert = selfSign(keyPair);
store(cert, keyPair, filename);
}
public X509Certificate getCertificate(String filename)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableEntryException {
KeyStore keyStore = KeyStore.getInstance(Const.KEY_TYPE);
try (FileInputStream fis = new FileInputStream(filename);) {
keyStore.load(fis, Const.KEY_PASSWORD.toCharArray());
}
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(Const.KEY_PASSWORD.toCharArray());
KeyStore.PrivateKeyEntry entry = (PrivateKeyEntry) keyStore.getEntry(Const.KEY_ALIAS, param);
return (X509Certificate) entry.getCertificate();
}
public PrivateKey getPrivateKey(String filename) throws KeyStoreException, FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableEntryException {
KeyStore keyStore = KeyStore.getInstance(Const.KEY_TYPE);
try (FileInputStream fis = new FileInputStream(filename);) {
keyStore.load(fis, Const.KEY_PASSWORD.toCharArray());
}
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(Const.KEY_PASSWORD.toCharArray());
KeyStore.PrivateKeyEntry entry = (PrivateKeyEntry) keyStore.getEntry(Const.KEY_ALIAS, param);
return entry.getPrivateKey();
}
private void store(Certificate cert, KeyPair keyPair, String filename)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
KeyStore keyStore = KeyStore.getInstance(Const.KEY_TYPE);
keyStore.load(null, null);
KeyStore.Entry entry = new PrivateKeyEntry(keyPair.getPrivate(), new Certificate[] { cert });
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(Const.KEY_PASSWORD.toCharArray());
keyStore.setEntry(Const.KEY_ALIAS, entry, param);
try (FileOutputStream fos = new FileOutputStream(filename)) {
keyStore.store(fos, Const.KEY_PASSWORD.toCharArray());
}
}
private Certificate selfSign(KeyPair keyPair) throws OperatorCreationException, CertificateException {
long now = System.currentTimeMillis();
Date startDate = new Date(now);
Calendar calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.add(Calendar.YEAR, 3);
Date endDate = calendar.getTime();
X500Name dnName = new X500Name(Const.DOMAIN_NAME);
// Using the current timestamp as the certificate serial number
BigInteger certSerialNumber = new BigInteger(Long.toString(now));
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(dnName, certSerialNumber, startDate, endDate, dnName,
subjectPublicKeyInfo);
ContentSigner contentSigner = new JcaContentSignerBuilder(Const.SIGN_ALGORITHM).setProvider(new BouncyCastleProvider())
.build(keyPair.getPrivate());
X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner);
X509Certificate selfSignedCertificate = new JcaX509CertificateConverter().getCertificate(certificateHolder);
return selfSignedCertificate;
}
private KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = null;
keyGen = KeyPairGenerator.getInstance(Const.KEY_ALGORITHM);
keyGen.initialize(2048);
return keyGen.generateKeyPair();
}
}
OCSPRequest作成
RFC6960で規定されているらしい。
OCSPRequest ::= SEQUENCE {
tbsRequest TBSRequest, #1.
optionalSignature [0] EXPLICIT Signature OPTIONAL } #2.
TBSRequest ::= SEQUENCE { #1.
version [0] EXPLICIT Version DEFAULT v1,
requestorName [1] EXPLICIT GeneralName OPTIONAL,
requestList SEQUENCE OF Request, #3.
requestExtensions [2] EXPLICIT Extensions OPTIONAL }
Signature ::= SEQUENCE { #2.
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING,
certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL}
Version ::= INTEGER { v1(0) }
Request ::= SEQUENCE { #3.
reqCert CertID, #4.
singleRequestExtensions [0] EXPLICIT Extensions OPTIONAL }
CertID ::= SEQUENCE { #5.
hashAlgorithm AlgorithmIdentifier,
issuerNameHash OCTET STRING, -- Hash of Issuer's DN
issuerKeyHash OCTET STRING, -- Hash of Issuers public key
serialNumber CertificateSerialNumber }
ASN.1で符号化されているもので、確認したい証明書の情報を格納する。
#n. で記載した部分がObjectとして代入されていくイメージ。
Extension部では拡張領域として自由なデータを加えてやり取りできる。
Extension はX.509で規定されているらしい。
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING }
実際に作成してみる。
public class OCSPReqGenerator {
public OCSPReq generate(X509Certificate cert) throws CertificateEncodingException, IOException, OperatorCreationException, OCSPException {
DigestCalculatorProvider digCalcProv = new BcDigestCalculatorProvider();
// sha256のOID
AlgorithmIdentifier algoId256 = new AlgorithmIdentifier(new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.1"));
CertificateID certId = new JcaCertificateID(digCalcProv.get(algoId256), cert, cert.getSerialNumber());
ExtensionsGenerator reqExGen = new ExtensionsGenerator();
ASN1Integer somethingFlag = new ASN1Integer(1L);
ASN1OctetString[] somethingAry = {new DEROctetString("somemsg".getBytes()), new DEROctetString("somemsg2".getBytes())};
DERSequence somethingSeq = new DERSequence(somethingAry);
// OIDは規則がある 1.2.392はjpの加盟団体とか
Extension ex1 = new Extension(new ASN1ObjectIdentifier("1.2.392.1.1"), false, somethingFlag.getEncoded());
Extension ex2 = new Extension(new ASN1ObjectIdentifier("1.2.392.1.2"), false, somethingSeq.getEncoded());
reqExGen.addExtension(ex1);
reqExGen.addExtension(ex2);
ExtensionsGenerator singleExGen = new ExtensionsGenerator();
// Nonceを付ける Nonce ::= OCTET STRING(SIZE(1..32)
String rndNum = RandomStringUtils.randomNumeric(32);
// Nonce OID
singleExGen.addExtension(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1.2"), true, rndNum.getBytes());
OCSPReqBuilder reqBuilder = new OCSPReqBuilder();
reqBuilder.addRequest(certId, reqExGen.generate());
reqBuilder.setRequestExtensions(singleExGen.generate());
return reqBuilder.build();
}
}
OCSPResponse作成
ResponseもRequest同様RFC6960から参照
OCSPResponse ::= SEQUENCE {
responseStatus OCSPResponseStatus,
responseBytes [0] EXPLICIT ResponseBytes OPTIONAL }
OCSPResponseStatus ::= ENUMERATED {
successful (0), -- Response has valid confirmations
malformedRequest (1), -- Illegal confirmation request
internalError (2), -- Internal error in issuer
tryLater (3), -- Try again later
-- (4) is not used
sigRequired (5), -- Must sign the request
unauthorized (6) -- Request unauthorized
}
#The value for responseBytes consists of an OBJECT IDENTIFIER and a
#response syntax identified by that OID encoded as an OCTET STRING.
ResponseBytes ::= SEQUENCE {
responseType OBJECT IDENTIFIER,
response OCTET STRING }
#For a basic OCSP responder, responseType will be id-pkix-ocsp-basic.
id-pkix-ocsp OBJECT IDENTIFIER ::= { id-ad-ocsp }
id-pkix-ocsp-basic OBJECT IDENTIFIER ::= { id-pkix-ocsp 1 }
#The value for response SHALL be the DER encoding of
#BasicOCSPResponse.
BasicOCSPResponse ::= SEQUENCE {
tbsResponseData ResponseData,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING,
certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
ResponseData ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
responderID ResponderID,
producedAt GeneralizedTime,
responses SEQUENCE OF SingleResponse,
responseExtensions [1] EXPLICIT Extensions OPTIONAL }
ResponderID ::= CHOICE {
byName [1] Name,
byKey [2] KeyHash }
KeyHash ::= OCTET STRING #-- SHA-1 hash of responder's public key
#(excluding the tag and length fields)
SingleResponse ::= SEQUENCE {
certID CertID,
certStatus CertStatus,
thisUpdate GeneralizedTime,
nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL,
singleExtensions [1] EXPLICIT Extensions OPTIONAL }
CertStatus ::= CHOICE {
good [0] IMPLICIT NULL,
revoked [1] IMPLICIT RevokedInfo,
unknown [2] IMPLICIT UnknownInfo }
RevokedInfo ::= SEQUENCE {
revocationTime GeneralizedTime,
revocationReason [0] EXPLICIT CRLReason OPTIONAL }
UnknownInfo ::= NULL
実際に作成してみる。
public class OCSPResponder {
public OCSPResp response(X509Certificate cert, PrivateKey privateKey, OCSPReq ocspReq) throws OperatorCreationException, OCSPException, CertificateEncodingException, IOException {
X509CertificateHolder certificateHolder = new X509CertificateHolder(cert.getEncoded());
BasicOCSPRespBuilder basicRespBuilder = new BasicOCSPRespBuilder(new RespID(certificateHolder.getSubject()));
for (Req req : ocspReq.getRequestList()) {
// ここで本来有効性を調べる
Extension flagEx = req.getSingleRequestExtensions().getExtension(new ASN1ObjectIdentifier("1.2.392.1.1"));
int somethingFlag = ASN1Integer.getInstance(flagEx.getParsedValue()).intValueExact();
if (somethingFlag == 1) {
basicRespBuilder.addResponse(req.getCertID(), CertificateStatus.GOOD);
} else {
basicRespBuilder.addResponse(req.getCertID(), new RevokedStatus(new Date(), 9));
}
}
BasicOCSPResp resp = basicRespBuilder.build(
new JcaContentSignerBuilder(Const.SIGN_ALGORITHM).build(privateKey),
new X509CertificateHolder[]{ certificateHolder },
new Date());
Extensions respExtensions = new Extensions(ocspReq.getExtension(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1.2")));
basicRespBuilder.setResponseExtensions(respExtensions);
OCSPRespBuilder respBuilder = new OCSPRespBuilder();
return respBuilder.build(OCSPRespBuilder.SUCCESSFUL, resp);
}
}
動作確認
public class Main {
public static void main(String[] args) throws Exception {
Certs certs = new Certs();
certs.generate(Const.FILE_NAME);
X509Certificate cert = certs.getCertificate(Const.FILE_NAME);
PrivateKey privateKey = certs.getPrivateKey(Const.FILE_NAME);
OCSPReqGenerator gen = new OCSPReqGenerator();
OCSPReq ocspReq = gen.generate(cert);
ASN1Primitive asnReq = ASN1Primitive.fromByteArray(ocspReq.getEncoded());
System.out.println("Request");
System.out.println(ASN1Dump.dumpAsString(asnReq));
OCSPResponder ocspResponder = new OCSPResponder();
OCSPResp ocspResp = ocspResponder.response(cert, privateKey, ocspReq);
ASN1Primitive asnResp = ASN1Primitive.fromByteArray(ocspResp.getEncoded());
System.out.println("Response");
System.out.println(ASN1Dump.dumpAsString(asnResp));
// null=有効
System.out.println(((BasicOCSPResp)ocspResp.getResponseObject()).getResponses()[0].getCertStatus());
}
}
出力結果
Request
Sequence
Sequence
Sequence
Sequence
Sequence
Sequence
ObjectIdentifier(2.16.840.1.101.3.4.2.1)
DER Octet String[32]
DER Octet String[32]
Integer(1673713983967)
Tagged [CONTEXT 0]
Sequence
Sequence
ObjectIdentifier(1.2.392.1.1)
DER Octet String[3]
Sequence
ObjectIdentifier(1.2.392.1.2)
DER Octet String[21]
Tagged [CONTEXT 2]
Sequence
Sequence
ObjectIdentifier(1.3.6.1.5.5.7.48.1.2)
Boolean(true)
DER Octet String[32]
Response
Sequence
DER Enumerated(0)
Tagged [CONTEXT 0]
Sequence
ObjectIdentifier(1.3.6.1.5.5.7.48.1.1)
DER Octet String[1113]
null
実際規定されている通りの構造になっている。以上。