/*
* 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;
}
}