blob: b0bbf338f0409749ecb1d8e373e0cee8e9700615 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2021 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.osgi.internal.signedcontent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.security.CodeSigner;
import java.security.Timestamp;
import java.security.cert.Certificate;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.zip.ZipFile;
import org.eclipse.osgi.internal.debug.Debug;
import org.eclipse.osgi.signedcontent.InvalidContentException;
import org.eclipse.osgi.signedcontent.SignedContent;
import org.eclipse.osgi.signedcontent.SignedContentEntry;
import org.eclipse.osgi.signedcontent.SignerInfo;
import org.eclipse.osgi.storage.bundlefile.BundleFile;
import org.eclipse.osgi.storage.bundlefile.DirBundleFile;
import org.eclipse.osgi.storage.bundlefile.ZipBundleFile;
public class SignedContentFromBundleFile implements SignedContent {
static abstract class BaseSignerInfo implements SignerInfo {
private volatile Certificate trustAnchor = null;
@Override
public Certificate getTrustAnchor() {
return trustAnchor;
}
@Override
public boolean isTrusted() {
return trustAnchor != null;
}
@Deprecated
@Override
public String getMessageDigestAlgorithm() {
return "unknown"; //$NON-NLS-1$
}
void setTrustAnchor(Certificate anchor) {
this.trustAnchor = anchor;
}
}
static class TimestampSignerInfo extends BaseSignerInfo {
private final Timestamp timestamp;
public TimestampSignerInfo(Timestamp timestamp) {
this.timestamp = timestamp;
}
@Override
public Certificate[] getCertificateChain() {
return timestamp.getSignerCertPath().getCertificates().toArray(new Certificate[0]);
}
Date getTimestamp() {
return timestamp.getTimestamp();
}
}
static class CodeSignerInfo extends BaseSignerInfo {
private final CodeSigner codeSigner;
private final TimestampSignerInfo timestamp;
public CodeSignerInfo(CodeSigner codeSigner) {
this.codeSigner = codeSigner;
Timestamp ts = codeSigner.getTimestamp();
this.timestamp = ts == null ? null : new TimestampSignerInfo(ts);
}
@Override
public Certificate[] getCertificateChain() {
return codeSigner.getSignerCertPath().getCertificates().toArray(new Certificate[0]);
}
TimestampSignerInfo getTSASignerInfo() {
return timestamp;
}
}
static class CodeSignerEntry implements SignedContentEntry {
private final String name;
private final List<CodeSignerInfo> signerInfos;
public CodeSignerEntry(List<CodeSignerInfo> signerInfos, String name) {
this.name = name;
this.signerInfos = signerInfos;
}
@Override
public String getName() {
return name;
}
@Override
public SignerInfo[] getSignerInfos() {
return signerInfos.toArray(new SignerInfo[0]);
}
@Override
public boolean isSigned() {
return !signerInfos.isEmpty();
}
@Override
public void verify() throws IOException, InvalidContentException {
// already verified
}
}
static class CorruptEntry implements SignedContentEntry {
final InvalidContentException verifyError;
final String name;
@Override
public String getName() {
return name;
}
@Override
public SignerInfo[] getSignerInfos() {
return new SignerInfo[0];
}
@Override
public boolean isSigned() {
return false;
}
@Override
public void verify() throws IOException, InvalidContentException {
throw verifyError;
}
public CorruptEntry(InvalidContentException verifyError, String name) {
super();
this.verifyError = verifyError;
this.name = name;
}
}
private final List<CodeSignerInfo> signerInfos = new ArrayList<>();
private final Map<String, SignedContentEntry> signedEntries;
public SignedContentFromBundleFile(BundleFile bundleFile) throws IOException {
signedEntries = getSignedEntries(() -> {
try {
return getJarInputStream(bundleFile);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, () -> bundleFile, signerInfos);
}
public SignedContentFromBundleFile(File bundleFile, Debug debug) throws IOException {
DirBundleFile tmpDirBundleFile = null;
if (bundleFile.isDirectory()) {
try {
tmpDirBundleFile = new DirBundleFile(bundleFile, false);
} catch (IOException e) {
// ignore and move on
}
}
DirBundleFile dirBundleFile = tmpDirBundleFile;
signedEntries = getSignedEntries(() -> {
try {
if (dirBundleFile != null) {
return getJarInputStream(dirBundleFile);
}
return new FileInputStream(bundleFile);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, () -> {
try {
if (dirBundleFile != null) {
return dirBundleFile;
}
// Make sure we have a ZipFile first, this will throw an IOException if not
// valid.
// Use SecureAction because it gives better errors about the path on exceptions
ZipFile temp = SignedBundleHook.secureAction.getZipFile(bundleFile, false);
temp.close();
return new ZipBundleFile(bundleFile, null, null, debug, false);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, signerInfos);
}
private static Map<String, SignedContentEntry> getSignedEntries(Supplier<InputStream> input,
Supplier<BundleFile> bundleFile, List<CodeSignerInfo> signerInfos) throws IOException {
Map<CodeSigner, CodeSignerInfo> codeSigners = new HashMap<>();
Map<String, SignedContentEntry> signedEntries = new LinkedHashMap<>();
try (JarInputStream jarInput = new JarInputStream(input.get())) {
for (JarEntry entry = jarInput.getNextJarEntry(); entry != null; entry = jarInput.getNextJarEntry()) {
// drain the entry so we can get the code signer
try {
for (byte[] drain = new byte[4096]; jarInput.read(drain, 0, drain.length) != -1;) {
// nothing
}
CodeSigner[] signers = entry.getCodeSigners();
if (signers != null) {
List<CodeSignerInfo> entryInfos = new ArrayList<>(signers.length);
for (CodeSigner codeSigner : signers) {
CodeSignerInfo info = codeSigners.computeIfAbsent(codeSigner, CodeSignerInfo::new);
entryInfos.add(info);
}
CodeSignerEntry signedEntry = new CodeSignerEntry(entryInfos, entry.getName());
signedEntries.put(entry.getName(), signedEntry);
}
} catch (SecurityException | IOException e) {
// assume corruption
signedEntries.put(entry.getName(),
new CorruptEntry(new InvalidContentException(entry.getName(), e), entry.getName()));
}
}
} catch (SecurityException e) {
Enumeration<String> paths = bundleFile.get().getEntryPaths("", true); //$NON-NLS-1$
while (paths.hasMoreElements()) {
String path = paths.nextElement();
if (!path.endsWith("/") && !signedEntries.containsKey(path)) { //$NON-NLS-1$
signedEntries.put(path, new CorruptEntry(new InvalidContentException(path, e), path));
}
}
} catch (UncheckedIOException e) {
throw e.getCause();
}
signerInfos.addAll(codeSigners.values());
return signedEntries;
}
private static InputStream getJarInputStream(BundleFile bundleFile) throws IOException {
File f = bundleFile.getBaseFile();
if (f == null || f.isDirectory()) {
return new BundleToJarInputStream(bundleFile);
}
return new FileInputStream(f);
}
@Override
public SignedContentEntry[] getSignedEntries() {
return signedEntries.values().toArray(new SignedContentEntry[0]);
}
@Override
public SignedContentEntry getSignedEntry(String name) {
return signedEntries.get(name);
}
@Override
public SignerInfo[] getSignerInfos() {
return signerInfos.toArray(new SignerInfo[0]);
}
@Override
public boolean isSigned() {
return !signerInfos.isEmpty();
}
@Override
public Date getSigningTime(SignerInfo signerInfo) {
if (signerInfo instanceof CodeSignerInfo) {
TimestampSignerInfo tsInfo = ((CodeSignerInfo) signerInfo).getTSASignerInfo();
if (tsInfo != null) {
return tsInfo.getTimestamp();
}
}
return null;
}
@Override
public SignerInfo getTSASignerInfo(SignerInfo signerInfo) {
if (signerInfo instanceof CodeSignerInfo) {
return ((CodeSignerInfo) signerInfo).getTSASignerInfo();
}
return null;
}
@Override
public void checkValidity(SignerInfo signerInfo)
throws CertificateExpiredException, CertificateNotYetValidException {
// TODO Auto-generated method stub
}
}