/* * Kontalk Android client * Copyright (C) 2017 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.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import org.spongycastle.asn1.misc.MiscObjectIdentifiers; import org.spongycastle.asn1.misc.NetscapeCertType; import org.spongycastle.asn1.x500.X500Name; import org.spongycastle.asn1.x500.X500NameBuilder; import org.spongycastle.asn1.x500.style.BCStyle; import org.spongycastle.asn1.x509.AlgorithmIdentifier; import org.spongycastle.asn1.x509.AuthorityKeyIdentifier; import org.spongycastle.asn1.x509.BasicConstraints; import org.spongycastle.asn1.x509.Extension; import org.spongycastle.asn1.x509.GeneralName; import org.spongycastle.asn1.x509.GeneralNames; import org.spongycastle.asn1.x509.KeyUsage; import org.spongycastle.asn1.x509.SubjectKeyIdentifier; import org.spongycastle.asn1.x509.SubjectPublicKeyInfo; import org.spongycastle.cert.X509CertificateHolder; import org.spongycastle.cert.X509v3CertificateBuilder; import org.spongycastle.cert.jcajce.JcaX509CertificateConverter; import org.spongycastle.cert.jcajce.JcaX509ExtensionUtils; import org.spongycastle.crypto.params.AsymmetricKeyParameter; import org.spongycastle.crypto.util.PrivateKeyFactory; import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPPrivateKey; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; import org.spongycastle.openpgp.PGPSecretKey; import org.spongycastle.openpgp.PGPSecretKeyRing; import org.spongycastle.openpgp.operator.KeyFingerPrintCalculator; import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.spongycastle.operator.ContentSigner; import org.spongycastle.operator.DefaultDigestAlgorithmIdentifierFinder; import org.spongycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; import org.spongycastle.operator.OperatorCreationException; import org.spongycastle.operator.bc.BcContentSignerBuilder; import org.spongycastle.operator.bc.BcDSAContentSignerBuilder; import org.spongycastle.operator.bc.BcRSAContentSignerBuilder; import android.os.Parcel; /** * 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 KeyFingerPrintCalculator sFingerprintCalculator = PGP.sFingerprintCalculator; public static final String PEM_TYPE_PRIVATE_KEY = "RSA PRIVATE KEY"; public static final String PEM_TYPE_CERTIFICATE = "CERTIFICATE"; private final static String DN_COMMON_PART_O = "OpenPGP to X.509 Bridge"; private X509Bridge() { } public static X509Certificate createCertificate(byte[] publicKeyData, PGPSecretKey secretKey, String passphrase) throws PGPException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, IOException, OperatorCreationException { PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator); return createCertificate(pubRing, secretKey, passphrase); } public static X509Certificate createCertificate(PGPPublicKeyRing publicKeyring, PGPSecretKey secretKey, String passphrase) throws PGPException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, IOException, OperatorCreationException { // extract the private key PGPDigestCalculatorProvider sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build(); PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder(sha1Calc) .setProvider(PGP.PROVIDER) .build(passphrase.toCharArray()); PGPPrivateKey privateKey = secretKey.extractPrivateKey(decryptor); return createCertificate(publicKeyring, privateKey); } public static X509Certificate createCertificate(byte[] privateKeyData, byte[] publicKeyData, String passphrase) throws PGPException, IOException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, OperatorCreationException { PGPSecretKeyRing secRing = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator); PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator); PGPDigestCalculatorProvider sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build(); PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder(sha1Calc) .setProvider(PGP.PROVIDER) .build(passphrase.toCharArray()); // secret key PGPSecretKey secKey = secRing.getSecretKey(); return createCertificate(pubRing, secKey.extractPrivateKey(decryptor)); } public static X509Certificate createCertificate(byte[] publicKeyData, PGPPrivateKey privateKey) throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, PGPException, IOException, OperatorCreationException { PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator); return createCertificate(pubRing, privateKey); } static X509Certificate createCertificate(PGPPublicKeyRing publicKeyRing, PGPPrivateKey privateKey) 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 = null; @SuppressWarnings("unchecked") Iterator<PGPPublicKey> iter = publicKeyRing.getPublicKeys(); while (iter.hasNext()) { PGPPublicKey pk = iter.next(); if (pk.isMasterKey()) { publicKey = pk; break; } } if (publicKey == null) throw new IllegalArgumentException("no master key found"); 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 PGPUserID uid = PGPUserID.parse(attrib); if (uid != null && uid.getEmail() != null) xmppAddrs.add(uid.getEmail()); } 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( PGP.convertPublicKey(publicKey), PGP.convertPrivateKey(privateKey), x509name, creationTime, validTo, xmppAddrs, publicKeyRing.getEncoded()); } /** * 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 * URI 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(); if (pubKeyAlgorithm.equals("DSA")) { AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder() .find("SHA1WithDSA"); AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder() .find(sigAlgId); signerBuilder = new BcDSAContentSignerBuilder(sigAlgId, digAlgId); } else if (pubKeyAlgorithm.equals("RSA")) { AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder() .find("SHA1WithRSAEncryption"); AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder() .find(sigAlgId); signerBuilder = new BcRSAContentSignerBuilder(sigAlgId, digAlgId); } else { 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, Locale.US, 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; } public static X509Certificate fromParcel(Parcel in) throws PGPException { // TODO return null; } public static X509Certificate load(byte[] certData) throws CertificateException, NoSuchProviderException { return load(new ByteArrayInputStream(certData)); } public static X509Certificate load(InputStream certData) throws CertificateException, NoSuchProviderException { CertificateFactory certFactory = CertificateFactory.getInstance("X.509", PGP.PROVIDER); return (X509Certificate) certFactory.generateCertificate(certData); } public static KeyStore exportCertificate(X509Certificate certificate, PrivateKey privateKey) throws KeyStoreException, NoSuchProviderException, NoSuchAlgorithmException, CertificateException, IOException { KeyStore store = KeyStore.getInstance("PKCS12", PGP.PROVIDER); store.load(null, null); store.setKeyEntry("Kontalk Personal Key", privateKey, null, new Certificate[] { certificate }); return store; } }