Skip to content

Commit

Permalink
Merge pull request #673 from amvanbaren/feature/issue-543
Browse files Browse the repository at this point in the history
Extension repository signing
  • Loading branch information
amvanbaren authored Apr 17, 2023
2 parents 902a037 + 5540665 commit 65f023e
Show file tree
Hide file tree
Showing 66 changed files with 1,745 additions and 216 deletions.
4 changes: 3 additions & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def versions = [
jobrunr: '5.1.2',
bucket4j: '0.4.0',
ehcache: '3.10.0',
tika: '2.6.0'
tika: '2.6.0',
bouncycastle: '1.69'
]
ext['junit-jupiter.version'] = versions.junit
sourceCompatibility = versions.java
Expand Down Expand Up @@ -71,6 +72,7 @@ dependencies {
implementation "org.springframework.security:spring-security-oauth2-jose"
implementation "org.springframework.session:spring-session-jdbc"
implementation "org.springframework.retry:spring-retry"
implementation "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}"
implementation "org.ehcache:ehcache:${versions.ehcache}"
implementation "com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:${versions.bucket4j}"
implementation "org.jobrunr:jobrunr-spring-boot-starter:${versions.jobrunr}"
Expand Down
2 changes: 2 additions & 0 deletions server/src/dev/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,5 @@ ovsx:
base-url: https://api.eclipse.org
publisher-agreement:
timezone: US/Eastern
integrity:
key-pair: create # create, renew, delete, 'undefined'
22 changes: 7 additions & 15 deletions server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -329,27 +329,19 @@ public void processEachResource(ExtensionVersion extVersion, Consumer<FileResour
.forEach(processor);
}

public FileResource getBinary(ExtensionVersion extVersion) {
public FileResource getBinary(ExtensionVersion extVersion, String binaryName) {
if(binaryName == null) {
binaryName = NamingUtil.toFileFormat(extVersion, ".vsix");
}

var binary = new FileResource();
binary.setExtension(extVersion);
binary.setName(getBinaryName(extVersion));
binary.setName(binaryName);
binary.setType(FileResource.DOWNLOAD);
binary.setContent(null);
return binary;
}

public String getBinaryName(ExtensionVersion extVersion) {
var extension = extVersion.getExtension();
var namespace = extension.getNamespace();
var resourceName = namespace.getName() + "." + extension.getName() + "-" + extVersion.getVersion();
if(!TargetPlatform.isUniversal(extVersion.getTargetPlatform())) {
resourceName += "@" + extVersion.getTargetPlatform();
}

resourceName += ".vsix";
return resourceName;
}

public FileResource generateSha256Checksum(ExtensionVersion extVersion) {
String hash = null;
try(var input = Files.newInputStream(extensionFile.getPath())) {
Expand All @@ -364,7 +356,7 @@ public FileResource generateSha256Checksum(ExtensionVersion extVersion) {

var sha256 = new FileResource();
sha256.setExtension(extVersion);
sha256.setName(getBinaryName(extVersion).replace(".vsix", ".sha256"));
sha256.setName(NamingUtil.toFileFormat(extVersion, ".sha256"));
sha256.setType(FileResource.DOWNLOAD_SHA256);
sha256.setContent(hash.getBytes(StandardCharsets.UTF_8));
return sha256;
Expand Down
13 changes: 6 additions & 7 deletions server/src/main/java/org/eclipse/openvsx/ExtensionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,20 @@ public class ExtensionService {
boolean requireLicense;

@Transactional
public ExtensionVersion mirrorVersion(TempFile extensionFile, PersonalAccessToken token, String binaryName, String timestamp) {
var download = doPublish(extensionFile, token, TimeUtil.fromUTCString(timestamp), false);
publishHandler.mirror(download, extensionFile);
download.setName(binaryName);
public ExtensionVersion mirrorVersion(TempFile extensionFile, String signatureName, PersonalAccessToken token, String binaryName, String timestamp) {
var download = doPublish(extensionFile, binaryName, token, TimeUtil.fromUTCString(timestamp), false);
publishHandler.mirror(download, extensionFile, signatureName);
return download.getExtension();
}

public ExtensionVersion publishVersion(InputStream content, PersonalAccessToken token) {
var extensionFile = createExtensionFile(content);
var download = doPublish(extensionFile, token, TimeUtil.getCurrentUTC(), true);
var download = doPublish(extensionFile, null, token, TimeUtil.getCurrentUTC(), true);
publishHandler.publishAsync(download, extensionFile, this);
return download.getExtension();
}

private FileResource doPublish(TempFile extensionFile, PersonalAccessToken token, LocalDateTime timestamp, boolean checkDependencies) {
private FileResource doPublish(TempFile extensionFile, String binaryName, PersonalAccessToken token, LocalDateTime timestamp, boolean checkDependencies) {
try (var processor = new ExtensionProcessor(extensionFile)) {
var extVersion = publishHandler.createExtensionVersion(processor, token, timestamp, checkDependencies);
if (requireLicense) {
Expand All @@ -78,7 +77,7 @@ private FileResource doPublish(TempFile extensionFile, PersonalAccessToken token
checkLicense(extVersion, license);
}

return processor.getBinary(extVersion);
return processor.getBinary(extVersion, binaryName);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ public interface IExtensionRegistry {
NamespaceDetailsJson getNamespaceDetails(String namespace);

ResponseEntity<byte[]> getNamespaceLogo(String namespaceName, String fileName);

String getPublicKey(String publicId);
}
78 changes: 63 additions & 15 deletions server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.eclipse.openvsx.eclipse.EclipseService;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.json.*;
import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.search.ExtensionSearch;
import org.eclipse.openvsx.search.ISearchService;
Expand Down Expand Up @@ -82,6 +83,9 @@ public class LocalRegistryService implements IExtensionRegistry {
@Autowired
CacheService cache;

@Autowired
ExtensionVersionIntegrityService integrityService;

@Override
public NamespaceJson getNamespace(String namespaceName) {
var namespace = repositories.findNamespace(namespaceName);
Expand Down Expand Up @@ -130,7 +134,7 @@ private Map<String, String> getDownloads(Extension extension, String targetPlatf
var download = files != null ? files.get(DOWNLOAD) : null;
if(download == null) {
var e = ev.getExtension();
logger.warn("Could not find download for: {}.{}-{}@{}", e.getNamespace().getName(), e.getName(), ev.getVersion(), ev.getTargetPlatform());
logger.warn("Could not find download for: {}", NamingUtil.toLogFormat(ev));
return null;
} else {
return new AbstractMap.SimpleEntry<>(ev.getTargetPlatform(), download);
Expand Down Expand Up @@ -182,7 +186,11 @@ public ResponseEntity<byte[]> getFile(String namespace, String extensionName, St
}

public boolean isType (String fileName){
var expectedTypes = List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, DOWNLOAD_SHA256, CHANGELOG, VSIXMANIFEST);
var expectedTypes = new ArrayList<>(List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, DOWNLOAD_SHA256, CHANGELOG, VSIXMANIFEST));
if(integrityService.isEnabled()) {
expectedTypes.add(DOWNLOAD_SIG);
}

return expectedTypes.stream().anyMatch(fileName::equalsIgnoreCase);
}

Expand Down Expand Up @@ -431,10 +439,26 @@ private SearchEntryJson toSearchEntryJson(Extension extension) {
var extVersion = versions.getLatest(extension, null, false, true);
var entry = extVersion.toSearchEntryJson();
entry.url = createApiUrl(serverUrl, "api", entry.namespace, entry.name);
entry.files = storageUtil.getFileUrls(extVersion, serverUrl, DOWNLOAD, DOWNLOAD_SHA256, ICON);
entry.files = storageUtil.getFileUrls(extVersion, serverUrl, withFileTypes(DOWNLOAD, ICON));
if(entry.files.containsKey(DOWNLOAD_SIG)) {
entry.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
}

return entry;
}

private String[] withFileTypes(String... types) {
var typesList = new ArrayList<>(List.of(types));
if(typesList.contains(DOWNLOAD)) {
typesList.add(DOWNLOAD_SHA256);
if(integrityService.isEnabled()) {
typesList.add(DOWNLOAD_SIG);
}
}

return typesList.toArray(String[]::new);
}

@Override
public ResponseEntity<byte[]> getNamespaceLogo(String namespaceName, String fileName) {
if(fileName == null) {
Expand Down Expand Up @@ -515,7 +539,7 @@ private Map<Long, List<FileResource>> getFileResources(List<ExtensionVersion> ex
return Collections.emptyMap();
}

var fileTypes = List.of(DOWNLOAD, DOWNLOAD_SHA256, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST);
var fileTypes = List.of(withFileTypes(DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST));
var extensionVersionIds = extensionVersions.stream()
.map(ExtensionVersion::getId)
.collect(Collectors.toSet());
Expand Down Expand Up @@ -674,8 +698,7 @@ public ExtensionJson publish(InputStream content, String tokenValue) throws Erro
var semver = extVersion.getSemanticVersion();
var newVersion = String.join(".", String.valueOf(semver.getMajor()), String.valueOf(semver.getMinor() + 1), "0");

json.warning = "A " + existingRelease + " already exists for " +
extension.getNamespace().getName() + "." + extension.getName() + "-" + extVersion.getVersion() + ".\n" +
json.warning = "A " + existingRelease + " already exists for " + NamingUtil.toLogFormat(extension.getNamespace().getName(), extension.getName(), extVersion.getVersion()) + ".\n" +
"To prevent update conflicts, we recommend that this " + thisRelease + " uses " + newVersion + " as its version instead.";
}

Expand All @@ -690,7 +713,8 @@ public ResultJson postReview(ReviewJson review, String namespace, String extensi
}
var extension = repositories.findExtension(extensionName, namespace);
if (extension == null || !extension.isActive()) {
return ResultJson.error("Extension not found: " + namespace + "." + extensionName);
var extensionId = NamingUtil.toExtensionId(namespace, extensionName);
return ResultJson.error("Extension not found: " + extensionId);
}
var activeReviews = repositories.findActiveReviews(extension, user);
if (!activeReviews.isEmpty()) {
Expand All @@ -711,7 +735,7 @@ public ResultJson postReview(ReviewJson review, String namespace, String extensi
search.updateSearchEntry(extension);
cache.evictExtensionJsons(extension);
cache.evictLatestExtensionVersion(extension);
return ResultJson.success("Added review for " + extension.getNamespace().getName() + "." + extension.getName());
return ResultJson.success("Added review for " + NamingUtil.toExtensionId(extension));
}

@Transactional(rollbackOn = ResponseStatusException.class)
Expand All @@ -722,7 +746,7 @@ public ResultJson deleteReview(String namespace, String extensionName) {
}
var extension = repositories.findExtension(extensionName, namespace);
if (extension == null || !extension.isActive()) {
return ResultJson.error("Extension not found: " + namespace + "." + extensionName);
return ResultJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extensionName));
}
var activeReviews = repositories.findActiveReviews(extension, user);
if (activeReviews.isEmpty()) {
Expand All @@ -738,7 +762,7 @@ public ResultJson deleteReview(String namespace, String extensionName) {
search.updateSearchEntry(extension);
cache.evictExtensionJsons(extension);
cache.evictLatestExtensionVersion(extension);
return ResultJson.success("Deleted review for " + extension.getNamespace().getName() + "." + extension.getName());
return ResultJson.success("Deleted review for " + NamingUtil.toExtensionId(extension));
}

private Extension getExtension(SearchHit<ExtensionSearch> searchHit) {
Expand Down Expand Up @@ -776,8 +800,14 @@ private List<SearchEntryJson> toSearchEntries(SearchHits<ExtensionSearch> search
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

var fileUrls = storageUtil.getFileUrls(latestVersions.values(), serverUrl, DOWNLOAD, DOWNLOAD_SHA256, ICON);
searchEntries.forEach((extensionId, searchEntry) -> searchEntry.files = fileUrls.get(latestVersions.get(extensionId).getId()));
var fileUrls = storageUtil.getFileUrls(latestVersions.values(), serverUrl, withFileTypes(DOWNLOAD, ICON));
searchEntries.forEach((extensionId, searchEntry) -> {
var extVersion = latestVersions.get(extensionId);
searchEntry.files = fileUrls.get(extVersion.getId());
if(searchEntry.files.containsKey(DOWNLOAD_SIG)) {
searchEntry.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
}
});
if (options.includeAllVersions) {
var allActiveVersions = repositories.findActiveVersions(extensions).stream()
.sorted(ExtensionVersion.SORT_COMPARATOR)
Expand Down Expand Up @@ -867,8 +897,11 @@ public ExtensionJson toExtensionVersionJson(ExtensionVersion extVersion, String
.forEach(e -> json.allVersions.put(e.getKey(), e.getValue()));
}

var fileUrls = storageUtil.getFileUrls(List.of(extVersion), serverUrl, DOWNLOAD, DOWNLOAD_SHA256, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST);
var fileUrls = storageUtil.getFileUrls(List.of(extVersion), serverUrl, withFileTypes(DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST));
json.files = fileUrls.get(extVersion.getId());
if(json.files.containsKey(DOWNLOAD_SIG)) {
json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
}
if (json.dependencies != null) {
json.dependencies.forEach(ref -> {
ref.url = createApiUrl(serverUrl, "api", ref.namespace, ref.extension);
Expand Down Expand Up @@ -928,12 +961,15 @@ public ExtensionJson toExtensionVersionJson(
json.allVersions.put(version, createApiUrl(versionBaseUrl, version));
}

json.files = Maps.newLinkedHashMapWithExpectedSize(6);
json.files = Maps.newLinkedHashMapWithExpectedSize(8);
var fileBaseUrl = UrlUtil.createApiFileBaseUrl(serverUrl, json.namespace, json.name, json.targetPlatform, json.version);
for (var resource : resources) {
var fileUrl = UrlUtil.createApiFileUrl(fileBaseUrl, resource.getName());
json.files.put(resource.getType(), fileUrl);
}
if(json.files.containsKey(DOWNLOAD_SIG)) {
json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
}

if (json.dependencies != null) {
json.dependencies.forEach(ref -> {
Expand Down Expand Up @@ -1001,12 +1037,15 @@ public ExtensionJson toExtensionVersionJsonV2(
}
}

json.files = Maps.newLinkedHashMapWithExpectedSize(6);
json.files = Maps.newLinkedHashMapWithExpectedSize(8);
var fileBaseUrl = UrlUtil.createApiFileBaseUrl(serverUrl, json.namespace, json.name, json.targetPlatform, json.version);
for (var resource : resources) {
var fileUrl = UrlUtil.createApiFileUrl(fileBaseUrl, resource.getName());
json.files.put(resource.getType(), fileUrl);
}
if(json.files.containsKey(DOWNLOAD_SIG)) {
json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
}

if (json.dependencies != null) {
json.dependencies.forEach(ref -> {
Expand Down Expand Up @@ -1051,4 +1090,13 @@ private boolean isVerified(ExtensionVersion extVersion, Map<Long, List<Namespace
return memberships.stream().anyMatch(m -> m.getRole().equalsIgnoreCase(NamespaceMembership.ROLE_OWNER))
&& memberships.stream().anyMatch(m -> m.getUser().getId() == user.getId());
}

public String getPublicKey(String publicId) {
var keyPair = repositories.findKeyPair(publicId);
if(keyPair == null) {
throw new NotFoundException();
}

return keyPair.getPublicKeyText();
}
}
Loading

0 comments on commit 65f023e

Please sign in to comment.