/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.social.legacy.crypto.x509;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.X509Principal;
import org.xwiki.social.legacy.crypto.internal.Convert;
import org.xwiki.social.legacy.crypto.x509.internal.AbstractX509CertificateWrapper;
/**
* X509 certificate wrapper with several additional helper methods, aimed to be more scripting-friendly.
*
* This class cannot be an interface because it extends AbstractX509CertificateWrapper which extends X509Certificate
* which is not an interface. Most bouncycastle code requires an X509Certificate so if we used an interface then
* it would just have to be casted every time somebody wanted to use it with non xwiki-crypto cryptographic apis.
*
* @version $Id: 29513e5ea49c8a1f1079f21e3964f9a46f4255b1 $
* @since 2.5M1
*/
public class XWikiX509Certificate extends AbstractX509CertificateWrapper
{
/** Supported certificate type. */
private static final String CERT_TYPE = "X509";
/** Digest algorithm used to generate the fingerprint. */
private static final String FINGERPRINT_ALGORITHM = "SHA1";
/** Marks the beginning of a certificate in PEM format. */
private static final String CERT_BEGIN = "-----BEGIN CERTIFICATE-----";
/** Marks the end of a certificate in PEM format. */
private static final String CERT_END = "-----END CERTIFICATE-----";
/** Certificate fingerprint. */
private final String fingerprint;
/** Certificate fingrprint of the issuer. */
private final String issuerFingerprint;
/**
* Create new {@link XWikiX509Certificate}. Assume that the certificate is self-signed.
*
* @param certificate the actual certificate to use
*/
public XWikiX509Certificate(X509Certificate certificate)
{
this(certificate, null);
}
/**
* Create new {@link XWikiX509Certificate}.
*
* @param certificate the actual certificate to use
* @param issuerFp fingerprint of the issuer certificate, null if self-signed
*/
public XWikiX509Certificate(X509Certificate certificate, String issuerFp)
{
super(certificate);
this.fingerprint = XWikiX509Certificate.calculateFingerprint(certificate);
if (issuerFp == null && (certificate instanceof XWikiX509Certificate)) {
this.issuerFingerprint = ((XWikiX509Certificate) certificate).getIssuerFingerprint();
} else if (issuerFp == null) {
this.issuerFingerprint = this.fingerprint;
} else {
this.issuerFingerprint = issuerFp;
}
}
/**
* Calculate the fingerprint of the given certificate. Throws a {@link RuntimeException} on errors.
*
* @param certificate the certificate to use
* @return certificate fingerprint in hex
*/
public static String calculateFingerprint(Certificate certificate)
{
try {
MessageDigest hash = MessageDigest.getInstance(FINGERPRINT_ALGORITHM);
BigInteger result = new BigInteger(1, hash.digest(certificate.getEncoded()));
return String.format("%0" + hash.getDigestLength() * 2 + "x", result);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
@Override
public int hashCode()
{
return this.fingerprint.hashCode();
}
@Override
public boolean equals(Object obj)
{
if (this == obj) {
return true;
}
if (obj instanceof XWikiX509Certificate) {
XWikiX509Certificate cert = (XWikiX509Certificate) obj;
return getFingerprint().equals(cert.getFingerprint());
}
return false;
}
@Override
public String toString()
{
final String format = "%20s : %s\n";
StringBuilder builder = new StringBuilder();
builder.append("XWikiX509Certificate\n");
builder.append("---------------------------------------------------------------\n");
builder.append(String.format(format, "Fingerprint", getFingerprint()));
builder.append(String.format(format, "SubjectDN", getAuthorName()));
builder.append(String.format(format, "IssuerDN", getIssuerName()));
builder.append(String.format(format, "Issuer Fingerprint", getIssuerFingerprint()));
builder.append(String.format(format, "SerialNumber", getSerialNumber().toString(16)));
builder.append(String.format(format, "Start Date", getNotBefore()));
builder.append(String.format(format, "Final Date", getNotAfter()));
builder.append(String.format(format, "Public Key Algorithm", getPublicKey().getAlgorithm()));
builder.append(String.format(format, "Signature Algorithm", getSigAlgName()));
try {
builder.append(this.toPEMString());
} catch (CertificateEncodingException exception) {
// ignore
}
return builder.toString();
}
/**
* @return the fingerprint
*/
public String getFingerprint()
{
return fingerprint;
}
/**
* Get the internal X509 certificate in a standard PEM format.
*
* @return the certificate in PEM format
* @throws CertificateEncodingException on errors
* @see XWikiX509Certificate#fromPEMString()
*/
public String toPEMString() throws CertificateEncodingException
{
StringBuilder builder = new StringBuilder();
builder.append(CERT_BEGIN);
builder.append(Convert.getNewline());
builder.append(Convert.toChunkedBase64String(this.certificate.getEncoded()));
builder.append(CERT_END);
builder.append(Convert.getNewline());
return builder.toString();
}
/**
* Constructor from a PEM formatted string.
* This constructor will search the given string until it finds {@link XWikiX509Certificate#CERT_BEGIN} and assume
* everything until the next {@link XWikiX509Certificate#CERT_END} is a valid PEM formatted certificate. If there
* are multiple certificates in the passed string the first will be parsed, its issuer fingerprint will be set to
* the fingerprint of the second certificate and all subsequent certificates will be ignored.
*
* @param pemEncoded a String containing an X509 certificate in PEM format
* @throws GeneralSecurityException If there isn't a valid {@link XWikiX509Certificate#CERT_BEGIN} or
* {@link XWikiX509Certificate#CERT_END} tag, or if there is an exception parsing
* the content inbetween.
* @return an XWikiX509Certificate from the PEM input.
* @see XWikiX509Certificate#toPEMString()
*/
public static XWikiX509Certificate fromPEMString(String pemEncoded) throws GeneralSecurityException
{
final byte[] base64Bytes = Convert.fromBase64String(pemEncoded, CERT_BEGIN, CERT_END);
final CertificateFactory factory = CertificateFactory.getInstance(CERT_TYPE);
final Certificate cert = factory.generateCertificate(new ByteArrayInputStream(base64Bytes));
if (!(cert instanceof X509Certificate)) {
throw new GeneralSecurityException("Unsupported certificate type: " + cert.getType());
}
// if there is a second certificate, use it to calculate correct issuer fingerprint
int second = pemEncoded.indexOf(CERT_BEGIN, pemEncoded.indexOf(CERT_END));
if (second > 0) {
byte[] cert2 = Convert.fromBase64String(pemEncoded.substring(second), CERT_BEGIN, CERT_END);
try {
String issuerFp = calculateFingerprint(factory.generateCertificate(new ByteArrayInputStream(cert2)));
return new XWikiX509Certificate((X509Certificate) cert, issuerFp);
} catch (GeneralSecurityException exception) {
// completely ignore the second certificate on error
}
}
// assume self-signed certificate by default
return new XWikiX509Certificate((X509Certificate) cert);
}
/**
* Convert a chain of {@link Certificate}s into a chain of {@link XWikiX509Certificate}s, correctly setting the
* issuer fingerprint. The last certificate in the chain is assumed to be self-signed.
* <p>
* Each certificate in the input chain must be a subclass of {@link X509Certificate}, otherwise a runtime exception
* is thrown (the type is Certificate[] and not X509Certificate[] just for convenience, since certificate factories
* create certificate chains of this type).</p>
*
* @param x509Chain a chain if X509 certificates
* @return a corresponding chain of {@link XWikiX509Certificate}s wrapping the certificates from the input chain
*/
public static XWikiX509Certificate[] fromCertificateChain(Certificate[] x509Chain)
{
XWikiX509Certificate[] outChain = new XWikiX509Certificate[x509Chain.length];
String issuerFP = null;
// go through the chain backwards so that we don't need to recalculate the issuer fingerprint
for (int i = x509Chain.length - 1; i >= 0; i--) {
if (!(x509Chain[i] instanceof X509Certificate)) {
throw new IllegalArgumentException("Only X509 certificates are supported, found: "
+ x509Chain[i].getType());
}
outChain[i] = new XWikiX509Certificate((X509Certificate) x509Chain[i], issuerFP);
issuerFP = outChain[i].getFingerprint();
}
return outChain;
}
/**
* Get issuer name of this certificate. Same as {@link #getAuthorName()} of the certificate
* obtained via {@link #getIssuerFingerprint()}.
*
* @return issuer name
*/
public String getIssuerName()
{
return getIssuerX500Principal().getName();
}
/**
* Get the fingerprint of the issuer certificate.
*
* @return issuer fingerprint
*/
public String getIssuerFingerprint()
{
return issuerFingerprint;
}
/**
* Get name of the author (subject name) of this certificate.
*
* @return author name
*/
public String getAuthorName()
{
return getSubjectX500Principal().getName();
}
/**
* Get user name (stored as UID in the distinguished subject name) of this certificate's author, or empty string
* if UID is not present.
*
* @return author UID
*/
public String getAuthorUID()
{
try {
X509Principal author = PrincipalUtil.getSubjectX509Principal(this.certificate);
if (author.getValues(X509Name.UID).size() == 0) {
return "";
}
return (String) author.getValues(X509Name.UID).get(0);
} catch (CertificateEncodingException exception) {
// should not happen
return "";
}
}
}