Published on

OCSPとPKI

Authors
  • avatar
    Name
    Kikusan
    Twitter

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

実際規定されている通りの構造になっている。以上。