Skip to content

Commit 65f023e

Browse files
authored
Merge pull request #673 from amvanbaren/feature/issue-543
Extension repository signing
2 parents 902a037 + 5540665 commit 65f023e

File tree

66 files changed

+1745
-216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1745
-216
lines changed

server/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def versions = [
3232
jobrunr: '5.1.2',
3333
bucket4j: '0.4.0',
3434
ehcache: '3.10.0',
35-
tika: '2.6.0'
35+
tika: '2.6.0',
36+
bouncycastle: '1.69'
3637
]
3738
ext['junit-jupiter.version'] = versions.junit
3839
sourceCompatibility = versions.java
@@ -71,6 +72,7 @@ dependencies {
7172
implementation "org.springframework.security:spring-security-oauth2-jose"
7273
implementation "org.springframework.session:spring-session-jdbc"
7374
implementation "org.springframework.retry:spring-retry"
75+
implementation "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}"
7476
implementation "org.ehcache:ehcache:${versions.ehcache}"
7577
implementation "com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:${versions.bucket4j}"
7678
implementation "org.jobrunr:jobrunr-spring-boot-starter:${versions.jobrunr}"

server/src/dev/resources/application.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,5 @@ ovsx:
130130
base-url: https://api.eclipse.org
131131
publisher-agreement:
132132
timezone: US/Eastern
133+
integrity:
134+
key-pair: create # create, renew, delete, 'undefined'

server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -329,27 +329,19 @@ public void processEachResource(ExtensionVersion extVersion, Consumer<FileResour
329329
.forEach(processor);
330330
}
331331

332-
public FileResource getBinary(ExtensionVersion extVersion) {
332+
public FileResource getBinary(ExtensionVersion extVersion, String binaryName) {
333+
if(binaryName == null) {
334+
binaryName = NamingUtil.toFileFormat(extVersion, ".vsix");
335+
}
336+
333337
var binary = new FileResource();
334338
binary.setExtension(extVersion);
335-
binary.setName(getBinaryName(extVersion));
339+
binary.setName(binaryName);
336340
binary.setType(FileResource.DOWNLOAD);
337341
binary.setContent(null);
338342
return binary;
339343
}
340344

341-
public String getBinaryName(ExtensionVersion extVersion) {
342-
var extension = extVersion.getExtension();
343-
var namespace = extension.getNamespace();
344-
var resourceName = namespace.getName() + "." + extension.getName() + "-" + extVersion.getVersion();
345-
if(!TargetPlatform.isUniversal(extVersion.getTargetPlatform())) {
346-
resourceName += "@" + extVersion.getTargetPlatform();
347-
}
348-
349-
resourceName += ".vsix";
350-
return resourceName;
351-
}
352-
353345
public FileResource generateSha256Checksum(ExtensionVersion extVersion) {
354346
String hash = null;
355347
try(var input = Files.newInputStream(extensionFile.getPath())) {
@@ -364,7 +356,7 @@ public FileResource generateSha256Checksum(ExtensionVersion extVersion) {
364356

365357
var sha256 = new FileResource();
366358
sha256.setExtension(extVersion);
367-
sha256.setName(getBinaryName(extVersion).replace(".vsix", ".sha256"));
359+
sha256.setName(NamingUtil.toFileFormat(extVersion, ".sha256"));
368360
sha256.setType(FileResource.DOWNLOAD_SHA256);
369361
sha256.setContent(hash.getBytes(StandardCharsets.UTF_8));
370362
return sha256;

server/src/main/java/org/eclipse/openvsx/ExtensionService.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,20 @@ public class ExtensionService {
5555
boolean requireLicense;
5656

5757
@Transactional
58-
public ExtensionVersion mirrorVersion(TempFile extensionFile, PersonalAccessToken token, String binaryName, String timestamp) {
59-
var download = doPublish(extensionFile, token, TimeUtil.fromUTCString(timestamp), false);
60-
publishHandler.mirror(download, extensionFile);
61-
download.setName(binaryName);
58+
public ExtensionVersion mirrorVersion(TempFile extensionFile, String signatureName, PersonalAccessToken token, String binaryName, String timestamp) {
59+
var download = doPublish(extensionFile, binaryName, token, TimeUtil.fromUTCString(timestamp), false);
60+
publishHandler.mirror(download, extensionFile, signatureName);
6261
return download.getExtension();
6362
}
6463

6564
public ExtensionVersion publishVersion(InputStream content, PersonalAccessToken token) {
6665
var extensionFile = createExtensionFile(content);
67-
var download = doPublish(extensionFile, token, TimeUtil.getCurrentUTC(), true);
66+
var download = doPublish(extensionFile, null, token, TimeUtil.getCurrentUTC(), true);
6867
publishHandler.publishAsync(download, extensionFile, this);
6968
return download.getExtension();
7069
}
7170

72-
private FileResource doPublish(TempFile extensionFile, PersonalAccessToken token, LocalDateTime timestamp, boolean checkDependencies) {
71+
private FileResource doPublish(TempFile extensionFile, String binaryName, PersonalAccessToken token, LocalDateTime timestamp, boolean checkDependencies) {
7372
try (var processor = new ExtensionProcessor(extensionFile)) {
7473
var extVersion = publishHandler.createExtensionVersion(processor, token, timestamp, checkDependencies);
7574
if (requireLicense) {
@@ -78,7 +77,7 @@ private FileResource doPublish(TempFile extensionFile, PersonalAccessToken token
7877
checkLicense(extVersion, license);
7978
}
8079

81-
return processor.getBinary(extVersion);
80+
return processor.getBinary(extVersion, binaryName);
8281
}
8382
}
8483

server/src/main/java/org/eclipse/openvsx/IExtensionRegistry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ public interface IExtensionRegistry {
3737
NamespaceDetailsJson getNamespaceDetails(String namespace);
3838

3939
ResponseEntity<byte[]> getNamespaceLogo(String namespaceName, String fileName);
40+
41+
String getPublicKey(String publicId);
4042
}

server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.eclipse.openvsx.eclipse.EclipseService;
2929
import org.eclipse.openvsx.entities.*;
3030
import org.eclipse.openvsx.json.*;
31+
import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService;
3132
import org.eclipse.openvsx.repositories.RepositoryService;
3233
import org.eclipse.openvsx.search.ExtensionSearch;
3334
import org.eclipse.openvsx.search.ISearchService;
@@ -82,6 +83,9 @@ public class LocalRegistryService implements IExtensionRegistry {
8283
@Autowired
8384
CacheService cache;
8485

86+
@Autowired
87+
ExtensionVersionIntegrityService integrityService;
88+
8589
@Override
8690
public NamespaceJson getNamespace(String namespaceName) {
8791
var namespace = repositories.findNamespace(namespaceName);
@@ -130,7 +134,7 @@ private Map<String, String> getDownloads(Extension extension, String targetPlatf
130134
var download = files != null ? files.get(DOWNLOAD) : null;
131135
if(download == null) {
132136
var e = ev.getExtension();
133-
logger.warn("Could not find download for: {}.{}-{}@{}", e.getNamespace().getName(), e.getName(), ev.getVersion(), ev.getTargetPlatform());
137+
logger.warn("Could not find download for: {}", NamingUtil.toLogFormat(ev));
134138
return null;
135139
} else {
136140
return new AbstractMap.SimpleEntry<>(ev.getTargetPlatform(), download);
@@ -182,7 +186,11 @@ public ResponseEntity<byte[]> getFile(String namespace, String extensionName, St
182186
}
183187

184188
public boolean isType (String fileName){
185-
var expectedTypes = List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, DOWNLOAD_SHA256, CHANGELOG, VSIXMANIFEST);
189+
var expectedTypes = new ArrayList<>(List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, DOWNLOAD_SHA256, CHANGELOG, VSIXMANIFEST));
190+
if(integrityService.isEnabled()) {
191+
expectedTypes.add(DOWNLOAD_SIG);
192+
}
193+
186194
return expectedTypes.stream().anyMatch(fileName::equalsIgnoreCase);
187195
}
188196

@@ -431,10 +439,26 @@ private SearchEntryJson toSearchEntryJson(Extension extension) {
431439
var extVersion = versions.getLatest(extension, null, false, true);
432440
var entry = extVersion.toSearchEntryJson();
433441
entry.url = createApiUrl(serverUrl, "api", entry.namespace, entry.name);
434-
entry.files = storageUtil.getFileUrls(extVersion, serverUrl, DOWNLOAD, DOWNLOAD_SHA256, ICON);
442+
entry.files = storageUtil.getFileUrls(extVersion, serverUrl, withFileTypes(DOWNLOAD, ICON));
443+
if(entry.files.containsKey(DOWNLOAD_SIG)) {
444+
entry.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
445+
}
446+
435447
return entry;
436448
}
437449

450+
private String[] withFileTypes(String... types) {
451+
var typesList = new ArrayList<>(List.of(types));
452+
if(typesList.contains(DOWNLOAD)) {
453+
typesList.add(DOWNLOAD_SHA256);
454+
if(integrityService.isEnabled()) {
455+
typesList.add(DOWNLOAD_SIG);
456+
}
457+
}
458+
459+
return typesList.toArray(String[]::new);
460+
}
461+
438462
@Override
439463
public ResponseEntity<byte[]> getNamespaceLogo(String namespaceName, String fileName) {
440464
if(fileName == null) {
@@ -515,7 +539,7 @@ private Map<Long, List<FileResource>> getFileResources(List<ExtensionVersion> ex
515539
return Collections.emptyMap();
516540
}
517541

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

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

@@ -690,7 +713,8 @@ public ResultJson postReview(ReviewJson review, String namespace, String extensi
690713
}
691714
var extension = repositories.findExtension(extensionName, namespace);
692715
if (extension == null || !extension.isActive()) {
693-
return ResultJson.error("Extension not found: " + namespace + "." + extensionName);
716+
var extensionId = NamingUtil.toExtensionId(namespace, extensionName);
717+
return ResultJson.error("Extension not found: " + extensionId);
694718
}
695719
var activeReviews = repositories.findActiveReviews(extension, user);
696720
if (!activeReviews.isEmpty()) {
@@ -711,7 +735,7 @@ public ResultJson postReview(ReviewJson review, String namespace, String extensi
711735
search.updateSearchEntry(extension);
712736
cache.evictExtensionJsons(extension);
713737
cache.evictLatestExtensionVersion(extension);
714-
return ResultJson.success("Added review for " + extension.getNamespace().getName() + "." + extension.getName());
738+
return ResultJson.success("Added review for " + NamingUtil.toExtensionId(extension));
715739
}
716740

717741
@Transactional(rollbackOn = ResponseStatusException.class)
@@ -722,7 +746,7 @@ public ResultJson deleteReview(String namespace, String extensionName) {
722746
}
723747
var extension = repositories.findExtension(extensionName, namespace);
724748
if (extension == null || !extension.isActive()) {
725-
return ResultJson.error("Extension not found: " + namespace + "." + extensionName);
749+
return ResultJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extensionName));
726750
}
727751
var activeReviews = repositories.findActiveReviews(extension, user);
728752
if (activeReviews.isEmpty()) {
@@ -738,7 +762,7 @@ public ResultJson deleteReview(String namespace, String extensionName) {
738762
search.updateSearchEntry(extension);
739763
cache.evictExtensionJsons(extension);
740764
cache.evictLatestExtensionVersion(extension);
741-
return ResultJson.success("Deleted review for " + extension.getNamespace().getName() + "." + extension.getName());
765+
return ResultJson.success("Deleted review for " + NamingUtil.toExtensionId(extension));
742766
}
743767

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

779-
var fileUrls = storageUtil.getFileUrls(latestVersions.values(), serverUrl, DOWNLOAD, DOWNLOAD_SHA256, ICON);
780-
searchEntries.forEach((extensionId, searchEntry) -> searchEntry.files = fileUrls.get(latestVersions.get(extensionId).getId()));
803+
var fileUrls = storageUtil.getFileUrls(latestVersions.values(), serverUrl, withFileTypes(DOWNLOAD, ICON));
804+
searchEntries.forEach((extensionId, searchEntry) -> {
805+
var extVersion = latestVersions.get(extensionId);
806+
searchEntry.files = fileUrls.get(extVersion.getId());
807+
if(searchEntry.files.containsKey(DOWNLOAD_SIG)) {
808+
searchEntry.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
809+
}
810+
});
781811
if (options.includeAllVersions) {
782812
var allActiveVersions = repositories.findActiveVersions(extensions).stream()
783813
.sorted(ExtensionVersion.SORT_COMPARATOR)
@@ -867,8 +897,11 @@ public ExtensionJson toExtensionVersionJson(ExtensionVersion extVersion, String
867897
.forEach(e -> json.allVersions.put(e.getKey(), e.getValue()));
868898
}
869899

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

931-
json.files = Maps.newLinkedHashMapWithExpectedSize(6);
964+
json.files = Maps.newLinkedHashMapWithExpectedSize(8);
932965
var fileBaseUrl = UrlUtil.createApiFileBaseUrl(serverUrl, json.namespace, json.name, json.targetPlatform, json.version);
933966
for (var resource : resources) {
934967
var fileUrl = UrlUtil.createApiFileUrl(fileBaseUrl, resource.getName());
935968
json.files.put(resource.getType(), fileUrl);
936969
}
970+
if(json.files.containsKey(DOWNLOAD_SIG)) {
971+
json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
972+
}
937973

938974
if (json.dependencies != null) {
939975
json.dependencies.forEach(ref -> {
@@ -1001,12 +1037,15 @@ public ExtensionJson toExtensionVersionJsonV2(
10011037
}
10021038
}
10031039

1004-
json.files = Maps.newLinkedHashMapWithExpectedSize(6);
1040+
json.files = Maps.newLinkedHashMapWithExpectedSize(8);
10051041
var fileBaseUrl = UrlUtil.createApiFileBaseUrl(serverUrl, json.namespace, json.name, json.targetPlatform, json.version);
10061042
for (var resource : resources) {
10071043
var fileUrl = UrlUtil.createApiFileUrl(fileBaseUrl, resource.getName());
10081044
json.files.put(resource.getType(), fileUrl);
10091045
}
1046+
if(json.files.containsKey(DOWNLOAD_SIG)) {
1047+
json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
1048+
}
10101049

10111050
if (json.dependencies != null) {
10121051
json.dependencies.forEach(ref -> {
@@ -1051,4 +1090,13 @@ private boolean isVerified(ExtensionVersion extVersion, Map<Long, List<Namespace
10511090
return memberships.stream().anyMatch(m -> m.getRole().equalsIgnoreCase(NamespaceMembership.ROLE_OWNER))
10521091
&& memberships.stream().anyMatch(m -> m.getUser().getId() == user.getId());
10531092
}
1093+
1094+
public String getPublicKey(String publicId) {
1095+
var keyPair = repositories.findKeyPair(publicId);
1096+
if(keyPair == null) {
1097+
throw new NotFoundException();
1098+
}
1099+
1100+
return keyPair.getPublicKeyText();
1101+
}
10541102
}

0 commit comments

Comments
 (0)