From 3701fa32f6e13a825d43fc58e1fdc6c2dca0017a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Nov 2024 12:17:09 +0000 Subject: [PATCH] Add support for subkeys Closes gh-3 --- README.adoc | 2 + action.yaml | 4 + .../ArtifactoryDeployIntegrationTests.java | 39 ++++ .../ArtifactoryDeployProperties.java | 2 +- .../actions/artifactorydeploy/Deployer.java | 6 +- .../openpgp/ArmoredAsciiSigner.java | 40 ++-- .../DeployableArtifactsSignerTests.java | 2 +- .../openpgp/ArmoredAsciiSignerTests.java | 86 +++++++-- .../openpgp/subkey-expected.asc | 14 ++ .../openpgp/test-private-subkey.txt | 110 +++++++++++ .../openpgp/test-private.txt | 179 +++++++++++------- 11 files changed, 379 insertions(+), 105 deletions(-) create mode 100644 src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/subkey-expected.asc create mode 100644 src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private-subkey.txt diff --git a/README.adoc b/README.adoc index 1b7fae3..867671e 100644 --- a/README.adoc +++ b/README.adoc @@ -39,6 +39,8 @@ It makes use of the "builds" and "artifact properties" features of Artifactory t Defaults to 1 - `signing-key`: A PGP/GPG signing key that will be used to sign artifacts before they are deployed - `signing-passphrase`: Passphrase of the signing key +- `signing-key-id`: ID of the signing key that will be used. + When omitted, the first key that is a signing key will be used diff --git a/action.yaml b/action.yaml index 38ff8aa..4ef3774 100644 --- a/action.yaml +++ b/action.yaml @@ -39,6 +39,9 @@ inputs: signing-passphrase: description: 'Passphrase of the signing key' required: false + signing-key-id: + description: 'ID of the signing key that will be used. When omitted, the first key that is a signing key will be used' + required: false artifact-properties: description: 'Properties to apply to the deployed artifacts. Each line should be of the form `::. includes and excludes are comma-separated Ant patterns. @@ -60,3 +63,4 @@ runs: - --artifactory.deploy.threads=${{ inputs.threads }} - --artifactory.signing.key=${{ inputs.signing-key }} - --artifactory.signing.passphrase=${{ inputs.signing-passphrase }} + - --artifactory.signing.key-id=${{ inputs.signing-key-id }} diff --git a/src/integrationTest/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployIntegrationTests.java b/src/integrationTest/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployIntegrationTests.java index 8c43958..d41faa0 100644 --- a/src/integrationTest/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployIntegrationTests.java +++ b/src/integrationTest/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployIntegrationTests.java @@ -166,4 +166,43 @@ void deployWithSignatures(@TempDir File temp) throws IOException { assertThat(buildInfoJson).extractingJsonPathArrayValue("buildInfo.modules.[0].artifacts").hasSize(4); } + @Test + void deployWithSignaturesFromSubkey(@TempDir File temp) throws IOException { + File example = new File(temp, "com/example/module/1.0.0"); + example.mkdirs(); + Files.writeString(new File(example, "module-1.0.0.jar").toPath(), "jar-file-content"); + Files.writeString(new File(example, "module-1.0.0.pom").toPath(), "pom-file-content"); + ArtifactoryDeploy.main(new String[] { + String.format("--artifactory.server.uri=http://%s:%s/artifactory", container.getHost(), + container.getFirstMappedPort()), + "--artifactory.server.username=admin", "--artifactory.server.password=password", + "--artifactory.deploy.repository=example-repo-local", "--artifactory.deploy.build.number=15", + "--artifactory.deploy.build.name=integration-test", "--artifactory.deploy.folder=" + temp, + "--artifactory.deploy.threads=2", + "--artifactory.signing.key=" + Files.readString(Path.of("src", "test", "resources", "io", "spring", + "github", "actions", "artifactorydeploy", "openpgp", "test-private-subkey.txt")), + "--artifactory.signing.passphrase=password", "--artifactory.signing.key-id=C3E2E826" }); + + RestTemplate rest = new RestTemplateBuilder().basicAuthentication("admin", "password") + .rootUri("http://%s:%s/artifactory/".formatted(container.getHost(), container.getFirstMappedPort())) + .build(); + assertThat(rest.getForObject("/example-repo-local/com/example/module/1.0.0/module-1.0.0.jar", String.class)) + .isEqualTo("jar-file-content"); + assertThat(rest.getForObject("/example-repo-local/com/example/module/1.0.0/module-1.0.0.pom", String.class)) + .isEqualTo("pom-file-content"); + assertThat(rest.getForObject("/example-repo-local/com/example/module/1.0.0/module-1.0.0.jar.asc", byte[].class)) + .isNotEmpty(); + assertThat(rest.getForObject("/example-repo-local/com/example/module/1.0.0/module-1.0.0.pom.asc", byte[].class)) + .isNotEmpty(); + String buildInfo = rest.getForObject("/api/build/integration-test/15", String.class); + BasicJsonTester jsonTester = new BasicJsonTester(getClass()); + JsonContent buildInfoJson = jsonTester.from(buildInfo); + assertThat(buildInfoJson).extractingJsonPathValue("buildInfo.name").isEqualTo("integration-test"); + assertThat(buildInfoJson).extractingJsonPathValue("buildInfo.number").isEqualTo("15"); + assertThat(buildInfoJson).extractingJsonPathValue("buildInfo.buildAgent.name").isEqualTo("Artifactory Action"); + assertThat(buildInfoJson).extractingJsonPathValue("buildInfo.agent.name").isEqualTo("GitHub Actions"); + assertThat(buildInfoJson).extractingJsonPathArrayValue("buildInfo.modules").hasSize(1); + assertThat(buildInfoJson).extractingJsonPathArrayValue("buildInfo.modules.[0].artifacts").hasSize(4); + } + } diff --git a/src/main/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployProperties.java b/src/main/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployProperties.java index 9ea61eb..b179948 100644 --- a/src/main/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployProperties.java +++ b/src/main/java/io/spring/github/actions/artifactorydeploy/ArtifactoryDeployProperties.java @@ -49,7 +49,7 @@ public Server(URI uri, String username, String password) { } - public record Signing(String key, String passphrase) { + public record Signing(String key, String passphrase, String keyId) { } public record Deploy(String project, String folder, String repository, int threads, Deploy.Build build, diff --git a/src/main/java/io/spring/github/actions/artifactorydeploy/Deployer.java b/src/main/java/io/spring/github/actions/artifactorydeploy/Deployer.java index 743f7a7..6a03ab0 100644 --- a/src/main/java/io/spring/github/actions/artifactorydeploy/Deployer.java +++ b/src/main/java/io/spring/github/actions/artifactorydeploy/Deployer.java @@ -168,15 +168,15 @@ private MultiValueMap signArtifactsIfNecessary( if (signing == null || !StringUtils.hasText(signing.key())) { return batchedArtifacts; } - return signArtifacts(batchedArtifacts, signing.key(), signing.passphrase(), buildProperties); + return signArtifacts(batchedArtifacts, signing.key(), signing.passphrase(), signing.keyId(), buildProperties); } private MultiValueMap signArtifacts( MultiValueMap batchedArtifacts, String signingKey, String signingPassphrase, - Map buildProperties) { + String signingKeyId, Map buildProperties) { try { console.log("Signing artifacts"); - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(signingKey, signingPassphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(signingKey, signingPassphrase, signingKeyId); return new DeployableArtifactsSigner(signer, buildProperties).addSignatures(batchedArtifacts); } catch (IOException ex) { diff --git a/src/main/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSigner.java b/src/main/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSigner.java index 9e56468..8c6a46c 100644 --- a/src/main/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSigner.java +++ b/src/main/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSigner.java @@ -74,20 +74,20 @@ public final class ArmoredAsciiSigner { private final Clock clock; - private ArmoredAsciiSigner(Clock clock, InputStream signingKeyInputStream, String passphrase) { - PGPSecretKey signingKey = getSigningKey(signingKeyInputStream); + private ArmoredAsciiSigner(Clock clock, InputStream signingKeyInputStream, String passphrase, String keyId) { + PGPSecretKey signingKey = getSigningKey(signingKeyInputStream, keyId); this.clock = clock; this.signingKey = signingKey; this.privateKey = extractPrivateKey(passphrase, signingKey); this.contentSigner = getContentSigner(signingKey.getPublicKey().getAlgorithm()); } - private PGPSecretKey getSigningKey(InputStream inputStream) { + private PGPSecretKey getSigningKey(InputStream inputStream, String keyId) { try { try (InputStream decoderStream = PGPUtil.getDecoderStream(inputStream)) { PGPSecretKeyRingCollection keyrings = new PGPSecretKeyRingCollection(decoderStream, FINGERPRINT_CALCULATOR); - return getSigningKey(keyrings); + return getSigningKey(keyrings, keyId); } } catch (Exception ex) { @@ -95,16 +95,23 @@ private PGPSecretKey getSigningKey(InputStream inputStream) { } } - private PGPSecretKey getSigningKey(PGPSecretKeyRingCollection keyrings) { + private PGPSecretKey getSigningKey(PGPSecretKeyRingCollection keyrings, String keyId) { for (PGPSecretKeyRing keyring : keyrings) { Iterable secretKeys = keyring::getSecretKeys; for (PGPSecretKey candidate : secretKeys) { - if (candidate.isSigningKey()) { + if (keyId != null) { + String candidateKeyId = String.format("%08X", 0xFFFFFFFFL & candidate.getKeyID()); + if (keyId.equals(candidateKeyId)) { + return candidate; + } + } + else if (candidate.isSigningKey()) { return candidate; } } } - throw new IllegalArgumentException("Keyring does not contain a suitable signing key"); + throw new IllegalArgumentException((keyId != null) ? "Keyring does not contain key '%s'".formatted(keyId) + : "Keyring does not contain a suitable signing key"); } private PGPPrivateKey extractPrivateKey(String passphrase, PGPSecretKey signingKey) { @@ -212,30 +219,33 @@ private void updateSignatureGenerator(InputStream source, PGPSignatureGenerator } /** - * Get an {@link ArmoredAsciiSigner} for the given {@code signingKey} and - * {@code passphrase}. The signing key may either contain a PGP private key block or - * reference a file. + * Get an {@link ArmoredAsciiSigner} for the given {@code signingKey}, + * {@code passphrase}, and {@code keyId}. The signing key may either contain a PGP + * private key block or reference a file. The key with the given {@code keyId} will be + * used for signing. If {@code keyId} is {@code null} that first key that is a + * {@link PGPSecretKey#isSigningKey() is a signing key} will be used. * @param signingKey the signing key (either the key itself or a reference to a file) * @param passphrase the passphrase to use + * @param keyId the ID of the key to use * @return an {@link ArmoredAsciiSigner} insance * @throws IOException on IO error */ - public static ArmoredAsciiSigner get(String signingKey, String passphrase) throws IOException { - return get(Clock.systemDefaultZone(), signingKey, passphrase); + public static ArmoredAsciiSigner get(String signingKey, String passphrase, String keyId) throws IOException { + return get(Clock.systemDefaultZone(), signingKey, passphrase, keyId); } - static ArmoredAsciiSigner get(Clock clock, String signingKey, String passphrase) throws IOException { + static ArmoredAsciiSigner get(Clock clock, String signingKey, String passphrase, String keyId) throws IOException { Assert.notNull(clock, "Clock must not be null"); Assert.notNull(signingKey, "SigningKey must not be null"); Assert.hasText(signingKey, "SigningKey must not be empty"); Assert.notNull(passphrase, "Passphrase must not be null"); if (isArmoredAscii(signingKey)) { byte[] bytes = signingKey.getBytes(StandardCharsets.UTF_8); - return new ArmoredAsciiSigner(clock, new ByteArrayInputStream(bytes), passphrase); + return new ArmoredAsciiSigner(clock, new ByteArrayInputStream(bytes), passphrase, keyId); } Assert.isTrue(!signingKey.contains("\n"), "Signing key does not contain a PGP private key block and does not reference a file"); - return new ArmoredAsciiSigner(clock, new FileInputStream(signingKey), passphrase); + return new ArmoredAsciiSigner(clock, new FileInputStream(signingKey), passphrase, keyId); } private static boolean isArmoredAscii(String signingKey) { diff --git a/src/test/java/io/spring/github/actions/artifactorydeploy/DeployableArtifactsSignerTests.java b/src/test/java/io/spring/github/actions/artifactorydeploy/DeployableArtifactsSignerTests.java index 3e98994..041fa02 100644 --- a/src/test/java/io/spring/github/actions/artifactorydeploy/DeployableArtifactsSignerTests.java +++ b/src/test/java/io/spring/github/actions/artifactorydeploy/DeployableArtifactsSignerTests.java @@ -56,7 +56,7 @@ class DeployableArtifactsSignerTests { void setup() throws IOException { String signingKey = new String(ArmoredAsciiSigner.class.getResourceAsStream("test-private.txt").readAllBytes(), StandardCharsets.UTF_8); - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(signingKey, "password"); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(signingKey, "password", null); this.signer = new DeployableArtifactsSigner(signer, this.properties); } diff --git a/src/test/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSignerTests.java b/src/test/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSignerTests.java index 4e525e5..e1c3f4e 100644 --- a/src/test/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSignerTests.java +++ b/src/test/java/io/spring/github/actions/artifactorydeploy/openpgp/ArmoredAsciiSignerTests.java @@ -53,8 +53,12 @@ class ArmoredAsciiSignerTests { private File signingKeyFile; + private File signingSubkeyFile; + private String signingKeyContent; + private String signingSubkeyContent; + private String passphrase = "password"; private File sourceFile; @@ -63,117 +67,161 @@ class ArmoredAsciiSignerTests { private String expectedSignature; + private String expectedSubkeySignature; + private File temp; @BeforeEach void setup(@TempDir File temp) throws Exception { this.temp = temp; this.signingKeyFile = copyClasspathFile("test-private.txt"); + this.signingSubkeyFile = copyClasspathFile("test-private-subkey.txt"); this.signingKeyContent = copyToString(this.signingKeyFile); + this.signingSubkeyContent = copyToString(this.signingSubkeyFile); this.sourceFile = copyClasspathFile("source.txt"); this.sourceContent = copyToString(this.sourceFile); this.expectedSignature = copyToString(ArmoredAsciiSignerTests.class.getResourceAsStream("expected.asc")); + this.expectedSubkeySignature = copyToString( + ArmoredAsciiSignerTests.class.getResourceAsStream("subkey-expected.asc")); } @Test void getWhenSigningKeyIsKeyReturnsSigner() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); + assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSignature); + } + + @Test + void getWhenSigningKeyIsKeyAndIdMatchesReturnsSigner() throws Exception { + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, "414E73D1"); assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSignature); } + @Test + void getWhenSigningKeyIsSubkeyAndIdMatchesReturnsSigner() throws Exception { + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingSubkeyContent, this.passphrase, + "C3E2E826"); + assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSubkeySignature); + } + @Test void getWhenSigningKeyIsFileReturnsSigner() throws Exception { ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyFile.getAbsolutePath(), - this.passphrase); + this.passphrase, null); assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSignature); } + @Test + void getWhenSigningKeyIsFileAndIdMatchesReturnsSigner() throws Exception { + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyFile.getAbsolutePath(), + this.passphrase, "414E73D1"); + assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSignature); + } + + @Test + void getWhenSigningKeyIsSubkeyFileAndIdMatchesReturnsSigner() throws Exception { + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingSubkeyFile.getAbsolutePath(), + this.passphrase, "C3E2E826"); + assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSubkeySignature); + } + @Test void getWhenClockIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> ArmoredAsciiSigner.get((Clock) null, this.signingKeyContent, this.passphrase)) + .isThrownBy(() -> ArmoredAsciiSigner.get((Clock) null, this.signingKeyContent, this.passphrase, null)) .withMessage("Clock must not be null"); } @Test void getWhenSigningKeyIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> ArmoredAsciiSigner.get((String) null, this.passphrase)) + assertThatIllegalArgumentException() + .isThrownBy(() -> ArmoredAsciiSigner.get((String) null, this.passphrase, null)) .withMessage("SigningKey must not be null"); } @Test void getWhenSigningKeyIsEmptyThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> ArmoredAsciiSigner.get("", this.passphrase)) + assertThatIllegalArgumentException().isThrownBy(() -> ArmoredAsciiSigner.get("", this.passphrase, null)) .withMessage("SigningKey must not be empty"); } @Test void getWhenSigningKeyIsMultiLineWithoutHeaderThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> ArmoredAsciiSigner.get("ab\ncd", this.passphrase)) + assertThatIllegalArgumentException().isThrownBy(() -> ArmoredAsciiSigner.get("ab\ncd", this.passphrase, null)) .withMessage("Signing key does not contain a PGP private key block and does not reference a file"); } @Test void getWhenSigningKeyIsMalformedThrowsException() throws Exception { String signingKey = copyToString(getClass().getResourceAsStream("test-bad-private.txt")); - assertThatIllegalStateException().isThrownBy(() -> ArmoredAsciiSigner.get(signingKey, this.passphrase)) + assertThatIllegalStateException().isThrownBy(() -> ArmoredAsciiSigner.get(signingKey, this.passphrase, null)) .withMessage("Unable to read signing key"); } @Test void getWhenPassphraseIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> ArmoredAsciiSigner.get(this.signingKeyContent, null)) + assertThatIllegalArgumentException() + .isThrownBy(() -> ArmoredAsciiSigner.get(this.signingKeyContent, null, null)) .withMessage("Passphrase must not be null"); } @Test void getWhenPassphraseIsWrongThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> ArmoredAsciiSigner.get(this.signingKeyContent, "bad")) + assertThatIllegalStateException().isThrownBy(() -> ArmoredAsciiSigner.get(this.signingKeyContent, "bad", null)) .withMessage("Unable to extract private key"); } + @Test + void getWhenKeyIdDoesNotMatchThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, "ABCD1234")) + .withMessage("Unable to read signing key") + .havingCause() + .withMessage("Keyring does not contain key 'ABCD1234'"); + } + @Test void signWithStringReturnsSignature() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThat(signer.sign(this.sourceContent)).isEqualTo(this.expectedSignature); } @Test void signWithStringWhenSourceIsNullThrowsException() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThatIllegalArgumentException().isThrownBy(() -> signer.sign((String) null)) .withMessage("Source must not be null"); } @Test void signWithInputStreamSourceReturnsSignature() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThat(signer.sign(new FileSystemResource(this.sourceFile))).isEqualTo(this.expectedSignature); } @Test void signWithInputStreamSourceWhenSourceIsNullThrowsException() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThatIllegalArgumentException().isThrownBy(() -> signer.sign((InputStreamSource) null)) .withMessage("Source must not be null"); } @Test void signWithInputStreamReturnsSignature() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThat(signer.sign(new FileInputStream(this.sourceFile))).isEqualTo(this.expectedSignature); } @Test void signWithInputStreamWhenSourceIsNullThrowsException() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThatIllegalArgumentException().isThrownBy(() -> signer.sign((InputStream) null)) .withMessage("Source must not be null"); } @Test void signWithInputStreamAndOutputStreamWritesSignature() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); ByteArrayOutputStream destination = new ByteArrayOutputStream(); signer.sign(new FileInputStream(this.sourceFile), destination); assertThat(destination.toByteArray()).asString(StandardCharsets.UTF_8).isEqualTo(this.expectedSignature); @@ -181,14 +229,14 @@ void signWithInputStreamAndOutputStreamWritesSignature() throws Exception { @Test void signWithInputStreamAndOutputStreamWritesWhenSourceIsNullThrowsException() throws IOException { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThatIllegalArgumentException().isThrownBy(() -> signer.sign(null, new ByteArrayOutputStream())) .withMessage("Source must not be null"); } @Test void signWithInputStreamAndOutputStreamWritesWhenDestinationIsNullThrowsException() throws Exception { - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(FIXED, this.signingKeyContent, this.passphrase, null); assertThatIllegalArgumentException().isThrownBy(() -> signer.sign(new FileInputStream(this.sourceFile), null)) .withMessage("Destination must not be null"); } @@ -197,7 +245,7 @@ void signWithInputStreamAndOutputStreamWritesWhenDestinationIsNullThrowsExceptio void signWithClockTickReturnsDifferentContent() throws Exception { Clock clock = mock(Clock.class); given(clock.instant()).willReturn(FIXED.instant(), Clock.offset(FIXED, Duration.ofSeconds(2)).instant()); - ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(clock, this.signingKeyContent, this.passphrase); + ArmoredAsciiSigner signer = ArmoredAsciiSigner.get(clock, this.signingKeyContent, this.passphrase, null); String signatureOne = signer.sign(this.sourceContent); String signatureTwo = signer.sign(this.sourceContent); assertThat(signatureOne).isNotEqualTo(signatureTwo); diff --git a/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/subkey-expected.asc b/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/subkey-expected.asc new file mode 100644 index 0000000..cfc361f --- /dev/null +++ b/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/subkey-expected.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP SIGNATURE----- + +iQGzBAABCAAdFiEEemq8K3fJ9YcNb7x5Fu4lPcPi6CYFAgAAAAAACgkQFu4lPcPi +6CYMgQv/S84lUhtcaTlaCDfZGGl8JzCU3PQuEOsJh0tXP2zQW1hlIdh0grALpVAt +KUsJbZPlsAxC0LdTIZ1iyqF46jLqG0UnsWSb6GJF/TeH+p7n7+SQcKd2nTIyS/XT +a8L4Ha7hBgCkZb8yKOiu1VKwUfvqhbk93UtBvJU8h4n3zoRHrnlLgyTpkNegbVOg +/BgLHKn9XnSbSpeZfagJS/j/Vs+YG0b3LGFky2MQdTVJnTGfcxtnbQHtMPSYr9wk +GRDHd6sTD8xDG9n8hDm5c8aQSksyMO5UKXimdXEG3cYypUlknqmoaN7wKuBaEwL4 +UtFrPm3nHcfI9mh+sbzhX1FDNgF1/PpJ+SDYGOJ8BEqobVZX7V1A3IAvezkNIlXB +D18+/X0MOIvvubkBdKgnPFEX0Mr5SbordsxQAmuGr6PhqLPcsvmc41vZ8cvWJHkZ +70epOAq9l1HNmzDnz9fPebAjngZB3lg4b3CoGJxy7QFavXdW10y3nt1RBHUOEqrL +FWigIzz7 +=widH +-----END PGP SIGNATURE----- diff --git a/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private-subkey.txt b/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private-subkey.txt new file mode 100644 index 0000000..e418415 --- /dev/null +++ b/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private-subkey.txt @@ -0,0 +1,110 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=u0nL +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private.txt b/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private.txt index 9c09542..5920e8a 100644 --- a/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private.txt +++ b/src/test/resources/io/spring/github/actions/artifactorydeploy/openpgp/test-private.txt @@ -8,39 +8,39 @@ dgWGLSv+8rx2BlLrfhgiCYr/3WGf5mrTYo6YuhPEyHiaZUtQal8WvfDMRT2K18Wr zrOde39TD0g2zOAw04G4sGvWaEn6edHZmcsUfTjyIAaLuTYH9UonFXMCO2QTrat+ /ebg0xS7KKMrVmWtS3mPLWi9VEVwNE4ChpHqPA47phGzYJyX54U1jEWj94x5I8bv S74PlCI9BUJm6XqwpSR9wltd2clBIW1d+LPvcNI/2hCIOAq2+XS+wJVi1hAUK9Ow -9RG6bXIlfmD7pTkAEQEAAf4HAwJy3WxV7Z/ZBPM7CfgcTQHvGQJp3RJ8y3GpZrug -5S+HvLpbb3G0nAc8GD2SDarW94J6RIf31/ivqZ7ZYj5Neje8dB7awXZdak6a7qAR -WY496ZDM0ohg36+UyoSavfGkgzoQU69d9tqdC5oiWc39LetNVRV+9V16Wuf40tWe -iBDHrDcvKDEdaIBH7ZQRXWi0jI7V50lXVFRHcPmREETIhjQuAX6x/VfcyszsPfN4 -PVddlQIshrDhQJF2UXMZ1i7F8ckqlJ9Rr3835ii8aAuoXdBIe5TfNiXoshl+GUt4 -6+nevATg2BWyygAZ+JSbGcrU+3jcZO4uw7oyC1k1PkvwSJmFSGGO6W02qQ58+fct -SaGhOJt06RvRc70iZW+pn2rDACNPerIMvOxOUp34FK2ZMbfLUbhs5eCyNMmX+2AH -SxQZx7CqVjJuDBIBEdDP+6isLoYx1VlwAuTzifj3IaxMzw9wZm3r5f2OtjiUKuG0 -AfCUb3+iAr8ho/SxcD+8QF5wOVVpOBQahrYTLoV5HbGZBIzHvj5bEEJrvIcPss98 -pXeSMAsEQ/Gx1gYJ4nzgxTHwTsrMPWeG9xj4wOIB79Gx3PZ2tFeQzXRN6IGmMjZ6 -3FAvLEBhpECluiaAz0O0yfQf1H38u06gm+I9IFFn4gtFSN/BtXIb9KbmJLqdnMJk -EwXRvu31YnZvhjhUmfmr/2dLbPAA/4BYcpDvCf7QnKb+PqliW2xTqjf8T+QgNDoB -D4pwWqlhxEevPNx3IXQRjc9VgC9cphD0QicMXV+9BIkhzoEC5738E69d9RKK5+ch -2kii6/2VcqYU4JHfOyramvp0xTC+LDGiR4uAoF383s7me988KXZ3HQVLxRliA4AD -E0s0RRtSHB/JNuUOIJ/WgQYhv38dDSvI4kqhwaK9pHqSjNpfRtAh8S0F1WZ6KBB6 -dDM6kpYwD/5Zjy7JrjExvIBMESD3KeGc3yjPbMmXut6kbUYgHfG3yUWeg1FUq9J9 -SCp1XtyCbV/dMxTblYNn6J+KP4yiIZfkXJtORIX0ck9Fs+Yi23UgnIEoif+upbBn -h+wNADB+TzNNGpcp3FaPSupxP6RnNeIB0V1fgiMoU0Vntg3x4RYdLcxolZ1bdn+w -DC83dsCMxN89TDHbqn4iZVRdwhqzDpH3o+epd+0JfZjj/IijgTmHj5gumAWO4HJb -Da7QKiLzATAqSsUBvYMmX8h3q56rEwNrEWrJx+gjRnDbMK1AvCCPhaT9yse/Jgmt -F8CdTgpXXCB4ANwlVPSei+5IIqUFU9Js3TBU1xu/7FSq/XLB7XKJpWi2Ix8/ugLS -VUlxYMzxkFpfJXVIxUPLvBCC1TOGFt6HSrQnYXJ0aWZhY3RvcnktcmVzb3VyY2Ug -PG5vbmVAZXhhbXBsZS5jb20+iQHUBBMBCAA+FiEEa20YkXMm681UfxMwutQ4eEFO -c9EFAmAd6tYCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQutQ4 -eEFOc9HBYQv/XtuZR4gYj4etKydLI2l9f5Ohk+BYvXnDQbVTOMAdCJS/C4nyh8o4 -0oEG2hlFlHWAfjO9BPMDTCntG+2bLE9HKv64VfTMXoWmNlDoe6o3QK976Kt/5Xrl -PjeNBaPPbkVbx0K4eVl+/vE8RCsH0kMQCJK4knen3eJIEHx5fySqrqed+z3br/0c -dBJNhpCVDGGmA2wGBBwjJwk33BNtRctf86Mq4DeyzNaj6IAXeUqf9/S0qKCLpQL5 -BOPFSMdar4xOlF/kY2ELgRP7eeC7k2hBdSyV5VGInk1T6FR78R+25A2LX0Rt5+tb -mbgURj7AnxQl/mnzFm4CIDU1ySFLrZ27KNO/uVlmYiYvzUTx6VYAXEYBouXKmUJA -gsEQZkPZf1HTImG1DBhpP5v0KtmuhMFk+n97ANgXo+DbXY7Z+7u9xvsiOvsRAyHG -tghLiP+ARfRC5ugWFsNYiRL1xh9tTZfwps3ZM2LXtUeCwFqz8HbWHRHFMwBhgHGI -zR88MjP3dee8nQWGBGAd6tYBDADjeL1IJgh8wwc8S98aMhHi9gDp/pxnsLdpqZHq +9RG6bXIlfmD7pTkAEQEAAf4HAwK2Z4Ink+4wMPXN5mJuA7jgdA+kvOs9fncuw0uf +h8TdfnS/gWFqjukZdhqmn+5RT0iUi9gBHTYXxpE81Mz8ajgTQVytY7Cv+95ElzsR +jEgtc7U69GHvJg0HuSkV4mmAhBB4Vypn+QCjlpoqYdGElP85StUrvMLIoUlS/ncG +pg1jjUzGbQmJyo2vE/+df8gCF0+J+SMT/sZHJ+xyfIp6YeZoodrr4u8K0PBvVIkb +rzpbj1Yt3k0ErTxrBnDEpLdnf2sfvQi0HnhD3x9ht5USKCdt93FQ7eScwdIQWQMo +HXoM4Rf5x4i8yzdjJ0dPdc31BEPl6I0ymbE6ZbSaeKXCqDEgANLCBkTKDCt7oHVH +5NiH2bNqUivW+mG6CRt+9evtIziZea4U8Nun9PHavxhsIS2dH68vratR65HpHBip +8Lxzmi0jLpjPyCmNkV+6ETk4IwQZZmHpvgS2TOzpWgRN/S10xdqdjY2ndknErLX/ +gOojd9AaNusQfwi43S/3U897ViYT4ETGhZYUCdVYn/uQ9NGLpgtkEDFurzC/Akbe +C6ah8ZJzXfFAEZ0q815fZg5QD9uUBNAyVUkDFvpLkLsPUEpX59XRyG+GmvZIAjvF +6O/cSHxEU/RkkMRucSKdOX7vjNPx1/WkG1xLkdeBAo4zzCcOJ2onpLgSKhIxEvhK ++pAhh3hKjLnUD8Iy7SryJ32+ot7H4nwzWhrQf/FhcHb7SypZ0n7YekWIZkmIcDDU +U3zqTzt3KaaUnuXdPsqVKiBJVjmzw3P5a5omUXBqWvt2qVV5qByxoih7qH0bMdqp +zpJWI/G3q/K69liKE5M5dj8+QuQpaVTIDcsYrRT5ft9blkR7xl77KBSH/ng0s6eK +jfOGdQtGXYkc7ohKBHMg73R19sUYRijtPJmOOs/LfXmvNRpl6M8IzBfnbnJr6+YD +Kmtys5UGYVdlvbPYEBF2sLrXOfXtAo6e5jSTOysakuQFyRJdNwFWczsDu1kFQ6N8 +i2Ug/EnWZiS7frTMLZZoL6kq/2/GPdpqYUzyeHHjXbMGuL/XfVv1jLxuLs1tRA0v +x+/RaPZ0ezx3einbCsPybzNhgR1fPdKhto/ANJUWdfwp4LmirpoK/DlK1YiRPVSC +J4fYuYtXWOXlfhDIwWSrYyxsIXak/5ss0ldlJ/gqR+Jdig09ccJ5JScIrG2KB4Ae +OdnN50yyXu8Bd4GlDM8wZ8o51K5FlMLcQj/cxrlvBg3qlIQrIxgLIQMYhBUiHngW +asLTG0rEx+eNtP2Wyud4AEU10LU9ACqAlrSBtiP28eWQjHFSNhyxpAoBQQwrcJEZ +ud1ty6x1AmVubj0aKIxr6bP/NpO+X5u6P7QnYXJ0aWZhY3RvcnktcmVzb3VyY2Ug +PG5vbmVAZXhhbXBsZS5jb20+iQHUBBMBCAA+AhsDBQsJCAcCBhUKCQgLAgQWAgMB +Ah4BAheAFiEEa20YkXMm681UfxMwutQ4eEFOc9EFAmc8eDEFCQj/wNsACgkQutQ4 +eEFOc9GfYQv/Sv0SVq3Q4hfl2YR8YB2WfTRirko8jNTjsXZzdRGCjBWuiHDfWYZ7 +sVdaI5Mi2FCw8WI9vbOdFcLbrXDywBb26mHlkr+mHp4ZAq5pFEHO/5ZdBDFGP8VC +MxcO5B0vpYrYC3LGYfhhyEZJJD+UDx0HeUdkhIfTGr+Oi3PNoNxoKHptAyPgX2Fb +iI+HqGomNlqA5GE0NWTpX37ARGfpJpStU9xWIgel/F1+1uyW18H9WNCm2A7R8xIO +sM5uUU6K+VAKAtYiD+eQGw2QcvKv1BTDhdeUGIqIBnSc58XDgLXNxvK9yr4IzECJ +3x+lPcanSuzqu/aDrCTYZNLM77wINxpk6ICM5RZgwgjad7qe/O54sygSrFBb/c6X +GtU58ZRqbfEtKs/lvxtU8G+Iwx++2wl7iDhY15qI+iXlgYC78Z5V4dd82nbJTIJp +xPsV6WhEJRQ/Gqj7xLDDYRnhFhCknSSii7QLKVVO8UXMOqP4pGtgQ8z2XY9gDsGE +3Wh1hye8uhA9nQWGBGAd6tYBDADjeL1IJgh8wwc8S98aMhHi9gDp/pxnsLdpqZHq PS6iPqsYLlWLKNDrzirtYERY+Z3zMpe6itAZn3BAMZGUdcnHsI3kaBCPBpnFT63X bvNYmO3Ot9M7tkjkyOhabHCF64bi3NqvB3h2TUU//pT6gZseoKgVB/dTZapHdpyu EKOMQ+Yqne86zmmLQYTKFVFILP3bucPohZCdVDpWlDqfCBrcVZrWQRqMOhA7xJ73 @@ -48,37 +48,84 @@ HPb/SgL4+oqoWBq412jbbL7OSbIGjX4nY15TGviv5mc4jKix/5PPKcvKLPcOb+LE R8UvoXdpOA0XaRB1Y5Ek5GbHMjeEc+Jn3UZngi9u8N0zsqtW2tBLMzbmwOQkUv1G ombVqf3DFO6ZKGRnGqbz4FRo71sSBG6K3kifEl1RR+cIZZukYK8+X5tuyjqha9FQ 3sMVkq4sgezKJBhTF69kPSdRPwGZfQJbPuH1tUaiXofXyZqwwGKZA1J6j+zyyONe -XLkvpX6AD4hsh8r1xpPDZPEdtGMAEQEAAf4HAwJCodBzM2ElKPNFOPUBAMT+Qx01 -ScYG9DoQ8O5d9zcudpyT4T7LwWCRSmA1N8K3QkZNez+Sh3/HIL84E3051l64RG/a -oOm7qeZq7agbIK82YtEyVO2Oes4UvZzKJ41OfXt4quXjKZcSiu/L5pmuz/GWFjS9 -U5IJrPPzrvKl6ohN3yCHUJNBokdO6HvhNCohagUqf5pRYsgp9nBttlw0BmTNF1AU -CpG1Oz+jOR204cUCkUONKkBa/oiBjNFu9y2jXdZNgVDRObMD1t9R0GkCkq8YwtMA -dPqrkHZhdHFxGNT5oEgpN48+oY5rKFZrJMT+rhuzdQKD9ayO64HLQSLBwhAFNPt9 -5ZQCJjz9c1MEwIVu8Fs+u3K6wW2R+fWuDeC9W0ea4gFlYOa6aPFaMkn+O3pQbdGs -5vc42xmreoeXeNac7NQUvsIorVoKD489ST2s9xzqKPw3xa/trvOGRMOOq/EPRtHU -/oo9/Pi5hev7YKaXR9UQic/LyKw5twAy+XZ6gbfb95Uy3QSn6GjxWEiAj9HR6zom -mBO5jFLvjIQlPmhSleNVLM+r0TM4rj/MgYtQDwSTWIY/a3U+154kpazPMMt/tg80 -B9oQdpl8TmtYcJD+tbZa3PpoagYnzRtK7MsPykCJ1MXMgqKPZl4nyDsO0S755vPR -2BUkGqv2V7p8N6Xkdee6q91NfW+UKUZ+mVtXtp1rSLPPGlXILvMtVN3/+ejxN7hJ -ZNVm+Q6s/wUjKe52LLBhXsThtoQ6zKaoQvvJLzglzLfn4lri86aOXcdlRnPCY8mG -sDDJrm7NS9omgOxgM9qea1Hitm2O75PqN+HxSYHXkg7+0UdyVyXMEO9EwfqsqYFw -hDSuNxna6maz2/+El98RvJL9dfjjdLWSpEpQvUF8jJ8EYwLpmxgJSSs4YoEHf0F0 -PmelI1sX0mMj+btT0QA5UPQg+bYV8tLntBTEUjGA0TGCtXoh8BuIqOGhDadfwfs/ -k4gtk4RQBUSIOkk2iJDjjYPnXhC0wEa2dbUcoVdOjdFTwINbHtsmZmKg7q4SVNzu -m02qKkYo60t5G1C3mT/Wlrbpf0bozSGG6Yqf5lNVH7yfeqsniGo//J6Q64nu6vzx -95e+QOJ+7DROvKjXx4BxWfLA3hRCk2jpt3BeY1NPZwo2pEaxxgf35LdzZBrxXK5J -FOqAxVyqkB3xyGT+T1vLMLAjIft64RC6Ap9AmiAM+6BvsaeXjjefeTbyAPpnXLzq -keakB/wOEQJtqOfaN9avoZ5B9LD6PSnEXh9dlolmr5+Wf4OZWjCHstTSsbgfLv7b -mfnxVDycD8wyRDY0oVjb+idC/mLYgbChHzoYUNdcyMuBS4kBvAQYAQgAJhYhBGtt -GJFzJuvNVH8TMLrUOHhBTnPRBQJgHerWAhsMBQkDwmcAAAoJELrUOHhBTnPRp0gM -AIHDlV1JQoAnLpx7zyhp88B0D6ABsDBmHvTDxxEOXZEDtNwGbGFyuC1PcApX3Xrp -IxRBmIzdwZ4xZvZCTqVTYkccgnbthEPgSZxo231+/SSo1jijuj8uFm6adoQHasGl -8k4zxZA6wi4/e6AVsULtpGMb34bob8LoPPZ71JiIzPhfwYmGimsWBCR7rtoeDreh -TkVE1/Uui4k+MagYUX+vCGsAs3QTiAWUTixS04K4YVBKzI2bOCqzSPZF+OJcpmA7 -vuM8q7TWvzhusvRK07shQ1Obn9TZTVahghZq2B+EGJJpVxWb/IzaPMS+YOuFeZiY -BCM6RJ6YMNAphH+XC5ugiZGcaAVvm+JIDFfK440M8oirJ0+Zo1Cbf8SEH+mtrpn0 -K/ew4qZf4ltUKD4h0kmkPRmEDN6L9yTTzuukJBgSlIr/XKaNfktk+o9E1Img1Lxd -qCECMewlytkLVR2QkcBlF8Rcajh0nNll2Sqd/BhPNGBrN7F+CpTia1eXIm+AajDE -KQ== -=DJH7 +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 +=siWW -----END PGP PRIVATE KEY BLOCK-----