GordianCoreCertificate.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.cert;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianCertificateId;
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.digest.GordianDigestSpec;
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.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.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.keypair.GordianCompositeKeyPair;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keypair.GordianCoreKeyPair;
import io.github.tonywasher.joceanus.gordianknot.impl.core.sign.GordianCoreSignatureFactory;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.TBSCertificate;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
import java.io.IOException;
import java.math.BigInteger;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
/**
* Certificate implementation.
*/
public class GordianCoreCertificate
implements GordianCertificate {
/**
* The Factory.
*/
private final GordianBaseFactory theFactory;
/**
* The Subject.
*/
private final GordianCertificateId theSubject;
/**
* The Issuer.
*/
private final GordianCertificateId theIssuer;
/**
* The (public only) KeyPair to which this Certificate belongs.
*/
private final GordianKeyPair theKeyPair;
/**
* The KeyUsage.
*/
private final GordianKeyPairUsage theKeyUsage;
/**
* The CAStatus.
*/
private final GordianCAStatus theCAStatus;
/**
* The Signature Algorithm.
*/
private final AlgorithmIdentifier theSigAlgId;
/**
* The SignatureSpec.
*/
private final GordianSignatureSpec theSigSpec;
/**
* The serial#.
*/
private final BigInteger theSerialNo;
/**
* The TBS Certificate.
*/
private final TBSCertificate theTbsCertificate;
/**
* The signature.
*/
private final byte[] theSignature;
/**
* The encoded representation.
*/
private final byte[] theEncoded;
/**
* Is the certificate self-signed?
*/
private final boolean isSelfSigned;
/**
* Create a new self-signed certificate.
*
* @param pFactory the factory
* @param pKeyPair the keyPair
* @param pSubject the name of the entity
* @throws GordianException on error
*/
public GordianCoreCertificate(final GordianBaseFactory pFactory,
final GordianKeyPair pKeyPair,
final X500Name pSubject) throws GordianException {
/* Check that the keyPair is OK */
if (isPublicOnly(pKeyPair)) {
throw new GordianLogicException("Invalid keyPair");
}
/* Store the parameters */
theFactory = pFactory;
theKeyPair = getPublicOnly(pKeyPair);
/* Determine the signatureSpec */
theSigSpec = determineSignatureSpecForKeyPair(theKeyPair);
/* Determine the algorithmId for the signatureSpec */
theSigAlgId = determineAlgIdForSignatureSpec(theSigSpec, theKeyPair);
/* Create the TBSCertificate */
theKeyUsage = new GordianKeyPairUsage(GordianKeyPairUse.CERTIFICATE);
theCAStatus = new GordianCAStatus(true);
theTbsCertificate = buildCertificate(null, pSubject);
theSerialNo = theTbsCertificate.getSerialNumber().getValue();
/* Create the signature */
theSignature = createSignature(pKeyPair);
isSelfSigned = true;
/* Create the ids */
theSubject = buildSubjectId();
theIssuer = buildIssuerId();
/* Store the encoded representation */
theEncoded = encodeCertificate();
}
/**
* Create a new certificate, signed by the relevant authority.
*
* @param pFactory the factory
* @param pSigner the signing keyPair/certificate
* @param pKeyPair the keyPair
* @param pSubject the name of the entity
* @param pUsage the key usage
* @throws GordianException on error
*/
public GordianCoreCertificate(final GordianBaseFactory pFactory,
final GordianKeyStorePair pSigner,
final GordianKeyPair pKeyPair,
final X500Name pSubject,
final GordianKeyPairUsage pUsage) throws GordianException {
/* Store the parameters */
theFactory = pFactory;
theKeyPair = getPublicOnly(pKeyPair);
theKeyUsage = pUsage;
/* Check that the signer is allowed to sign certificates */
final GordianKeyPair mySignerPair = pSigner.getKeyPair();
final GordianCoreCertificate mySignerCert = (GordianCoreCertificate) pSigner.getCertificateChain().getFirst();
if (!mySignerCert.getUsage().hasUse(GordianKeyPairUse.CERTIFICATE)
|| !mySignerCert.isValidNow()
|| isPublicOnly(mySignerPair)) {
throw new GordianLogicException("Invalid signer");
}
/* Determine CA Status */
theCAStatus = new GordianCAStatus(theKeyUsage, mySignerCert.theCAStatus);
/* Determine the signatureSpec */
theSigSpec = determineSignatureSpecForKeyPair(pSigner.getKeyPair());
/* Determine the algorithm Id for the signatureSpec */
theSigAlgId = determineAlgIdForSignatureSpec(theSigSpec, pSigner.getKeyPair());
/* Create the TBSCertificate */
theTbsCertificate = buildCertificate(mySignerCert, pSubject);
theSerialNo = theTbsCertificate.getSerialNumber().getValue();
/* Create the signature */
theSignature = createSignature(mySignerPair);
isSelfSigned = false;
/* Create the ids */
theSubject = buildSubjectId();
theIssuer = buildIssuerId();
/* Store the encoded representation */
theEncoded = encodeCertificate();
}
/**
* Parse a certificate.
*
* @param pFactory the factory
* @param pSequence the DER representation of the certificate
* @throws GordianException on error
*/
public GordianCoreCertificate(final GordianBaseFactory pFactory,
final byte[] pSequence) throws GordianException {
this(pFactory, Certificate.getInstance(pSequence));
}
/**
* Parse a certificate.
*
* @param pFactory the factory
* @param pCertificate the certificate
* @throws GordianException on error
*/
public GordianCoreCertificate(final GordianBaseFactory pFactory,
final Certificate pCertificate) throws GordianException {
/* Protect against exceptions */
try {
/* Store the parameters */
theFactory = pFactory;
/* Extract the details */
theTbsCertificate = pCertificate.getTBSCertificate();
theSigAlgId = pCertificate.getSignatureAlgorithm();
theSignature = pCertificate.getSignature().getBytes();
/* Determine the signatureSpec for the algorithmId */
theSigSpec = determineSignatureSpecForAlgId(theSigAlgId);
if (theSigSpec == null) {
throw new GordianDataException("Unsupported Signature AlgorithmId: " + theSigAlgId);
}
/* Derive the keyPair */
theKeyPair = parseEncodedKey();
/* Access the extensions */
final Extensions myExtensions = theTbsCertificate.getExtensions();
theKeyUsage = GordianCertUtils.determineUsage(myExtensions);
theCAStatus = GordianCAStatus.determineStatus(myExtensions);
/* Create the ids */
theSubject = buildSubjectId();
theIssuer = buildIssuerId();
theSerialNo = theTbsCertificate.getSerialNumber().getValue();
/* Determine whether we are self-signed */
final X500Name mySignerName = getSubjectName();
isSelfSigned = mySignerName.equals(getIssuerName());
/* Store the encoded representation */
theEncoded = pCertificate.getEncoded();
} catch (IOException e) {
throw new GordianIOException("Failed to parse certificate", e);
}
}
/**
* Obtain the factory.
*
* @return the factory
*/
protected GordianBaseFactory getFactory() {
return theFactory;
}
/**
* Build the issuerId for a certificate.
*
* @return get the issuer id
*/
private GordianCoreCertificateId buildSubjectId() {
return new GordianCoreCertificateId(theTbsCertificate.getSubject(), GordianCertUtils.determineSubjectId(theTbsCertificate));
}
/**
* Build the issuer Id for a certificate.
*
* @return get the issuer id
*/
private GordianCoreCertificateId buildIssuerId() {
return new GordianCoreCertificateId(theTbsCertificate.getIssuer(), GordianCertUtils.determineIssuerId(theTbsCertificate));
}
@Override
public boolean isValidOnDate(final Date pDate) {
/* Access the date */
return pDate.compareTo(theTbsCertificate.getStartDate().getDate()) >= 0
&& pDate.compareTo(theTbsCertificate.getEndDate().getDate()) <= 0;
}
/**
* Obtain the certificate.
*
* @return the certificate
*/
public Certificate getCertificate() {
return Certificate.getInstance(getEncoded());
}
@Override
public byte[] getEncoded() {
return theEncoded.clone();
}
@Override
public GordianCertificateId getSubject() {
return theSubject;
}
@Override
public GordianCertificateId getIssuer() {
return isSelfSigned ? theSubject : theIssuer;
}
/**
* Obtain the subject of the certificate.
*
* @return the subject
*/
public X500Name getSubjectName() {
return theSubject.getName();
}
/**
* Obtain the subjectId of the certificate.
*
* @return the subjectId
*/
byte[] getSubjectId() {
return theSubject.getId();
}
/**
* Obtain the issuer of the certificate.
*
* @return the issuer name
*/
X500Name getIssuerName() {
return theIssuer.getName();
}
/**
* Obtain the issuerId of the certificate.
*
* @return the issuerId
*/
byte[] getIssuerId() {
return isSelfSigned
? getSubjectId()
: theIssuer.getId();
}
@Override
public boolean isSelfSigned() {
return isSelfSigned;
}
@Override
public GordianKeyPairUsage getUsage() {
return theKeyUsage;
}
@Override
public GordianKeyPair getKeyPair() {
return theKeyPair;
}
/**
* Obtain the serial#.
*
* @return the serial number.
*/
public BigInteger getSerialNo() {
return theSerialNo;
}
/**
* Obtain the signatureSpec.
*
* @return the signatureSpec
*/
protected GordianSignatureSpec getSignatureSpec() {
return theSigSpec;
}
/**
* Is the keyPair publicOnly?
*
* @param pKeyPair the keyPair
* @return true/false
*/
protected boolean isPublicOnly(final GordianKeyPair pKeyPair) {
return pKeyPair.isPublicOnly();
}
/**
* get public only version of key.
*
* @param pKeyPair the key
* @return the publicOnly version
*/
protected GordianKeyPair getPublicOnly(final GordianKeyPair pKeyPair) {
return pKeyPair instanceof GordianCompositeKeyPair myComposite
? myComposite.getPublicOnly()
: ((GordianCoreKeyPair) pKeyPair).getPublicOnly();
}
/**
* Determine the signatureSpec for the key.
*
* @param pKeyPair the keyPair
* @return the signatureSpec
*/
GordianSignatureSpec determineSignatureSpecForKeyPair(final GordianKeyPair pKeyPair) {
return theFactory.getAsyncFactory().getSignatureFactory().defaultForKeyPair(pKeyPair.getKeyPairSpec());
}
/**
* Determine the signatureSpec for the algorithmId.
*
* @param pAlgId the algorithmId
* @return the signatureSpec
*/
GordianSignatureSpec determineSignatureSpecForAlgId(final AlgorithmIdentifier pAlgId) {
final GordianCoreSignatureFactory mySigns = (GordianCoreSignatureFactory) getFactory().getAsyncFactory().getSignatureFactory();
return mySigns.getSpecForIdentifier(pAlgId);
}
/**
* Determine the algorithmId for the signatureSpec.
*
* @param pSpec the signatureSpec
* @param pSigner the signer
* @return the algorithmId
*/
AlgorithmIdentifier determineAlgIdForSignatureSpec(final GordianSignatureSpec pSpec,
final GordianKeyPair pSigner) {
final GordianCoreSignatureFactory mySigns = (GordianCoreSignatureFactory) getFactory().getAsyncFactory().getSignatureFactory();
return mySigns.getIdentifierForSpecAndKeyPair(pSpec, pSigner);
}
/**
* Validate that the keyPair public Key matches.
*
* @param pKeyPair the key pair
* @return matches true/false
*/
public boolean checkMatchingPublicKey(final GordianKeyPair pKeyPair) {
return pKeyPair instanceof GordianCompositeKeyPair myComposite
? myComposite.checkMatchingPublicKey(getKeyPair())
: ((GordianCoreKeyPair) pKeyPair).checkMatchingPublicKey(getKeyPair());
}
/**
* parse encodedKey.
*
* @return the parsed key
* @throws GordianException on error
*/
protected GordianKeyPair parseEncodedKey() throws GordianException {
/* Derive the keyPair */
final GordianKeyPairFactory myFactory = getFactory().getAsyncFactory().getKeyPairFactory();
final X509EncodedKeySpec myX509 = getX509KeySpec();
final GordianKeyPairSpec myKeySpec = myFactory.determineKeyPairSpec(myX509);
final GordianKeyPairGenerator myGenerator = myFactory.getKeyPairGenerator(myKeySpec);
return myGenerator.derivePublicOnlyKeyPair(myX509);
}
/**
* Obtain the encoded publicKey.
*
* @return the encoded bytes
* @throws GordianException on error
*/
protected byte[] getPublicKeyEncoded() throws GordianException {
/* Access the keyPair */
final GordianKeyPair myPair = getKeyPair();
/* Access the keyPair generator */
final GordianKeyPairFactory myFactory = getFactory().getAsyncFactory().getKeyPairFactory();
final GordianKeyPairGenerator myGenerator = myFactory.getKeyPairGenerator(myPair.getKeyPairSpec());
/* Obtain the publicKey Info */
return myGenerator.getX509Encoding(myPair).getEncoded();
}
/**
* Obtain the digestSpec.
*
* @return the digestSpec
*/
protected GordianDigestSpec getDigestSpec() {
return getSignatureSpec().getDigestSpec();
}
/**
* Create the signer.
*
* @return the signer
* @throws GordianException on error
*/
protected GordianSignature createSigner() throws GordianException {
/* Create the signer */
final GordianCoreSignatureFactory mySigns = (GordianCoreSignatureFactory) getFactory().getAsyncFactory().getSignatureFactory();
return mySigns.createSigner(getSignatureSpec());
}
@Override
public boolean validateCertificate(final GordianCertificate pSigner) throws GordianException {
/* Check that the certificate is not self-signed */
if (isSelfSigned) {
throw new GordianDataException("Root certificate used as intermediary");
}
/* Reject attempt to validate using a miniCertificate */
if (pSigner instanceof GordianMiniCertificate) {
throw new GordianDataException("MiniCertificate is not valid to describe signer");
}
/* Check that the signing certificate is correct */
final GordianCoreCertificate mySigner = (GordianCoreCertificate) pSigner;
final X500Name mySignerName = mySigner.getSubjectName();
final byte[] mySignerId = mySigner.getSubjectId();
if (!mySignerName.equals(getIssuerName())
|| !Arrays.equals(mySignerId, getIssuerId())) {
throw new GordianDataException("Incorrect signer certificate");
}
/* Check that the signing certificate is valid */
if (!pSigner.getUsage().hasUse(GordianKeyPairUse.CERTIFICATE)
|| !pSigner.isValidNow()) {
throw new GordianDataException("Invalid signer certificate");
}
/* Check that the signature is valid */
return validateSignature(pSigner.getKeyPair());
}
/**
* Validate a root certificate.
*
* @return valid? true/false
* @throws GordianException on error
*/
public boolean validateRootCertificate() throws GordianException {
/* Check that the certificate is self-signed */
if (!isSelfSigned) {
throw new GordianDataException("Non-root certificate used as root");
}
/* Check that the certificate is valid self-signed */
if (!theKeyUsage.hasUse(GordianKeyPairUse.CERTIFICATE)
|| theCAStatus.getPathLen() != null) {
throw new GordianDataException("Invalid root certificate");
}
/* Check that the issuerId is null */
if (theTbsCertificate.getIssuerUniqueId() != null) {
throw new GordianDataException("Root certificate has distinct issuerUniqueId");
}
/* Check that the signature is valid */
return validateSignature(theKeyPair);
}
/**
* Create a certificate.
*
* @param pSigner the signing certificate
* @param pSubject the name of the certificate
* @return the theCertificate
* @throws GordianException on error
*/
private TBSCertificate buildCertificate(final GordianCoreCertificate pSigner,
final X500Name pSubject) throws GordianException {
/* Create the name of the certificate */
final X500Name myIssuer = pSigner == null
? pSubject
: pSigner.getSubjectName();
/* Using the current timestamp as the certificate serial number */
final BigInteger mySerialNo = GordianCertUtils.newSerialNo();
/* Create the startDate and endDate for the certificate */
final Date myStart = new Date(System.currentTimeMillis());
final Calendar myCalendar = Calendar.getInstance();
myCalendar.setTime(myStart);
myCalendar.add(Calendar.YEAR, 1);
final Date myEnd = myCalendar.getTime();
/* Obtain the publicKey Info */
final byte[] myPublicKeyEncoded = getPublicKeyEncoded();
final SubjectPublicKeyInfo myPublicKeyInfo = SubjectPublicKeyInfo.getInstance(myPublicKeyEncoded);
/* Build basic information */
final V3TBSCertificateGenerator myCertBuilder = new V3TBSCertificateGenerator();
myCertBuilder.setSubject(pSubject);
myCertBuilder.setIssuer(myIssuer);
myCertBuilder.setStartDate(new Time(myStart));
myCertBuilder.setEndDate(new Time(myEnd));
myCertBuilder.setSerialNumber(new ASN1Integer(mySerialNo));
myCertBuilder.setSubjectPublicKeyInfo(myPublicKeyInfo);
myCertBuilder.setSignature(theSigAlgId);
final byte[] mySubjectId = GordianCertUtils.createKeyId(theFactory, myPublicKeyEncoded);
final byte[] myIssuerId = pSigner == null ? null : pSigner.getSubjectId();
/* Create extensions for the certificate */
myCertBuilder.setExtensions(GordianCertUtils.createExtensions(theCAStatus, theKeyUsage, mySubjectId, myIssuerId));
/* Generate the TBS Certificate */
return myCertBuilder.generateTBSCertificate();
}
/**
* Create the signature.
*
* @param pSigner the signer
* @return the generated signature
* @throws GordianException on error
*/
private byte[] createSignature(final GordianKeyPair pSigner) throws GordianException {
/* Protect against exceptions */
try {
/* Build the signature */
final GordianSignature mySigner = createSigner();
mySigner.initForSigning(GordianSignParams.keyPair(pSigner));
final GordianStreamConsumer myConsumer = new GordianStreamConsumer(mySigner);
final ASN1OutputStream myOut = ASN1OutputStream.create(myConsumer);
myOut.writeObject(theTbsCertificate);
myOut.close();
/* Create the signature */
return mySigner.sign();
} catch (IOException e) {
throw new GordianIOException("Failed to create signature", e);
}
}
/**
* Obtain the X509EncodedKeySpec.
*
* @return the keySpec
* @throws GordianException on error
*/
public X509EncodedKeySpec getX509KeySpec() throws GordianException {
/* Protect against exceptions */
try {
/* Obtain the X509 keySpec */
final SubjectPublicKeyInfo myInfo = theTbsCertificate.getSubjectPublicKeyInfo();
return new X509EncodedKeySpec(myInfo.getEncoded());
} catch (IOException e) {
throw new GordianIOException("Failed to extract keySpec", e);
}
}
/**
* Validate the signature.
*
* @param pSigner the signer
* @return true/false is the signature valid?
* @throws GordianException on error
*/
private boolean validateSignature(final GordianKeyPair pSigner) throws GordianException {
/* Protect against exceptions */
try {
/* Build the signature */
final GordianSignature myValidator = createSigner();
myValidator.initForVerify(GordianSignParams.keyPair(pSigner));
final GordianStreamConsumer myConsumer = new GordianStreamConsumer(myValidator);
final ASN1OutputStream myOut = ASN1OutputStream.create(myConsumer);
myOut.writeObject(theTbsCertificate);
myOut.close();
/* Verify the signature */
return myValidator.verify(theSignature);
} catch (IOException e) {
throw new GordianIOException("Failed to validate signature", e);
}
}
/**
* Create the DERSequence for a certificate.
*
* @return the DERSequence
* @throws GordianException on error
*/
private byte[] encodeCertificate() throws GordianException {
/* Protect against exceptions */
try {
/* Create the DERSequence */
final ASN1EncodableVector myVector = new ASN1EncodableVector();
myVector.add(theTbsCertificate);
myVector.add(theSigAlgId);
myVector.add(new DERBitString(theSignature));
final DERSequence mySeq = new DERSequence(myVector);
return mySeq.getEncoded();
} catch (IOException e) {
throw new GordianIOException("Failed to generate encoding", e);
}
}
@Override
public boolean equals(final Object pThat) {
/* Handle the trivial case */
if (pThat == this) {
return true;
}
if (pThat == null) {
return false;
}
/* Ensure object is correct class */
return pThat instanceof GordianCoreCertificate myThat
&& Arrays.equals(theEncoded, myThat.theEncoded);
}
@Override
public int hashCode() {
return Arrays.hashCode(theEncoded);
}
}