GordianCRMParser.java

/*
 * GordianKnot: Security Suite
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.gordianknot.impl.core.keystore;

import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreement;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementFactory;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementParams;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementSpec;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianLength;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianKeyPairUsage;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianKeyPairUse;
import io.github.tonywasher.joceanus.gordianknot.api.encrypt.GordianEncryptor;
import io.github.tonywasher.joceanus.gordianknot.api.encrypt.GordianEncryptorFactory;
import io.github.tonywasher.joceanus.gordianknot.api.encrypt.GordianEncryptorSpec;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianAsyncFactory;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPair;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairFactory;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairGenerator;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairSpec;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySet;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry.GordianKeyStorePair;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignParams;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignature;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignatureSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cert.GordianCertUtils;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cert.GordianCoreCertificate;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianIOException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianLogicException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.sign.GordianCoreSignatureFactory;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.cms.EncryptedContentInfo;
import org.bouncycastle.asn1.cms.EnvelopedData;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.cms.KeyTransRecipientInfo;
import org.bouncycastle.asn1.cms.RecipientInfo;
import org.bouncycastle.asn1.crmf.AttributeTypeAndValue;
import org.bouncycastle.asn1.crmf.CertReqMsg;
import org.bouncycastle.asn1.crmf.CertRequest;
import org.bouncycastle.asn1.crmf.CertTemplate;
import org.bouncycastle.asn1.crmf.EncKeyWithID;
import org.bouncycastle.asn1.crmf.PKMACValue;
import org.bouncycastle.asn1.crmf.POPOPrivKey;
import org.bouncycastle.asn1.crmf.POPOSigningKey;
import org.bouncycastle.asn1.crmf.ProofOfPossession;
import org.bouncycastle.asn1.crmf.SubsequentMessage;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;

import java.io.IOException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

/**
 * Certificate Request Message Parser.
 */
public class GordianCRMParser {
    /**
     * The testData length.
     */
    private static final int TESTLEN = 1024;

    /**
     * The gateway.
     */
    private final GordianBaseKeyStoreGateway theGateway;

    /**
     * The builder.
     */
    private final GordianCRMBuilder theBuilder;

    /**
     * Constructor.
     *
     * @param pGateway the gateway
     * @param pBuilder the builder
     */
    GordianCRMParser(final GordianBaseKeyStoreGateway pGateway,
                     final GordianCRMBuilder pBuilder) {
        /* Store parameters */
        theGateway = pGateway;
        theBuilder = pBuilder;
    }

    /**
     * Does a certificate request require encryption?
     *
     * @param pCertReq the certificate request
     * @return true/false
     */
    static boolean requiresEncryption(final CertReqMsg pCertReq) {
        /* Only encipherment keys may be encrypted */
        final ProofOfPossession myProof = pCertReq.getPop();
        if (myProof.getType() != ProofOfPossession.TYPE_KEY_ENCIPHERMENT) {
            return false;
        }

        /* Check that we are responding with an encrypted certificate */
        final POPOPrivKey myPOP = POPOPrivKey.getInstance(myProof.getObject());
        final int myProofType = myPOP.getType();
        return myProofType == POPOPrivKey.subsequentMessage
                && ASN1Integer.getInstance(myPOP.getValue()).intValueExact() == SubsequentMessage.encrCert.intValueExact();
    }

    /**
     * process a certificate request.
     *
     * @param pRequest the request
     * @return the signed certificate chain
     * @throws GordianException on error
     */
    List<GordianCertificate> processCertificateRequest(final CertReqMsg pRequest) throws GordianException {
        /* Derive the certificate request message */
        final CertRequest myCertReq = pRequest.getCertReq();
        final ProofOfPossession myProof = pRequest.getPop();
        final AttributeTypeAndValue[] myAttrs = pRequest.getRegInfo();
        final CertTemplate myTemplate = myCertReq.getCertTemplate();
        final X500Name mySubject = myTemplate.getSubject();
        final SubjectPublicKeyInfo myPublic = myTemplate.getPublicKey();
        final GordianKeyPairUsage myUsage = GordianCertUtils.determineUsage(myTemplate.getExtensions());

        /* Check the PKMacValue */
        checkPKMACValue(mySubject, myAttrs, myPublic);

        /* Derive keyPair and create certificate chain */
        final GordianKeyPair myPair = deriveKeyPair(myProof, myCertReq, mySubject, myPublic);
        final GordianBaseKeyStoreManager myKeyStoreMgr = (GordianBaseKeyStoreManager) theGateway.getKeyStoreManager();
        final GordianKeyStorePair mySigner = theGateway.getSigner();
        if (mySigner == null) {
            throw new GordianLogicException("Null keyPairSigner");
        }
        return myKeyStoreMgr.signKeyPair(myPair, mySubject, myUsage, mySigner);
    }

