Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ public interface Configuration {
*/
String index(String deflt);

/**
* The path to the trusted checksum file corresponding to index file
*
*/
String checksumFile();

/**
* Content added to the index file. Content maybe one line without CR/LF as
* long as there is a comma or whitespace separating the GAVs. Further same
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package aQute.bnd.repository.maven.provider;

import static aQute.bnd.osgi.Constants.BSN_SOURCE_SUFFIX;
import static aQute.bnd.repository.maven.provider.TrustedChecksums.createTrustedChecksumFile;
import static aQute.bnd.service.tags.Tags.parse;

import java.io.Closeable;
Expand Down Expand Up @@ -133,6 +134,7 @@ public class MavenBndRepository extends BaseRepository implements RepositoryPlug
private final AtomicReference<Throwable> open = new AtomicReference<>();
Optional<Workspace> workspace;
private AtomicBoolean polling = new AtomicBoolean(false);
private TrustedChecksums trustedChecksums;

/**
* Put result
Expand Down Expand Up @@ -708,17 +710,22 @@ synchronized boolean init() {
}
}

File indexFile = getIndexFile();
this.trustedChecksums = loadTrustedChecksumFile(indexFile);
this.trustedChecksums.open();

for (MavenBackingRepository mbr : release) {
if (mbr.isRemote()) {
remote = true;
mbr.setTrustedChecksums(trustedChecksums);
break;
}
}
if (!remote)
for (MavenBackingRepository mbr : snapshot) {
if (mbr.isRemote()) {
remote = true;
mbr.setTrustedChecksums(trustedChecksums);
break;
}
}
Expand All @@ -732,7 +739,6 @@ synchronized boolean init() {
storageMvn.setSonatypePublishSnapshotUrl(sonatypeSnapshotUrl);
}

File indexFile = getIndexFile();
Processor domain = (registry != null) ? registry.getPlugin(Processor.class) : null;
String source = configuration.source();
if (source != null) {
Expand Down Expand Up @@ -765,6 +771,20 @@ synchronized boolean init() {
}
}

private TrustedChecksums loadTrustedChecksumFile(File indexFile) {
String checksumFile = configuration.checksumFile();

File trustedChecksumFile;
if (checksumFile != null) {
trustedChecksumFile = IO.getFile(base, checksumFile);
} else {
File indexDir = indexFile.getParentFile() != null ? indexFile.getParentFile() : base;
trustedChecksumFile = IO.getFile(indexDir, indexFile.getName() + ".checksums");
}

return new TrustedChecksums(trustedChecksumFile);
}

private void validateUris(List<MavenBackingRepository> release, Formatter f) {
release.stream()
.map(mb -> {
Expand Down Expand Up @@ -911,13 +931,21 @@ public String tooltip(Object... target) throws Exception {
if (status != null)
f.format("STATUS = %s", status);
else {
String status = getStatus();
f.format("MavenBndRepository : %s\n", getName());
if (status != null) {
f.format("Status : %s\n", status);
}
f.format("Tags : %s\n", getTags());
f.format("Revisions : %s\n", index.getArchives()
.size());
f.format("Storage : %s\n", localRepo);
f.format("Index : %s\n", index.indexFile);
f.format("Format : %s\n", index.isPom ? "pom.xml" : "text");
File trustedChecksumsFile = trustedChecksums.getFile();
if (trustedChecksumsFile.exists()) {
f.format("Trusted Checksums File : %s\n", trustedChecksumsFile);
}
f.format("Release repos : \n %s\n", storage.getReleaseRepositories()
.stream()
.filter(Objects::nonNull)
Expand Down Expand Up @@ -1143,4 +1171,13 @@ public HttpClient getClient() {
return client;
}

void createTrustedChecksumsFile() {
try {
createTrustedChecksumFile(storage, trustedChecksums.getFile(), index.archives.keySet());
} catch (Exception e) {
throw Exceptions.duck(e);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ void convertTextXml() throws Exception {
repo.index.convertTextXml();
}

void createTrustedChecksumsFile() throws Exception {
repo.createTrustedChecksumsFile();
}

private String format(MultiMap<Archive, MavenVersion> overlap) {
Justif j = new Justif(140, 50, 60, 70, 80, 90, 100, 110);
j.formatter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ Map<String, Runnable> getRepoActions(final Clipboard clipboard) throws Exception
map.put("Convert :: Text <--> pom.xml", () -> {
convertTextXmlRevisions();
});

map.put("Create Trusted Checksums file", () -> {
createTrustedChecksumsFile();
});
map.put("Update Revisions :: Dry run to clipboard - Update to higher MICRO revision", () -> {
clipboard.copy(preview(micro));
});
Expand Down Expand Up @@ -278,4 +280,13 @@ private void convertTextXmlRevisions() {
}
}

private void createTrustedChecksumsFile() {
try {
mbr.createTrustedChecksumsFile();
repo.refresh();
} catch (Exception e) {
throw Exceptions.duck(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package aQute.bnd.repository.maven.provider;

import static java.util.stream.Collectors.toSet;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import aQute.lib.io.IO;
import aQute.lib.strings.Strings;
import aQute.libg.cryptography.MD5;
import aQute.libg.cryptography.SHA1;
import aQute.libg.cryptography.SHA256;
import aQute.libg.cryptography.SHA512;
import aQute.maven.api.Archive;
import aQute.maven.api.IMavenRepo;

/**
* Helper for handling trusted checksum verification for Maven artifacts
* {@link IndexFile}
*/
public final class TrustedChecksums {

private final File sidecarFile;
private Map<Archive, ArtifactChecksum> index = Map.of();

TrustedChecksums(File trustedChecksumFile) {
this.sidecarFile = trustedChecksumFile;
}

public File getFile() {
return this.sidecarFile;
}

public ArtifactChecksum get(Archive a) {
return index.get(a);
}

public void open() throws IOException {
if (sidecarFile == null || !sidecarFile.isFile()) {
return;
}

Set<ArtifactChecksum> set = read(IO.collect(sidecarFile));
index = set.stream()
.collect(Collectors.toMap(acs -> acs.archive(), acs -> acs));

}

public static void createTrustedChecksumFile(IMavenRepo repo, File checksumFile, Collection<Archive> sortedArchives)
throws Exception {

try (FileOutputStream fos = new FileOutputStream(
checksumFile);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {

writer.write("# Trusted Checksums for each GAV in the maven index");
writer.newLine();

for (Archive arch : sortedArchives) {
File localFile = repo.toLocalFile(arch);
String line = arch.toString() + "=sha1:" + SHA1.digest(localFile)
.asHex();
writer.write(line);
writer.newLine();
}
writer.flush();
}
}

private Set<ArtifactChecksum> read(String source) {
Set<ArtifactChecksum> archives = Strings.splitLinesAsStream(source)
.map(s -> ArtifactChecksum.parse(s))
.filter(Objects::nonNull)
.collect(toSet());
return archives;
}

public record ArtifactChecksum(Archive archive, String hashType, String hash) {
public static ArtifactChecksum parse(String line) {
if (line.startsWith("#") || line.isEmpty())
return null;

String[] parts = line.split("=", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid checksum line format: " + line);
}

Archive archive = Archive.valueOf(parts[0]);

int colon = parts[1].indexOf(':');
if (colon < 0) {
throw new IllegalArgumentException("Missing hash type separator: " + line);
}

return new ArtifactChecksum(archive, parts[1].substring(0, colon), parts[1].substring(colon + 1));
}
}

public static String computeHash(File file, String hashType) throws Exception {
return switch (hashType.toLowerCase()) {
case "sha-256", "sha256" -> SHA256.digest(file)
.asHex();
case "sha-512", "sha512" -> SHA512.digest(file)
.asHex();
case "md5" -> MD5.digest(file)
.asHex();
case "sha1", "sha-1" -> SHA1.digest(file)
.asHex();
default -> throw new IllegalArgumentException("Unsupported hash type: " + hashType);
};
}

@Override
public String toString() {
return String.valueOf(sidecarFile);
}


public static class TrustedChecksumException extends Exception {

private static final long serialVersionUID = 1L;

public TrustedChecksumException(String message) {
super(message);
}

}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@Version("2.3.0")
@Version("2.4.0")
package aQute.bnd.repository.maven.provider;

import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package aQute.maven.provider;

import static aQute.bnd.repository.maven.provider.TrustedChecksums.computeHash;
import static java.util.stream.Collectors.toList;

import java.io.Closeable;
Expand All @@ -18,6 +19,9 @@
import java.util.regex.Pattern;

import aQute.bnd.http.HttpClient;
import aQute.bnd.repository.maven.provider.TrustedChecksums;
import aQute.bnd.repository.maven.provider.TrustedChecksums.ArtifactChecksum;
import aQute.bnd.repository.maven.provider.TrustedChecksums.TrustedChecksumException;
import aQute.bnd.service.url.State;
import aQute.bnd.service.url.TaggedData;
import aQute.bnd.version.MavenVersion;
Expand All @@ -44,6 +48,7 @@ public abstract class MavenBackingRepository implements Closeable {
final String id;
final File local;
final Reporter reporter;
TrustedChecksums trustedChecksums;

public MavenBackingRepository(File root, String base, Reporter reporter) throws Exception {
this.local = root;
Expand Down Expand Up @@ -254,4 +259,45 @@ void refreshSnapshots() {
revisions.clear();
}

public void setTrustedChecksums(TrustedChecksums trustedChecksums) {
this.trustedChecksums = trustedChecksums;
}

/**
* Validates a downloaded artifact against a configured trusted checksum.
*
* @param path the repository-relative artifact path
* @param file the downloaded artifact file
* @return {@code true} if a trusted checksum exists for the artifact and
* the file successfully matches it; {@code false} if no trusted
* checksum is configured
* @throws Exception if checksum calculation fails or the file does not
* match the configured trusted checksum. In the mismatch case
* the file is deleted before the exception is thrown.
*/
boolean checkTrustedChecksum(String path, File file) throws Exception {
Archive archive = Archive.fromFilepath(path);
if (trustedChecksums == null || archive == null) {
return false;
}

ArtifactChecksum expected = trustedChecksums.get(archive);
if (expected == null) {
return false;
}

String fileHash = computeHash(file, expected.hashType());

// Reuse existing checkDigest() with format: "algorithm=hash"
try {
checkDigest(fileHash, expected.hash(), file);
} catch (Exception e) {
throw new TrustedChecksumException(e.getMessage());
}

return true;
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public TaggedData fetch(String path, File dest, boolean force) throws Exception
IO.mkdirs(dest.getParentFile());
IO.delete(dest);
IO.copy(source, dest);

// Check Trusted checksum
checkTrustedChecksum(path, dest);

return new TaggedData(toURI(path), 200, dest);
} else {
return new TaggedData(toURI(path), 404, dest);
Expand Down
Loading
Loading