Skip to content

Commit

Permalink
Merge pull request #23911 Trusted GPG-key should only accept 160-bit …
Browse files Browse the repository at this point in the history
…fingerprints

Backport from #23667 to 7.6.1

Fixes #23910

Co-authored-by: Balint Hegyi <bhegyi@gradle.com>
  • Loading branch information
bot-gradle and hegyibalint committed Feb 22, 2023
2 parents 952184f + cd9779f commit bc3da47
Show file tree
Hide file tree
Showing 16 changed files with 462 additions and 134 deletions.
Expand Up @@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit
import static org.gradle.security.fixtures.SigningFixtures.getValidPublicKeyLongIdHexString
import static org.gradle.security.fixtures.SigningFixtures.signAsciiArmored
import static org.gradle.security.fixtures.SigningFixtures.validPublicKeyHexString
import static org.gradle.security.internal.SecuritySupport.toLongIdHexString
import static org.gradle.security.internal.SecuritySupport.toHexString

class DependencyVerificationSignatureCheckIntegTest extends AbstractSignatureVerificationIntegrationTest {

Expand Down Expand Up @@ -64,34 +64,6 @@ class DependencyVerificationSignatureCheckIntegTest extends AbstractSignatureVer
succeeds ":compileJava"
}

def "doesn't need checksums if signature is verified and trust using long id"() {
createMetadataFile {
keyServer(keyServerFixture.uri)
verifySignatures()
addTrustedKey("org:foo:1.0", validPublicKeyLongIdHexString)
addTrustedKey("org:foo:1.0", validPublicKeyLongIdHexString, "pom", "pom")
}

given:
javaLibrary()
uncheckedModule("org", "foo", "1.0") {
withSignature {
signAsciiArmored(it)
}
}
buildFile << """
dependencies {
implementation "org:foo:1.0"
}
"""

when:
serveValidKey()

then:
succeeds ":compileJava"
}

