| /******************************************************************************* |
| * 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 |
| |
| } |
| |
| } |