    /**
     * process a certificate response.
     *
     * @param pResponse the certificate response
     * @param pKeyPair  the keyPair
     */
    public void processCertificateResponse(final GordianCertResponseASN1 pResponse,
                                           final GordianKeyStorePair pKeyPair) throws GordianException {
        /* Decrypt if necessary */
        if (pResponse.isEncrypted()) {
            final GordianCRMEncryptor myEncryptor = theGateway.getEncryptor();
            pResponse.decryptCertificate(myEncryptor, pKeyPair);
        }

        /* Check the MACValue */
        final GordianCoreCertificate myCert = (GordianCoreCertificate) pKeyPair.getCertificateChain().get(0);
        final X500Name myName = myCert.getSubjectName();
        final byte[] myMACSecret = theGateway.getMACSecret(myName);
        final ASN1Object myMACData = pResponse.getMACData();
        final PKMACValue mySent = pResponse.getMACValue();

        /* If we have a mismatch on security */
        if ((myMACSecret == null) != (mySent == null)) {
            throw new GordianDataException("Mismatch on PKMAC Security");
        }

        /* If we have a MACValue */
        if (mySent != null) {
            /* Calculate the PKMACValue and compare with the value that was sent */
            theBuilder.checkPKMACValue(myMACSecret, myMACData, mySent);
        }

        /* Check that the publicKey matches */
        final GordianCoreCertificate myNewCert = pResponse.getCertificate(theGateway.getEncryptor());
        if (!myNewCert.checkMatchingPublicKey(pKeyPair.getKeyPair())) {
            throw new GordianDataException("Mismatch on publicKey");
        }

        /* Check that subjectName matches */
        final GordianCoreCertificate myOldCert = (GordianCoreCertificate) pKeyPair.getCertificateChain().get(0);
        if (!myNewCert.getSubjectName().equals(myOldCert.getSubjectName())) {
            throw new GordianDataException("Mismatch on subjectName");
        }
    }

    /**
     * Derive the privateKey.
     *
     * @param pProof   the proof of possession
     * @param pSubject the subject name
     * @return the PKCS8Encoded privateKey
     * @throws GordianException on error
     */
    PKCS8EncodedKeySpec derivePrivateKey(final ProofOfPossession pProof,
                                         final X500Name pSubject) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Extract details */
            final POPOPrivKey myEncrypt = (POPOPrivKey) pProof.getObject();
            final EnvelopedData myData = (EnvelopedData) myEncrypt.getValue();
            final RecipientInfo myRecipient = RecipientInfo.getInstance(myData.getRecipientInfos().getObjectAt(0));
            final EncryptedContentInfo myContent = myData.getEncryptedContentInfo();
            final byte[] myEncryptedPrivKey = myContent.getEncryptedContent().getOctets();
            final KeyTransRecipientInfo myRecInfo = (KeyTransRecipientInfo) myRecipient.getInfo();

            /* Derive the keySet */
            final GordianKeySet myKeySet = deriveKeySetFromRecInfo(myRecInfo);

            /* Decrypt the privateKey/ID */
            final EncKeyWithID myKeyWithId = EncKeyWithID.getInstance(myKeySet.decryptBytes(myEncryptedPrivKey));

            /* Check that the ID matches */
            final X500Name myName = X500Name.getInstance(GeneralName.getInstance(myKeyWithId.getIdentifier()).getName());
            if (!myName.equals(pSubject)) {
                throw new GordianDataException("Mismatch on subjectID");
            }

