| /* |
| * 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. |
| */ |
| package org.apache.tomcat.buildutil; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipInputStream; |
| import java.util.zip.ZipOutputStream; |
| |
| import javax.xml.soap.MessageFactory; |
| import javax.xml.soap.SOAPBody; |
| import javax.xml.soap.SOAPConnection; |
| import javax.xml.soap.SOAPConnectionFactory; |
| import javax.xml.soap.SOAPConstants; |
| import javax.xml.soap.SOAPElement; |
| import javax.xml.soap.SOAPEnvelope; |
| import javax.xml.soap.SOAPException; |
| import javax.xml.soap.SOAPMessage; |
| import javax.xml.soap.SOAPPart; |
| |
| import org.apache.tomcat.util.codec.binary.Base64; |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.DirectoryScanner; |
| import org.apache.tools.ant.Task; |
| import org.apache.tools.ant.types.FileSet; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| /** |
| * Ant task that submits a file to the Symantec code-signing service. |
| */ |
| public class SignCode extends Task { |
| |
| private static final URL SIGNING_SERVICE_URL; |
| |
| private static final String NS = "cod"; |
| |
| private static final MessageFactory SOAP_MSG_FACTORY; |
| |
| static { |
| try { |
| SIGNING_SERVICE_URL = new URL( |
| "https://api-appsec-cws.ws.symantec.com/webtrust/SigningService"); |
| } catch (MalformedURLException e) { |
| throw new IllegalArgumentException(e); |
| } |
| try { |
| SOAP_MSG_FACTORY = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL); |
| } catch (SOAPException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| |
| private final List<FileSet> filesets = new ArrayList<>(); |
| private String userName; |
| private String password; |
| private String partnerCode; |
| private String keyStore; |
| private String keyStorePassword; |
| private String applicationName; |
| private String applicationVersion; |
| private String signingService; |
| private boolean debug; |
| |
| public void addFileset(FileSet fileset) { |
| filesets.add(fileset); |
| } |
| |
| |
| public void setUserName(String userName) { |
| this.userName = userName; |
| } |
| |
| |
| public void setPassword(String password) { |
| this.password = password; |
| } |
| |
| |
| public void setPartnerCode(String partnerCode) { |
| this.partnerCode = partnerCode; |
| } |
| |
| |
| public void setKeyStore(String keyStore) { |
| this.keyStore = keyStore; |
| } |
| |
| |
| public void setKeyStorePassword(String keyStorePassword) { |
| this.keyStorePassword = keyStorePassword; |
| } |
| |
| |
| public void setApplicationName(String applicationName) { |
| this.applicationName = applicationName; |
| } |
| |
| |
| public void setApplicationVersion(String applicationVersion) { |
| this.applicationVersion = applicationVersion; |
| } |
| |
| |
| public void setSigningService(String signingService) { |
| this.signingService = signingService; |
| } |
| |
| |
| public void setDebug(String debug) { |
| this.debug = Boolean.parseBoolean(debug); |
| } |
| |
| |
| @Override |
| public void execute() throws BuildException { |
| |
| List<File> filesToSign = new ArrayList<>(); |
| |
| // Process the filesets and populate the list of files that need to be |
| // signed. |
| for (FileSet fileset : filesets) { |
| DirectoryScanner ds = fileset.getDirectoryScanner(getProject()); |
| File basedir = ds.getBasedir(); |
| String[] files = ds.getIncludedFiles(); |
| if (files.length > 0) { |
| for (int i = 0; i < files.length; i++) { |
| File file = new File(basedir, files[i]); |
| filesToSign.add(file); |
| } |
| } |
| } |
| |
| // Set up the TLS client |
| System.setProperty("javax.net.ssl.keyStore", keyStore); |
| System.setProperty("javax.net.ssl.keyStorePassword", keyStorePassword); |
| |
| try { |
| String signingSetID = makeSigningRequest(filesToSign); |
| downloadSignedFiles(filesToSign, signingSetID); |
| } catch (SOAPException | IOException e) { |
| throw new BuildException(e); |
| } |
| } |
| |
| |
| private String makeSigningRequest(List<File> filesToSign) throws SOAPException, IOException { |
| log("Constructing the code signing request"); |
| |
| SOAPMessage message = SOAP_MSG_FACTORY.createMessage(); |
| SOAPBody body = populateEnvelope(message, NS); |
| |
| SOAPElement requestSigning = body.addChildElement("requestSigning", NS); |
| SOAPElement requestSigningRequest = |
| requestSigning.addChildElement("requestSigningRequest", NS); |
| |
| addCredentials(requestSigningRequest, this.userName, this.password, this.partnerCode); |
| |
| SOAPElement applicationName = |
| requestSigningRequest.addChildElement("applicationName", NS); |
| applicationName.addTextNode(this.applicationName); |
| |
| SOAPElement applicationVersion = |
| requestSigningRequest.addChildElement("applicationVersion", NS); |
| applicationVersion.addTextNode(this.applicationVersion); |
| |
| SOAPElement signingServiceName = |
| requestSigningRequest.addChildElement("signingServiceName", NS); |
| signingServiceName.addTextNode(this.signingService); |
| |
| List<String> fileNames = getFileNames(filesToSign); |
| |
| SOAPElement commaDelimitedFileNames = |
| requestSigningRequest.addChildElement("commaDelimitedFileNames", NS); |
| commaDelimitedFileNames.addTextNode(listToString(fileNames)); |
| |
| SOAPElement application = |
| requestSigningRequest.addChildElement("application", NS); |
| application.addTextNode(getApplicationString(fileNames, filesToSign)); |
| |
| // Send the message |
| SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance(); |
| SOAPConnection connection = soapConnectionFactory.createConnection(); |
| |
| log("Sending singing request to server and waiting for response"); |
| SOAPMessage response = connection.call(message, SIGNING_SERVICE_URL); |
| |
| if (debug) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(2 * 1024); |
| response.writeTo(baos); |
| log(baos.toString("UTF-8")); |
| } |
| |
| log("Processing response"); |
| SOAPElement responseBody = response.getSOAPBody(); |
| |
| // Should come back signed |
| NodeList bodyNodes = responseBody.getChildNodes(); |
| NodeList requestSigningResponseNodes = bodyNodes.item(0).getChildNodes(); |
| NodeList returnNodes = requestSigningResponseNodes.item(0).getChildNodes(); |
| |
| String signingSetID = null; |
| String signingSetStatus = null; |
| |
| for (int i = 0; i < returnNodes.getLength(); i++) { |
| Node returnNode = returnNodes.item(i); |
| if (returnNode.getLocalName().equals("signingSetID")) { |
| signingSetID = returnNode.getTextContent(); |
| } else if (returnNode.getLocalName().equals("signingSetStatus")) { |
| signingSetStatus = returnNode.getTextContent(); |
| } |
| } |
| |
| if (!signingService.contains("TEST") && !"SIGNED".equals(signingSetStatus) || |
| signingService.contains("TEST") && !"INITIALIZED".equals(signingSetStatus) ) { |
| throw new BuildException("Signing failed. Status was: " + signingSetStatus); |
| } |
| |
| return signingSetID; |
| } |
| |
| |
| private String listToString(List<String> list) { |
| StringBuilder sb = new StringBuilder(list.size() * 6); |
| boolean doneFirst = false; |
| for (String s : list) { |
| if (doneFirst) { |
| sb.append(','); |
| } else { |
| doneFirst = true; |
| } |
| sb.append(s); |
| } |
| return sb.toString(); |
| } |
| |
| |
| private void downloadSignedFiles(List<File> filesToSign, String id) |
| throws SOAPException, IOException { |
| |
| log("Downloading signed files. The signing set ID is: " + id); |
| |
| SOAPMessage message = SOAP_MSG_FACTORY.createMessage(); |
| SOAPBody body = populateEnvelope(message, NS); |
| |
| SOAPElement getSigningSetDetails = body.addChildElement("getSigningSetDetails", NS); |
| SOAPElement getSigningSetDetailsRequest = |
| getSigningSetDetails.addChildElement("getSigningSetDetailsRequest", NS); |
| |
| addCredentials(getSigningSetDetailsRequest, this.userName, this.password, this.partnerCode); |
| |
| SOAPElement signingSetID = |
| getSigningSetDetailsRequest.addChildElement("signingSetID", NS); |
| signingSetID.addTextNode(id); |
| |
| SOAPElement returnApplication = |
| getSigningSetDetailsRequest.addChildElement("returnApplication", NS); |
| returnApplication.addTextNode("true"); |
| |
| // Send the message |
| SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance(); |
| SOAPConnection connection = soapConnectionFactory.createConnection(); |
| |
| log("Requesting signed files from server and waiting for response"); |
| SOAPMessage response = connection.call(message, SIGNING_SERVICE_URL); |
| |
| log("Processing response"); |
| SOAPElement responseBody = response.getSOAPBody(); |
| |
| // Check for success |
| |
| // Extract the signed file(s) from the ZIP |
| NodeList bodyNodes = responseBody.getChildNodes(); |
| NodeList getSigningSetDetailsResponseNodes = bodyNodes.item(0).getChildNodes(); |
| NodeList returnNodes = getSigningSetDetailsResponseNodes.item(0).getChildNodes(); |
| |
| String result = null; |
| String data = null; |
| |
| for (int i = 0; i < returnNodes.getLength(); i++) { |
| Node returnNode = returnNodes.item(i); |
| if (returnNode.getLocalName().equals("result")) { |
| result = returnNode.getChildNodes().item(0).getTextContent(); |
| } else if (returnNode.getLocalName().equals("signingSet")) { |
| data = returnNode.getChildNodes().item(1).getTextContent(); |
| } |
| } |
| |
| if (!"0".equals(result)) { |
| throw new BuildException("Download failed. Result code was: " + result); |
| } |
| |
| extractFilesFromApplicationString(data, filesToSign); |
| } |
| |
| |
| private static SOAPBody populateEnvelope(SOAPMessage message, String namespace) |
| throws SOAPException { |
| SOAPPart soapPart = message.getSOAPPart(); |
| SOAPEnvelope envelope = soapPart.getEnvelope(); |
| envelope.addNamespaceDeclaration( |
| "soapenv","http://schemas.xmlsoap.org/soap/envelope/"); |
| envelope.addNamespaceDeclaration( |
| namespace,"http://api.ws.symantec.com/webtrust/codesigningservice"); |
| return envelope.getBody(); |
| } |
| |
| |
| private static void addCredentials(SOAPElement requestSigningRequest, |
| String user, String pwd, String code) throws SOAPException { |
| SOAPElement authToken = requestSigningRequest.addChildElement("authToken", NS); |
| SOAPElement userName = authToken.addChildElement("userName", NS); |
| userName.addTextNode(user); |
| SOAPElement password = authToken.addChildElement("password", NS); |
| password.addTextNode(pwd); |
| SOAPElement partnerCode = authToken.addChildElement("partnerCode", NS); |
| partnerCode.addTextNode(code); |
| } |
| |
| |
| /** |
| * Signing service requires unique files names. Since files will be returned |
| * in order, use dummy names that we know are unique but retain the file |
| * extension since the signing service appears to use it to figure out what |
| * to sign and how to sign it. |
| */ |
| private static List<String> getFileNames(List<File> filesToSign) { |
| List<String> result = new ArrayList<>(filesToSign.size()); |
| |
| for (int i = 0; i < filesToSign.size(); i++) { |
| File f = filesToSign.get(i); |
| String fileName = f.getName(); |
| int extIndex = fileName.lastIndexOf('.'); |
| String newName; |
| if (extIndex < 0) { |
| newName = Integer.toString(i); |
| } else { |
| newName = Integer.toString(i) + fileName.substring(extIndex); |
| } |
| result.add(newName); |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Zips the files, base 64 encodes the resulting zip and then returns the |
| * string. It would be far more efficient to stream this directly to the |
| * signing server but the files that need to be signed are relatively small |
| * and this simpler to write. |
| * |
| * @param fileNames Modified names of files |
| * @param files Files to be signed |
| */ |
| private static String getApplicationString(List<String> fileNames, List<File> files) |
| throws IOException { |
| // 16 MB should be more than enough for Tomcat |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(16 * 1024 * 1024); |
| try (ZipOutputStream zos = new ZipOutputStream(baos)) { |
| byte[] buf = new byte[32 * 1024]; |
| for (int i = 0; i < files.size(); i++) { |
| try (FileInputStream fis = new FileInputStream(files.get(i))) { |
| ZipEntry zipEntry = new ZipEntry(fileNames.get(i)); |
| zos.putNextEntry(zipEntry); |
| int numRead; |
| while ( (numRead = fis.read(buf)) >= 0) { |
| zos.write(buf, 0, numRead); |
| } |
| } |
| } |
| } |
| |
| return Base64.encodeBase64String(baos.toByteArray()); |
| } |
| |
| |
| /** |
| * Removes base64 encoding, unzips the files and writes the new files over |
| * the top of the old ones. |
| */ |
| private static void extractFilesFromApplicationString(String data, List<File> files) |
| throws IOException { |
| ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decodeBase64(data)); |
| try (ZipInputStream zis = new ZipInputStream(bais)) { |
| byte[] buf = new byte[32 * 1024]; |
| for (int i = 0; i < files.size(); i ++) { |
| try (FileOutputStream fos = new FileOutputStream(files.get(i))) { |
| zis.getNextEntry(); |
| int numRead; |
| while ( (numRead = zis.read(buf)) >= 0) { |
| fos.write(buf, 0 , numRead); |
| } |
| } |
| } |
| } |
| } |
| } |