Java tutorial
/************************************************************************* * * * SignServer: The OpenSource Automated Signing Server * * * * This software 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 any later version. * * * * See terms of license at gnu.org. * * * *************************************************************************/ package org.signserver.module.tsa; import java.io.IOException; import java.math.BigInteger; import java.security.*; import java.security.cert.*; import java.security.cert.Certificate; import java.util.*; import javax.persistence.EntityManager; import org.apache.log4j.Logger; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1TaggedObject; import org.bouncycastle.asn1.DERInteger; import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.cms.CMSAttributes; import org.bouncycastle.asn1.cms.ContentInfo; import org.bouncycastle.asn1.cms.Time; import org.bouncycastle.asn1.ess.ESSCertID; import org.bouncycastle.asn1.ess.SigningCertificate; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Attribute; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.IssuerSerial; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSTypedData; import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator; import org.bouncycastle.cms.SignerInfoGenerator; import org.bouncycastle.cms.SignerInfoGeneratorBuilder; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.ejbca.util.Base64; import org.signserver.common.*; import org.signserver.module.tsa.bc.MSAuthCodeCMSUtils; import org.signserver.module.tsa.bc.TimeStampRequest; import org.signserver.module.tsa.bc.TimeStampResponseGenerator; import org.signserver.server.ITimeSource; import org.signserver.server.WorkerContext; import org.signserver.server.archive.Archivable; import org.signserver.server.archive.DefaultArchivable; import org.signserver.server.cryptotokens.ICryptoInstance; import org.signserver.server.cryptotokens.ICryptoToken; import org.signserver.server.log.LogMap; import org.signserver.server.signers.BaseSigner; /** * A Signer signing Time-stamp request compatible with Microsoft Authenticode * * Implements a ISigner and have the following properties: * * <table border="1"> * <tr> * <td>TIMESOURCE</td> * <td> * property containing the classpath to the ITimeSource implementation * that should be used. (default LocalComputerTimeSource) * </td> * </tr> * * </table> * * Specifying a signer certificate (normally the SIGNERCERT property) is required * as information from that certificate will be used to indicate which signer * signed the time-stamp token. * * The SIGNERCERTCHAIN property contains all certificates included in the token * if the client requests the certificates. The RFC specified that the signer * certificate MUST be included in the list returned. * * * @author Marcus Lundblad * @version $Id: MSAuthCodeTimeStampSigner.java 5977 2015-03-27 10:30:50Z netmackan $ */ public class MSAuthCodeTimeStampSigner extends BaseSigner { /** Log4j instance for actual implementation class. */ private static final Logger LOG = Logger.getLogger(MSAuthCodeTimeStampSigner.class); /** Random generator algorithm. */ private static String algorithm = "SHA1PRNG"; /** Random generator. */ private transient SecureRandom random; private static final BigInteger LOWEST = new BigInteger("0080000000000000", 16); private static final BigInteger HIGHEST = new BigInteger("7FFFFFFFFFFFFFFF", 16); //Private Property constants public static final String TIMESOURCE = "TIMESOURCE"; public static final String SIGNATUREALGORITHM = "SIGNATUREALGORITHM"; public static final String ACCEPTEDALGORITHMS = "ACCEPTEDALGORITHMS"; public static final String ACCEPTEDPOLICIES = "ACCEPTEDPOLICIES"; public static final String ACCEPTEDEXTENSIONS = "ACCEPTEDEXTENSIONS"; //public static final String DEFAULTDIGESTOID = "DEFAULTDIGESTOID"; public static final String DEFAULTTSAPOLICYOID = "DEFAULTTSAPOLICYOID"; public static final String ACCURACYMICROS = "ACCURACYMICROS"; public static final String ACCURACYMILLIS = "ACCURACYMILLIS"; public static final String ACCURACYSECONDS = "ACCURACYSECONDS"; public static final String ORDERING = "ORDERING"; public static final String TSA = "TSA"; public static final String REQUIREVALIDCHAIN = "REQUIREVALIDCHAIN"; public static final String INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE = "INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE"; private static final String dataOID = "1.2.840.113549.1.7.1"; private static final String msOID = "1.3.6.1.4.1.311.3.2.1"; private static final String DEFAULT_WORKERLOGGER = DefaultTimeStampLogger.class.getName(); private static final String DEFAULT_TIMESOURCE = "org.signserver.server.LocalComputerTimeSource"; private static final String DEFAULT_SIGNATUREALGORITHM = "SHA1withRSA"; /** MIME type for the request data. **/ private static final String REQUEST_CONTENT_TYPE = "application/octect-stream"; /** MIME type for the response data. **/ private static final String RESPONSE_CONTENT_TYPE = "application/octet-stream"; private ITimeSource timeSource = null; private String signatureAlgo; private boolean validChain = true; private boolean includeSigningCertificateAttribute; private List<String> configErrors; @Override public void init(final int signerId, final WorkerConfig config, final WorkerContext workerContext, final EntityManager workerEntityManager) { super.init(signerId, config, workerContext, workerEntityManager); // Overrides the default worker logger to be this worker // implementation's default instead of the WorkerSessionBean's if (config.getProperty("WORKERLOGGER") == null) { config.setProperty("WORKERLOGGER", DEFAULT_WORKERLOGGER); } // Check that the timestamp server is properly configured try { timeSource = getTimeSource(); if (LOG.isDebugEnabled()) { LOG.debug("TimeStampSigner[" + signerId + "]: " + "Using TimeSource: " + timeSource.getClass().getName()); } signatureAlgo = config.getProperty(SIGNATUREALGORITHM); if (signatureAlgo == null) { signatureAlgo = DEFAULT_SIGNATUREALGORITHM; } } catch (SignServerException e) { LOG.error("Could not create time source: " + e.getMessage()); } if (LOG.isDebugEnabled()) { LOG.debug("bctsp version: " + TimeStampResponseGenerator.class.getPackage().getImplementationVersion() + ", " + TimeStampRequest.class.getPackage().getImplementationVersion()); } // Validate certificates in signer certificate chain final String requireValidChain = config.getProperty(REQUIREVALIDCHAIN, Boolean.FALSE.toString()); if (Boolean.parseBoolean(requireValidChain)) { validChain = validateChain(); } includeSigningCertificateAttribute = Boolean .parseBoolean(config.getProperty(INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE, "false")); configErrors = new LinkedList<String>(); if (hasSetIncludeCertificateLevels) { configErrors.add(WorkerConfig.PROPERTY_INCLUDE_CERTIFICATE_LEVELS + " is not supported."); } } /** * The main method performing the actual timestamp operation. * Expects the signRequest to be a GenericSignRequest contining a * TimeStampRequest * * @param signRequest * @param requestContext * @return the sign response * @see org.signserver.server.IProcessable#processData(org.signserver.common.ProcessRequest, org.signserver.common.RequestContext) */ public ProcessResponse processData(final ProcessRequest signRequest, final RequestContext requestContext) throws IllegalRequestException, CryptoTokenOfflineException, SignServerException { // Log values final LogMap logMap = LogMap.getInstance(requestContext); try { final ISignRequest sReq = (ISignRequest) signRequest; final byte[] requestbytes = (byte[]) sReq.getRequestData(); if (requestbytes == null || requestbytes.length == 0) { LOG.error("Request must contain data"); throw new IllegalRequestException("Request must contain data"); } // Check that the request contains a valid TimeStampRequest object. if (!(signRequest instanceof GenericSignRequest)) { final IllegalRequestException exception = new IllegalRequestException( "Recieved request wasn't an expected GenericSignRequest. "); LOG.error("Received request wasn't an expected GenericSignRequest"); throw exception; } if (!((sReq.getRequestData() instanceof TimeStampRequest) || (sReq.getRequestData() instanceof byte[]))) { final IllegalRequestException exception = new IllegalRequestException( "Recieved request data wasn't an expected TimeStampRequest. "); LOG.error("Received request data wasn't an expected TimeStampRequest"); throw exception; } if (!validChain) { LOG.error("Certificate chain not correctly configured"); throw new CryptoTokenOfflineException("Certificate chain not correctly configured"); } ASN1Primitive asn1obj = ASN1Primitive.fromByteArray(Base64.decode(requestbytes)); ASN1Sequence asn1seq = ASN1Sequence.getInstance(asn1obj); if (asn1seq.size() != 2) { LOG.error("Wrong structure, should be an ASN1Sequence with 2 elements"); throw new IllegalRequestException("Wrong structure, should be an ASN1Sequence with 2 elements"); } ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(asn1seq.getObjectAt(0)); ASN1Sequence asn1seq1 = ASN1Sequence.getInstance(asn1seq.getObjectAt(1)); final ContentInfo ci = new ContentInfo(asn1seq1); if (!oid.getId().equals(msOID)) { LOG.error("Invalid OID in request: " + oid.getId()); throw new IllegalRequestException("Invalid OID in request: " + oid.getId()); } if (asn1seq1.size() != 2) { LOG.error( "Wrong structure, should be an ASN1Sequence with 2 elements as the value of element 0 in the outer ASN1Sequence"); throw new IllegalRequestException( "Wrong structure, should be an ASN1Sequence with 2 elements as the value of element 0 in the outer ASN1Sequence"); } oid = ASN1ObjectIdentifier.getInstance(asn1seq1.getObjectAt(0)); if (!oid.getId().equals(dataOID)) { throw new IllegalRequestException("Wrong contentType OID: " + oid.getId()); } ASN1TaggedObject tag = ASN1TaggedObject.getInstance(asn1seq1.getObjectAt(1)); if (tag.getTagNo() != 0) { throw new IllegalRequestException("Wrong tag no (should be 0): " + tag.getTagNo()); } ASN1OctetString octets = ASN1OctetString.getInstance(tag.getObject()); byte[] content = octets.getOctets(); final ITimeSource timeSrc; final Date date; byte[] der; ICryptoInstance crypto = null; try { crypto = acquireCryptoInstance(ICryptoToken.PURPOSE_SIGN, signRequest, requestContext); // get signing cert certificate chain and private key List<Certificate> certList = this.getSigningCertificateChain(crypto); if (certList == null) { throw new SignServerException("Null certificate chain. This signer needs a certificate."); } Certificate[] certs = (Certificate[]) certList.toArray(new Certificate[certList.size()]); // Sign X509Certificate x509cert = (X509Certificate) certs[0]; timeSrc = getTimeSource(); if (LOG.isDebugEnabled()) { LOG.debug("TimeSource: " + timeSrc.getClass().getName()); } date = timeSrc.getGenTime(); if (date == null) { throw new ServiceUnavailableException("Time source is not available"); } ASN1EncodableVector signedAttributes = new ASN1EncodableVector(); signedAttributes.add(new Attribute(CMSAttributes.signingTime, new DERSet(new Time(date)))); if (includeSigningCertificateAttribute) { try { final DERInteger serial = new DERInteger(x509cert.getSerialNumber()); final X509CertificateHolder certHolder = new X509CertificateHolder(x509cert.getEncoded()); final X500Name issuer = certHolder.getIssuer(); final GeneralName name = new GeneralName(issuer); final GeneralNames names = new GeneralNames(name); final IssuerSerial is = new IssuerSerial(names, ASN1Integer.getInstance(serial)); final ESSCertID essCertid = new ESSCertID( MessageDigest.getInstance("SHA-1").digest(x509cert.getEncoded()), is); signedAttributes.add(new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificate, new DERSet(new SigningCertificate(essCertid)))); } catch (NoSuchAlgorithmException e) { LOG.error("Can't find SHA-1 implementation: " + e.getMessage()); throw new SignServerException("Can't find SHA-1 implementation", e); } } AttributeTable signedAttributesTable = new AttributeTable(signedAttributes); DefaultSignedAttributeTableGenerator signedAttributeGenerator = new DefaultSignedAttributeTableGenerator( signedAttributesTable); final String provider = cryptoToken.getProvider(ICryptoToken.PROVIDERUSAGE_SIGN); SignerInfoGeneratorBuilder signerInfoBuilder = new SignerInfoGeneratorBuilder( new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()); signerInfoBuilder.setSignedAttributeGenerator(signedAttributeGenerator); JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(signatureAlgo); contentSigner.setProvider(provider); final SignerInfoGenerator sig = signerInfoBuilder.build(contentSigner.build(crypto.getPrivateKey()), new X509CertificateHolder(x509cert.getEncoded())); JcaCertStore cs = new JcaCertStore(certList); CMSTypedData cmspba = new CMSProcessableByteArray(content); CMSSignedData cmssd = MSAuthCodeCMSUtils.generate(cmspba, true, Arrays.asList(sig), MSAuthCodeCMSUtils.getCertificatesFromStore(cs), Collections.emptyList(), ci); der = ASN1Primitive.fromByteArray(cmssd.getEncoded()).getEncoded(); } finally { releaseCryptoInstance(crypto, requestContext); } // Log values logMap.put(ITimeStampLogger.LOG_TSA_TIME, String.valueOf(date.getTime())); logMap.put(ITimeStampLogger.LOG_TSA_TIMESOURCE, timeSrc.getClass().getSimpleName()); final String archiveId = createArchiveId(requestbytes, (String) requestContext.get(RequestContext.TRANSACTION_ID)); final GenericSignResponse signResponse; byte[] signedbytes = Base64.encode(der, false); logMap.put(ITimeStampLogger.LOG_TSA_TIMESTAMPRESPONSE_ENCODED, new String(signedbytes)); final Collection<? extends Archivable> archivables = Arrays.asList( new DefaultArchivable(Archivable.TYPE_REQUEST, REQUEST_CONTENT_TYPE, requestbytes, archiveId), new DefaultArchivable(Archivable.TYPE_RESPONSE, RESPONSE_CONTENT_TYPE, signedbytes, archiveId)); if (signRequest instanceof GenericServletRequest) { signResponse = new GenericServletResponse(sReq.getRequestID(), signedbytes, getSigningCertificate(signRequest, requestContext), archiveId, archivables, RESPONSE_CONTENT_TYPE); } else { signResponse = new GenericSignResponse(sReq.getRequestID(), signedbytes, getSigningCertificate(signRequest, requestContext), archiveId, archivables); } // The client can be charged for the request requestContext.setRequestFulfilledByWorker(true); return signResponse; } catch (IOException e) { final IllegalRequestException exception = new IllegalRequestException("IOException: " + e.getMessage(), e); LOG.error("IOException: ", e); logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage()); throw exception; } catch (CMSException e) { final SignServerException exception = new SignServerException(e.getMessage(), e); LOG.error("CMSException: ", e); logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage()); throw exception; } catch (OperatorCreationException e) { final SignServerException exception = new SignServerException(e.getMessage(), e); LOG.error("OperatorCreationException: ", e); logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage()); throw exception; } catch (CertificateEncodingException e) { final SignServerException exception = new SignServerException(e.getMessage(), e); LOG.error("CertificateEncodingException: ", e); logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage()); throw exception; } catch (ArrayIndexOutOfBoundsException e) { // the BC base64 decoder doesn't check the the base64 input length... final IllegalRequestException exception = new IllegalRequestException( "ArrayIndexOutOfBoundsException: " + e.getMessage(), e); LOG.error("ArrayIndexOutOfBoundsException: ", e); logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage()); throw exception; } } /** * @return a time source interface expected to provide accurate time */ private ITimeSource getTimeSource() throws SignServerException { if (timeSource == null) { try { String classpath = this.config.getProperties().getProperty(TIMESOURCE); if (classpath == null) { classpath = DEFAULT_TIMESOURCE; } final Class<?> implClass = Class.forName(classpath); final Object obj = implClass.newInstance(); timeSource = (ITimeSource) obj; timeSource.init(config.getProperties()); } catch (ClassNotFoundException e) { throw new SignServerException("Class not found", e); } catch (IllegalAccessException iae) { throw new SignServerException("Illegal access", iae); } catch (InstantiationException ie) { throw new SignServerException("Instantiation error", ie); } } return timeSource; } /** Generates a number of serial number bytes. The number returned should * be a positive number. * * @return a BigInteger with a new random serial number. */ public BigInteger getSerno() { if (random == null) { try { random = SecureRandom.getInstance(algorithm); } catch (NoSuchAlgorithmException e) { LOG.error(e); } } final byte[] sernobytes = new byte[8]; boolean ok = false; BigInteger serno = null; while (!ok) { random.nextBytes(sernobytes); serno = new BigInteger(sernobytes).abs(); // Must be within the range 0080000000000000 - 7FFFFFFFFFFFFFFF if ((serno.compareTo(LOWEST) >= 0) && (serno.compareTo(HIGHEST) <= 0)) { ok = true; } } return serno; } /** * @return True if each certificate in the certificate chain can be verified * by the next certificate (if any). This does not check that the last * certificate is a trusted certificate as the root certificate is normally * not included. */ private boolean validateChain() { boolean result = true; try { final List<Certificate> signingCertificateChain = getSigningCertificateChain(); if (signingCertificateChain != null) { List<Certificate> chain = (List<Certificate>) signingCertificateChain; for (int i = 0; i < chain.size(); i++) { Certificate subject = chain.get(i); // If we have the issuer we can validate the certificate if (chain.size() > i + 1) { Certificate issuer = chain.get(i + 1); try { subject.verify(issuer.getPublicKey(), "BC"); } catch (CertificateException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject); } result = false; } catch (NoSuchAlgorithmException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject); } result = false; } catch (InvalidKeyException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject); } result = false; } catch (NoSuchProviderException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject); } result = false; } catch (SignatureException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject); } result = false; } } } } else { // This would be a bug LOG.error("Certificate chain was not an list!"); result = false; } } catch (CryptoTokenOfflineException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Unable to get signer certificate or chain: " + ex.getMessage()); } result = false; } return result; } @Override protected List<String> getFatalErrors() { final List<String> result = new LinkedList<String>(); result.addAll(super.getFatalErrors()); result.addAll(configErrors); try { // Check signer certificate chain if required if (!validChain) { result.add("Not strictly valid chain and " + REQUIREVALIDCHAIN + " specified"); if (LOG.isDebugEnabled()) { LOG.debug("Signer " + workerId + ": " + REQUIREVALIDCHAIN + " specified but the chain was not found valid"); } } // Check if certificat has the required EKU final Certificate certificate = getSigningCertificate(); try { if (certificate instanceof X509Certificate) { final X509Certificate cert = (X509Certificate) certificate; final List<String> ekus = cert.getExtendedKeyUsage(); if (ekus == null || !ekus.contains(KeyPurposeId.id_kp_timeStamping.getId())) { result.add("Missing extended key usage timeStamping"); } if (cert.getCriticalExtensionOIDs() == null || !cert.getCriticalExtensionOIDs() .contains(org.bouncycastle.asn1.x509.X509Extension.extendedKeyUsage.getId())) { result.add("The extended key usage extension must be present and marked as critical"); } // if extended key usage contains timeStamping and also other // usages if (ekus != null && ekus.contains(KeyPurposeId.id_kp_timeStamping.getId()) && ekus.size() > 1) { result.add("No other extended key usages than timeStamping is allowed"); } } else { result.add("Unsupported certificate type"); } } catch (CertificateParsingException ex) { result.add("Unable to parse certificate"); if (LOG.isDebugEnabled()) { LOG.debug("Signer " + workerId + ": Unable to parse certificate: " + ex.getMessage()); } } } catch (CryptoTokenOfflineException ex) { result.add("No signer certificate available"); if (LOG.isDebugEnabled()) { LOG.debug("Signer " + workerId + ": Could not get signer certificate: " + ex.getMessage()); } } // check time source if (timeSource.getGenTime() == null) { result.add("Time source not available"); if (LOG.isDebugEnabled()) { LOG.debug("Signer " + workerId + ": time source not available"); } } return result; } }