Skip to content
Merged
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 @@ -19,10 +19,10 @@
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.alexmond.jhelm.core.model.Chart;
import org.alexmond.jhelm.core.service.ChartLoader;
import org.alexmond.jhelm.core.service.SignatureService;
import org.alexmond.jhelm.core.service.SigningKey;

/**
* Packages a chart directory into a versioned {@code .tgz} archive and optionally
Expand Down Expand Up @@ -57,18 +57,18 @@ public File packageChart(String chartPath) throws Exception {
* @return the created archive file
*/
public File packageChart(String chartPath, String keyringPath, String keyId, char[] passphrase) throws Exception {
PGPSecretKey secretKey = signatureService.loadSecretKey(keyringPath, keyId);
return packageChart(chartPath, secretKey, passphrase);
SigningKey signingKey = signatureService.loadSigningKey(keyringPath, keyId);
return packageChart(chartPath, signingKey, passphrase);
}

/**
* Packages a chart directory into a {@code .tgz} archive and optionally signs it.
* @param chartPath path to the chart directory
* @param secretKey PGP secret key for signing, or {@code null} to skip signing
* @param signingKey signing key, or {@code null} to skip signing
* @param passphrase key passphrase, or {@code null} if not signing
* @return the created archive file
*/
public File packageChart(String chartPath, PGPSecretKey secretKey, char[] passphrase) throws Exception {
public File packageChart(String chartPath, SigningKey signingKey, char[] passphrase) throws Exception {
Chart chart = chartLoader.load(new File(chartPath));
String archiveName = chart.getMetadata().getName() + "-" + chart.getMetadata().getVersion() + ".tgz";

Expand All @@ -80,8 +80,8 @@ public File packageChart(String chartPath, PGPSecretKey secretKey, char[] passph
log.info("Successfully packaged chart and saved it to: {}", archiveFile.getAbsolutePath());
}

if (secretKey != null) {
String provContent = signatureService.sign(archiveFile, chart.getMetadata(), secretKey, passphrase);
if (signingKey != null) {
String provContent = signatureService.sign(archiveFile, chart.getMetadata(), signingKey, passphrase);
File provFile = new File(destDir, archiveName + ".prov");
Files.writeString(provFile.toPath(), provContent);
if (log.isInfoEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.alexmond.jhelm.core.service.SignatureService;
import org.alexmond.jhelm.core.service.VerificationKeyring;

/**
* Verifies a packaged chart archive against its PGP provenance file.
Expand Down Expand Up @@ -34,9 +34,9 @@ public void verify(String chartTgzPath, String keyringPath) throws Exception {
}

String provContent = Files.readString(provFile.toPath());
PGPPublicKeyRingCollection publicKeys = signatureService.loadPublicKeyring(keyringPath);
VerificationKeyring keyring = signatureService.loadVerificationKeyring(keyringPath);

signatureService.verify(chartFile, provContent, publicKeys);
signatureService.verify(chartFile, provContent, keyring);
if (log.isInfoEnabled()) {
log.info("Verification succeeded for {}", chartFile.getName());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.alexmond.jhelm.core.exception;

/**
* Exception thrown when a PGP signing or verification operation fails for reasons other
* than a rejected signature — for example when a keyring cannot be read, a signing key
* cannot be located, or the underlying OpenPGP library reports an error. Wraps the
* checked {@code IOException}/{@code PGPException} thrown by the signing layer so that
* the BouncyCastle exception types stay off the public API.
*/
public class SignatureException extends JhelmException {

/**
* Creates a signature failure with the given detail message.
* @param message a description of why the operation failed
*/
public SignatureException(String message) {
super(message);
}

/**
* Creates a signature failure wrapping an underlying cause.
* @param message a description of why the operation failed
* @param cause the underlying error being wrapped
*/
public SignatureException(String message, Throwable cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.alexmond.jhelm.core.exception.SignatureException;
import org.alexmond.jhelm.core.model.ChartMetadata;

/**
Expand All @@ -55,107 +56,116 @@ public class SignatureService {
* Signs a chart archive and produces a Helm-compatible provenance file content.
* @param chartTgz the chart archive file
* @param metadata the chart metadata
* @param secretKey the PGP secret key to sign with
* @param signingKey the signing key to sign with
* @param passphrase the key passphrase
* @return the provenance file content (PGP clear-signed YAML)
* @throws IOException if the chart file cannot be read
* @throws PGPException if signing fails
* @throws SignatureException if the chart file cannot be read or signing fails
*/
public String sign(File chartTgz, ChartMetadata metadata, PGPSecretKey secretKey, char[] passphrase)
throws IOException, PGPException {
String digest = computeSha256(chartTgz);
String yamlContent = buildProvenanceYaml(metadata, chartTgz.getName(), digest);
public String sign(File chartTgz, ChartMetadata metadata, SigningKey signingKey, char[] passphrase) {
PGPSecretKey secretKey = signingKey.pgpSecretKey();
try {
String digest = computeSha256(chartTgz);
String yamlContent = buildProvenanceYaml(metadata, chartTgz.getName(), digest);

PGPPrivateKey privateKey = secretKey
.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(passphrase));
PGPPrivateKey privateKey = secretKey
.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(passphrase));

PGPSignatureGenerator sigGen = new PGPSignatureGenerator(
new JcaPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA256)
.setProvider("BC"),
secretKey.getPublicKey());
sigGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, privateKey);
PGPSignatureGenerator sigGen = new PGPSignatureGenerator(
new JcaPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA256)
.setProvider("BC"),
secretKey.getPublicKey());
sigGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, privateKey);

ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ArmoredOutputStream armoredOut = new ArmoredOutputStream(out)) {
armoredOut.beginClearText(HashAlgorithmTags.SHA256);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ArmoredOutputStream armoredOut = new ArmoredOutputStream(out)) {
armoredOut.beginClearText(HashAlgorithmTags.SHA256);

byte[] yamlBytes = yamlContent.getBytes(StandardCharsets.UTF_8);
armoredOut.write(yamlBytes);
sigGen.update(yamlBytes);
byte[] yamlBytes = yamlContent.getBytes(StandardCharsets.UTF_8);
armoredOut.write(yamlBytes);
sigGen.update(yamlBytes);

armoredOut.endClearText();
armoredOut.endClearText();

try (BCPGOutputStream bcpgOut = new BCPGOutputStream(armoredOut)) {
sigGen.generate().encode(bcpgOut);
try (BCPGOutputStream bcpgOut = new BCPGOutputStream(armoredOut)) {
sigGen.generate().encode(bcpgOut);
}
}
}

return out.toString(StandardCharsets.UTF_8);
return out.toString(StandardCharsets.UTF_8);
}
catch (IOException | PGPException ex) {
throw new SignatureException("Failed to sign chart: " + chartTgz.getName(), ex);
}
}

/**
* Verifies a chart archive against its provenance file.
* @param chartTgz the chart archive file
* @param provContent the provenance file content
* @param publicKeys the public keyring collection
* @throws IOException if files cannot be read
* @throws PGPException if verification fails
* @param keyring the public keyring to verify against
* @throws SignatureException if files cannot be read or the OpenPGP library reports
* an error
* @throws SignatureVerificationException if the signature is invalid or the digest
* does not match
*/
public void verify(File chartTgz, String provContent, PGPPublicKeyRingCollection publicKeys)
throws IOException, PGPException {
// Extract the signed data and signature from the clear-signed message
String signedData = extractSignedData(provContent);
byte[] signatureBytes = extractSignatureBytes(provContent);

// Verify the PGP signature
PGPObjectFactory pgpFactory = new PGPObjectFactory(signatureBytes, new JcaKeyFingerprintCalculator());
PGPSignatureList sigList = (PGPSignatureList) pgpFactory.nextObject();
if (sigList == null || sigList.isEmpty()) {
throw new SignatureVerificationException("No PGP signature found in provenance file");
}
PGPSignature signature = sigList.get(0);
public void verify(File chartTgz, String provContent, VerificationKeyring keyring) {
PGPPublicKeyRingCollection publicKeys = keyring.pgpPublicKeys();
try {
// Extract the signed data and signature from the clear-signed message
String signedData = extractSignedData(provContent);
byte[] signatureBytes = extractSignatureBytes(provContent);

// Verify the PGP signature
PGPObjectFactory pgpFactory = new PGPObjectFactory(signatureBytes, new JcaKeyFingerprintCalculator());
PGPSignatureList sigList = (PGPSignatureList) pgpFactory.nextObject();
if (sigList == null || sigList.isEmpty()) {
throw new SignatureVerificationException("No PGP signature found in provenance file");
}
PGPSignature signature = sigList.get(0);

PGPPublicKey verifyKey = publicKeys.getPublicKey(signature.getKeyID());
if (verifyKey == null) {
throw new SignatureVerificationException(
"No public key found for key ID " + Long.toHexString(signature.getKeyID()));
}
PGPPublicKey verifyKey = publicKeys.getPublicKey(signature.getKeyID());
if (verifyKey == null) {
throw new SignatureVerificationException(
"No public key found for key ID " + Long.toHexString(signature.getKeyID()));
}

signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), verifyKey);
byte[] signedBytes = signedData.getBytes(StandardCharsets.UTF_8);
signature.update(signedBytes);
signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), verifyKey);
byte[] signedBytes = signedData.getBytes(StandardCharsets.UTF_8);
signature.update(signedBytes);

