blob: 3c6061cf18bcea24dd9ed42f2ef360e93f0ff59d [file] [log] [blame]
/*
* Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.security.jarsigner;
import com.sun.jarsigner.ContentSigner;
import com.sun.jarsigner.ContentSignerParameters;
import sun.security.tools.PathList;
import sun.security.tools.jarsigner.TimestampedSigner;
import sun.security.util.ManifestDigester;
import sun.security.util.SignatureFileVerifier;
import sun.security.x509.AlgorithmId;
import java.io.*;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.*;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* An immutable utility class to sign a jar file.
* <p>
* A caller creates a {@code JarSigner.Builder} object, (optionally) sets
* some parameters, and calls {@link JarSigner.Builder#build build} to create
* a {@code JarSigner} object. This {@code JarSigner} object can then
* be used to sign a jar file.
* <p>
* Unless otherwise stated, calling a method of {@code JarSigner} or
* {@code JarSigner.Builder} with a null argument will throw
* a {@link NullPointerException}.
* <p>
* Example:
* <pre>
* JarSigner signer = new JarSigner.Builder(key, certPath)
* .digestAlgorithm("SHA-1")
* .signatureAlgorithm("SHA1withDSA")
* .build();
* try (ZipFile in = new ZipFile(inputFile);
* FileOutputStream out = new FileOutputStream(outputFile)) {
* signer.sign(in, out);
* }
* </pre>
*
* @since 9
*/
public final class JarSigner {
/**
* A mutable builder class that can create an immutable {@code JarSigner}
* from various signing-related parameters.
*
* @since 9
*/
public static class Builder {
// Signer materials:
final PrivateKey privateKey;
final X509Certificate[] certChain;
// JarSigner options:
// Support multiple digestalg internally. Can be null, but not empty
String[] digestalg;
String sigalg;
// Precisely should be one provider for each digestalg, maybe later
Provider digestProvider;
Provider sigProvider;
URI tsaUrl;
String signerName;
BiConsumer<String,String> handler;
// Implementation-specific properties:
String tSAPolicyID;
String tSADigestAlg;
boolean signManifest = true;
boolean externalSF = true;
String altSignerPath;
String altSigner;
/**
* Creates a {@code JarSigner.Builder} object with
* a {@link KeyStore.PrivateKeyEntry} object.
*
* @param entry the {@link KeyStore.PrivateKeyEntry} of the signer.
*/
public Builder(KeyStore.PrivateKeyEntry entry) {
this.privateKey = entry.getPrivateKey();
try {
// called internally, no need to clone
Certificate[] certs = entry.getCertificateChain();
this.certChain = Arrays.copyOf(certs, certs.length,
X509Certificate[].class);
} catch (ArrayStoreException ase) {
// Wrong type, not X509Certificate. Won't document.
throw new IllegalArgumentException(
"Entry does not contain X509Certificate");
}
}
/**
* Creates a {@code JarSigner.Builder} object with a private key and
* a certification path.
*
* @param privateKey the private key of the signer.
* @param certPath the certification path of the signer.
* @throws IllegalArgumentException if {@code certPath} is empty, or
* the {@code privateKey} algorithm does not match the algorithm
* of the {@code PublicKey} in the end entity certificate
* (the first certificate in {@code certPath}).
*/
public Builder(PrivateKey privateKey, CertPath certPath) {
List<? extends Certificate> certs = certPath.getCertificates();
if (certs.isEmpty()) {
throw new IllegalArgumentException("certPath cannot be empty");
}
if (!privateKey.getAlgorithm().equals
(certs.get(0).getPublicKey().getAlgorithm())) {
throw new IllegalArgumentException
("private key algorithm does not match " +
"algorithm of public key in end entity " +
"certificate (the 1st in certPath)");
}
this.privateKey = privateKey;
try {
this.certChain = certs.toArray(new X509Certificate[certs.size()]);
} catch (ArrayStoreException ase) {
// Wrong type, not X509Certificate.
throw new IllegalArgumentException(
"Entry does not contain X509Certificate");
}
}
/**
* Sets the digest algorithm. If no digest algorithm is specified,
* the default algorithm returned by {@link #getDefaultDigestAlgorithm}
* will be used.
*
* @param algorithm the standard name of the algorithm. See
* the {@code MessageDigest} section in the <a href=
* "{@docRoot}/../technotes/guides/security/StandardNames.html#MessageDigest">
* Java Cryptography Architecture Standard Algorithm Name
* Documentation</a> for information about standard algorithm names.
* @return the {@code JarSigner.Builder} itself.
* @throws NoSuchAlgorithmException if {@code algorithm} is not available.
*/
public Builder digestAlgorithm(String algorithm) throws NoSuchAlgorithmException {
MessageDigest.getInstance(Objects.requireNonNull(algorithm));
this.digestalg = new String[]{algorithm};
this.digestProvider = null;
return this;
}
/**
* Sets the digest algorithm from the specified provider.
* If no digest algorithm is specified, the default algorithm
* returned by {@link #getDefaultDigestAlgorithm} will be used.
*
* @param algorithm the standard name of the algorithm. See
* the {@code MessageDigest} section in the <a href=
* "{@docRoot}/../technotes/guides/security/StandardNames.html#MessageDigest">
* Java Cryptography Architecture Standard Algorithm Name
* Documentation</a> for information about standard algorithm names.
* @param provider the provider.
* @return the {@code JarSigner.Builder} itself.
* @throws NoSuchAlgorithmException if {@code algorithm} is not
* available in the specified provider.
*/
public Builder digestAlgorithm(String algorithm, Provider provider)
throws NoSuchAlgorithmException {
MessageDigest.getInstance(
Objects.requireNonNull(algorithm),
Objects.requireNonNull(provider));
this.digestalg = new String[]{algorithm};
this.digestProvider = provider;
return this;
}
/**
* Sets the signature algorithm. If no signature algorithm
* is specified, the default signature algorithm returned by
* {@link #getDefaultSignatureAlgorithm} for the private key
* will be used.
*
* @param algorithm the standard name of the algorithm. See
* the {@code Signature} section in the <a href=
* "{@docRoot}/../technotes/guides/security/StandardNames.html#Signature">
* Java Cryptography Architecture Standard Algorithm Name
* Documentation</a> for information about standard algorithm names.
* @return the {@code JarSigner.Builder} itself.
* @throws NoSuchAlgorithmException if {@code algorithm} is not available.
* @throws IllegalArgumentException if {@code algorithm} is not
* compatible with the algorithm of the signer's private key.
*/
public Builder signatureAlgorithm(String algorithm)
throws NoSuchAlgorithmException {
// Check availability
Signature.getInstance(Objects.requireNonNull(algorithm));
AlgorithmId.checkKeyAndSigAlgMatch(
privateKey.getAlgorithm(), algorithm);
this.sigalg = algorithm;
this.sigProvider = null;
return this;
}
/**
* Sets the signature algorithm from the specified provider. If no
* signature algorithm is specified, the default signature algorithm
* returned by {@link #getDefaultSignatureAlgorithm} for the private
* key will be used.
*
* @param algorithm the standard name of the algorithm. See
* the {@code Signature} section in the <a href=
* "{@docRoot}/../technotes/guides/security/StandardNames.html#Signature">
* Java Cryptography Architecture Standard Algorithm Name
* Documentation</a> for information about standard algorithm names.
* @param provider the provider.
* @return the {@code JarSigner.Builder} itself.
* @throws NoSuchAlgorithmException if {@code algorithm} is not
* available in the specified provider.
* @throws IllegalArgumentException if {@code algorithm} is not
* compatible with the algorithm of the signer's private key.
*/
public Builder signatureAlgorithm(String algorithm, Provider provider)
throws NoSuchAlgorithmException {
// Check availability
Signature.getInstance(
Objects.requireNonNull(algorithm),
Objects.requireNonNull(provider));
AlgorithmId.checkKeyAndSigAlgMatch(
privateKey.getAlgorithm(), algorithm);
this.sigalg = algorithm;
this.sigProvider = provider;
return this;
}
/**
* Sets the URI of the Time Stamping Authority (TSA).
*
* @param uri the URI.
* @return the {@code JarSigner.Builder} itself.
*/
public Builder tsa(URI uri) {
this.tsaUrl = Objects.requireNonNull(uri);
return this;
}
/**
* Sets the signer name. The name will be used as the base name for
* the signature files. All lowercase characters will be converted to
* uppercase for signature file names. If a signer name is not
* specified, the string "SIGNER" will be used.
*
* @param name the signer name.
* @return the {@code JarSigner.Builder} itself.
* @throws IllegalArgumentException if {@code name} is empty or has
* a size bigger than 8, or it contains characters not from the
* set "a-zA-Z0-9_-".
*/
public Builder signerName(String name) {
if (name.isEmpty() || name.length() > 8) {
throw new IllegalArgumentException("Name too long");
}
name = name.toUpperCase(Locale.ENGLISH);
for (int j = 0; j < name.length(); j++) {
char c = name.charAt(j);
if (!
((c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
(c == '-') ||
(c == '_'))) {
throw new IllegalArgumentException(
"Invalid characters in name");
}
}
this.signerName = name;
return this;
}
/**
* Sets en event handler that will be triggered when a {@link JarEntry}
* is to be added, signed, or updated during the signing process.
* <p>
* The handler can be used to display signing progress. The first
* argument of the handler can be "adding", "signing", or "updating",
* and the second argument is the name of the {@link JarEntry}
* being processed.
*
* @param handler the event handler.
* @return the {@code JarSigner.Builder} itself.
*/
public Builder eventHandler(BiConsumer<String,String> handler) {
this.handler = Objects.requireNonNull(handler);
return this;
}
/**
* Sets an additional implementation-specific property indicated by
* the specified key.
*
* @implNote This implementation supports the following properties:
* <ul>
* <li>"tsaDigestAlg": algorithm of digest data in the timestamping
* request. The default value is the same as the result of
* {@link #getDefaultDigestAlgorithm}.
* <li>"tsaPolicyId": TSAPolicyID for Timestamping Authority.
* No default value.
* <li>"internalsf": "true" if the .SF file is included inside the
* signature block, "false" otherwise. Default "false".
* <li>"sectionsonly": "true" if the .SF file only contains the hash
* value for each section of the manifest and not for the whole
* manifest, "false" otherwise. Default "false".
* </ul>
* All property names are case-insensitive.
*
* @param key the name of the property.
* @param value the value of the property.
* @return the {@code JarSigner.Builder} itself.
* @throws UnsupportedOperationException if the key is not supported
* by this implementation.
* @throws IllegalArgumentException if the value is not accepted as
* a legal value for this key.
*/
public Builder setProperty(String key, String value) {
Objects.requireNonNull(key);
Objects.requireNonNull(value);
switch (key.toLowerCase(Locale.US)) {
case "tsadigestalg":
try {
MessageDigest.getInstance(value);
} catch (NoSuchAlgorithmException nsae) {
throw new IllegalArgumentException(
"Invalid tsadigestalg", nsae);
}
this.tSADigestAlg = value;
break;
case "tsapolicyid":
this.tSAPolicyID = value;
break;
case "internalsf":
switch (value) {
case "true":
externalSF = false;
break;
case "false":
externalSF = true;
break;
default:
throw new IllegalArgumentException(
"Invalid internalsf value");
}
break;
case "sectionsonly":
switch (value) {
case "true":
signManifest = false;
break;
case "false":
signManifest = true;
break;
default:
throw new IllegalArgumentException(
"Invalid signManifest value");
}
break;
case "altsignerpath":
altSignerPath = value;
break;
case "altsigner":
altSigner = value;
break;
default:
throw new UnsupportedOperationException(
"Unsupported key " + key);
}
return this;
}
/**
* Gets the default digest algorithm.
*
* @implNote This implementation returns "SHA-256". The value may
* change in the future.
*
* @return the default digest algorithm.
*/
public static String getDefaultDigestAlgorithm() {
return "SHA-256";
}
/**
* Gets the default signature algorithm for a private key.
* For example, SHA256withRSA for a 2048-bit RSA key, and
* SHA384withECDSA for a 384-bit EC key.
*
* @implNote This implementation makes use of comparable strengths
* as defined in Tables 2 and 3 of NIST SP 800-57 Part 1-Rev.4.
* Specifically, if a DSA or RSA key with a key size greater than 7680
* bits, or an EC key with a key size greater than or equal to 512 bits,
* SHA-512 will be used as the hash function for the signature.
* If a DSA or RSA key has a key size greater than 3072 bits, or an
* EC key has a key size greater than or equal to 384 bits, SHA-384 will
* be used. Otherwise, SHA-256 will be used. The value may
* change in the future.
*
* @param key the private key.
* @return the default signature algorithm. Returns null if a default
* signature algorithm cannot be found. In this case,
* {@link #signatureAlgorithm} must be called to specify a
* signature algorithm. Otherwise, the {@link #build} method
* will throw an {@link IllegalArgumentException}.
*/
public static String getDefaultSignatureAlgorithm(PrivateKey key) {
return AlgorithmId.getDefaultSigAlgForKey(Objects.requireNonNull(key));
}
/**
* Builds a {@code JarSigner} object from the parameters set by the
* setter methods.
* <p>
* This method does not modify internal state of this {@code Builder}
* object and can be called multiple times to generate multiple
* {@code JarSigner} objects. After this method is called, calling
* any method on this {@code Builder} will have no effect on
* the newly built {@code JarSigner} object.
*
* @return the {@code JarSigner} object.
* @throws IllegalArgumentException if a signature algorithm is not
* set and cannot be derived from the private key using the
* {@link #getDefaultSignatureAlgorithm} method.
*/
public JarSigner build() {
return new JarSigner(this);
}
}
private static final String META_INF = "META-INF/";
// All fields in Builder are duplicated here as final. Those not
// provided but has a default value will be filled with default value.
// Precisely, a final array field can still be modified if only
// reference is copied, no clone is done because we are concerned about
// casual change instead of malicious attack.
// Signer materials:
private final PrivateKey privateKey;
private final X509Certificate[] certChain;
// JarSigner options:
private final String[] digestalg;
private final String sigalg;
private final Provider digestProvider;
private final Provider sigProvider;
private final URI tsaUrl;
private final String signerName;
private final BiConsumer<String,String> handler;
// Implementation-specific properties:
private final String tSAPolicyID;
private final String tSADigestAlg;
private final boolean signManifest; // "sign" the whole manifest
private final boolean externalSF; // leave the .SF out of the PKCS7 block
private final String altSignerPath;
private final String altSigner;
private JarSigner(JarSigner.Builder builder) {
this.privateKey = builder.privateKey;
this.certChain = builder.certChain;
if (builder.digestalg != null) {
// No need to clone because builder only accepts one alg now
this.digestalg = builder.digestalg;
} else {
this.digestalg = new String[] {
Builder.getDefaultDigestAlgorithm() };
}
this.digestProvider = builder.digestProvider;
if (builder.sigalg != null) {
this.sigalg = builder.sigalg;
} else {
this.sigalg = JarSigner.Builder
.getDefaultSignatureAlgorithm(privateKey);
if (this.sigalg == null) {
throw new IllegalArgumentException(
"No signature alg for " + privateKey.getAlgorithm());
}
}
this.sigProvider = builder.sigProvider;
this.tsaUrl = builder.tsaUrl;
if (builder.signerName == null) {
this.signerName = "SIGNER";
} else {
this.signerName = builder.signerName;
}
this.handler = builder.handler;
if (builder.tSADigestAlg != null) {
this.tSADigestAlg = builder.tSADigestAlg;
} else {
this.tSADigestAlg = Builder.getDefaultDigestAlgorithm();
}
this.tSAPolicyID = builder.tSAPolicyID;
this.signManifest = builder.signManifest;
this.externalSF = builder.externalSF;
this.altSigner = builder.altSigner;
this.altSignerPath = builder.altSignerPath;
}
/**
* Signs a file into an {@link OutputStream}. This method will not close
* {@code file} or {@code os}.
*
* @param file the file to sign.
* @param os the output stream.
* @throws JarSignerException if the signing fails.
*/
public void sign(ZipFile file, OutputStream os) {
try {
sign0(Objects.requireNonNull(file),
Objects.requireNonNull(os));
} catch (SocketTimeoutException | CertificateException e) {
// CertificateException is thrown when the received cert from TSA
// has no id-kp-timeStamping in its Extended Key Usages extension.
throw new JarSignerException("Error applying timestamp", e);
} catch (IOException ioe) {
throw new JarSignerException("I/O error", ioe);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new JarSignerException("Error in signer materials", e);
} catch (SignatureException se) {
throw new JarSignerException("Error creating signature", se);
}
}
/**
* Returns the digest algorithm for this {@code JarSigner}.
* <p>
* The return value is never null.
*
* @return the digest algorithm.
*/
public String getDigestAlgorithm() {
return digestalg[0];
}
/**
* Returns the signature algorithm for this {@code JarSigner}.
* <p>
* The return value is never null.
*
* @return the signature algorithm.
*/
public String getSignatureAlgorithm() {
return sigalg;
}
/**
* Returns the URI of the Time Stamping Authority (TSA).
*
* @return the URI of the TSA.
*/
public URI getTsa() {
return tsaUrl;
}
/**
* Returns the signer name of this {@code JarSigner}.
* <p>
* The return value is never null.
*
* @return the signer name.
*/
public String getSignerName() {
return signerName;
}
/**
* Returns the value of an additional implementation-specific property
* indicated by the specified key. If a property is not set but has a
* default value, the default value will be returned.
*
* @implNote See {@link JarSigner.Builder#setProperty} for a list of
* properties this implementation supports. All property names are
* case-insensitive.
*
* @param key the name of the property.
* @return the value for the property.
* @throws UnsupportedOperationException if the key is not supported
* by this implementation.
*/
public String getProperty(String key) {
Objects.requireNonNull(key);
switch (key.toLowerCase(Locale.US)) {
case "tsadigestalg":
return tSADigestAlg;
case "tsapolicyid":
return tSAPolicyID;
case "internalsf":
return Boolean.toString(!externalSF);
case "sectionsonly":
return Boolean.toString(!signManifest);
case "altsignerpath":
return altSignerPath;
case "altsigner":
return altSigner;
default:
throw new UnsupportedOperationException(
"Unsupported key " + key);
}
}
private void sign0(ZipFile zipFile, OutputStream os)
throws IOException, CertificateException, NoSuchAlgorithmException,
SignatureException, InvalidKeyException {
MessageDigest[] digests;
try {
digests = new MessageDigest[digestalg.length];
for (int i = 0; i < digestalg.length; i++) {
if (digestProvider == null) {
digests[i] = MessageDigest.getInstance(digestalg[i]);
} else {
digests[i] = MessageDigest.getInstance(
digestalg[i], digestProvider);
}
}
} catch (NoSuchAlgorithmException asae) {
// Should not happen. User provided alg were checked, and default
// alg should always be available.
throw new AssertionError(asae);
}
PrintStream ps = new PrintStream(os);
ZipOutputStream zos = new ZipOutputStream(ps);
Manifest manifest = new Manifest();
Map<String, Attributes> mfEntries = manifest.getEntries();
// The Attributes of manifest before updating
Attributes oldAttr = null;
boolean mfModified = false;
boolean mfCreated = false;
byte[] mfRawBytes = null;
// Check if manifest exists
ZipEntry mfFile;
if ((mfFile = getManifestFile(zipFile)) != null) {
// Manifest exists. Read its raw bytes.
mfRawBytes = zipFile.getInputStream(mfFile).readAllBytes();
manifest.read(new ByteArrayInputStream(mfRawBytes));
oldAttr = (Attributes) (manifest.getMainAttributes().clone());
} else {
// Create new manifest
Attributes mattr = manifest.getMainAttributes();
mattr.putValue(Attributes.Name.MANIFEST_VERSION.toString(),
"1.0");
String javaVendor = System.getProperty("java.vendor");
String jdkVersion = System.getProperty("java.version");
mattr.putValue("Created-By", jdkVersion + " (" + javaVendor
+ ")");
mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
mfCreated = true;
}
/*
* For each entry in jar
* (except for signature-related META-INF entries),
* do the following:
*
* - if entry is not contained in manifest, add it to manifest;
* - if entry is contained in manifest, calculate its hash and
* compare it with the one in the manifest; if they are
* different, replace the hash in the manifest with the newly
* generated one. (This may invalidate existing signatures!)
*/
Vector<ZipEntry> mfFiles = new Vector<>();
boolean wasSigned = false;
for (Enumeration<? extends ZipEntry> enum_ = zipFile.entries();
enum_.hasMoreElements(); ) {
ZipEntry ze = enum_.nextElement();
if (ze.getName().startsWith(META_INF)) {
// Store META-INF files in vector, so they can be written
// out first
mfFiles.addElement(ze);
if (SignatureFileVerifier.isBlockOrSF(
ze.getName().toUpperCase(Locale.ENGLISH))) {
wasSigned = true;
}
if (SignatureFileVerifier.isSigningRelated(ze.getName())) {
// ignore signature-related and manifest files
continue;
}
}
if (manifest.getAttributes(ze.getName()) != null) {
// jar entry is contained in manifest, check and
// possibly update its digest attributes
if (updateDigests(ze, zipFile, digests,
manifest)) {
mfModified = true;
}
} else if (!ze.isDirectory()) {
// Add entry to manifest
Attributes attrs = getDigestAttributes(ze, zipFile, digests);
mfEntries.put(ze.getName(), attrs);
mfModified = true;
}
}
// Recalculate the manifest raw bytes if necessary
if (mfModified) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
manifest.write(baos);
if (wasSigned) {
byte[] newBytes = baos.toByteArray();
if (mfRawBytes != null
&& oldAttr.equals(manifest.getMainAttributes())) {
/*
* Note:
*
* The Attributes object is based on HashMap and can handle
* continuation columns. Therefore, even if the contents are
* not changed (in a Map view), the bytes that it write()
* may be different from the original bytes that it read()
* from. Since the signature on the main attributes is based
* on raw bytes, we must retain the exact bytes.
*/
int newPos = findHeaderEnd(newBytes);
int oldPos = findHeaderEnd(mfRawBytes);
if (newPos == oldPos) {
System.arraycopy(mfRawBytes, 0, newBytes, 0, oldPos);
} else {
// cat oldHead newTail > newBytes
byte[] lastBytes = new byte[oldPos +
newBytes.length - newPos];
System.arraycopy(mfRawBytes, 0, lastBytes, 0, oldPos);
System.arraycopy(newBytes, newPos, lastBytes, oldPos,
newBytes.length - newPos);
newBytes = lastBytes;
}
}
mfRawBytes = newBytes;
} else {
mfRawBytes = baos.toByteArray();
}
}
// Write out the manifest
if (mfModified) {
// manifest file has new length
mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
}
if (handler != null) {
if (mfCreated) {
handler.accept("adding", mfFile.getName());
} else if (mfModified) {
handler.accept("updating", mfFile.getName());
}
}
zos.putNextEntry(mfFile);
zos.write(mfRawBytes);
// Calculate SignatureFile (".SF") and SignatureBlockFile
ManifestDigester manDig = new ManifestDigester(mfRawBytes);
SignatureFile sf = new SignatureFile(digests, manifest, manDig,
signerName, signManifest);
byte[] block;
Signature signer;
if (sigProvider == null ) {
signer = Signature.getInstance(sigalg);
} else {
signer = Signature.getInstance(sigalg, sigProvider);
}
signer.initSign(privateKey);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sf.write(baos);
byte[] content = baos.toByteArray();
signer.update(content);
byte[] signature = signer.sign();
@SuppressWarnings("deprecation")
ContentSigner signingMechanism = null;
if (altSigner != null) {
signingMechanism = loadSigningMechanism(altSigner,
altSignerPath);
}
@SuppressWarnings("deprecation")
ContentSignerParameters params =
new JarSignerParameters(null, tsaUrl, tSAPolicyID,
tSADigestAlg, signature,
signer.getAlgorithm(), certChain, content, zipFile);
block = sf.generateBlock(params, externalSF, signingMechanism);
String sfFilename = sf.getMetaName();
String bkFilename = sf.getBlockName(privateKey);
ZipEntry sfFile = new ZipEntry(sfFilename);
ZipEntry bkFile = new ZipEntry(bkFilename);
long time = System.currentTimeMillis();
sfFile.setTime(time);
bkFile.setTime(time);
// signature file
zos.putNextEntry(sfFile);
sf.write(zos);
if (handler != null) {
if (zipFile.getEntry(sfFilename) != null) {
handler.accept("updating", sfFilename);
} else {
handler.accept("adding", sfFilename);
}
}
// signature block file
zos.putNextEntry(bkFile);
zos.write(block);
if (handler != null) {
if (zipFile.getEntry(bkFilename) != null) {
handler.accept("updating", bkFilename);
} else {
handler.accept("adding", bkFilename);
}
}
// Write out all other META-INF files that we stored in the
// vector
for (int i = 0; i < mfFiles.size(); i++) {
ZipEntry ze = mfFiles.elementAt(i);
if (!ze.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME)
&& !ze.getName().equalsIgnoreCase(sfFilename)
&& !ze.getName().equalsIgnoreCase(bkFilename)) {
if (handler != null) {
if (manifest.getAttributes(ze.getName()) != null) {
handler.accept("signing", ze.getName());
} else if (!ze.isDirectory()) {
handler.accept("adding", ze.getName());
}
}
writeEntry(zipFile, zos, ze);
}
}
// Write out all other files
for (Enumeration<? extends ZipEntry> enum_ = zipFile.entries();
enum_.hasMoreElements(); ) {
ZipEntry ze = enum_.nextElement();
if (!ze.getName().startsWith(META_INF)) {
if (handler != null) {
if (manifest.getAttributes(ze.getName()) != null) {
handler.accept("signing", ze.getName());
} else {
handler.accept("adding", ze.getName());
}
}
writeEntry(zipFile, zos, ze);
}
}
zipFile.close();
zos.close();
}
private void writeEntry(ZipFile zf, ZipOutputStream os, ZipEntry ze)
throws IOException {
ZipEntry ze2 = new ZipEntry(ze.getName());
ze2.setMethod(ze.getMethod());
ze2.setTime(ze.getTime());
ze2.setComment(ze.getComment());
ze2.setExtra(ze.getExtra());
if (ze.getMethod() == ZipEntry.STORED) {
ze2.setSize(ze.getSize());
ze2.setCrc(ze.getCrc());
}
os.putNextEntry(ze2);
writeBytes(zf, ze, os);
}
private void writeBytes
(ZipFile zf, ZipEntry ze, ZipOutputStream os) throws IOException {
try (InputStream is = zf.getInputStream(ze)) {
is.transferTo(os);
}
}
private boolean updateDigests(ZipEntry ze, ZipFile zf,
MessageDigest[] digests,
Manifest mf) throws IOException {
boolean update = false;
Attributes attrs = mf.getAttributes(ze.getName());
String[] base64Digests = getDigests(ze, zf, digests);
for (int i = 0; i < digests.length; i++) {
// The entry name to be written into attrs
String name = null;
try {
// Find if the digest already exists. An algorithm could have
// different names. For example, last time it was SHA, and this
// time it's SHA-1.
AlgorithmId aid = AlgorithmId.get(digests[i].getAlgorithm());
for (Object key : attrs.keySet()) {
if (key instanceof Attributes.Name) {
String n = key.toString();
if (n.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST")) {
String tmp = n.substring(0, n.length() - 7);
if (AlgorithmId.get(tmp).equals(aid)) {
name = n;
break;
}
}
}
}
} catch (NoSuchAlgorithmException nsae) {
// Ignored. Writing new digest entry.
}
if (name == null) {
name = digests[i].getAlgorithm() + "-Digest";
attrs.putValue(name, base64Digests[i]);
update = true;
} else {
// compare digests, and replace the one in the manifest
// if they are different
String mfDigest = attrs.getValue(name);
if (!mfDigest.equalsIgnoreCase(base64Digests[i])) {
attrs.putValue(name, base64Digests[i]);
update = true;
}
}
}
return update;
}
private Attributes getDigestAttributes(
ZipEntry ze, ZipFile zf, MessageDigest[] digests)
throws IOException {
String[] base64Digests = getDigests(ze, zf, digests);
Attributes attrs = new Attributes();
for (int i = 0; i < digests.length; i++) {
attrs.putValue(digests[i].getAlgorithm() + "-Digest",
base64Digests[i]);
}
return attrs;
}
/*
* Returns manifest entry from given jar file, or null if given jar file
* does not have a manifest entry.
*/
private ZipEntry getManifestFile(ZipFile zf) {
ZipEntry ze = zf.getEntry(JarFile.MANIFEST_NAME);
if (ze == null) {
// Check all entries for matching name
Enumeration<? extends ZipEntry> enum_ = zf.entries();
while (enum_.hasMoreElements() && ze == null) {
ze = enum_.nextElement();
if (!JarFile.MANIFEST_NAME.equalsIgnoreCase
(ze.getName())) {
ze = null;
}
}
}
return ze;
}
private String[] getDigests(
ZipEntry ze, ZipFile zf, MessageDigest[] digests)
throws IOException {
int n, i;
try (InputStream is = zf.getInputStream(ze)) {
long left = ze.getSize();
byte[] buffer = new byte[8192];
while ((left > 0)
&& (n = is.read(buffer, 0, buffer.length)) != -1) {
for (i = 0; i < digests.length; i++) {
digests[i].update(buffer, 0, n);
}
left -= n;
}
}
// complete the digests
String[] base64Digests = new String[digests.length];
for (i = 0; i < digests.length; i++) {
base64Digests[i] = Base64.getEncoder()
.encodeToString(digests[i].digest());
}
return base64Digests;
}
@SuppressWarnings("fallthrough")
private int findHeaderEnd(byte[] bs) {
// Initial state true to deal with empty header
boolean newline = true; // just met a newline
int len = bs.length;
for (int i = 0; i < len; i++) {
switch (bs[i]) {
case '\r':
if (i < len - 1 && bs[i + 1] == '\n') i++;
// fallthrough
case '\n':
if (newline) return i + 1; //+1 to get length
newline = true;
break;
default:
newline = false;
}
}
// If header end is not found, it means the MANIFEST.MF has only
// the main attributes section and it does not end with 2 newlines.
// Returns the whole length so that it can be completely replaced.
return len;
}
/*
* Try to load the specified signing mechanism.
* The URL class loader is used.
*/
@SuppressWarnings("deprecation")
private ContentSigner loadSigningMechanism(String signerClassName,
String signerClassPath) {
// construct class loader
String cpString; // make sure env.class.path defaults to dot
// do prepends to get correct ordering
cpString = PathList.appendPath(
System.getProperty("env.class.path"), null);
cpString = PathList.appendPath(
System.getProperty("java.class.path"), cpString);
cpString = PathList.appendPath(signerClassPath, cpString);
URL[] urls = PathList.pathToURLs(cpString);
ClassLoader appClassLoader = new URLClassLoader(urls);
try {
// attempt to find signer
Class<?> signerClass = appClassLoader.loadClass(signerClassName);
Object signer = signerClass.newInstance();
return (ContentSigner) signer;
} catch (ClassNotFoundException|InstantiationException|
IllegalAccessException|ClassCastException e) {
throw new IllegalArgumentException(
"Invalid altSigner or altSignerPath", e);
}
}
static class SignatureFile {
/**
* SignatureFile
*/
Manifest sf;
/**
* .SF base name
*/
String baseName;
public SignatureFile(MessageDigest digests[],
Manifest mf,
ManifestDigester md,
String baseName,
boolean signManifest) {
this.baseName = baseName;
String version = System.getProperty("java.version");
String javaVendor = System.getProperty("java.vendor");
sf = new Manifest();
Attributes mattr = sf.getMainAttributes();
mattr.putValue(Attributes.Name.SIGNATURE_VERSION.toString(), "1.0");
mattr.putValue("Created-By", version + " (" + javaVendor + ")");
if (signManifest) {
for (MessageDigest digest: digests) {
mattr.putValue(digest.getAlgorithm() + "-Digest-Manifest",
Base64.getEncoder().encodeToString(
md.manifestDigest(digest)));
}
}
// create digest of the manifest main attributes
ManifestDigester.Entry mde =
md.get(ManifestDigester.MF_MAIN_ATTRS, false);
if (mde != null) {
for (MessageDigest digest: digests) {
mattr.putValue(digest.getAlgorithm() +
"-Digest-" + ManifestDigester.MF_MAIN_ATTRS,
Base64.getEncoder().encodeToString(
mde.digest(digest)));
}
} else {
throw new IllegalStateException
("ManifestDigester failed to create " +
"Manifest-Main-Attribute entry");
}
// go through the manifest entries and create the digests
Map<String, Attributes> entries = sf.getEntries();
for (String name: mf.getEntries().keySet()) {
mde = md.get(name, false);
if (mde != null) {
Attributes attr = new Attributes();
for (MessageDigest digest: digests) {
attr.putValue(digest.getAlgorithm() + "-Digest",
Base64.getEncoder().encodeToString(
mde.digest(digest)));
}
entries.put(name, attr);
}
}
}
// Write .SF file
public void write(OutputStream out) throws IOException {
sf.write(out);
}
// get .SF file name
public String getMetaName() {
return "META-INF/" + baseName + ".SF";
}
// get .DSA (or .DSA, .EC) file name
public String getBlockName(PrivateKey privateKey) {
String keyAlgorithm = privateKey.getAlgorithm();
return "META-INF/" + baseName + "." + keyAlgorithm;
}
// Generates the PKCS#7 content of block file
@SuppressWarnings("deprecation")
public byte[] generateBlock(ContentSignerParameters params,
boolean externalSF,
ContentSigner signingMechanism)
throws NoSuchAlgorithmException,
IOException, CertificateException {
if (signingMechanism == null) {
signingMechanism = new TimestampedSigner();
}
return signingMechanism.generateSignedData(
params,
externalSF,
params.getTimestampingAuthority() != null
|| params.getTimestampingAuthorityCertificate() != null);
}
}
@SuppressWarnings("deprecation")
class JarSignerParameters implements ContentSignerParameters {
private String[] args;
private URI tsa;
private byte[] signature;
private String signatureAlgorithm;
private X509Certificate[] signerCertificateChain;
private byte[] content;
private ZipFile source;
private String tSAPolicyID;
private String tSADigestAlg;
JarSignerParameters(String[] args, URI tsa,
String tSAPolicyID, String tSADigestAlg,
byte[] signature, String signatureAlgorithm,
X509Certificate[] signerCertificateChain,
byte[] content, ZipFile source) {
Objects.requireNonNull(signature);
Objects.requireNonNull(signatureAlgorithm);
Objects.requireNonNull(signerCertificateChain);
this.args = args;
this.tsa = tsa;
this.tSAPolicyID = tSAPolicyID;
this.tSADigestAlg = tSADigestAlg;
this.signature = signature;
this.signatureAlgorithm = signatureAlgorithm;
this.signerCertificateChain = signerCertificateChain;
this.content = content;
this.source = source;
}
public String[] getCommandLine() {
return args;
}
public URI getTimestampingAuthority() {
return tsa;
}
public X509Certificate getTimestampingAuthorityCertificate() {
// We don't use this param. Always provide tsaURI.
return null;
}
public String getTSAPolicyID() {
return tSAPolicyID;
}
public String getTSADigestAlg() {
return tSADigestAlg;
}
public byte[] getSignature() {
return signature;
}
public String getSignatureAlgorithm() {
return signatureAlgorithm;
}
public X509Certificate[] getSignerCertificateChain() {
return signerCertificateChain;
}
public byte[] getContent() {
return content;
}
public ZipFile getSource() {
return source;
}
}
}