/*
* eID Applet Project.
* Copyright (C) 2009 FedICT.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* 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, see
* http://www.gnu.org/licenses/.
*/
package test.unit.be.fedict.eid.applet.service.signer;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.InvalidParameterException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.crypto.Cipher;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1StreamParser;
import org.bouncycastle.asn1.util.ASN1Dump;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.SignerId;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.joda.time.DateTime;
import org.junit.BeforeClass;
import org.junit.Test;
public class CMSTest {
private static final Log LOG = LogFactory.getLog(CMSTest.class);
@BeforeClass
public static void beforeClass() {
Security.addProvider(new BouncyCastleProvider());
}
@Test
public void testPkcs1Signature() throws Exception {
// setup
KeyPair keyPair = PkiTestUtils.generateKeyPair();
byte[] toBeSigned = "hello world".getBytes();
// operate
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(keyPair.getPrivate());
signature.update(toBeSigned);
byte[] signatureValue = signature.sign();
// verify
signature.initVerify(keyPair.getPublic());
signature.update(toBeSigned);
boolean signatureResult = signature.verify(signatureValue);
assertTrue(signatureResult);
}
/**
* CMS signature with external data and external certificate. The CMS only
* contains the signature and some certificate selector.
*
* @throws Exception
*/
@Test
public void testBasicCmsSignature() throws Exception {
// setup
KeyPair keyPair = PkiTestUtils.generateKeyPair();
DateTime notBefore = new DateTime();
DateTime notAfter = notBefore.plusMonths(1);
X509Certificate certificate = generateSelfSignedCertificate(keyPair, "CN=Test", notBefore, notAfter);
byte[] toBeSigned = "hello world".getBytes();
// operate
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
generator.addSigner(keyPair.getPrivate(), certificate, CMSSignedDataGenerator.DIGEST_SHA1);
CMSProcessable content = new CMSProcessableByteArray(toBeSigned);
CMSSignedData signedData = generator.generate(content, false, (String) null);
byte[] cmsSignature = signedData.getEncoded();
LOG.debug("CMS signature: " + ASN1Dump.dumpAsString(new ASN1StreamParser(cmsSignature).readObject()));
// verify
signedData = new CMSSignedData(content, cmsSignature);
SignerInformationStore signers = signedData.getSignerInfos();
Iterator<SignerInformation> iter = signers.getSigners().iterator();
while (iter.hasNext()) {
SignerInformation signer = iter.next();
SignerId signerId = signer.getSID();
LOG.debug("signer: " + signerId);
assertTrue(signerId.match(certificate));
assertTrue(signer.verify(keyPair.getPublic(), BouncyCastleProvider.PROVIDER_NAME));
}
LOG.debug("content type: " + signedData.getSignedContentTypeOID());
}
/**
* CMS signature with embedded data and external certificate. The CMS only
* contains the original content, signature and some certificate selector.
*
* @throws Exception
*/
@Test
public void testCmsSignatureWithContent() throws Exception {
// setup
KeyPair keyPair = PkiTestUtils.generateKeyPair();
DateTime notBefore = new DateTime();
DateTime notAfter = notBefore.plusMonths(1);
X509Certificate certificate = generateSelfSignedCertificate(keyPair, "CN=Test", notBefore, notAfter);
byte[] toBeSigned = "hello world".getBytes();
// operate
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
generator.addSigner(keyPair.getPrivate(), certificate, CMSSignedDataGenerator.DIGEST_SHA1);
CMSProcessable content = new CMSProcessableByteArray(toBeSigned);
CMSSignedData signedData = generator.generate(content, true, (String) null);
byte[] cmsSignature = signedData.getEncoded();
LOG.debug("CMS signature: " + ASN1Dump.dumpAsString(new ASN1StreamParser(cmsSignature).readObject()));
// verify
signedData = new CMSSignedData(cmsSignature);
SignerInformationStore signers = signedData.getSignerInfos();
Iterator<SignerInformation> iter = signers.getSigners().iterator();
while (iter.hasNext()) {
SignerInformation signer = iter.next();
SignerId signerId = signer.getSID();
LOG.debug("signer: " + signerId);
assertTrue(signerId.match(certificate));
assertTrue(signer.verify(keyPair.getPublic(), BouncyCastleProvider.PROVIDER_NAME));
}
byte[] data = (byte[]) signedData.getSignedContent().getContent();
assertArrayEquals(toBeSigned, data);
LOG.debug("content type: " + signedData.getSignedContentTypeOID());
}
/**
* CMS signature with external data and embedded certificate. The CMS only
* contains the signature, signing certificate and some certificate
* selector.
*
* @throws Exception
*/
@Test
public void testCmsSignatureWithCertificate() throws Exception {
// setup
KeyPair keyPair = PkiTestUtils.generateKeyPair();
DateTime notBefore = new DateTime();
DateTime notAfter = notBefore.plusMonths(1);
X509Certificate certificate = generateSelfSignedCertificate(keyPair, "CN=Test", notBefore, notAfter);
byte[] toBeSigned = "hello world".getBytes();
// operate
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
/*
* addSigner requires the certificate to be able to calculate the key
* selector.
*/
generator.addSigner(keyPair.getPrivate(), certificate, CMSSignedDataGenerator.DIGEST_SHA1);
List<X509Certificate> certList = new LinkedList<X509Certificate>();
certList.add(certificate);
CertStore certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList));
generator.addCertificatesAndCRLs(certStore);
CMSProcessable content = new CMSProcessableByteArray(toBeSigned);
CMSSignedData signedData = generator.generate(content, false, (String) null);
byte[] cmsSignature = signedData.getEncoded();
LOG.debug("CMS signature: " + ASN1Dump.dumpAsString(new ASN1StreamParser(cmsSignature).readObject()));
// verify
signedData = new CMSSignedData(content, cmsSignature);
certStore = signedData.getCertificatesAndCRLs("Collection", BouncyCastleProvider.PROVIDER_NAME);
SignerInformationStore signers = signedData.getSignerInfos();
Iterator<SignerInformation> iter = signers.getSigners().iterator();
while (iter.hasNext()) {
SignerInformation signer = iter.next();
SignerId signerId = signer.getSID();
LOG.debug("signer: " + signerId);
assertTrue(signerId.match(certificate));
assertTrue(signer.verify(keyPair.getPublic(), BouncyCastleProvider.PROVIDER_NAME));
X509Certificate storedCert = (X509Certificate) certStore.getCertificates(signerId).iterator().next();
assertEquals(certificate, storedCert);
}
LOG.debug("content type: " + signedData.getSignedContentTypeOID());
}
public static class SHA1WithRSASignature extends Signature {
private static final Log LOG = LogFactory.getLog(SHA1WithRSASignature.class);
private static final ThreadLocal<byte[]> digestValues = new ThreadLocal<byte[]>();
private static final ThreadLocal<byte[]> signatureValues = new ThreadLocal<byte[]>();
private final MessageDigest messageDigest;
public SHA1WithRSASignature() throws NoSuchAlgorithmException {
super("SHA1withRSA");
LOG.debug("constructor");
this.messageDigest = MessageDigest.getInstance("SHA1");
}
@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
throw new UnsupportedOperationException();
}
@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
LOG.debug("engineInitSign: " + privateKey.getAlgorithm());
}
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
throw new UnsupportedOperationException();
}
@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
throw new UnsupportedOperationException();
}
@Override
protected byte[] engineSign() throws SignatureException {
LOG.debug("engineSign");
byte[] signatureValue = SHA1WithRSASignature.signatureValues.get();
if (null != signatureValue) {
SHA1WithRSASignature.signatureValues.set(null);
return signatureValue;
}
return "dummy".getBytes();
}
public static void setSignatureValue(byte[] signatureValue) {
SHA1WithRSASignature.signatureValues.set(signatureValue);
}
@Override
protected void engineUpdate(byte b) throws SignatureException {
throw new UnsupportedOperationException();
}
@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
LOG.debug("engineUpdate(b,off,len): off=" + off + "; len=" + len);
this.messageDigest.update(b, off, len);
byte[] digestValue = this.messageDigest.digest();
SHA1WithRSASignature.digestValues.set(digestValue);
}
@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
throw new UnsupportedOperationException();
}
public static byte[] getDigestValue() {
return SHA1WithRSASignature.digestValues.get();
}
}
private static class CMSTestProvider extends Provider {
private static final long serialVersionUID = 1L;
public static final String NAME = "CMSTestProvider";
private CMSTestProvider() {
super(NAME, 1.0, "CMS Test Security Provider");
put("Signature.SHA1withRSA", SHA1WithRSASignature.class.getName());
}
}
@Test
public void testRetrieveCMSDigestValue() throws Exception {
// setup
KeyPair keyPair = PkiTestUtils.generateKeyPair();
DateTime notBefore = new DateTime();
DateTime notAfter = notBefore.plusMonths(1);
X509Certificate certificate = generateSelfSignedCertificate(keyPair, "CN=Test", notBefore, notAfter);
byte[] toBeSigned = "hello world".getBytes();
// operate
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
generator.addSigner(keyPair.getPrivate(), certificate, CMSSignedDataGenerator.DIGEST_SHA1);
CMSProcessable content = new CMSProcessableByteArray(toBeSigned);
CMSTestProvider provider = new CMSTestProvider();
generator.generate(content, false, provider);
byte[] digestValue = SHA1WithRSASignature.getDigestValue();
assertNotNull(digestValue);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestValue);
byte[] signatureValue = cipher.doFinal(digestInfoValue);
SHA1WithRSASignature.setSignatureValue(signatureValue);
generator = new CMSSignedDataGenerator();
generator.addSigner(keyPair.getPrivate(), certificate, CMSSignedDataGenerator.DIGEST_SHA1);
content = new CMSProcessableByteArray(toBeSigned);
provider = new CMSTestProvider();
CMSSignedData signedData = generator.generate(content, false, provider);
byte[] cmsSignature = signedData.getEncoded();
LOG.debug("CMS signature: " + ASN1Dump.dumpAsString(new ASN1StreamParser(cmsSignature).readObject()));
// verify
content = new CMSProcessableByteArray(toBeSigned);
signedData = new CMSSignedData(content, cmsSignature);
SignerInformationStore signers = signedData.getSignerInfos();
Iterator<SignerInformation> iter = signers.getSigners().iterator();
while (iter.hasNext()) {
SignerInformation signer = iter.next();
SignerId signerId = signer.getSID();
LOG.debug("signer: " + signerId);
assertTrue(signerId.match(certificate));
assertTrue(signer.verify(keyPair.getPublic(), BouncyCastleProvider.PROVIDER_NAME));
}
LOG.debug("content type: " + signedData.getSignedContentTypeOID());
}
private X509Certificate generateSelfSignedCertificate(KeyPair keyPair, String subjectDn, DateTime notBefore,
DateTime notAfter) throws IOException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException,
SignatureException, CertificateException {
PublicKey subjectPublicKey = keyPair.getPublic();
PrivateKey issuerPrivateKey = keyPair.getPrivate();
String signatureAlgorithm = "SHA1WithRSAEncryption";
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.reset();
certificateGenerator.setPublicKey(subjectPublicKey);
certificateGenerator.setSignatureAlgorithm(signatureAlgorithm);
certificateGenerator.setNotBefore(notBefore.toDate());
certificateGenerator.setNotAfter(notAfter.toDate());
X509Principal issuerDN = new X509Principal(subjectDn);
certificateGenerator.setIssuerDN(issuerDN);
certificateGenerator.setSubjectDN(new X509Principal(subjectDn));
certificateGenerator.setSerialNumber(new BigInteger(128, new SecureRandom()));
certificateGenerator.addExtension(X509Extensions.SubjectKeyIdentifier, false,
createSubjectKeyId(subjectPublicKey));
PublicKey issuerPublicKey;
issuerPublicKey = subjectPublicKey;
certificateGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
createAuthorityKeyId(issuerPublicKey));
certificateGenerator.addExtension(X509Extensions.BasicConstraints, false, new BasicConstraints(true));
X509Certificate certificate;
certificate = certificateGenerator.generate(issuerPrivateKey);
/*
* Next certificate factory trick is needed to make sure that the
* certificate delivered to the caller is provided by the default
* security provider instead of BouncyCastle. If we don't do this trick
* we might run into trouble when trying to use the CertPath validator.
*/
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
certificate = (X509Certificate) certificateFactory
.generateCertificate(new ByteArrayInputStream(certificate.getEncoded()));
return certificate;
}
private SubjectKeyIdentifier createSubjectKeyId(PublicKey publicKey) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(publicKey.getEncoded());
SubjectPublicKeyInfo info = new SubjectPublicKeyInfo((ASN1Sequence) new ASN1InputStream(bais).readObject());
return new SubjectKeyIdentifier(info);
}
private AuthorityKeyIdentifier createAuthorityKeyId(PublicKey publicKey) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(publicKey.getEncoded());
SubjectPublicKeyInfo info = new SubjectPublicKeyInfo((ASN1Sequence) new ASN1InputStream(bais).readObject());
return new AuthorityKeyIdentifier(info);
}
}