if (!signature.verify()) {
throw new SignatureVerificationException("PGP signature verification failed");
}
log.info("PGP signature verified successfully");
if (!signature.verify()) {
throw new SignatureVerificationException("PGP signature verification failed");
}
log.info("PGP signature verified successfully");

// Verify the chart digest
String expectedDigest = extractDigest(signedData);
if (expectedDigest == null) {
throw new SignatureVerificationException("No digest found in provenance YAML");
}
// Verify the chart digest
String expectedDigest = extractDigest(signedData);
if (expectedDigest == null) {
throw new SignatureVerificationException("No digest found in provenance YAML");
}

String actualDigest = "sha256:" + computeSha256(chartTgz);
if (!expectedDigest.equals(actualDigest)) {
throw new SignatureVerificationException(
"Digest mismatch: expected " + expectedDigest + ", got " + actualDigest);
String actualDigest = "sha256:" + computeSha256(chartTgz);
if (!expectedDigest.equals(actualDigest)) {
throw new SignatureVerificationException(
"Digest mismatch: expected " + expectedDigest + ", got " + actualDigest);
}
log.info("Chart digest verified: {}", actualDigest);
}
catch (IOException | PGPException ex) {
throw new SignatureException("Failed to verify chart: " + chartTgz.getName(), ex);
}
log.info("Chart digest verified: {}", actualDigest);
}

