FELIX-518 - add unit test for signed DPs:
- created utility to create signed deployment packages for use in itests;
- verify that signed DPs are correctly installed;
- some small cleanups and typos fixed.
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1724647 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java
index 89f4ce9..512381b 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java
@@ -511,6 +511,6 @@
*/
protected boolean isMetaInfFile(String name) {
name = name.toUpperCase(Locale.US);
- return name.startsWith("META-INF/") && (name.endsWith("/INDEX.LIST") || name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RS"));
+ return name.startsWith("META-INF/") && (name.endsWith("/INDEX.LIST") || name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA") || name.endsWith(".EC"));
}
}
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java
index 5396cfc..7dff1da 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java
@@ -18,7 +18,8 @@
*/
package org.apache.felix.deploymentadmin;
-import java.io.Closeable;
+import static org.apache.felix.deploymentadmin.Utils.closeSilently;
+
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
@@ -53,7 +54,7 @@
private OutputStream m_entryOS;
public ContentCopyingJarInputStream(InputStream in, File indexFile, File contentDir) throws IOException {
- super(in);
+ super(in, true /* verify */);
m_contentDir = contentDir;
@@ -122,17 +123,6 @@
m_indexFileWriter = null;
}
- private void closeSilently(Closeable resource) {
- try {
- if (resource != null) {
- resource.close();
- }
- }
- catch (IOException e) {
- // Ignore, not much we can do about this...
- }
- }
-
/**
* Creates a verbatim copy of the manifest, when it is read from the original JAR.
*/
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
index 1c17ba4..42f2c32 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
@@ -181,6 +181,8 @@
jarInput = new ContentCopyingJarInputStream(sourceInput, tempIndex, tempContents);
if (jarInput.getManifest() == null) {
+ Utils.closeSilently(jarInput);
+
m_log.log(LogService.LOG_ERROR, "Stream does not contain a valid deployment package: missing manifest!");
throw new DeploymentException(CODE_MISSING_HEADER, "No manifest present in deployment package!");
}
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java
index ed1189c..00ac13b 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java
@@ -33,10 +33,8 @@
* deployment package manifest and can interpret the various manifest entries and headers the OSGi specification defines.
*/
public class DeploymentPackageManifest implements Constants {
-
private final Manifest m_manifest;
private final Version m_version;
-
private final List m_bundleInfos = new ArrayList();
private final List m_resourceInfos = new ArrayList();
private final String m_symbolicName;
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java
index 795192b..7c68997 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java
@@ -28,75 +28,68 @@
*
*/
public class NonCloseableStream extends InputStream {
-
private final InputStream m_input;
- private boolean m_closed;
+ private volatile boolean m_closed;
- public NonCloseableStream(InputStream m_input) {
- this.m_input = m_input;
+ public NonCloseableStream(InputStream input) {
+ m_input = input;
}
- // stream should not be actually closed, subsequent calls to read methods will throw an exception though
-
- public void close() throws IOException {
- if (m_closed) {
- throw new IOException("Unable to read, stream is closed.");
- }
- m_closed = true;
- }
-
- public int read() throws IOException {
- return m_input.read();
- }
-
- public int read(byte[] b, int off, int len) throws IOException {
- if (m_closed) {
- throw new IOException("Unable to read, stream is closed.");
- }
- return m_input.read(b, off, len);
- }
-
- public int read(byte[] b) throws IOException {
- if (m_closed) {
- throw new IOException("Unable to read, stream is closed.");
- }
- return m_input.read(b);
- }
-
- // No mark & reset support
-
- public boolean markSupported() {
- return false;
- }
-
- public void mark(int readlimit) {
- // do nothing
- }
-
- public void reset() throws IOException {
- throw new IOException("Mark and reset are not available on this type of stream.");
- }
-
- // Unaffected methods
-
public int available() throws IOException {
return m_input.available();
}
- public int hashCode() {
- return m_input.hashCode();
- }
-
- public long skip(long n) throws IOException {
- return m_input.skip(n);
+ public void close() throws IOException {
+ // stream should not be actually closed, subsequent calls to read methods will throw an exception though
+ assertOpen();
+ m_closed = true;
}
public boolean equals(Object obj) {
return m_input.equals(obj);
}
+ public int hashCode() {
+ return m_input.hashCode();
+ }
+
+ public void mark(int readlimit) {
+ // do nothing
+ }
+
+ public boolean markSupported() {
+ return false;
+ }
+
+ public int read() throws IOException {
+ return m_input.read();
+ }
+
+ public int read(byte[] b) throws IOException {
+ assertOpen();
+ return m_input.read(b);
+ }
+
+ public int read(byte[] b, int off, int len) throws IOException {
+ assertOpen();
+ return m_input.read(b, off, len);
+ }
+
+ public void reset() throws IOException {
+ throw new IOException("Mark and reset are not available on this type of stream.");
+ }
+
+ public long skip(long n) throws IOException {
+ return m_input.skip(n);
+ }
+
public String toString() {
return m_input.toString();
}
+ private void assertOpen() throws IOException {
+ if (m_closed) {
+ throw new IOException("Unable to read, stream is closed.");
+ }
+ }
}
diff --git a/deploymentadmin/itest/pom.xml b/deploymentadmin/itest/pom.xml
index 4a45f59..5a0296d 100644
--- a/deploymentadmin/itest/pom.xml
+++ b/deploymentadmin/itest/pom.xml
@@ -117,6 +117,18 @@
</dependency>
<dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ <version>1.54</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <version>1.54</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.3</version>
@@ -148,14 +160,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
- <target>1.6</target>
- <source>1.6</source>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java
index f13df3f..546956a 100644
--- a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java
@@ -25,6 +25,7 @@
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
+import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -38,6 +39,7 @@
import junit.framework.TestCase;
import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.After;
import org.junit.Before;
import org.ops4j.pax.exam.Configuration;
@@ -74,24 +76,23 @@
protected volatile AtomicInteger m_gate = new AtomicInteger(0);
protected volatile String m_testBundleBasePath;
protected volatile Map<String, List<Version>> m_initialBundles;
-
- private int cnt = 0;
-
+
+ private int cnt = 0;
+
@Configuration
public Option[] config() throws Exception {
- return options(
- bootDelegationPackage("sun.*"),
- systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("ERROR"),
+ return options(bootDelegationPackage("sun.*"), systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("ERROR"),
- mavenBundle("org.apache.felix", "org.apache.felix.metatype").versionAsInProject(),
+ mavenBundle("org.apache.felix", "org.apache.felix.metatype").versionAsInProject(),
mavenBundle("org.apache.felix", "org.apache.felix.dependencymanager").versionAsInProject(),
- mavenBundle("org.apache.felix", "org.apache.felix.deploymentadmin").versionAsInProject(),
+ mavenBundle("org.apache.felix", "org.apache.felix.deploymentadmin").versionAsInProject(),
mavenBundle("org.apache.felix", "org.apache.felix.eventadmin").versionAsInProject(),
mavenBundle("org.apache.felix", "org.apache.felix.configadmin").versionAsInProject(),
mavenBundle("commons-codec", "commons-codec").versionAsInProject(),
-
- junitBundles()
- );
+ mavenBundle("org.bouncycastle", "bcprov-jdk15on").versionAsInProject(),
+ mavenBundle("org.bouncycastle", "bcpkix-jdk15on").versionAsInProject(),
+
+ junitBundles());
}
@Before
@@ -110,9 +111,9 @@
}
}
});
-
+
m_initialBundles = new HashMap<String, List<Version>>();
-
+
for (Bundle bundle : m_context.getBundles()) {
List<Version> versions = m_initialBundles.get(bundle.getSymbolicName());
if (versions == null) {
@@ -121,12 +122,16 @@
}
versions.add(bundle.getVersion());
}
+
+ Security.addProvider(new BouncyCastleProvider());
}
-
+
@After
public void tearDown() throws Exception {
System.setProperty("rp1", "");
System.setProperty("bundle3", "");
+
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
}
protected void assertBundleExists(String symbolicName, String version) {
@@ -168,24 +173,26 @@
}
return result;
}
-
+
protected final DeploymentPackage installDeploymentPackage(DeploymentPackageBuilder dpBuilder) throws Exception {
return installDeploymentPackage(dpBuilder.generate());
}
-
+
protected final DeploymentPackage installDeploymentPackage(InputStream is) throws Exception {
try {
return m_deploymentAdmin.installDeploymentPackage(is);
- } finally {
+ }
+ finally {
try {
is.close();
- } catch (IOException e) {
+ }
+ catch (IOException e) {
// Nothing we can do about this, but log it...
e.printStackTrace();
}
}
}
-
+
protected final int countDeploymentPackages() {
return m_deploymentAdmin.listDeploymentPackages().length;
}
@@ -193,17 +200,17 @@
protected DeploymentPackageBuilder createNewDeploymentPackageBuilder(String version) {
return createDeploymentPackageBuilder(String.format("itest%d", ++cnt), version);
}
-
+
protected DeploymentPackageBuilder createDeploymentPackageBuilder(String symName, String version) {
return DeploymentPackageBuilder.create(symName, version);
}
-
+
protected Map<String, List<Version>> getCurrentBundles() {
Map<String, List<Version>> bundles = new HashMap<String, List<Version>>();
for (Bundle bundle : m_context.getBundles()) {
String symbolicName = bundle.getSymbolicName();
Version version = bundle.getVersion();
-
+
// Is is not part of any of the initially provisioned bundles?
List<Version> versions = m_initialBundles.get(symbolicName);
if ((versions == null) || !versions.contains(version)) {
@@ -217,7 +224,7 @@
}
return bundles;
}
-
+
protected String getSymbolicName(String baseName) {
return "testbundles.".concat(baseName);
}
@@ -241,10 +248,11 @@
}
protected URL getTestBundleURL(String baseName) throws MalformedURLException {
- return getTestBundleURL(baseName, "1.0.0");
+ return getTestBundleURL(baseName, "1.0.0");
}
+
protected URL getTestBundleURL(String baseName, String version) throws MalformedURLException {
- return getTestBundleURL(baseName, baseName, version);
+ return getTestBundleURL(baseName, baseName, version);
}
protected URL getTestBundleURL(String artifactName, String baseName, String version) throws MalformedURLException {
@@ -253,7 +261,7 @@
assertTrue("No such bundle: " + f, f.exists() && f.isFile());
return f.toURI().toURL();
}
-
+
protected boolean isBundleActive(Bundle bundle) {
return isBundleInState(bundle, Bundle.ACTIVE);
}
@@ -280,7 +288,7 @@
protected boolean isBundleRemoved(String symbolicName, String version) {
return isBundleRemoved(symbolicName, new Version(version));
}
-
+
protected boolean isBundleRemoved(String symbolicName, Version version) {
Map<String, List<Version>> bundles = getCurrentBundles();
@@ -297,13 +305,13 @@
FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class);
frameworkWiring.resolveBundles(Arrays.asList(bundles));
-
+
for (Bundle bundle : bundles) {
if ((bundle.getState() & Bundle.RESOLVED) == 0) {
return false;
}
}
-
+
return true;
}
}
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DPSignerTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DPSignerTest.java
new file mode 100644
index 0000000..499906b
--- /dev/null
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DPSignerTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.felix.deploymentadmin.itest;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.apache.felix.deploymentadmin.itest.BaseIntegrationTest.TEST_FAILING_BUNDLE_RP1;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.URL;
+import java.security.Security;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+
+import org.apache.felix.deploymentadmin.itest.util.CertificateUtil;
+import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.KeyType;
+import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.SignerInfo;
+import org.apache.felix.deploymentadmin.itest.util.DPSigner;
+import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test cases for {@link DPSigner}.
+ */
+public class DPSignerTest {
+
+ @Test
+ public void testSignArtifactsOk() throws Exception {
+ URL dpProps = getClass().getResource("/dp.properties");
+ assertNotNull(dpProps);
+
+ DeploymentPackageBuilder builder = DeploymentPackageBuilder.create("dpSignerTest1", "1.0.0");
+ builder.add(builder.createLocalizationResource().setUrl(dpProps).setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setFilename("dp.properties"));
+
+ SignerInfo signerInfo = CertificateUtil.createSelfSignedCert("CN=testCert", KeyType.RSA);
+
+ DPSigner signer = new DPSigner();
+
+ byte[] rawData;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ signer.sign(builder, signerInfo.getPrivate(), signerInfo.getCert(), baos);
+ rawData = baos.toByteArray();
+ }
+
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(rawData); JarInputStream jis = new JarInputStream(bais, true)) {
+ JarEntry entry;
+ while ((entry = jis.getNextJarEntry()) != null) {
+ assertNotNull(entry);
+ jis.closeEntry();
+ }
+
+ assertNotNull(jis.getManifest());
+ }
+ }
+
+ @Before
+ public void setUp() {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @After
+ public void tearDown() {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+}
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java
index 2b0ff9a..24f4ffa 100644
--- a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java
@@ -18,9 +18,13 @@
*/
package org.apache.felix.deploymentadmin.itest;
+import static org.apache.felix.deploymentadmin.itest.util.CertificateUtil.createSelfSignedCert;
+
import java.io.File;
import java.net.URL;
+import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.KeyType;
+import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.SignerInfo;
import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder;
import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder.JarManifestManipulatingFilter;
import org.junit.Test;
@@ -35,8 +39,7 @@
* Provides test cases regarding the use of "normal" deployment packages in DeploymentAdmin.
*/
@RunWith(PaxExam.class)
-public class InstallDeploymentPackageTest extends BaseIntegrationTest
-{
+public class InstallDeploymentPackageTest extends BaseIntegrationTest {
/**
* FELIX-518 - Test that DP with localization and signature files are properly deployed.
*/
@@ -44,36 +47,40 @@
public void testInstallDeploymentPackageWithLocalizationAndSignatureFilesOk() throws Exception {
URL dpProps = getClass().getResource("/dp.properties");
assertNotNull(dpProps);
-
- DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
- dpBuilder
- .addSignatures()
- .add(dpBuilder.createLocalizationResource().setUrl(dpProps).setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setFilename("dp.properties"))
- .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1")));
- installDeploymentPackage(dpBuilder);
+ SignerInfo signer = createSelfSignedCert("CN=dpTest", KeyType.EC);
+
+ DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
+ dpBuilder.signOutput(signer.getPrivate(), signer.getCert())
+ .add(dpBuilder.createLocalizationResource().setUrl(dpProps).setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setFilename("dp.properties"))
+ .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1")))
+ .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")))
+ .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")));
+
+ installDeploymentPackage(dpBuilder); // should succeed.
+
+ assertBundleExists("testbundles.bundle1", "1.0.0");
+ assertBundleExists("testbundles.bundle2", "1.0.0");
+ assertBundleExists("testbundles.rp1", "1.0.0");
}
-
-
+
/**
* FELIX-4409/4410/4463 - test the installation of an invalid deployment package.
*/
@Test
- public void testInstallInvalidDeploymentPackageFail() throws Exception
- {
+ public void testInstallInvalidDeploymentPackageFail() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
- // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA spec...
+ // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA
+ // spec...
dpBuilder
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0")))
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi2", "bundleapi2", "2.0.0")));
- try
- {
+ try {
installDeploymentPackage(dpBuilder);
fail("DeploymentException expected!");
}
- catch (DeploymentException e)
- {
+ catch (DeploymentException e) {
// Ok; expected...
}
@@ -86,10 +93,10 @@
* FELIX-1835 - test whether we can install bundles with a non-root path inside the DP.
*/
@Test
- public void testInstallBundlesWithPathsOk() throws Exception
- {
+ public void testInstallBundlesWithPathsOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
- // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA spec...
+ // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA
+ // spec...
dpBuilder
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0")).setFilename("bundles/bundleapi1.jar"))
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0")).setFilename("bundles/bundleimpl1.jar"));
@@ -106,11 +113,11 @@
}
/**
- * Tests that adding the dependency for a bundle in an update package causes the depending bundle to be resolved and started.
+ * Tests that adding the dependency for a bundle in an update package causes the depending bundle to be resolved and
+ * started.
*/
@Test
- public void testInstallBundleWithDependencyInPackageUpdateOk() throws Exception
- {
+ public void testInstallBundleWithDependencyInPackageUpdateOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
// missing bundle1 as dependency...
dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")));
@@ -140,11 +147,11 @@
}
/**
- * Tests that installing a bundle with a dependency installed by another deployment package is not started, but is resolved.
+ * Tests that installing a bundle with a dependency installed by another deployment package is not started, but is
+ * resolved.
*/
@Test
- public void testInstallBundleWithDependencyInSeparatePackageOk() throws Exception
- {
+ public void testInstallBundleWithDependencyInSeparatePackageOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")));
@@ -183,8 +190,7 @@
* Tests that if an exception is thrown in the start method of a bundle, the installation is not rolled back.
*/
@Test
- public void testInstallBundleWithExceptionThrownInStartCausesNoRollbackOk() throws Exception
- {
+ public void testInstallBundleWithExceptionThrownInStartCausesNoRollbackOk() throws Exception {
System.setProperty("bundle3", "start");
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
@@ -208,11 +214,11 @@
}
/**
- * Tests that installing a bundle along with a fragment bundle succeeds (DA should not try to start the fragment, see FELIX-4167).
+ * Tests that installing a bundle along with a fragment bundle succeeds (DA should not try to start the fragment,
+ * see FELIX-4167).
*/
@Test
- public void testInstallBundleWithFragmentOk() throws Exception
- {
+ public void testInstallBundleWithFragmentOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")))
@@ -236,8 +242,7 @@
* Tests that installing a bundle whose dependencies cannot be met, is installed, but not started.
*/
@Test
- public void testInstallBundleWithMissingDependencyOk() throws Exception
- {
+ public void testInstallBundleWithMissingDependencyOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")));
@@ -258,8 +263,7 @@
* Tests that installing a bundle along with other (non-bundle) artifacts succeeds.
*/
@Test
- public void testInstallBundleWithOtherArtifactsOk() throws Exception
- {
+ public void testInstallBundleWithOtherArtifactsOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder
.add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1")))
@@ -282,8 +286,7 @@
* Tests that installing a new bundle works as expected.
*/
@Test
- public void testInstallSingleValidBundleOk() throws Exception
- {
+ public void testInstallSingleValidBundleOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")));
@@ -302,8 +305,7 @@
* Tests that installing two bundles works as expected.
*/
@Test
- public void testInstallTwoValidBundlesOk() throws Exception
- {
+ public void testInstallTwoValidBundlesOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")))
@@ -324,11 +326,11 @@
}
/**
- * Tests that if an exception is thrown during the uninstall of a bundle, the installation/update continues and succeeds.
+ * Tests that if an exception is thrown during the uninstall of a bundle, the installation/update continues and
+ * succeeds.
*/
@Test
- public void testUninstallBundleWithExceptionThrownInStopCauseNoRollbackOk() throws Exception
- {
+ public void testUninstallBundleWithExceptionThrownInStopCauseNoRollbackOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")))
@@ -365,8 +367,7 @@
* Tests that if an exception is thrown during the stop of a bundle, the installation/update continues and succeeds.
*/
@Test
- public void testUpdateBundleWithExceptionThrownInStopCauseNoRollbackOk() throws Exception
- {
+ public void testUpdateBundleWithExceptionThrownInStopCauseNoRollbackOk() throws Exception {
DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");
dpBuilder
.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")))
@@ -402,7 +403,8 @@
}
/**
- * Tests that we can correctly rollback the installation of a deployment package for bundles that have their data area populated.
+ * Tests that we can correctly rollback the installation of a deployment package for bundles that have their data
+ * area populated.
*/
@Test
public void testRollbackWithPopulatedDataAreaOk() throws Exception {
@@ -434,7 +436,7 @@
// Simulate an upgrade for our bundle, which should cause its data area to be retained...
dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1");
dpBuilder
- .add(dpBuilder.createBundleResource().setVersion("1.1.0").setUrl(getTestBundleURL("bundle1")).setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0")))
+ .add(dpBuilder.createBundleResource().setVersion("1.1.0").setUrl(getTestBundleURL("bundle1")).setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0")))
.add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1")))
.add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml")));
@@ -449,7 +451,7 @@
// We should still have this bundle..
bundle1 = getBundle("testbundles.bundle1");
assertNotNull("Unable to get installed test bundle?!", bundle1);
-
+
dataArea = bundle1.getDataFile("");
assertNotNull("No data area obtained for test bundle?!", dataArea);
@@ -460,7 +462,7 @@
}
/**
- * Tests that we can correctly install a deployment package with bundles that have their data area populated.
+ * Tests that we can correctly install a deployment package with bundles that have their data area populated.
*/
@Test
public void testUpgradeWithPopulatedDataAreaOk() throws Exception {
@@ -497,7 +499,7 @@
// We should still have this bundle..
bundle1 = getBundle("testbundles.bundle1");
assertNotNull("Unable to get installed test bundle?!", bundle1);
-
+
dataArea = bundle1.getDataFile("");
assertNotNull("No data area obtained for test bundle?!", dataArea);
@@ -508,7 +510,7 @@
}
/**
- * Tests that we can correctly install a deployment package with bundles that have their data area populated.
+ * Tests that we can correctly install a deployment package with bundles that have their data area populated.
*/
@Test
public void testUninstallBundleWithPopulatedDataAreaOk() throws Exception {
@@ -548,8 +550,9 @@
// Data area should be restored exactly as-is...
assertFalse("Data area not purged?!", dataArea.exists());
}
+
/**
- * Tests that we can correctly install a deployment package with bundles that have their data area populated.
+ * Tests that we can correctly install a deployment package with bundles that have their data area populated.
*/
@Test
public void testRollbackUninstallBundleWithPopulatedDataAreaOk() throws Exception {
@@ -594,7 +597,7 @@
// We should still have this bundle..
bundle1 = getBundle("testbundles.bundle1");
assertNotNull("Unable to get installed test bundle?!", bundle1);
-
+
dataArea = bundle1.getDataFile("");
assertNotNull("No data area obtained for test bundle?!", dataArea);
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java
index a4a6939..9ca98e6 100644
--- a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java
@@ -18,6 +18,8 @@
*/
package org.apache.felix.deploymentadmin.itest.util;
+import java.io.IOException;
+import java.io.InputStream;
import java.net.URL;
public class ArtifactData {
@@ -38,6 +40,13 @@
m_filename = filename;
}
+ public final InputStream createInputStream() throws IOException {
+ if (m_filter != null) {
+ return m_filter.createInputStream(m_url);
+ }
+ return m_url.openStream();
+ }
+
public String getFilename() {
return m_filename;
}
@@ -77,7 +86,7 @@
public boolean isMissing() {
return m_missing;
}
-
+
public boolean isResourceProcessorNeeded() {
return m_needRP;
}
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/CertificateUtil.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/CertificateUtil.java
new file mode 100644
index 0000000..ad81fbb
--- /dev/null
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/CertificateUtil.java
@@ -0,0 +1,123 @@
+/*
+ * 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.felix.deploymentadmin.itest.util;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Random;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+/**
+ * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
+ */
+public class CertificateUtil {
+
+ public enum KeyType {
+ RSA, DSA, EC;
+ }
+
+ public static class SignerInfo {
+ private final KeyPair m_keyPair;
+ private final X509Certificate m_cert;
+
+ public SignerInfo(KeyPair keyPair, X509Certificate cert) {
+ m_keyPair = keyPair;
+ m_cert = cert;
+ }
+
+ public X509Certificate getCert() {
+ return m_cert;
+ }
+
+ public PrivateKey getPrivate() {
+ return m_keyPair.getPrivate();
+ }
+
+ public PublicKey getPublic() {
+ return m_keyPair.getPublic();
+ }
+ }
+
+ public static SignerInfo createSelfSignedCert(String commonName, KeyType type) throws Exception {
+ KeyPairGenerator kpGen;
+ switch (type) {
+ case RSA:
+ kpGen = KeyPairGenerator.getInstance("RSA");
+ kpGen.initialize(1024);
+ break;
+ case DSA:
+ kpGen = KeyPairGenerator.getInstance("DSA");
+ break;
+ case EC:
+ kpGen = KeyPairGenerator.getInstance("EC");
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid key type!");
+ }
+
+ KeyPair keyPair = kpGen.generateKeyPair();
+ X509Certificate cert = createSelfSignedCert(commonName, keyPair);
+
+ return new SignerInfo(keyPair, cert);
+ }
+
+ private static X509Certificate createSelfSignedCert(String commonName, KeyPair keypair) throws Exception {
+ PublicKey publicKey = keypair.getPublic();
+ String keyAlg = DPSigner.getSignatureAlgorithm(publicKey);
+
+ X500Name issuer = new X500Name(commonName);
+ BigInteger serial = BigInteger.probablePrime(16, new Random());
+ Date notBefore = new Date(System.currentTimeMillis() - 1000);
+ Date notAfter = new Date(notBefore.getTime() + 6000);
+
+ SubjectPublicKeyInfo pubKeyInfo;
+ try (ASN1InputStream is = new ASN1InputStream(publicKey.getEncoded())) {
+ pubKeyInfo = SubjectPublicKeyInfo.getInstance(is.readObject());
+ }
+
+ X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuer, serial, notBefore, notAfter, issuer, pubKeyInfo);
+ builder.addExtension(new Extension(Extension.basicConstraints, true, new DEROctetString(new BasicConstraints(false))));
+
+ X509CertificateHolder certHolder = builder.build(new JcaContentSignerBuilder(keyAlg).build(keypair.getPrivate()));
+ return new JcaX509CertificateConverter().getCertificate(certHolder);
+ }
+
+ /**
+ * Creates a new CertificateUtil instance.
+ */
+ private CertificateUtil() {
+ // Nop
+ }
+
+}
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DPSigner.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DPSigner.java
new file mode 100644
index 0000000..351533c
--- /dev/null
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DPSigner.java
@@ -0,0 +1,273 @@
+/*
+ * 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.felix.deploymentadmin.itest.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAKey;
+import java.security.interfaces.ECKey;
+import java.security.interfaces.RSAKey;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DigestCalculatorProvider;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+
+/**
+ * Signs a deployment package using a given keypair.
+ */
+public class DPSigner {
+ private static final String META_INF = "META-INF/";
+
+ public static String getSignatureAlgorithm(Key key) {
+ if (key instanceof RSAKey) {
+ return "SHA256withRSA";
+ }
+ else if (key instanceof DSAKey) {
+ return "SHA1withDSA";
+ }
+ else if (key instanceof ECKey) {
+ return "SHA256withECDSA";
+ }
+ else {
+ throw new IllegalArgumentException("Invalid/unsupported key: " + key.getClass().getName());
+ }
+ }
+ private static String getBlockFileExtension(Key key) {
+ if (key instanceof RSAKey) {
+ return ".RSA";
+ }
+ else if (key instanceof DSAKey) {
+ return ".DSA";
+ }
+ else if (key instanceof ECKey) {
+ return ".EC";
+ }
+ else {
+ throw new IllegalArgumentException("Invalid/unsupported key: " + key.getClass().getName());
+ }
+ }
+ private final MessageDigest m_digest;
+
+ private final String m_digestAlg;
+
+ private final String m_baseName;
+
+ public DPSigner() {
+ this("DP");
+ }
+
+ public DPSigner(String baseName) {
+ try {
+ m_baseName = META_INF.concat(baseName);
+ m_digest = MessageDigest.getInstance("SHA-256");
+ m_digestAlg = m_digest.getAlgorithm();
+ }
+ catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("SHA-256 not supported by default?!");
+ }
+ }
+
+ public void addDigestAttribute(Attributes attrs, ArtifactData file) throws IOException {
+ attrs.putValue(m_digestAlg.concat("-Digest"), calculateDigest(file));
+ }
+
+ public void sign(DeploymentPackageBuilder builder, PrivateKey privKey, X509Certificate cert, OutputStream os) throws Exception {
+ Manifest manifest = builder.createManifest();
+ List<ArtifactData> artifacts = builder.getArtifactList();
+ sign(manifest, artifacts, privKey, cert, os);
+ }
+
+ public void sign(Manifest manifest, List<ArtifactData> files, PrivateKey privKey, X509Certificate cert, OutputStream os) throws Exception {
+ // For each file, add its signature to the manifest
+ for (ArtifactData file : files) {
+ String filename = file.getFilename();
+ Attributes attrs = manifest.getAttributes(filename);
+ addDigestAttribute(attrs, file);
+ }
+
+ try (ZipOutputStream zos = new ZipOutputStream(os)) {
+ writeSignedManifest(manifest, zos, privKey, cert);
+
+ for (ArtifactData file : files) {
+ ZipEntry entry = new ZipEntry(file.getFilename());
+ zos.putNextEntry(entry);
+
+ try (InputStream is = file.createInputStream()) {
+ byte[] buf = new byte[1024];
+ int read;
+ while ((read = is.read(buf)) > 0) {
+ zos.write(buf, 0, read);
+ }
+ }
+ }
+ }
+ }
+
+ public void writeSignedManifest(Manifest manifest, ZipOutputStream zos, PrivateKey privKey, X509Certificate cert) throws Exception {
+ zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
+ manifest.write(zos);
+ zos.closeEntry();
+
+ long now = System.currentTimeMillis();
+
+ // Determine the signature-file manifest...
+ Manifest sf = createSignatureFile(manifest);
+
+ byte[] sfRawBytes;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ sf.write(baos);
+ sfRawBytes = baos.toByteArray();
+ }
+
+ ZipEntry sigFileEntry = new ZipEntry(m_baseName.concat(".SF"));
+ sigFileEntry.setTime(now);
+ zos.putNextEntry(sigFileEntry);
+ // Write the actual entry data...
+ zos.write(sfRawBytes, 0, sfRawBytes.length);
+ zos.closeEntry();
+
+ // Create a PKCS#7 signature...
+ byte[] encoded = calculateSignatureBlock(privKey, cert, sfRawBytes);
+
+ ZipEntry blockFileEntry = new ZipEntry(m_baseName.concat(getBlockFileExtension(privKey)));
+ blockFileEntry.setTime(now);
+ zos.putNextEntry(blockFileEntry);
+ zos.write(encoded);
+ zos.closeEntry();
+ }
+
+ private String calculateDigest(ArtifactData file) throws IOException {
+ m_digest.reset();
+ try (InputStream is = file.createInputStream()) {
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = is.read(buffer)) > 0) {
+ m_digest.update(buffer, 0, read);
+ }
+ }
+ return Base64.encodeBase64String(m_digest.digest());
+ }
+
+ private String calculateDigest(byte[] rawData) throws IOException {
+ m_digest.reset();
+ m_digest.update(rawData, 0, rawData.length);
+ return Base64.encodeBase64String(m_digest.digest());
+ }
+
+ private byte[] calculateSignatureBlock(PrivateKey privKey, X509Certificate cert, byte[] sfRawBytes) throws Exception {
+ String signatureAlgorithm = getSignatureAlgorithm(privKey);
+
+ DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();
+ ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).build(privKey);
+
+ CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
+ gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider).build(signer, cert));
+ gen.addCertificates(new JcaCertStore(Arrays.asList(cert)));
+
+ CMSSignedData sigData = gen.generate(new CMSProcessableByteArray(sfRawBytes));
+
+ return sigData.getEncoded();
+ }
+
+ private Manifest createSignatureFile(Manifest manifest) throws IOException {
+ byte[] mfRawBytes;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ manifest.write(baos);
+ mfRawBytes = baos.toByteArray();
+ }
+
+ Manifest sf = new Manifest();
+ Attributes sfMain = sf.getMainAttributes();
+ Map<String, Attributes> sfEntries = sf.getEntries();
+
+ sfMain.put(Attributes.Name.SIGNATURE_VERSION, "1.0");
+ sfMain.putValue("Created-By", "Apache Felix DeploymentPackageBuilder");
+ sfMain.putValue(m_digestAlg + "-Digest-Manifest", calculateDigest(mfRawBytes));
+ sfMain.putValue(m_digestAlg + "-Digest-Manifest-Main-Attribute", calculateDigest(getRawBytesMainAttributes(manifest)));
+
+ for (Entry<String, Attributes> entry : manifest.getEntries().entrySet()) {
+ String name = entry.getKey();
+ byte[] entryData = getRawBytesAttributes(entry.getValue());
+
+ sfEntries.put(name, getDigestAttributes(entryData));
+ }
+ return sf;
+ }
+
+ private Attributes getDigestAttributes(byte[] rawData) throws IOException {
+ Attributes attrs = new Attributes();
+ attrs.putValue(m_digestAlg + "-Digest", calculateDigest(rawData));
+ return attrs;
+ }
+
+ private byte[] getRawBytesAttributes(Attributes attrs) throws IOException {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) {
+
+ Method m = Attributes.class.getDeclaredMethod("write", DataOutputStream.class);
+ m.setAccessible(true);
+ m.invoke(attrs, dos);
+
+ return baos.toByteArray();
+ }
+ catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException("Failed to get raw bytes of main attributes!", e);
+ }
+ }
+
+ private byte[] getRawBytesMainAttributes(Manifest manifest) throws IOException {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) {
+ Attributes attrs = manifest.getMainAttributes();
+
+ Method m = Attributes.class.getDeclaredMethod("writeMain", DataOutputStream.class);
+ m.setAccessible(true);
+ m.invoke(attrs, dos);
+
+ return baos.toByteArray();
+ }
+ catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException("Failed to get raw bytes of main attributes!", e);
+ }
+ }
+}
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java
index 727f20d..a050ef3 100644
--- a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java
@@ -25,6 +25,8 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@@ -32,9 +34,11 @@
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
import org.osgi.framework.Version;
@@ -100,6 +104,27 @@
private static final int BUFFER_SIZE = 32 * 1024;
+ private final DPSigner m_signer;
+ private final String m_symbolicName;
+ private final String m_version;
+ private final List<ArtifactData> m_localizationFiles = new ArrayList<ArtifactData>();
+ private final List<ArtifactData> m_bundles = new ArrayList<ArtifactData>();
+ private final List<ArtifactData> m_processors = new ArrayList<ArtifactData>();
+ private final List<ArtifactData> m_artifacts = new ArrayList<ArtifactData>();
+
+ private String m_fixPackageVersion;
+ private boolean m_verification;
+ private PrivateKey m_signingKey;
+ private X509Certificate m_signingCert;
+
+ private DeploymentPackageBuilder(String symbolicName, String version) {
+ m_symbolicName = symbolicName;
+ m_version = version;
+
+ m_verification = true;
+ m_signer = new DPSigner();
+ }
+
/**
* Creates a new deployment package builder.
*
@@ -111,25 +136,15 @@
return new DeploymentPackageBuilder(name, version);
}
- private final String m_symbolicName;
- private final String m_version;
- private final List<ArtifactData> m_localizationFiles = new ArrayList<ArtifactData>();
- private final List<ArtifactData> m_bundles = new ArrayList<ArtifactData>();
- private final List<ArtifactData> m_processors = new ArrayList<ArtifactData>();
-
- private final List<ArtifactData> m_artifacts = new ArrayList<ArtifactData>();
-
- private String m_fixPackageVersion;
-
- private boolean m_addSignatures;
- private boolean m_verification;
-
- private DeploymentPackageBuilder(String symbolicName, String version) {
- m_symbolicName = symbolicName;
- m_version = version;
-
- m_addSignatures = false;
- m_verification = true;
+ static void closeSilently(Closeable resource) {
+ if (resource != null) {
+ try {
+ resource.close();
+ }
+ catch (IOException e) {
+ // Ignore...
+ }
+ }
}
/**
@@ -156,11 +171,6 @@
return this;
}
- public DeploymentPackageBuilder addSignatures() {
- m_addSignatures = true;
- return this;
- }
-
/**
* Creates a new deployment package builder with the same symbolic name as this builder.
*
@@ -210,7 +220,6 @@
public InputStream generate() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
generate(baos);
-
return new ByteArrayInputStream(baos.toByteArray());
}
@@ -223,26 +232,8 @@
* @throws Exception if something goes wrong while validating or generating
*/
public void generate(OutputStream output) throws Exception {
- List<ArtifactData> artifacts = new ArrayList<ArtifactData>();
- artifacts.addAll(m_localizationFiles);
- artifacts.addAll(m_bundles);
- artifacts.addAll(m_processors);
- artifacts.addAll(m_artifacts);
-
- if (m_verification) {
- validateProcessedArtifacts();
- validateMissingArtifacts(artifacts);
- }
-
- Manifest m = createManifest(artifacts);
-
- // The order in which the actual entries are added to the JAR is different than we're using for the manifest...
- artifacts.clear();
- artifacts.addAll(m_bundles);
- artifacts.addAll(m_processors);
- artifacts.addAll(m_localizationFiles);
- artifacts.addAll(m_artifacts);
-
+ Manifest m = createManifest();
+ List<ArtifactData> artifacts = getArtifactList();
writeStream(artifacts, m, output);
}
@@ -283,6 +274,47 @@
return this;
}
+ /**
+ * Enables the creating of a signed deployment package, equivalent to creating a signed JAR file.
+ * <p>
+ * This method assumes the use of self-signed certificates for the signing process.
+ * </p>
+ *
+ * @param signingKey the private key of the signer;
+ * @param signingCert the public certificate of the signer.
+ * @return this builder.
+ */
+ public DeploymentPackageBuilder signOutput(PrivateKey signingKey, X509Certificate signingCert) {
+ m_signingKey = signingKey;
+ m_signingCert = signingCert;
+ return this;
+ }
+
+ final Manifest createManifest() throws Exception {
+ List<ArtifactData> artifacts = new ArrayList<ArtifactData>();
+ artifacts.addAll(m_localizationFiles);
+ artifacts.addAll(m_bundles);
+ artifacts.addAll(m_processors);
+ artifacts.addAll(m_artifacts);
+
+ if (m_verification) {
+ validateProcessedArtifacts();
+ validateMissingArtifacts(artifacts);
+ }
+
+ return createManifest(artifacts);
+ }
+
+ final List<ArtifactData> getArtifactList() {
+ // The order in which the actual entries are added to the JAR is different than we're using for the manifest...
+ List<ArtifactData> artifacts = new ArrayList<ArtifactData>();
+ artifacts.addAll(m_bundles);
+ artifacts.addAll(m_processors);
+ artifacts.addAll(m_localizationFiles);
+ artifacts.addAll(m_artifacts);
+ return artifacts;
+ }
+
private Manifest createManifest(List<ArtifactData> files) throws Exception {
Manifest manifest = new Manifest();
Attributes main = manifest.getMainAttributes();
@@ -296,39 +328,40 @@
Map<String, Attributes> entries = manifest.getEntries();
- Iterator<ArtifactData> filesIter = files.iterator();
- while (filesIter.hasNext()) {
- ArtifactData file = filesIter.next();
-
- Attributes a = new Attributes();
- a.putValue("Name", file.getFilename());
+ for (ArtifactData file : files) {
+ Attributes attrs = new Attributes();
+ attrs.putValue("Name", file.getFilename());
if (file.isBundle()) {
- a.putValue("Bundle-SymbolicName", file.getSymbolicName());
- a.putValue("Bundle-Version", file.getVersion());
+ attrs.putValue("Bundle-SymbolicName", file.getSymbolicName());
+ attrs.putValue("Bundle-Version", file.getVersion());
if (file.isCustomizer()) {
- a.putValue("DeploymentPackage-Customizer", "true");
- a.putValue("Deployment-ProvidesResourceProcessor", file.getProcessorPid());
+ attrs.putValue("DeploymentPackage-Customizer", "true");
+ attrs.putValue("Deployment-ProvidesResourceProcessor", file.getProcessorPid());
}
}
else if (file.isResourceProcessorNeeded()) {
- a.putValue("Resource-Processor", file.getProcessorPid());
+ attrs.putValue("Resource-Processor", file.getProcessorPid());
}
if (file.isMissing()) {
- a.putValue("DeploymentPackage-Missing", "true");
+ attrs.putValue("DeploymentPackage-Missing", "true");
}
- if (m_addSignatures) {
- a.putValue("SHA-256-Digest", "bogusdata=");
+ if (isAddSignatures()) {
+ m_signer.addDigestAttribute(attrs, file);
}
- entries.put(file.getFilename(), a);
+ entries.put(file.getFilename(), attrs);
}
return manifest;
}
+ private boolean isAddSignatures() {
+ return m_signingKey != null && m_signingCert != null;
+ }
+
private void validateMissingArtifacts(List<ArtifactData> files) throws Exception {
boolean missing = false;
@@ -367,44 +400,21 @@
}
}
- private InputStream getArtifactDataInputStream(ArtifactData file) throws IOException {
- ResourceFilter filter = file.getFilter();
- if (filter != null) {
- return filter.createInputStream(file.getURL());
- }
- return file.getURL().openStream();
- }
-
private void writeStream(List<ArtifactData> files, Manifest manifest, OutputStream outputStream) throws Exception {
byte[] buffer = new byte[BUFFER_SIZE];
- JarOutputStream output = null;
- InputStream is = null;
- try {
- output = new JarOutputStream(outputStream, manifest);
- if (m_addSignatures) {
- // Empty file index...
- output.putNextEntry(new JarEntry("META-INF/INDEX.LIST"));
- output.write(new byte[0]);
- output.closeEntry();
-
- // Create a signature file + signature block
- Manifest mf = new Manifest();
- mf.getMainAttributes().put(Attributes.Name.SIGNATURE_VERSION, "1.0");
- mf.getMainAttributes().putValue("SHA-256-Digest-Manifest", "bogusdata=");
-
- output.putNextEntry(new JarEntry("META-INF/DP.SF"));
- mf.write(output);
- output.closeEntry();
-
- output.putNextEntry(new JarEntry("META-INF/DP.DSA"));
- output.write(new byte[] { 1, 2, 3, 4 });
+ try (JarOutputStream output = new JarOutputStream(outputStream)) {
+ // Write out the manifest...
+ if (isAddSignatures()) {
+ m_signer.writeSignedManifest(manifest, output, m_signingKey, m_signingCert);
+ }
+ else {
+ output.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
+ manifest.write(output);
output.closeEntry();
}
- Iterator<ArtifactData> filesIter = files.iterator();
- while (filesIter.hasNext()) {
- ArtifactData file = filesIter.next();
+ for (ArtifactData file : files) {
if (file.isMissing()) {
// No need to write the 'missing' files...
continue;
@@ -412,34 +422,16 @@
output.putNextEntry(new JarEntry(file.getFilename()));
- is = getArtifactDataInputStream(file);
- try {
+ try (InputStream is = file.createInputStream()) {
int bytes;
while ((bytes = is.read(buffer)) != -1) {
output.write(buffer, 0, bytes);
}
}
finally {
- closeSilently(is);
-
output.closeEntry();
}
}
}
- finally {
- closeSilently(is);
- closeSilently(output);
- }
- }
-
- static void closeSilently(Closeable resource) {
- if (resource != null) {
- try {
- resource.close();
- }
- catch (IOException e) {
- // Ignore...
- }
- }
}
}