/* ==================================================================== | |
Licensed to the Apache Software Foundation (ASF) under one or more | |
contributor license agreements. See the NOTICE file distributed with | |
this work for additional information regarding copyright ownership. | |
The ASF licenses this file to You under the Apache License, Version 2.0 | |
(the "License"); you may not use this file except in compliance with | |
the License. You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
==================================================================== */ | |
/* ==================================================================== | |
This product contains an ASLv2 licensed version of the OOXML signer | |
package from the eID Applet project | |
http://code.google.com/p/eid-applet/source/browse/trunk/README.txt | |
Copyright (C) 2008-2014 FedICT. | |
================================================================= */ | |
package org.apache.poi.poifs.crypt.dsig.services; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
import java.math.BigInteger; | |
import java.net.HttpURLConnection; | |
import java.net.InetAddress; | |
import java.net.InetSocketAddress; | |
import java.net.Proxy; | |
import java.net.URL; | |
import java.nio.charset.Charset; | |
import java.security.MessageDigest; | |
import java.security.SecureRandom; | |
import java.security.cert.X509Certificate; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import javax.xml.bind.DatatypeConverter; | |
import org.apache.poi.poifs.crypt.CryptoFunctions; | |
import org.apache.poi.poifs.crypt.HashAlgorithm; | |
import org.apache.poi.poifs.crypt.dsig.SignatureConfig; | |
import org.apache.poi.util.HexDump; | |
import org.apache.poi.util.IOUtils; | |
import org.apache.poi.util.POILogFactory; | |
import org.apache.poi.util.POILogger; | |
import org.bouncycastle.asn1.ASN1ObjectIdentifier; | |
import org.bouncycastle.asn1.cmp.PKIFailureInfo; | |
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; | |
import org.bouncycastle.asn1.x500.X500Name; | |
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; | |
import org.bouncycastle.cert.X509CertificateHolder; | |
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; | |
import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator; | |
import org.bouncycastle.cms.SignerId; | |
import org.bouncycastle.cms.SignerInformationVerifier; | |
import org.bouncycastle.cms.bc.BcRSASignerInfoVerifierBuilder; | |
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; | |
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; | |
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; | |
import org.bouncycastle.tsp.TimeStampRequest; | |
import org.bouncycastle.tsp.TimeStampRequestGenerator; | |
import org.bouncycastle.tsp.TimeStampResponse; | |
import org.bouncycastle.tsp.TimeStampToken; | |
/** | |
* A TSP time-stamp service implementation. | |
* | |
* @author Frank Cornelis | |
* | |
*/ | |
public class TSPTimeStampService implements TimeStampService { | |
private static final POILogger LOG = POILogFactory.getLogger(TSPTimeStampService.class); | |
private SignatureConfig signatureConfig; | |
/** | |
* Maps the digest algorithm to corresponding OID value. | |
*/ | |
public ASN1ObjectIdentifier mapDigestAlgoToOID(HashAlgorithm digestAlgo) { | |
switch (digestAlgo) { | |
case sha1: return X509ObjectIdentifiers.id_SHA1; | |
case sha256: return NISTObjectIdentifiers.id_sha256; | |
case sha384: return NISTObjectIdentifiers.id_sha384; | |
case sha512: return NISTObjectIdentifiers.id_sha512; | |
default: | |
throw new IllegalArgumentException("unsupported digest algo: " + digestAlgo); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
public byte[] timeStamp(byte[] data, RevocationData revocationData) | |
throws Exception { | |
// digest the message | |
MessageDigest messageDigest = CryptoFunctions.getMessageDigest(signatureConfig.getTspDigestAlgo()); | |
byte[] digest = messageDigest.digest(data); | |
// generate the TSP request | |
BigInteger nonce = new BigInteger(128, new SecureRandom()); | |
TimeStampRequestGenerator requestGenerator = new TimeStampRequestGenerator(); | |
requestGenerator.setCertReq(true); | |
String requestPolicy = signatureConfig.getTspRequestPolicy(); | |
if (requestPolicy != null) { | |
requestGenerator.setReqPolicy(new ASN1ObjectIdentifier(requestPolicy)); | |
} | |
ASN1ObjectIdentifier digestAlgoOid = mapDigestAlgoToOID(signatureConfig.getTspDigestAlgo()); | |
TimeStampRequest request = requestGenerator.generate(digestAlgoOid, digest, nonce); | |
byte[] encodedRequest = request.getEncoded(); | |
// create the HTTP POST request | |
Proxy proxy = Proxy.NO_PROXY; | |
if (signatureConfig.getProxyUrl() != null) { | |
URL proxyUrl = new URL(signatureConfig.getProxyUrl()); | |
String host = proxyUrl.getHost(); | |
int port = proxyUrl.getPort(); | |
proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByName(host), (port == -1 ? 80 : port))); | |
} | |
HttpURLConnection huc = (HttpURLConnection)new URL(signatureConfig.getTspUrl()).openConnection(proxy); | |
if (signatureConfig.getTspUser() != null) { | |
String userPassword = signatureConfig.getTspUser() + ":" + signatureConfig.getTspPass(); | |
String encoding = DatatypeConverter.printBase64Binary(userPassword.getBytes(Charset.forName("iso-8859-1"))); | |
huc.setRequestProperty("Authorization", "Basic " + encoding); | |
} | |
huc.setRequestMethod("POST"); | |
huc.setConnectTimeout(20000); | |
huc.setReadTimeout(20000); | |
huc.setDoOutput(true); // also sets method to POST. | |
huc.setRequestProperty("User-Agent", signatureConfig.getUserAgent()); | |
huc.setRequestProperty("Content-Type", signatureConfig.isTspOldProtocol() | |
? "application/timestamp-request" | |
: "application/timestamp-query"); // "; charset=ISO-8859-1"); | |
OutputStream hucOut = huc.getOutputStream(); | |
hucOut.write(encodedRequest); | |
// invoke TSP service | |
huc.connect(); | |
int statusCode = huc.getResponseCode(); | |
if (statusCode != 200) { | |
LOG.log(POILogger.ERROR, "Error contacting TSP server ", signatureConfig.getTspUrl() + | |
", had status code " + statusCode + "/" + huc.getResponseMessage()); | |
throw new IOException("Error contacting TSP server " + signatureConfig.getTspUrl() + | |
", had status code " + statusCode + "/" + huc.getResponseMessage()); | |
} | |
// HTTP input validation | |
String contentType = huc.getHeaderField("Content-Type"); | |
if (null == contentType) { | |
throw new RuntimeException("missing Content-Type header"); | |
} | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
IOUtils.copy(huc.getInputStream(), bos); | |
LOG.log(POILogger.DEBUG, "response content: ", HexDump.dump(bos.toByteArray(), 0, 0)); | |
if (!contentType.startsWith(signatureConfig.isTspOldProtocol() | |
? "application/timestamp-response" | |
: "application/timestamp-reply" | |
)) { | |
throw new RuntimeException("invalid Content-Type: " + contentType + | |
// dump the first few bytes | |
": " + HexDump.dump(bos.toByteArray(), 0, 0, 200)); | |
} | |
if (bos.size() == 0) { | |
throw new RuntimeException("Content-Length is zero"); | |
} | |
// TSP response parsing and validation | |
TimeStampResponse timeStampResponse = new TimeStampResponse(bos.toByteArray()); | |
timeStampResponse.validate(request); | |
if (0 != timeStampResponse.getStatus()) { | |
LOG.log(POILogger.DEBUG, "status: " + timeStampResponse.getStatus()); | |
LOG.log(POILogger.DEBUG, "status string: " + timeStampResponse.getStatusString()); | |
PKIFailureInfo failInfo = timeStampResponse.getFailInfo(); | |
if (null != failInfo) { | |
LOG.log(POILogger.DEBUG, "fail info int value: " + failInfo.intValue()); | |
if (/*PKIFailureInfo.unacceptedPolicy*/(1 << 8) == failInfo.intValue()) { | |
LOG.log(POILogger.DEBUG, "unaccepted policy"); | |
} | |
} | |
throw new RuntimeException("timestamp response status != 0: " | |
+ timeStampResponse.getStatus()); | |
} | |
TimeStampToken timeStampToken = timeStampResponse.getTimeStampToken(); | |
SignerId signerId = timeStampToken.getSID(); | |
BigInteger signerCertSerialNumber = signerId.getSerialNumber(); | |
X500Name signerCertIssuer = signerId.getIssuer(); | |
LOG.log(POILogger.DEBUG, "signer cert serial number: " + signerCertSerialNumber); | |
LOG.log(POILogger.DEBUG, "signer cert issuer: " + signerCertIssuer); | |
// TSP signer certificates retrieval | |
Collection<X509CertificateHolder> certificates = timeStampToken.getCertificates().getMatches(null); | |
X509CertificateHolder signerCert = null; | |
Map<X500Name, X509CertificateHolder> certificateMap = new HashMap<X500Name, X509CertificateHolder>(); | |
for (X509CertificateHolder certificate : certificates) { | |
if (signerCertIssuer.equals(certificate.getIssuer()) | |
&& signerCertSerialNumber.equals(certificate.getSerialNumber())) { | |
signerCert = certificate; | |
} | |
certificateMap.put(certificate.getSubject(), certificate); | |
} | |
// TSP signer cert path building | |
if (signerCert == null) { | |
throw new RuntimeException("TSP response token has no signer certificate"); | |
} | |
List<X509Certificate> tspCertificateChain = new ArrayList<X509Certificate>(); | |
JcaX509CertificateConverter x509converter = new JcaX509CertificateConverter(); | |
x509converter.setProvider("BC"); | |
X509CertificateHolder certificate = signerCert; | |
do { | |
LOG.log(POILogger.DEBUG, "adding to certificate chain: " + certificate.getSubject()); | |
tspCertificateChain.add(x509converter.getCertificate(certificate)); | |
if (certificate.getSubject().equals(certificate.getIssuer())) { | |
break; | |
} | |
certificate = certificateMap.get(certificate.getIssuer()); | |
} while (null != certificate); | |
// verify TSP signer signature | |
X509CertificateHolder holder = new X509CertificateHolder(tspCertificateChain.get(0).getEncoded()); | |
DefaultCMSSignatureAlgorithmNameGenerator nameGen = new DefaultCMSSignatureAlgorithmNameGenerator(); | |
DefaultSignatureAlgorithmIdentifierFinder sigAlgoFinder = new DefaultSignatureAlgorithmIdentifierFinder(); | |
DefaultDigestAlgorithmIdentifierFinder hashAlgoFinder = new DefaultDigestAlgorithmIdentifierFinder(); | |
BcDigestCalculatorProvider calculator = new BcDigestCalculatorProvider(); | |
BcRSASignerInfoVerifierBuilder verifierBuilder = new BcRSASignerInfoVerifierBuilder(nameGen, sigAlgoFinder, hashAlgoFinder, calculator); | |
SignerInformationVerifier verifier = verifierBuilder.build(holder); | |
timeStampToken.validate(verifier); | |
// verify TSP signer certificate | |
if (signatureConfig.getTspValidator() != null) { | |
signatureConfig.getTspValidator().validate(tspCertificateChain, revocationData); | |
} | |
LOG.log(POILogger.DEBUG, "time-stamp token time: " | |
+ timeStampToken.getTimeStampInfo().getGenTime()); | |
return timeStampToken.getEncoded(); | |
} | |
public void setSignatureConfig(SignatureConfig signatureConfig) { | |
this.signatureConfig = signatureConfig; | |
} | |
} |