/* * 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.crypto.script; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.util.Collection; import java.util.Date; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.xwiki.component.annotation.Component; import org.xwiki.crypto.KeyPairGenerator; import org.xwiki.crypto.params.cipher.asymmetric.AsymmetricKeyPair; import org.xwiki.crypto.params.cipher.asymmetric.PrivateKeyParameters; import org.xwiki.crypto.params.cipher.asymmetric.PublicKeyParameters; import org.xwiki.crypto.params.generator.asymmetric.RSAKeyGenerationParameters; import org.xwiki.crypto.pkix.CertificateChainBuilder; import org.xwiki.crypto.pkix.CertificateGeneratorFactory; import org.xwiki.crypto.pkix.CertificateProvider; import org.xwiki.crypto.pkix.CertifyingSigner; import org.xwiki.crypto.pkix.X509ExtensionBuilder; import org.xwiki.crypto.pkix.params.CertifiedKeyPair; import org.xwiki.crypto.pkix.params.CertifiedPublicKey; import org.xwiki.crypto.pkix.params.x509certificate.DistinguishedName; import org.xwiki.crypto.pkix.params.x509certificate.X509CertificateGenerationParameters; import org.xwiki.crypto.pkix.params.x509certificate.X509CertificateParameters; import org.xwiki.crypto.pkix.params.x509certificate.X509CertifiedPublicKey; import org.xwiki.crypto.pkix.params.x509certificate.extension.ExtendedKeyUsages; import org.xwiki.crypto.pkix.params.x509certificate.extension.KeyUsage; import org.xwiki.crypto.pkix.params.x509certificate.extension.X509DnsName; import org.xwiki.crypto.pkix.params.x509certificate.extension.X509GeneralName; import org.xwiki.crypto.pkix.params.x509certificate.extension.X509IpAddress; import org.xwiki.crypto.pkix.params.x509certificate.extension.X509Rfc822Name; import org.xwiki.crypto.signer.CMSSignedDataGenerator; import org.xwiki.crypto.signer.CMSSignedDataVerifier; import org.xwiki.crypto.signer.SignerFactory; import org.xwiki.crypto.signer.param.CMSSignedDataGeneratorParameters; import org.xwiki.crypto.signer.param.CMSSignedDataVerified; import org.xwiki.crypto.signer.param.CMSSignerInfo; import org.xwiki.script.service.ScriptService; import org.xwiki.stability.Unstable; /** * Script service allowing a user to create keys pairs and issue certificates. * * @version $Id: faf0af2f6e9bd47ec7e14c5f0223b53863194047 $ * @since 8.4RC1 */ @Component @Named(CryptoScriptService.ROLEHINT + '.' + RSACryptoScriptService.ROLEHINT) @Singleton @Unstable public class RSACryptoScriptService implements ScriptService { /** * The role hint of this component. */ public static final String ROLEHINT = "rsa"; private static final Charset UTF8 = Charset.forName("UTF-8"); @Inject @Named("RSA") private KeyPairGenerator keyPairGenerator; @Inject @Named("SHA1withRSAEncryption") private SignerFactory signerFactory; @Inject private Provider<X509ExtensionBuilder> extensionBuilder; @Inject @Named("X509") private CertificateGeneratorFactory certificateGeneratorFactory; @Inject private CMSSignedDataGenerator cmsSignedDataGenerator; @Inject @Named("X509") private CertificateChainBuilder certificateChainBuilder; @Inject private CMSSignedDataVerifier cmsSignedDataVerifier; /** * Generate a new RSA key pair. * * The key strength will be {@value RSAKeyGenerationParameters#DEFAULT_STRENGTH}. * The key public exponent will be 0x10001. * The probability a chosen prime could not be a real prime will be smaller * than 2^-{@value RSAKeyGenerationParameters#DEFAULT_CERTAINTY}. * * @return an new asymmetric key pair. */ public AsymmetricKeyPair generateKeyPair() { return keyPairGenerator.generate(); } /** * Generate a new RSA key pair of given strength. The strength should be given in number of bytes, so for * a 2048 bits key, you should use 256 (bytes) as the integer parameter. The minimum valid strength is 2. * * The key public exponent will be 0x10001. * The probability a chosen prime could not be a real prime will be smaller * than 2^-{@value RSAKeyGenerationParameters#DEFAULT_CERTAINTY}. * * @param strength the strength in bytes. * @return an new asymmetric key pair. */ public AsymmetricKeyPair generateKeyPair(int strength) { return keyPairGenerator.generate(new RSAKeyGenerationParameters(strength)); } /** * Build a new instance with all custom parameters. The strength * should be given in number of bytes, so for a 2048 bits key, you should use 256 (bytes) as the integer parameter. * The minimum valid strength is 2. The exponent should be an odd number. The probability a chosen prime could * not be a real prime will be smaller than 2^certainty. * * @param strength the key strength in bytes. * @param publicExponent the public exponent. * @param certainty certainty for prime evaluation. * * @return an new asymmetric key pair. */ public AsymmetricKeyPair generateKeyPair(int strength, BigInteger publicExponent, int certainty) { return keyPairGenerator.generate(new RSAKeyGenerationParameters(strength, publicExponent, certainty)); } /** * Create a CertifiedKeyPair from a private key and a certificate. * * @param privateKey the private key. * @param certificate the certified public key. * @return a certified key pair. */ public CertifiedKeyPair createCertifiedKeyPair(PrivateKeyParameters privateKey, CertifiedPublicKey certificate) { return new CertifiedKeyPair(privateKey, certificate); } /** * Create a self-signed certificate for a Root CA. * * @param keyPair the keypair to issue the certificate for and used for signing it. * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @return a certified public key. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedKeyPair issueRootCACertificate(AsymmetricKeyPair keyPair, String dn, int validity) throws IOException, GeneralSecurityException { return new CertifiedKeyPair( keyPair.getPrivate(), certificateGeneratorFactory.getInstance(signerFactory.getInstance(true, keyPair.getPrivate()), new X509CertificateGenerationParameters( validity, extensionBuilder.get().addBasicConstraints(true) .addKeyUsage(true, EnumSet.of(KeyUsage.keyCertSign, KeyUsage.cRLSign)) .build())) .generate(new DistinguishedName(dn), keyPair.getPublic(), new X509CertificateParameters()) ); } /** * Create an intermediate CA certificate. * * @param issuer the certified keypair for issuing the certificate * @param keyPair the keyPair of the public key to certify * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @return a certified keypair. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedKeyPair issueIntermediateCertificate(CertifiedKeyPair issuer, AsymmetricKeyPair keyPair, String dn, int validity) throws IOException, GeneralSecurityException { return new CertifiedKeyPair( keyPair.getPrivate(), issueIntermediateCertificate(issuer, keyPair.getPublic(), dn, validity) ); } /** * Create an intermediate CA certificate. * * @param privateKey the private key for signing the certificate * @param issuer the certificate of the issuer of the certificate * @param publicKey the public key to certify * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @return a certified public key. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedPublicKey issueIntermediateCertificate(PrivateKeyParameters privateKey, CertifiedPublicKey issuer, PublicKeyParameters publicKey, String dn, int validity) throws IOException, GeneralSecurityException { return issueIntermediateCertificate(new CertifiedKeyPair(privateKey, issuer), publicKey, dn, validity); } /** * Create an intermediate CA certificate. * * @param issuer the certified keypair for issuing the certificate * @param publicKey the public key to certify * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @return a certified public key. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedPublicKey issueIntermediateCertificate(CertifiedKeyPair issuer, PublicKeyParameters publicKey, String dn, int validity) throws IOException, GeneralSecurityException { return certificateGeneratorFactory.getInstance( CertifyingSigner.getInstance(true, issuer, signerFactory), new X509CertificateGenerationParameters( validity, extensionBuilder.get().addBasicConstraints(0) .addKeyUsage(EnumSet.of(KeyUsage.keyCertSign, KeyUsage.cRLSign)) .build())) .generate(new DistinguishedName(dn), publicKey, new X509CertificateParameters()); } /** * Create an end entity certificate. * * @param issuer the certified keypair for issuing the certificate * @param keyPair the keyPair of the public key to certify * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @param subjectAltName the alternative names for the certificate * @return a certified keypair. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedKeyPair issueCertificate(CertifiedKeyPair issuer, AsymmetricKeyPair keyPair, String dn, int validity, List<X509GeneralName> subjectAltName) throws IOException, GeneralSecurityException { return new CertifiedKeyPair( keyPair.getPrivate(), issueCertificate(issuer, keyPair.getPublic(), dn, validity, subjectAltName) ); } /** * Create an end entity certificate. * * @param privateKey the private key for signing the certificate * @param issuer the certificate of the issuer of the certificate * @param publicKey the public key to certify * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @param subjectAltName the alternative names for the certificate * @return a certified public key. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedPublicKey issueCertificate(PrivateKeyParameters privateKey, CertifiedPublicKey issuer, PublicKeyParameters publicKey, String dn, int validity, List<X509GeneralName> subjectAltName) throws IOException, GeneralSecurityException { return issueCertificate(new CertifiedKeyPair(privateKey, issuer), publicKey, dn, validity, subjectAltName); } /** * Create an end entity certificate. By default, the key can be used for encryption and signing. If the end entity * contains some alternate subject names of type X509Rfc822Name a extended email protection usage is added. If the * end entity contains some alternate subject names of type X509DnsName or X509IpAddress extended server and client * authentication usages are added. * * @param issuer the keypair for issuing the certificate * @param publicKey the public key to certify * @param dn the distinguished name for the new the certificate. * @param validity the validity of the certificate from now in days. * @param subjectAltName the alternative names for the certificate * @return a certified public key. * @throws IOException in case on error while reading the public key. * @throws GeneralSecurityException in case of error. */ public CertifiedPublicKey issueCertificate(CertifiedKeyPair issuer, PublicKeyParameters publicKey, String dn, int validity, List<X509GeneralName> subjectAltName) throws IOException, GeneralSecurityException { X509CertificateParameters params; X509ExtensionBuilder builder = extensionBuilder.get().addKeyUsage(EnumSet.of(KeyUsage.digitalSignature, KeyUsage.dataEncipherment)); if (subjectAltName != null) { params = new X509CertificateParameters( extensionBuilder.get().addSubjectAltName(false, subjectAltName.toArray(new X509GeneralName[]{})) .build()); Set<String> extUsage = new HashSet<String>(); for (X509GeneralName genName : subjectAltName) { if (genName instanceof X509Rfc822Name) { extUsage.add(ExtendedKeyUsages.EMAIL_PROTECTION); } else if (genName instanceof X509DnsName || genName instanceof X509IpAddress) { extUsage.add(ExtendedKeyUsages.SERVER_AUTH); extUsage.add(ExtendedKeyUsages.CLIENT_AUTH); } builder.addExtendedKeyUsage(false, new ExtendedKeyUsages(extUsage)); } } else { params = new X509CertificateParameters(); } return certificateGeneratorFactory.getInstance( CertifyingSigner.getInstance(true, issuer, signerFactory), new X509CertificateGenerationParameters(validity, builder.build())) .generate(new DistinguishedName(dn), publicKey, params); } /** * Generate a CMS (Cryptographic Message Syntax) signature for a given byte content. The resulting signature * might contains the content itself. * * @param data the data to be signed * @param keyPair the certified key pair used for signing * @param embedContent if true, the signed content is embedded with the signature. * @return the resulting signature encoded ASN.1 and in accordance with RFC 3852. * @throws GeneralSecurityException on error. */ public byte[] cmsSign(byte[] data, CertifiedKeyPair keyPair, boolean embedContent) throws GeneralSecurityException { return cmsSign(data, keyPair, null, null, embedContent); } /** * Generate a CMS (Cryptographic Message Syntax) signature for a given byte content. The resulting signature * might contains the content itself and the certificate chain of the key used to sign. * * @param data the data to be signed * @param keyPair the certified key pair used for signing * @param certificateProvider Optionally, a certificate provider for obtaining the chain of certificate to embed. * If null, no certificate are embedded with the signature. * @param embedContent if true, the signed content is embedded with the signature. * @return the resulting signature encoded ASN.1 and in accordance with RFC 3852. * @throws GeneralSecurityException on error. */ public byte[] cmsSign(byte[] data, CertifiedKeyPair keyPair, CertificateProvider certificateProvider, boolean embedContent) throws GeneralSecurityException { return cmsSign(data, keyPair, certificateProvider, null, embedContent); } /** * Generate a CMS (Cryptographic Message Syntax) signature for a given byte content. The resulting signature * might contains the content itself and the certificate chain of the key used to sign. * * @param data the data to be signed * @param keyPair the certified key pair used for signing * @param certificateProvider Optionally, a certificate provider for obtaining the chain of certificate to embed. * If null, no certificate are embedded with the signature. * @param existingSignature if not null, a existing signature on the same data that should be kept. * @param embedContent if true, the signed content is embedded with the signature. * @return the resulting signature encoded ASN.1 and in accordance with RFC 3852. * @throws GeneralSecurityException on error. */ public byte[] cmsSign(byte[] data, CertifiedKeyPair keyPair, CertificateProvider certificateProvider, CMSSignedDataVerified existingSignature, boolean embedContent) throws GeneralSecurityException { CMSSignedDataGeneratorParameters parameters = new CMSSignedDataGeneratorParameters() .addSigner(CertifyingSigner.getInstance(true, keyPair, signerFactory)); if (existingSignature != null) { for (CMSSignerInfo existingSigner : existingSignature.getSignatures()) { parameters.addSignature(existingSigner); } } Set<CertifiedPublicKey> certs = new HashSet<CertifiedPublicKey>(); if (existingSignature != null && existingSignature.getCertificates() != null) { certs.addAll(existingSignature.getCertificates()); } if (certificateProvider != null) { if (existingSignature != null) { for (CMSSignerInfo existingSigner : existingSignature.getSignatures()) { if (existingSigner.getSubjectKeyIdentifier() != null) { addCertificateChain( certificateProvider.getCertificate(existingSigner.getSubjectKeyIdentifier()), certificateProvider, certs); } else { addCertificateChain( certificateProvider.getCertificate(existingSigner.getIssuer(), existingSigner.getSerialNumber()), certificateProvider, certs); } } } addCertificateChain(keyPair.getCertificate(), certificateProvider, certs); } if (!certs.isEmpty()) { parameters.addCertificates(certs); } return cmsSignedDataGenerator.generate(data, parameters, embedContent); } private void addCertificateChain(CertifiedPublicKey certificate, CertificateProvider certificateProvider, Collection<CertifiedPublicKey> certs) { Collection<CertifiedPublicKey> chain = certificateChainBuilder.build(certificate, certificateProvider); if (chain != null) { certs.addAll(chain); } } /** * Verify a CMS signature with embedded content and containing all the certificate required for validation. * * @param signature the CMS signature to verify. The signature should have the signed content embedded as well as * all the certificates for the signers. * @return result of the verification. * @throws GeneralSecurityException on error. */ public CMSSignedDataVerified cmsVerify(byte[] signature) throws GeneralSecurityException { return cmsSignedDataVerifier.verify(signature); } /** * Verify a CMS signature without embedded content but containing all the certificate required for validation. * * @param signature the CMS signature to verify. * @param data the content to verify the signature against, or null of the content is embedded in the signature. * @return a the result of the verification. * @throws GeneralSecurityException on error. */ public CMSSignedDataVerified cmsVerify(byte[] signature, byte[] data) throws GeneralSecurityException { return cmsSignedDataVerifier.verify(signature, data); } /** * Verify a CMS signature with embedded content, but requiring external certificates to be validated. * * @param signature the CMS signature to verify. * @param certificateProvider Optionally, a certificate provider for obtaining the chain of certificate for * verifying the signatures. If null, certificat should all be embedded in the signature. * @return a the result of the verification. * @throws GeneralSecurityException on error. */ public CMSSignedDataVerified cmsVerify(byte[] signature, CertificateProvider certificateProvider) throws GeneralSecurityException { return cmsSignedDataVerifier.verify(signature, certificateProvider); } /** * Verify a CMS signature without embedded content, and requiring external certificates to be validated. * * @param signature the CMS signature to verify. * @param data the content to verify the signature against, or null of the content is embedded in the signature. * @param certificateProvider Optionally, a certificate provider for obtaining the chain of certificate for * verifying the signatures. If null, certificat should all be embedded in the signature. * @return a the result of the verification. * @throws GeneralSecurityException on error. */ public CMSSignedDataVerified cmsVerify(byte[] signature, byte[] data, CertificateProvider certificateProvider) throws GeneralSecurityException { return cmsSignedDataVerifier.verify(signature, data, certificateProvider); } /** * Check that an X509 certificate chain is complete and valid now. * * @param chain the ordered chain of certificate starting from root CA. * @return true if the chain is a X509 certificate chain complete and valid on the given date. */ public boolean checkX509CertificateChainValidity(Collection<CertifiedPublicKey> chain) { return checkX509CertificateChainValidity(chain, null); } /** * Check that an X509 certificate chain is complete and is valid on a given date. * * @param chain the ordered chain of certificate starting from root CA. * @param date the date to check the validity for, or null to check for now. * @return true if the chain is a X509 certificate chain complete and valid on the given date. */ public boolean checkX509CertificateChainValidity(Collection<CertifiedPublicKey> chain, Date date) { if (chain == null || chain.isEmpty()) { return false; } Date checkDate = (date != null) ? date : new Date(); boolean rootExpected = true; for (CertifiedPublicKey cert : chain) { if (!(cert instanceof X509CertifiedPublicKey)) { return false; } if (rootExpected) { if (!((X509CertifiedPublicKey) cert).isRootCA()) { return false; } rootExpected = false; } if (!((X509CertifiedPublicKey) cert).isValidOn(checkDate)) { return false; } } return true; } }