def "if signature is verified and checksum is declared in configuration, verify checksum (terse output=#terse)"() {
createMetadataFile {
keyServer(keyServerFixture.uri)
Expand Down Expand Up @@ -496,8 +468,8 @@ If the artifacts are trustworthy, you will need to update the gradle/verificatio
createMetadataFile {
keyServer(keyServerFixture.uri)
verifySignatures()
addTrustedKeyByFileName("org:foo:1.0", "foo-1.0-classy.jar", trustedKey)
addTrustedKey("org:foo:1.0", trustedKey, "pom", "pom")
addTrustedKeyByFileName("org:foo:1.0", "foo-1.0-classy.jar", validPublicKeyHexString)
addTrustedKey("org:foo:1.0", validPublicKeyHexString, "pom", "pom")
}
given:
Expand Down Expand Up @@ -527,12 +499,6 @@ If the artifacts are trustworthy, you will need to update the gradle/verificatio
if (GradleContextualExecuter.isConfigCache()) {
failure.assertOutputContains("Configuration cache entry discarded.")
}
where:
trustedKey << [
validPublicKeyHexString,
validPublicKeyLongIdHexString
]
}
def "reasonable error message if key server fails to answer (terse output=#terse)"() {
Expand Down Expand Up @@ -586,7 +552,7 @@ This can indicate that a dependency has been compromised. Please carefully verif
def keyring = newKeyRing()
def secondServer = new KeyServer(temporaryFolder.createDir("keyserver-${UUID.randomUUID()}"))
secondServer.registerPublicKey(keyring.publicKey)
def pkId = toLongIdHexString(keyring.publicKey.keyID)
def pkId = toHexString(keyring.publicKey.fingerprint)
secondServer.start()
createMetadataFile {
keyServer(keyServerFixture.uri)
Expand Down Expand Up @@ -1296,7 +1262,7 @@ This can indicate that a dependency has been compromised. Please carefully verif
def "passes verification if an artifact is signed with multiple keys and one of them is ignored"() {
def keyring = newKeyRing()
keyServerFixture.registerPublicKey(keyring.publicKey)
def pkId = toLongIdHexString(keyring.publicKey.keyID)
def pkId = toHexString(keyring.publicKey.fingerprint)
createMetadataFile {
keyServer(keyServerFixture.uri)
verifySignatures()
Expand Down Expand Up @@ -1464,7 +1430,7 @@ If the artifacts are trustworthy, you will need to update the gradle/verificatio
def "can read public keys from #keyRingFormat keyring"() {
// key will not be published on the server fixture but available locally
def keyring = newKeyRing()
def pkId = toLongIdHexString(keyring.publicKey.keyID)
def pkId = toHexString(keyring.publicKey.fingerprint)

createMetadataFile {
disableKeyServers()
Expand Down Expand Up @@ -1687,7 +1653,7 @@ This can indicate that a dependency has been compromised. Please carefully verif
@Issue("https://github.com/gradle/gradle/issues/18440")
def "fails on a bad verification file change after previous successful build when key servers are disabled"() {
def keyring = newKeyRing()
def pkId = toLongIdHexString(keyring.publicKey.keyID)
def pkId = toHexString(keyring.publicKey.fingerprint)

createMetadataFile {
disableKeyServers()
Expand Down Expand Up @@ -1771,6 +1737,86 @@ This can indicate that a dependency has been compromised. Please carefully verif
succeeds ":compileJava"
}

def "fails verification if a per artifact trusted key is not a fingerprint"() {
createMetadataFile {
keyServer(keyServerFixture.uri)
verifySignatures()
addTrustedKey("org:foo:1.0", validPublicKeyHexString)
addTrustedKey("org:foo:1.0", validPublicKeyHexString, "pom", "pom")
}

// We need to manually replace the key in the XML, as 'createMetadataFile' will already fail if we use a non-fingerprint ID
def longId = validPublicKeyHexString.substring(validPublicKeyHexString.length() - 16)
file("gradle/verification-metadata.xml").replace(validPublicKeyHexString, longId)

given:
terseConsoleOutput(terse)
javaLibrary()
uncheckedModule("org", "foo", "1.0") {
withSignature {
signAsciiArmored(it)
}
}
buildFile << """
dependencies {
implementation "org:foo:1.0"
}
"""

when:
fails ":compileJava"

then:
if (GradleContextualExecuter.isConfigCache()) {
failure.assertOutputContains("Configuration cache entry discarded.")
}
failureCauseContains("An error happened meanwhile verifying 'org:foo:1.0'")
failureCauseContains("The following trusted GPG IDs are not in a minimum 160-bit fingerprint format")
failureCauseContains("'${longId}'")

where:
terse << [true, false]
}

def "fails verification if a globally trusted key is not a fingerprint"() {
createMetadataFile {
keyServer(keyServerFixture.uri)
verifySignatures()
addGloballyTrustedKey(validPublicKeyHexString, "org", "foo", "1.0", "foo-1.0-classified.jar", false)
}

// We need to manually replace the key in the XML, as 'createMetadataFile' will already fail if we use a non-fingerprint ID
def longId = validPublicKeyHexString.substring(validPublicKeyHexString.length() - 16)
file("gradle/verification-metadata.xml").replace(validPublicKeyHexString, longId)

given:
terseConsoleOutput(terse)
javaLibrary()
uncheckedModule("org", "foo", "1.0") {
withSignature {
signAsciiArmored(it)
}
}
buildFile << """
dependencies {
implementation "org:foo:1.0"
}
"""

when:
fails ":compileJava"

then:
if (GradleContextualExecuter.isConfigCache()) {
failure.assertOutputContains("Configuration cache entry discarded.")
}
failureCauseContains("The following trusted GPG IDs are not in a minimum 160-bit fingerprint format")
failureCauseContains("'${longId}'")

where:
terse << [true, false]
}

private static void tamperWithFile(File file) {
file.bytes = [0, 1, 2, 3] + file.readBytes().toList() as byte[]
}
Expand Down
Expand Up @@ -45,7 +45,7 @@
import org.gradle.api.internal.artifacts.ivyservice.resolveengine.oldresult.TransientConfigurationResultsLoader;
import org.gradle.api.internal.artifacts.transform.ArtifactTransforms;
import org.gradle.api.internal.artifacts.transform.VariantSelector;
import org.gradle.api.internal.artifacts.verification.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.exceptions.DependencyVerificationException;
import org.gradle.api.internal.attributes.AttributeContainerInternal;
import org.gradle.api.internal.file.FileCollectionInternal;
import org.gradle.api.internal.file.FileCollectionStructureVisitor;
Expand Down
Expand Up @@ -27,7 +27,7 @@
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.verification.writer.WriteDependencyVerificationFile;
import org.gradle.api.internal.artifacts.ivyservice.resolutionstrategy.ExternalResourceCachePolicy;
import org.gradle.api.internal.artifacts.repositories.resolver.MetadataFetchingCost;
import org.gradle.api.internal.artifacts.verification.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.exceptions.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.signatures.BuildTreeDefinedKeys;
import org.gradle.api.internal.artifacts.verification.signatures.SignatureVerificationServiceFactory;
import org.gradle.api.internal.component.ArtifactType;
Expand Down
Expand Up @@ -30,7 +30,7 @@
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.ModuleComponentRepository;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.verification.report.DependencyVerificationReportWriter;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.verification.report.VerificationReport;
import org.gradle.api.internal.artifacts.verification.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.exceptions.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.serializer.DependencyVerificationsXmlReader;
import org.gradle.api.internal.artifacts.verification.signatures.BuildTreeDefinedKeys;
import org.gradle.api.internal.artifacts.verification.signatures.SignatureVerificationService;
Expand Down
Expand Up @@ -34,7 +34,7 @@
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.verification.DefaultKeyServers;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.verification.DependencyVerificationOverride;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.verification.utils.PGPUtils;
import org.gradle.api.internal.artifacts.verification.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.exceptions.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.model.ChecksumKind;
import org.gradle.api.internal.artifacts.verification.model.IgnoredKey;
import org.gradle.api.internal.artifacts.verification.serializer.DependencyVerificationsXmlReader;
Expand Down
@@ -0,0 +1,72 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.gradle.api.internal.artifacts.verification.exceptions;

import org.gradle.api.GradleException;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.internal.logging.text.TreeFormatter;

import java.util.function.Consumer;

public class ComponentVerificationException extends GradleException {

private final ModuleComponentIdentifier component;
private final Consumer<TreeFormatter> causeErrorFormatter;

/**
* Creates a new exception when a component cannot be verified - because of some reason.
*
* @param component the component which failed the verification
*/
public ComponentVerificationException(String message, ModuleComponentIdentifier component) {
super(message);
this.component = component;
this.causeErrorFormatter = null;
}

/**
* Creates a new exception when a component cannot be verified - because of some reason.
*
* @param component the component which failed the verification
* @param causeErrorFormatter a consumer, which will be called with a {@link TreeFormatter}, and can put extra details what happened
*/
public ComponentVerificationException(ModuleComponentIdentifier component, Consumer<TreeFormatter> causeErrorFormatter) {
this.component = component;
this.causeErrorFormatter = causeErrorFormatter;
}

@Override
public String getMessage() {
final TreeFormatter treeFormatter = new TreeFormatter();
// Add our header first
treeFormatter.node(
String.format(
"An error happened meanwhile verifying '%s:%s:%s':",
component.getGroup(), component.getModule(), component.getVersion()
)
);

if (this.causeErrorFormatter != null) {
treeFormatter.startChildren();
// Let the underlying exception explain the situation
causeErrorFormatter.accept(treeFormatter);
treeFormatter.endChildren();

}
return treeFormatter.toString();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors.
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package org.gradle.api.internal.artifacts.verification;
package org.gradle.api.internal.artifacts.verification.exceptions;

import org.gradle.api.GradleException;
import org.gradle.internal.exceptions.Contextual;
Expand Down
@@ -0,0 +1,70 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.gradle.api.internal.artifacts.verification.exceptions;

import org.gradle.api.GradleException;
import org.gradle.api.internal.DocumentationRegistry;
import org.gradle.internal.logging.text.TreeFormatter;

import java.util.List;

/**
* Exception class used when a GPG IDs were not correct.
*
* <p>
* An example is using short/long IDs instead of fingerprints when trusting keys
*/
public class InvalidGpgKeyIdsException extends GradleException {
private final List<String> wrongKeys;

/**
* Creates a new exception with a list of incorrect keys.
*
* @param wrongKeys the list of incorrect IDs, which will be nicely formatted as part of the exception messages so the user can find them
*/
public InvalidGpgKeyIdsException(List<String> wrongKeys) {
this.wrongKeys = wrongKeys;
}

/**
* Formats a nice error message by using a {@link TreeFormatter}.
*
* <p>
* Idea for this method is that you can pass a higher-level {@link TreeFormatter} into here, and get a coherent, nice error message printed out - so the user will see a nice chain of causes.
*/
public void formatMessage(TreeFormatter formatter) {
final DocumentationRegistry documentationRegistry = new DocumentationRegistry();
final String documentLink = documentationRegistry.getDocumentationFor("dependency_verification", "sec:understanding-signature-verification");

formatter.node(
String.format("The following trusted GPG IDs are not in a minimum 160-bit fingerprint format (see: %s):", documentLink)
);
formatter.startChildren();
wrongKeys
.stream()
.map(key -> String.format("'%s'", key))
.forEach(formatter::node);
formatter.endChildren();
}

@Override
public String getMessage() {
final TreeFormatter treeFormatter = new TreeFormatter();
formatMessage(treeFormatter);
return treeFormatter.toString();
}
}
Expand Up @@ -20,7 +20,7 @@
import org.gradle.api.artifacts.ModuleIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier;
import org.gradle.api.internal.artifacts.verification.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.exceptions.DependencyVerificationException;
import org.gradle.api.internal.artifacts.verification.model.ChecksumKind;
import org.gradle.api.internal.artifacts.verification.model.IgnoredKey;
import org.gradle.api.internal.artifacts.verification.verifier.DependencyVerifier;
Expand Down

0 comments on commit bc3da47

Please sign in to comment.