            /* myName and Subject should be identical */
            return new PKCS8EncodedKeySpec(myKeyWithId.getPrivateKey().getEncoded());

        } catch (IOException e) {
            throw new GordianIOException("Failed to derive encrypted privateKey", e);
        }
    }

    /**
     * Derive the keySet via a keyPairSet issuer.
     *
     * @param pRecInfo the recipient info
     * @return the keySet
     * @throws GordianException on error
     */
    private GordianKeySet deriveKeySetFromRecInfo(final KeyTransRecipientInfo pRecInfo) throws GordianException {
        /* Access issuer details */
        final IssuerAndSerialNumber myIssId = (IssuerAndSerialNumber) pRecInfo.getRecipientIdentifier().getId();

        /* Locate issuer */
        final GordianBaseKeyStore myKeyStore = (GordianBaseKeyStore) theGateway.getKeyStore();
        final String myAlias = myKeyStore.findIssuerCert(myIssId);
        final Function<String, char[]> myResolver = theGateway.getPasswordResolver();
        final char[] myPassword = myResolver.apply(myAlias);
        if (myPassword == null) {
            throw new GordianDataException("No password available for issuer");
        }
        final GordianKeyStoreEntry myIssuerEntry = myKeyStore.getEntry(myAlias, myPassword);
        Arrays.fill(myPassword, (char) 0);

        /* Access details */
        final GordianKeyStorePair myIssuer = (GordianKeyStorePair) myIssuerEntry;
        final GordianCertificate myCert = myIssuer.getCertificateChain().get(0);
        final GordianCRMEncryptor myEncryptor = theGateway.getEncryptor();
        return myEncryptor.deriveKeySetFromRecInfo(pRecInfo, myCert, myIssuer.getKeyPair());
    }

    /**
     * Derive and check the keyPair.
     *
     * @param pProof     the proof of possession
     * @param pCertReq   the certificate request
     * @param pSubject   the subject name
     * @param pPublicKey the publicKey
     * @return the keyPair
     * @throws GordianException on error
     */
    private GordianKeyPair deriveKeyPair(final ProofOfPossession pProof,
                                         final CertRequest pCertReq,
                                         final X500Name pSubject,
                                         final SubjectPublicKeyInfo pPublicKey) throws GordianException {
        /* Handle signed keyPair */
        switch (pProof.getType()) {
            case ProofOfPossession.TYPE_SIGNING_KEY:
                return deriveSignedKeyPair(pProof, pCertReq, pPublicKey);
            case ProofOfPossession.TYPE_KEY_ENCIPHERMENT:
                return deriveEncryptedKeyPair(pProof, pSubject, pPublicKey);
            default:
                throw new GordianDataException("Unsupported proof type");
        }
    }

    /**
     * Derive a signed keyPair.
     *
     * @param pProof     the proof of possession
     * @param pCertReq   the certificate request
     * @param pPublicKey the publicKey
     * @return the keyPair
     * @throws GordianException on error
     */
    private GordianKeyPair deriveSignedKeyPair(final ProofOfPossession pProof,
                                               final CertRequest pCertReq,
                                               final SubjectPublicKeyInfo pPublicKey) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Derive the public Key */
            final GordianAsyncFactory myFactory = theGateway.getFactory().getAsyncFactory();
            final GordianKeyPairFactory myKPFactory = myFactory.getKeyPairFactory();
            final X509EncodedKeySpec myX509Spec = new X509EncodedKeySpec(pPublicKey.getEncoded());
            final GordianKeyPairSpec myKeySpec = myKPFactory.determineKeyPairSpec(myX509Spec);
            final GordianKeyPairGenerator myGenerator = myKPFactory.getKeyPairGenerator(myKeySpec);
            final GordianKeyPair myKeyPair = myGenerator.derivePublicOnlyKeyPair(myX509Spec);

            /* Access the verifier */
            final POPOSigningKey mySigning = (POPOSigningKey) pProof.getObject();
            final AlgorithmIdentifier myAlgId = mySigning.getAlgorithmIdentifier();
            final GordianCoreSignatureFactory mySignFactory = (GordianCoreSignatureFactory) myFactory.getSignatureFactory();
            final GordianSignatureSpec mySignSpec = mySignFactory.getSpecForIdentifier(myAlgId);
            final GordianSignature myVerifier = mySignFactory.createSigner(mySignSpec);

            /* Verify the signature */
            final byte[] mySignature = mySigning.getSignature().getBytes();
            myVerifier.initForVerify(GordianSignParams.keyPair(myKeyPair));
            myVerifier.update(pCertReq.getEncoded());
            if (!myVerifier.verify(mySignature)) {
                throw new GordianDataException("Verification of keyPair failed");
            }
            return myKeyPair;

        } catch (IOException e) {
            throw new GordianIOException("Failed to derive signed keyPair", e);
        }
    }

    /**
     * Derive an encrypted keyPair.
     *
     * @param pProof     the proof of possession
     * @param pSubject   the subject name
     * @param pPublicKey the publicKey
     * @return the keyPair
     * @throws GordianException on error
     */
    private GordianKeyPair deriveEncryptedKeyPair(final ProofOfPossession pProof,
                                                  final X500Name pSubject,
                                                  final SubjectPublicKeyInfo pPublicKey) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Access the generator */
            final GordianKeyPairFactory myFactory = theGateway.getFactory().getAsyncFactory().getKeyPairFactory();
            final X509EncodedKeySpec myX509Spec = new X509EncodedKeySpec(pPublicKey.getEncoded());
            final GordianKeyPairSpec myKeySpec = myFactory.determineKeyPairSpec(myX509Spec);
            final GordianKeyPairGenerator myGenerator = myFactory.getKeyPairGenerator(myKeySpec);

            /* Determine type of proof of Possession */
            final POPOPrivKey myPOP = POPOPrivKey.getInstance(pProof.getObject());
            final int myProofType = myPOP.getType();

            /* Handle encryptedKey */
            if (myProofType == POPOPrivKey.encryptedKey) {
                /* derive the privateKey and full keyPair */
                final PKCS8EncodedKeySpec myPKCS8Spec = derivePrivateKey(pProof, pSubject);
                final GordianKeyPair myKeyPair = myGenerator.deriveKeyPair(myX509Spec, myPKCS8Spec);

                /* Check that the privateKey matches the publicKey */
                checkPrivateKey(myKeyPair);
            } else if (myProofType != POPOPrivKey.subsequentMessage
                    || ASN1Integer.getInstance(myPOP.getValue()).intValueExact() != SubsequentMessage.encrCert.intValueExact()) {
                throw new GordianDataException("Unsupported ProofType");
            }

            /* Return the public only value */
            return myGenerator.derivePublicOnlyKeyPair(myX509Spec);

        } catch (IOException e) {
            throw new GordianIOException("Failed to derive encrypted keyPair", e);
        }
    }

    /**
     * Check PrivateKey.
     *
     * @param pKeyPair the keyPair
     * @throws GordianException on error
     */
    private void checkPrivateKey(final GordianKeyPair pKeyPair) throws GordianException {
        /* Access details */
        final GordianAsyncFactory myFactory = theGateway.getFactory().getAsyncFactory();
        final GordianKeyPairSpec mySpec = pKeyPair.getKeyPairSpec();

        /* Check for encryption private key */
        final GordianEncryptorSpec myEncSpec = myFactory.getEncryptorFactory().defaultForKeyPair(mySpec);
        if (myEncSpec != null) {
            checkEncryptionPrivateKey(pKeyPair);
            return;
        }

        /* Check for agreement private key */
        final GordianAgreementSpec myAgreeSpec = myFactory.getAgreementFactory().defaultForKeyPair(mySpec);
        if (myAgreeSpec != null) {
            checkAgreementPrivateKey(pKeyPair);
            return;
        }

        /* Reject the request */
        throw new GordianDataException("Unable to verify privateKey");
    }

    /**
     * Check Encryption PrivateKey.
     *
     * @param pKeyPair the keyPair
     * @throws GordianException on error
     */
    private void checkEncryptionPrivateKey(final GordianKeyPair pKeyPair) throws GordianException {
        /* Create the data to encrypt */
        final byte[] mySrc = new byte[TESTLEN];
        final GordianBaseFactory myFactory = theGateway.getFactory();
        myFactory.getRandomSource().getRandom().nextBytes(mySrc);

        /* Access details */
        final GordianEncryptorFactory myEncFactory = myFactory.getAsyncFactory().getEncryptorFactory();
        final GordianKeyPairSpec mySpec = pKeyPair.getKeyPairSpec();
        final GordianEncryptorSpec myEncSpec = myEncFactory.defaultForKeyPair(mySpec);

        /* Create and initialise encryptors */
        final GordianEncryptor mySender = myEncFactory.createEncryptor(myEncSpec);
        final GordianEncryptor myReceiver = myEncFactory.createEncryptor(myEncSpec);

        /* Handle Initialisation */
        mySender.initForEncrypt(pKeyPair);
        myReceiver.initForDecrypt(pKeyPair);

        /* Perform the encryption and decryption for all zeros */
        final byte[] myEncrypted = mySender.encrypt(mySrc);
        final byte[] myResult = myReceiver.decrypt(myEncrypted);

        /* Check the decryption */
        if (!org.bouncycastle.util.Arrays.areEqual(mySrc, myResult)) {
            throw new GordianDataException("Private key failed validation");
        }
    }

    /**
     * Check Agreement PrivateKey.
     *
     * @param pKeyPair the keyPair
     * @throws GordianException on error
     */
    private void checkAgreementPrivateKey(final GordianKeyPair pKeyPair) throws GordianException {
        /* Access details */
        final GordianBaseFactory myFactory = theGateway.getFactory();
        final GordianAgreementFactory myAgreeFactory = myFactory.getAsyncFactory().getAgreementFactory();
        final GordianKeyPairSpec mySpec = pKeyPair.getKeyPairSpec();
        final GordianAgreementSpec myAgreeSpec = myAgreeFactory.defaultForKeyPair(mySpec);

        /* Create agreement */
        final GordianCertificate myCert = myAgreeFactory.newMiniCertificate(GordianCRMEncryptor.SERVER, pKeyPair,
                new GordianKeyPairUsage(GordianKeyPairUse.AGREEMENT));
        GordianAgreementParams myParams = myAgreeFactory.newAgreementParams(myAgreeSpec, GordianLength.LEN_256.getByteLength())
                .setServerCertificate(myCert);
        final GordianAgreement mySender = myAgreeFactory.createAgreement(myParams);
        final byte[] myClientHello = mySender.nextMessage();
        final GordianAgreement myResponder = myAgreeFactory.parseAgreementMessage(myClientHello);
        myParams = myResponder.getAgreementParams().setServerCertificate(myCert);
        myResponder.updateParams(myParams);

        /* Check the agreements */
        final byte[] myFirst = (byte[]) mySender.getResult();
        final byte[] mySecond = (byte[]) myResponder.getResult();
        if (!Arrays.equals(myFirst, mySecond)) {
            throw new GordianDataException("Private key failed validation");
        }
    }

    /**
     * Check PKMacValue.
     *
     * @param pSubject   the subject name
     * @param pAttrs     the attributes
     * @param pPublicKey the public key
     * @throws GordianException on error
     */
    private void checkPKMACValue(final X500Name pSubject,
                                 final AttributeTypeAndValue[] pAttrs,
                                 final SubjectPublicKeyInfo pPublicKey) throws GordianException {
        /* Loop through the Attrs */
        AttributeTypeAndValue myAttr = null;
        if (pAttrs != null) {
            for (AttributeTypeAndValue myCurr : pAttrs) {
                if (GordianCRMBuilder.MACVALUEATTROID.equals(myCurr.getType())) {
                    myAttr = myCurr;
                    break;
                }
            }
        }

        /* If we have a mismatch on security */
        final byte[] myMACSecret = theGateway.getMACSecret(pSubject);
        if ((myMACSecret == null) != (myAttr == null)) {
            throw new GordianDataException("Mismatch on PKMAC Security");
        }

        /* If we have a MACValue */
        if (myAttr != null) {
            /* Calculate the PKMACValue and compare with the value that was sent */
            final PKMACValue mySent = PKMACValue.getInstance(myAttr.getValue());
            theBuilder.checkPKMACValue(myMACSecret, pPublicKey, mySent);
        }
    }
}