diff --git a/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/PackageAction.java b/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/PackageAction.java index 0fb17a59..32ad88d8 100644 --- a/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/PackageAction.java +++ b/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/PackageAction.java @@ -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 @@ -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"; @@ -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()) { diff --git a/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/VerifyAction.java b/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/VerifyAction.java index 74cccd25..19872fce 100644 --- a/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/VerifyAction.java +++ b/jhelm-core/src/main/java/org/alexmond/jhelm/core/action/VerifyAction.java @@ -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. @@ -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()); } diff --git a/jhelm-core/src/main/java/org/alexmond/jhelm/core/exception/SignatureException.java b/jhelm-core/src/main/java/org/alexmond/jhelm/core/exception/SignatureException.java new file mode 100644 index 00000000..131441bf --- /dev/null +++ b/jhelm-core/src/main/java/org/alexmond/jhelm/core/exception/SignatureException.java @@ -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); + } + +} diff --git a/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SignatureService.java b/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SignatureService.java index 267de66d..4528067a 100644 --- a/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SignatureService.java +++ b/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SignatureService.java @@ -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; /** @@ -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()); @@ -167,25 +177,30 @@ public PGPSecretKey loadSecretKey(String keyringPath, String keyId) throws IOExc Iterator 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); } } diff --git a/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SigningKey.java b/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SigningKey.java new file mode 100644 index 00000000..28baa146 --- /dev/null +++ b/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/SigningKey.java @@ -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. + * + *