/**
* Loads a PGP secret key from a keyring file by matching a substring of the key's
* user ID.
* Loads a signing key from a keyring file by matching a substring of the key's user
* ID.
* @param keyringPath path to the secret keyring file
* @param keyId substring to match against key user IDs
* @return the matching secret key
* @throws IOException if the keyring cannot be read
* @throws PGPException if the keyring is invalid
* @return an opaque handle to the matching signing key
* @throws SignatureException if the keyring cannot be read, is invalid, or contains
* no matching signing key
*/
public PGPSecretKey loadSecretKey(String keyringPath, String keyId) throws IOException, PGPException {
public SigningKey loadSigningKey(String keyringPath, String keyId) {
try (InputStream in = PGPUtil.getDecoderStream(new FileInputStream(keyringPath))) {
PGPSecretKeyRingCollection keyRings = new PGPSecretKeyRingCollection(in, new JcaKeyFingerprintCalculator());

Expand All @@ -167,25 +177,30 @@ public PGPSecretKey loadSecretKey(String keyringPath, String keyId) throws IOExc
Iterator<String> userIds = key.getUserIDs();
while (userIds.hasNext()) {
if (userIds.next().contains(keyId)) {
return key;
return new SigningKey(key);
}
}
}
}
}
throw new PGPException("No signing key found matching: " + keyId);
catch (IOException | PGPException ex) {
throw new SignatureException("Failed to load signing key from keyring: " + keyringPath, ex);
}
throw new SignatureException("No signing key found matching: " + keyId);
}

/**
* Loads a PGP public keyring collection from a file.
* Loads a public keyring from a file for use in chart verification.
* @param keyringPath path to the public keyring file
* @return the public keyring collection
* @throws IOException if the keyring cannot be read
* @throws PGPException if the keyring is invalid
* @return an opaque handle to the loaded public keyring
* @throws SignatureException if the keyring cannot be read or is invalid
*/
public PGPPublicKeyRingCollection loadPublicKeyring(String keyringPath) throws IOException, PGPException {
public VerificationKeyring loadVerificationKeyring(String keyringPath) {
try (InputStream in = PGPUtil.getDecoderStream(new FileInputStream(keyringPath))) {
return new PGPPublicKeyRingCollection(in, new JcaKeyFingerprintCalculator());
return new VerificationKeyring(new PGPPublicKeyRingCollection(in, new JcaKeyFingerprintCalculator()));
}
catch (IOException | PGPException ex) {
throw new SignatureException("Failed to load public keyring: " + keyringPath, ex);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.alexmond.jhelm.core.service;

import org.bouncycastle.openpgp.PGPSecretKey;

/**
* Opaque handle to a PGP secret key used for signing chart provenance files. Wrapping the
* underlying BouncyCastle {@code PGPSecretKey} keeps the OpenPGP implementation off the
* public API surface, so consumers can hold and pass a signing key around without a
* compile-time dependency on BouncyCastle.
*
* <p>
* Obtain instances from {@link SignatureService#loadSigningKey(String, String)}. Only
* {@link SignatureService}, which lives in this package, can unwrap the contained key.
*/
public final class SigningKey {

private final PGPSecretKey secretKey;

/**
* Wraps the given secret key. Package-private so only {@link SignatureService} can
* construct instances.
* @param secretKey the underlying PGP secret key
*/
SigningKey(PGPSecretKey secretKey) {
this.secretKey = secretKey;
}

/**
* Returns the wrapped PGP secret key. Package-private so the BouncyCastle type stays
* internal to this package.
* @return the underlying PGP secret key
*/
PGPSecretKey pgpSecretKey() {
return this.secretKey;
}

}
Loading
Loading