/* * Kontalk Java client * Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.crypto; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Object; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.DERBitString; import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.misc.MiscObjectIdentifiers; import org.bouncycastle.asn1.misc.NetscapeCertType; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import org.bouncycastle.crypto.params.AsymmetricKeyParameter; import org.bouncycastle.crypto.util.PrivateKeyFactory; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.bc.BcContentSignerBuilder; import org.bouncycastle.operator.bc.BcDSAContentSignerBuilder; import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; /** * Utility methods for bridging OpenPGP keys with X.509 certificates.<br> * Inspired by the Foaf server project. * https://svn.java.net/svn/sommer~svn/trunk/misc/FoafServer/pgpx509/src/net/java/dev/sommer/foafserver/utils/PgpX509Bridge.java * @author Daniele Ricci */ public class X509Bridge { private static final String DN_COMMON_PART_O = "OpenPGP to X.509 Bridge"; private static final String PEM_TYPE_CERTIFICATE = "CERTIFICATE"; private X509Bridge() {} public static byte[] encode(X509Certificate cert) throws CertificateEncodingException, IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(); try (PemWriter writer = new PemWriter(new OutputStreamWriter(stream))) { writer.writeObject(new PemObject(X509Bridge.PEM_TYPE_CERTIFICATE, cert.getEncoded())); } return stream.toByteArray(); } public static X509Certificate createCertificate(PGPKeyPair keyPair, byte[] publicKeyRingData) throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, PGPException, IOException, OperatorCreationException { X500NameBuilder x500NameBuilder = new X500NameBuilder(); /* * The X.509 Name to be the subject DN is prepared. * The CN is extracted from the Secret Key user ID. */ x500NameBuilder.addRDN(BCStyle.O, DN_COMMON_PART_O); PGPPublicKey publicKey = keyPair.getPublicKey(); List<String> xmppAddrs = new LinkedList<>(); for (@SuppressWarnings("unchecked") Iterator<Object> it = publicKey.getUserIDs(); it.hasNext();) { String attrib = it.next().toString(); x500NameBuilder.addRDN(BCStyle.CN, attrib); // extract email for the subjectAltName String email = PGPUtils.parseUID(attrib)[2]; if (!email.isEmpty()) xmppAddrs.add(email); } X500Name x509name = x500NameBuilder.build(); /* * To check the signature from the certificate on the recipient side, * the creation time needs to be embedded in the certificate. * It seems natural to make this creation time be the "not-before" * date of the X.509 certificate. * Unlimited PGP keys have a validity of 0 second. In this case, * the "not-after" date will be the same as the not-before date. * This is something that needs to be checked by the service * receiving this certificate. */ Date creationTime = publicKey.getCreationTime(); Date validTo = null; if (publicKey.getValidSeconds()>0) validTo = new Date(creationTime.getTime() + 1000L * publicKey.getValidSeconds()); return createCertificate( PGPUtils.convertPublicKey(publicKey), PGPUtils.convertPrivateKey(keyPair.getPrivateKey()), x509name, creationTime, validTo, xmppAddrs, publicKeyRingData); } /** * Creates a self-signed certificate from a public and private key. The * (critical) key-usage extension is set up with: digital signature, * non-repudiation, key-encipherment, key-agreement and certificate-signing. * The (non-critical) Netscape extension is set up with: SSL client and * S/MIME. A URI subjectAltName may also be set up. * * @param pubKey * public key * @param privKey * private key * @param subject * subject (and issuer) DN for this certificate, RFC 2253 format * preferred. * @param startDate * date from which the certificate will be valid * (defaults to current date and time if null) * @param endDate * date until which the certificate will be valid * (defaults to start date and time if null) * @param subjectAltNames * URIs to be placed in subjectAltName * @return self-signed certificate */ private static X509Certificate createCertificate(PublicKey pubKey, PrivateKey privKey, X500Name subject, Date startDate, Date endDate, List<String> subjectAltNames, byte[] publicKeyData) throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, IOException, OperatorCreationException { /* * Sets the signature algorithm. */ BcContentSignerBuilder signerBuilder; String pubKeyAlgorithm = pubKey.getAlgorithm(); switch (pubKeyAlgorithm) { case "DSA": { AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder() .find("SHA1WithDSA"); AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder() .find(sigAlgId); signerBuilder = new BcDSAContentSignerBuilder(sigAlgId, digAlgId); break; } case "RSA": { AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder() .find("SHA1WithRSAEncryption"); AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder() .find(sigAlgId); signerBuilder = new BcRSAContentSignerBuilder(sigAlgId, digAlgId); break; } default: throw new RuntimeException("Algorithm not recognised: " + pubKeyAlgorithm); } AsymmetricKeyParameter keyp = PrivateKeyFactory.createKey(privKey.getEncoded()); ContentSigner signer = signerBuilder.build(keyp); /* * Sets up the validity dates. */ if (startDate == null) { startDate = new Date(System.currentTimeMillis()); } if (endDate == null) { endDate = startDate; } X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( /* * Sets up the subject distinguished name. * Since it's a self-signed certificate, issuer and subject are the * same. */ subject, /* * The serial-number of this certificate is 1. It makes sense * because it's self-signed. */ BigInteger.ONE, startDate, endDate, subject, /* * Sets the public-key to embed in this certificate. */ SubjectPublicKeyInfo.getInstance(pubKey.getEncoded()) ); /* * Adds the Basic Constraint (CA: true) extension. */ certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); /* * Adds the Key Usage extension. */ certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage( KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment | KeyUsage.keyAgreement | KeyUsage.keyCertSign)); /* * Adds the Netscape certificate type extension. */ certBuilder.addExtension(MiscObjectIdentifiers.netscapeCertType, false, new NetscapeCertType( NetscapeCertType.sslClient | NetscapeCertType.smime)); JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); /* * Adds the subject key identifier extension. */ SubjectKeyIdentifier subjectKeyIdentifier = extUtils.createSubjectKeyIdentifier(pubKey); certBuilder.addExtension(Extension.subjectKeyIdentifier, false, subjectKeyIdentifier); /* * Adds the authority key identifier extension. */ AuthorityKeyIdentifier authorityKeyIdentifier = extUtils.createAuthorityKeyIdentifier(pubKey); certBuilder.addExtension(Extension.authorityKeyIdentifier, false, authorityKeyIdentifier); /* * Adds the subject alternative-name extension. */ if (subjectAltNames != null && subjectAltNames.size() > 0) { GeneralName[] names = new GeneralName[subjectAltNames.size()]; for (int i = 0; i < names.length; i++) names[i] = new GeneralName(GeneralName.otherName, new XmppAddrIdentifier(subjectAltNames.get(i))); certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(names)); } /* * Adds the PGP public key block extension. */ SubjectPGPPublicKeyInfo publicKeyExtension = new SubjectPGPPublicKeyInfo(publicKeyData); certBuilder.addExtension(SubjectPGPPublicKeyInfo.OID, false, publicKeyExtension); /* * Creates and sign this certificate with the private key * corresponding to the public key of the certificate * (hence the name "self-signed certificate"). */ X509CertificateHolder holder = certBuilder.build(signer); /* * Checks that this certificate has indeed been correctly signed. */ X509Certificate cert = new JcaX509CertificateConverter().getCertificate(holder); cert.verify(pubKey); return cert; } /** * A custom X.509 extension for a PGP public key. * @author Daniele Ricci */ private static class SubjectPGPPublicKeyInfo extends ASN1Object { // based on UUID 24e844a0-6cbc-11e3-8997-0002a5d5c51b static final ASN1ObjectIdentifier OID = new ASN1ObjectIdentifier("2.25.49058212633447845622587297037800555803"); private final DERBitString keyData; public SubjectPGPPublicKeyInfo(byte[] publicKey) { keyData = new DERBitString(publicKey); } @Override public ASN1Primitive toASN1Primitive() { return keyData; } } private static class XmppAddrIdentifier extends DLSequence { static final ASN1ObjectIdentifier OID = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.8.5"); XmppAddrIdentifier(String jid) { super(new ASN1Encodable[] { OID, new DERUTF8String(jid) }); } } }