+ * 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; + } + +} diff --git a/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/VerificationKeyring.java b/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/VerificationKeyring.java new file mode 100644 index 00000000..46c362ea --- /dev/null +++ b/jhelm-core/src/main/java/org/alexmond/jhelm/core/service/VerificationKeyring.java @@ -0,0 +1,38 @@ +package org.alexmond.jhelm.core.service; + +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; + +/** + * Opaque handle to a collection of PGP public keys used to verify chart provenance files. + * Wrapping the underlying BouncyCastle {@code PGPPublicKeyRingCollection} keeps the + * OpenPGP implementation off the public API surface, so consumers can hold and pass a + * keyring around without a compile-time dependency on BouncyCastle. + * + *

+ * Obtain instances from {@link SignatureService#loadVerificationKeyring(String)}. Only + * {@link SignatureService}, which lives in this package, can unwrap the contained + * keyring. + */ +public final class VerificationKeyring { + + private final PGPPublicKeyRingCollection publicKeys; + + /** + * Wraps the given public keyring collection. Package-private so only + * {@link SignatureService} can construct instances. + * @param publicKeys the underlying PGP public keyring collection + */ + VerificationKeyring(PGPPublicKeyRingCollection publicKeys) { + this.publicKeys = publicKeys; + } + + /** + * Returns the wrapped PGP public keyring collection. Package-private so the + * BouncyCastle type stays internal to this package. + * @return the underlying PGP public keyring collection + */ + PGPPublicKeyRingCollection pgpPublicKeys() { + return this.publicKeys; + } + +} diff --git a/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/PackageActionTest.java b/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/PackageActionTest.java index e2f4cc59..a8e33c10 100644 --- a/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/PackageActionTest.java +++ b/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/PackageActionTest.java @@ -1,21 +1,28 @@ package org.alexmond.jhelm.core.action; import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Security; import java.util.Date; +import java.util.List; import org.alexmond.jhelm.core.service.ChartLoader; import org.alexmond.jhelm.core.service.SignatureService; +import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; @@ -94,8 +101,10 @@ void testPackageChartCreatesArchive() throws Exception { void testPackageChartWithSignatureCreatesProvFile() throws Exception { Path chartDir = createMinimalChart("signed-chart", "2.0.0"); packageAction.setDestination(tempDir.toFile()); + Path keyringFile = writeSecretKeyring(); - File archive = packageAction.packageChart(chartDir.toString(), secretKey, PASSPHRASE); + File archive = packageAction.packageChart(chartDir.toString(), keyringFile.toString(), "test@example.com", + PASSPHRASE); assertTrue(archive.exists()); File provFile = new File(archive.getAbsolutePath() + ".prov"); @@ -147,6 +156,17 @@ private Set listTgzEntries(File tgzFile) throws Exception { return names; } + private Path writeSecretKeyring() throws Exception { + Path keyringFile = tempDir.resolve("secring.gpg"); + PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection( + List.of(new PGPSecretKeyRing(secretKey.getEncoded(), new JcaKeyFingerprintCalculator()))); + try (OutputStream out = new FileOutputStream(keyringFile.toFile()); + ArmoredOutputStream armoredOut = new ArmoredOutputStream(out)) { + collection.encode(armoredOut); + } + return keyringFile; + } + private Path createMinimalChart(String name, String version) throws Exception { Path chartDir = tempDir.resolve(name); Files.createDirectories(chartDir.resolve("templates")); diff --git a/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/VerifyActionTest.java b/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/VerifyActionTest.java index df8e3146..6c1cbf2f 100644 --- a/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/VerifyActionTest.java +++ b/jhelm-core/src/test/java/org/alexmond/jhelm/core/action/VerifyActionTest.java @@ -13,6 +13,7 @@ import org.alexmond.jhelm.core.service.SignatureService; import org.alexmond.jhelm.core.service.SignatureService.SignatureVerificationException; +import org.alexmond.jhelm.core.service.SigningKey; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; @@ -21,7 +22,10 @@ import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; @@ -137,12 +141,24 @@ private File createSignedChart(String name, String content) throws Exception { .apiVersion("v2") .build(); - String provContent = signatureService.sign(chartFile, metadata, secretKey, PASSPHRASE); + SigningKey signingKey = signatureService.loadSigningKey(writeSecretKeyring().toString(), "test@example.com"); + String provContent = signatureService.sign(chartFile, metadata, signingKey, PASSPHRASE); Files.writeString(tempDir.resolve(name + ".prov"), provContent); return chartFile; } + private Path writeSecretKeyring() throws Exception { + Path keyringFile = tempDir.resolve("secring.gpg"); + PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection( + List.of(new PGPSecretKeyRing(secretKey.getEncoded(), new JcaKeyFingerprintCalculator()))); + try (OutputStream out = new FileOutputStream(keyringFile.toFile()); + ArmoredOutputStream armoredOut = new ArmoredOutputStream(out)) { + collection.encode(armoredOut); + } + return keyringFile; + } + private Path writePublicKeyring() throws Exception { Path keyringFile = tempDir.resolve("pubring.gpg"); try (OutputStream out = new FileOutputStream(keyringFile.toFile()); diff --git a/jhelm-core/src/test/java/org/alexmond/jhelm/core/service/SignatureServiceTest.java b/jhelm-core/src/test/java/org/alexmond/jhelm/core/service/SignatureServiceTest.java index c9e52788..66d3fb83 100644 --- a/jhelm-core/src/test/java/org/alexmond/jhelm/core/service/SignatureServiceTest.java +++ b/jhelm-core/src/test/java/org/alexmond/jhelm/core/service/SignatureServiceTest.java @@ -11,6 +11,7 @@ import java.util.Date; import java.util.List; +import org.alexmond.jhelm.core.exception.SignatureException; import org.alexmond.jhelm.core.model.ChartMetadata; import org.alexmond.jhelm.core.service.SignatureService.SignatureVerificationException; import org.bouncycastle.bcpg.ArmoredOutputStream; @@ -18,7 +19,6 @@ import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -52,6 +52,10 @@ class SignatureServiceTest { private static PGPPublicKeyRingCollection publicKeys; + private static SigningKey signingKey; + + private static VerificationKeyring verificationKeyring; + private SignatureService signatureService; @TempDir @@ -66,6 +70,8 @@ static void generateKeys() throws Exception { PGPKeyRingGenerator keyRingGen = createKeyRingGenerator(USER_ID); secretKey = keyRingGen.generateSecretKeyRing().getSecretKey(); publicKeys = new PGPPublicKeyRingCollection(List.of(keyRingGen.generatePublicKeyRing())); + signingKey = new SigningKey(secretKey); + verificationKeyring = new VerificationKeyring(publicKeys); } @BeforeEach @@ -78,7 +84,7 @@ void testSignProducesValidClearSignedMessage() throws Exception { File chartFile = createTempChartFile("test-chart-0.1.0.tgz", "chart-content"); ChartMetadata metadata = ChartMetadata.builder().name("test-chart").version("0.1.0").apiVersion("v2").build(); - String provContent = signatureService.sign(chartFile, metadata, secretKey, PASSPHRASE); + String provContent = signatureService.sign(chartFile, metadata, signingKey, PASSPHRASE); assertNotNull(provContent); assertTrue(provContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")); @@ -101,9 +107,9 @@ void testSignAndVerifyRoundTrip() throws Exception { .type("application") .build(); - String provContent = signatureService.sign(chartFile, metadata, secretKey, PASSPHRASE); + String provContent = signatureService.sign(chartFile, metadata, signingKey, PASSPHRASE); - assertDoesNotThrow(() -> signatureService.verify(chartFile, provContent, publicKeys)); + assertDoesNotThrow(() -> signatureService.verify(chartFile, provContent, verificationKeyring)); } @Test @@ -111,7 +117,7 @@ void testVerifyFailsWithTamperedChart() throws Exception { File chartFile = createTempChartFile("tampered-1.0.0.tgz", "original-content"); ChartMetadata metadata = ChartMetadata.builder().name("tampered").version("1.0.0").apiVersion("v2").build(); - String provContent = signatureService.sign(chartFile, metadata, secretKey, PASSPHRASE); + String provContent = signatureService.sign(chartFile, metadata, signingKey, PASSPHRASE); // Overwrite the chart file with different content try (OutputStream os = new FileOutputStream(chartFile)) { @@ -119,20 +125,20 @@ void testVerifyFailsWithTamperedChart() throws Exception { } SignatureVerificationException ex = assertThrows(SignatureVerificationException.class, - () -> signatureService.verify(chartFile, provContent, publicKeys)); + () -> signatureService.verify(chartFile, provContent, verificationKeyring)); assertTrue(ex.getMessage().contains("Digest mismatch")); } @Test void testVerifyFailsWithWrongPublicKey() throws Exception { PGPKeyRingGenerator otherGen = createKeyRingGenerator("Other "); - PGPPublicKeyRingCollection wrongKeys = new PGPPublicKeyRingCollection( - List.of(otherGen.generatePublicKeyRing())); + VerificationKeyring wrongKeys = new VerificationKeyring( + new PGPPublicKeyRingCollection(List.of(otherGen.generatePublicKeyRing()))); File chartFile = createTempChartFile("wrongkey-1.0.0.tgz", "content"); ChartMetadata metadata = ChartMetadata.builder().name("wrongkey").version("1.0.0").apiVersion("v2").build(); - String provContent = signatureService.sign(chartFile, metadata, secretKey, PASSPHRASE); + String provContent = signatureService.sign(chartFile, metadata, signingKey, PASSPHRASE); SignatureVerificationException ex = assertThrows(SignatureVerificationException.class, () -> signatureService.verify(chartFile, provContent, wrongKeys)); @@ -180,7 +186,7 @@ void testExtractSignedDataValid() throws Exception { File chartFile = createTempChartFile("extract-1.0.0.tgz", "data"); ChartMetadata metadata = ChartMetadata.builder().name("extract").version("1.0.0").apiVersion("v2").build(); - String provContent = signatureService.sign(chartFile, metadata, secretKey, PASSPHRASE); + String provContent = signatureService.sign(chartFile, metadata, signingKey, PASSPHRASE); String signedData = signatureService.extractSignedData(provContent); assertNotNull(signedData); @@ -220,9 +226,9 @@ void testLoadSecretKeyFromFile() throws Exception { collection.encode(armoredOut); } - PGPSecretKey loaded = signatureService.loadSecretKey(keyFile.toString(), "test@example.com"); + SigningKey loaded = signatureService.loadSigningKey(keyFile.toString(), "test@example.com"); assertNotNull(loaded); - assertEquals(secretKey.getKeyID(), loaded.getKeyID()); + assertEquals(secretKey.getKeyID(), loaded.pgpSecretKey().getKeyID()); } @Test @@ -235,8 +241,8 @@ void testLoadSecretKeyNotFound() throws Exception { collection.encode(armoredOut); } - assertThrows(PGPException.class, - () -> signatureService.loadSecretKey(keyFile.toString(), "nonexistent@example.com")); + assertThrows(SignatureException.class, + () -> signatureService.loadSigningKey(keyFile.toString(), "nonexistent@example.com")); } @Test @@ -247,16 +253,16 @@ void testLoadPublicKeyringFromFile() throws Exception { publicKeys.encode(armoredOut); } - PGPPublicKeyRingCollection loaded = signatureService.loadPublicKeyring(keyFile.toString()); + VerificationKeyring loaded = signatureService.loadVerificationKeyring(keyFile.toString()); assertNotNull(loaded); - assertTrue(loaded.contains(secretKey.getKeyID())); + assertTrue(loaded.pgpPublicKeys().contains(secretKey.getKeyID())); } @Test void testVerifyRejectsNoSignatureContent() { File chartFile = tempDir.resolve("nosig.tgz").toFile(); assertThrows(SignatureVerificationException.class, - () -> signatureService.verify(chartFile, "no signature content", publicKeys)); + () -> signatureService.verify(chartFile, "no signature content", verificationKeyring)); } @SuppressWarnings("deprecation") // JcaPGPKeyPair 3-arg constructor; 4-arg version has