Skip to content

Commit

Permalink
Merge pull request #937 from amvanbaren/sigzip-manifest
Browse files Browse the repository at this point in the history
Generate .signature.manifest
  • Loading branch information
amvanbaren authored Jun 7, 2024
2 parents e318870 + ffbf2d4 commit 32252d3
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
* ****************************************************************************** */
package org.eclipse.openvsx.publish;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.observation.ObservationRegistry;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
Expand All @@ -21,6 +26,7 @@
import org.eclipse.openvsx.entities.ExtensionVersion;
import org.eclipse.openvsx.entities.FileResource;
import org.eclipse.openvsx.entities.SignatureKeyPair;
import org.eclipse.openvsx.util.ArchiveUtil;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.NamingUtil;
import org.eclipse.openvsx.util.TempFile;
Expand All @@ -34,6 +40,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
Expand Down Expand Up @@ -121,6 +128,11 @@ public FileResource generateSignature(FileResource download, TempFile extensionF
zip.write(signer.generateSignature());
zip.closeEntry();

var manifestEntry = new ZipEntry(".signature.manifest");
zip.putNextEntry(manifestEntry);
zip.write(generateSignatureManifest(extensionFile));
zip.closeEntry();

// Add dummy file to the archive because VS Code checks if it exists
var dummyEntry = new ZipEntry(".signature.p7s");
zip.putNextEntry(dummyEntry);
Expand All @@ -135,4 +147,35 @@ public FileResource generateSignature(FileResource download, TempFile extensionF

return resource;
}

private byte[] generateSignatureManifest(TempFile extensionFile) throws IOException {
var base64 = new Base64();
var mapper = new ObjectMapper();
var manifestEntries = mapper.createObjectNode();
try(var zip = new ZipFile(extensionFile.getPath().toFile())) {
zip.stream()
.filter(entry -> !entry.isDirectory())
.forEach(entry -> {
var content = ArchiveUtil.readEntry(zip, entry, ObservationRegistry.NOOP);
var manifestEntry = generateManifestEntry(content, mapper, base64);
manifestEntries.set(new String(base64.encode(entry.getName().getBytes(StandardCharsets.UTF_8))), manifestEntry);
});
}

var manifest = mapper.createObjectNode();
manifest.set("package", generateManifestEntry(Files.readAllBytes(extensionFile.getPath()), mapper, base64));
manifest.set("entries", manifestEntries);
return mapper.writeValueAsBytes(manifest);
}

private JsonNode generateManifestEntry(byte[] content, ObjectMapper mapper, Base64 base64) {
var manifestEntry = mapper.createObjectNode();
manifestEntry.put("size", content.length);

var manifestEntryDigests = mapper.createObjectNode();
var sha256 = new String(base64.encode(DigestUtils.sha256(content)));
manifestEntryDigests.put("sha256", sha256);
manifestEntry.set("digests", manifestEntryDigests);
return manifestEntry;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/** ******************************************************************************
* Copyright (c) 2024 Precies. Software OU and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
* ****************************************************************************** */
package org.eclipse.openvsx.publish;

import io.micrometer.observation.ObservationRegistry;
import jakarta.persistence.EntityManager;
import org.eclipse.openvsx.cache.CacheService;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.migration.GenerateKeyPairJobService;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.util.ArchiveUtil;
import org.eclipse.openvsx.util.TempFile;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.io.IOException;
import java.nio.file.Files;
import java.util.zip.ZipFile;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@MockBean({ CacheService.class, RepositoryService.class })
public class ExtensionVersionIntegrityServiceTest {

@MockBean
EntityManager entityManager;

@Autowired
ExtensionVersionIntegrityService integrityService;

@Autowired
GenerateKeyPairJobService keyPairService;

@Test
public void testGenerateSignature() throws IOException {
keyPairService.generateKeyPair();
var keyPairCaptor = ArgumentCaptor.forClass(SignatureKeyPair.class);
Mockito.verify(entityManager).persist(keyPairCaptor.capture());
var keyPair = keyPairCaptor.getValue();

var namespace = new Namespace();
namespace.setName("foo");

var extension = new Extension();
extension.setName("bar");
extension.setNamespace(namespace);

var extVersion = new ExtensionVersion();
extVersion.setVersion("1.0.0");
extVersion.setTargetPlatform("universal");
extVersion.setExtension(extension);

var download = new FileResource();
download.setExtension(extVersion);

var sigzipContent = new byte[0];
try (
var stream = getClass().getResource("ms-python.python-2024.7.11511013.vsix").openStream();
var extensionFile = new TempFile("ms-python", ".vsix")
) {
Files.write(extensionFile.getPath(), stream.readAllBytes());
var signature = integrityService.generateSignature(download, extensionFile, keyPair);
sigzipContent = signature.getContent();
}

try(var temp = new TempFile("ms-python", ".sigzip")) {
Files.write(temp.getPath(), sigzipContent);
try (
var sigzip = new ZipFile(temp.getPath().toFile());
var expectedSigZip = new ZipFile(getClass().getResource("ms-python.python-2024.7.11511013.sigzip").getPath())
) {
expectedSigZip.stream()
.forEach(expectedEntry -> {
var entry = sigzip.getEntry(expectedEntry.getName());
assertNotNull(entry);
if(expectedEntry.getName().equals(".signature.manifest")) {
assertEquals(
new String(ArchiveUtil.readEntry(expectedSigZip, expectedEntry, ObservationRegistry.NOOP)),
new String(ArchiveUtil.readEntry(sigzip, entry, ObservationRegistry.NOOP))
);
}
});

var entry = sigzip.getEntry(".signature.sig");
assertNotNull(entry);
assertTrue(ArchiveUtil.readEntry(sigzip, entry, ObservationRegistry.NOOP).length > 0);
}
}
}

@TestConfiguration
static class TestConfig {
@Bean
ExtensionVersionIntegrityService extensionVersionIntegrityService(EntityManager entityManager, CacheService cacheService) {
return new ExtensionVersionIntegrityService(entityManager, cacheService);
}

@Bean
GenerateKeyPairJobService generateKeyPairJobService(EntityManager entityManager, RepositoryService repositoryService) {
return new GenerateKeyPairJobService(entityManager, repositoryService);
}
}
}
Binary file not shown.
Binary file not shown.

0 comments on commit 32252d3

Please sign in to comment.