| /* ==================================================================== |
| 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.StandardCharsets; |
| import java.security.MessageDigest; |
| import java.security.SecureRandom; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Base64; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.message.SimpleMessage; |
| 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.poifs.crypt.dsig.SignatureInfo; |
| import org.apache.poi.util.HexDump; |
| import org.apache.poi.util.IOUtils; |
| 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; |
| |
| import static org.apache.logging.log4j.util.Unbox.box; |
| |
| /** |
| * A TSP time-stamp service implementation. |
| * |
| * @author Frank Cornelis |
| * |
| */ |
| public class TSPTimeStampService implements TimeStampService { |
| |
| private static final Logger LOG = LogManager.getLogger(TSPTimeStampService.class); |
| |
| /** |
| * 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","squid:S2647"}) |
| public byte[] timeStamp(SignatureInfo signatureInfo, byte[] data, RevocationData revocationData) throws Exception { |
| SignatureConfig signatureConfig = signatureInfo.getSignatureConfig(); |
| |
| // 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))); |
| } |
| |
| ByteArrayOutputStream bos; |
| String contentType; |
| HttpURLConnection huc = (HttpURLConnection)new URL(signatureConfig.getTspUrl()).openConnection(proxy); |
| try { |
| if (signatureConfig.getTspUser() != null) { |
| String userPassword = signatureConfig.getTspUser() + ":" + signatureConfig.getTspPass(); |
| String encoding = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.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) { |
| final String message = "Error contacting TSP server " + signatureConfig.getTspUrl() + |
| ", had status code " + statusCode + "/" + huc.getResponseMessage(); |
| LOG.atError().log(message); |
| throw new IOException(message); |
| } |
| |
| // HTTP input validation |
| contentType = huc.getHeaderField("Content-Type"); |
| if (null == contentType) { |
| throw new RuntimeException("missing Content-Type header"); |
| } |
| |
| bos = new ByteArrayOutputStream(); |
| IOUtils.copy(huc.getInputStream(), bos); |
| LOG.atDebug().log(() -> new SimpleMessage("response content: " + HexDump.dump(bos.toByteArray(), 0, 0))); |
| } finally { |
| huc.disconnect(); |
| } |
| |
| 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.atDebug().log("status: {}", box(timeStampResponse.getStatus())); |
| LOG.atDebug().log("status string: {}", timeStampResponse.getStatusString()); |
| PKIFailureInfo failInfo = timeStampResponse.getFailInfo(); |
| if (null != failInfo) { |
| LOG.atDebug().log("fail info int value: {}", box(failInfo.intValue())); |
| if (/*PKIFailureInfo.unacceptedPolicy*/(1 << 8) == failInfo.intValue()) { |
| LOG.atDebug().log("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.atDebug().log("signer cert serial number: {}", signerCertSerialNumber); |
| LOG.atDebug().log("signer cert issuer: {}", signerCertIssuer); |
| |
| // TSP signer certificates retrieval |
| Collection<X509CertificateHolder> certificates = timeStampToken.getCertificates().getMatches(null); |
| |
| X509CertificateHolder signerCert = null; |
| Map<X500Name, X509CertificateHolder> certificateMap = new HashMap<>(); |
| 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<>(); |
| JcaX509CertificateConverter x509converter = new JcaX509CertificateConverter(); |
| x509converter.setProvider("BC"); |
| X509CertificateHolder certificate = signerCert; |
| do { |
| LOG.atDebug().log("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.atDebug().log("time-stamp token time: {}", timeStampToken.getTimeStampInfo().getGenTime()); |
| |
| return timeStampToken.getEncoded(); |
| } |
| } |