Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

checksum-dependency: use full pgp fingerprints for verification #61

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ This library is distributed under terms of Apache License 2.0

Change log
----------
v1.86
* checksum-dependency: use full fingerprint for PGP verification

v1.85
* licence-gather: better support for build cache by adding PathSensitivity
* checksum-dependency: cache PGP public keys under `%{ROOT_DIR}/gradle/checksum-dependency-plugin/cached-pgp-keys`
Expand Down
3 changes: 3 additions & 0 deletions plugins/checksum-dependency-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ Verification options

Changelog
---------
v1.86
* Use full fingerprint for PGP verification

v1.85
* Cache public PGP keys under `%{ROOT_DIR}/gradle/checksum-dependency-plugin/cached-pgp-keys` directory
* Bump org.bouncycastle:bcpg-jdk15on to 1.70
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,9 @@ class ChecksumDependency(
val signatures = art.file.toSignatureList()
keysToVerify[art] = signatures
for (sign in signatures) {
if (verificationDb.isIgnored(sign.keyID)) {
logger.debug("Public key ${sign.keyID.hexKey} is ignored via <ignored-keys>, so ${art.id.artifactDependency} is assumed to be not signed with that key")
val signKey = sign.pgpShortKeyId
if (verificationDb.isIgnored(signKey)) {
logger.debug("Public key $signKey is ignored via <ignored-keys>, so ${art.id.artifactDependency} is assumed to be not signed with that key")
continue
}
}
Expand All @@ -235,31 +236,35 @@ class ChecksumDependency(
logger.debug { "Resolved signature $signatureDependency" }
receivedSignatures.add(signatureDependency)
for (sign in art.file.toSignatureList()) {
if (verificationDb.isIgnored(sign.keyID)) {
logger.debug("Public key ${sign.keyID.hexKey} is ignored via <ignored-keys>, so ${art.id.artifactDependency} is assumed to be not signed with that key")
val signKey = sign.pgpShortKeyId
if (verificationDb.isIgnored(signKey)) {
logger.debug("Public key $signKey is ignored via <ignored-keys>, so ${art.id.artifactDependency} is assumed to be not signed with that key")
continue
}
val verifySignature = keyStore
.getKeyAsync(sign.keyID, signatureDependency, executors)
.thenAcceptAsync({ publicKey ->
if (publicKey == null) {
logger.warn("Public key ${sign.keyID.hexKey} is not found. The key was used to sign ${art.id.artifactDependency}." +
.getKeyAsync(signKey, signatureDependency, executors)
.thenAcceptAsync({ publicKeys ->
if (publicKeys.isEmpty()) {
logger.warn("Public key $signKey is not found. The key was used to sign ${art.id.artifactDependency}." +
" Please ask dependency author to publish the PGP key otherwise signature verification is not possibles")
verificationDb.ignoreKey(sign.keyID)
verificationDb.ignoreKey(signKey)
return@thenAcceptAsync
}
logger.debug { "Verifying signature ${sign.keyID.hexKey} for ${art.id.artifactDependency}" }
val file = originalFiles[dependencyChecksum.id]!!
val validSignature = signatureVerificationTimer(file.length()) {
verifySignature(file, sign, publicKey)
}
if (validSignature) {
synchronized(dependencyChecksum) {
dependencyChecksum.pgpKeys += sign.keyID
for (publicKey in publicKeys) {
val fullKeyId = publicKey.pgpFullKeyId
logger.debug { "Verifying signature $fullKeyId for ${art.id.artifactDependency}" }
val file = originalFiles[dependencyChecksum.id]!!
val validSignature = signatureVerificationTimer(file.length()) {
verifySignature(file, sign, publicKey)
}
if (validSignature) {
synchronized(dependencyChecksum) {
dependencyChecksum.pgpKeys += fullKeyId
}
}
logger.log(if (validSignature) LogLevel.DEBUG else LogLevel.LIFECYCLE) {
"${if (validSignature) "OK" else "KO"}: verification of ${art.id.artifactDependency} via $fullKeyId"
}
}
logger.log(if (validSignature) LogLevel.DEBUG else LogLevel.LIFECYCLE) {
"${if (validSignature) "OK" else "KO"}: verification of ${art.id.artifactDependency} via ${publicKey.keyID.hexKey}"
}
}, executors.cpu)
verifyPgpTasks.add(verifySignature)
Expand Down Expand Up @@ -326,7 +331,14 @@ class ChecksumDependency(
}

private fun verifySignature(file: File, sign: PGPSignature, publicKey: PGPPublicKey): Boolean {
sign.init(BcPGPContentVerifierBuilderProvider(), publicKey)
try {
sign.init(BcPGPContentVerifierBuilderProvider(), publicKey)
} catch (e: Throwable) {
e.addSuppressed(
Throwable("Verifying $file with key ${publicKey.pgpFullKeyId}, sign ${sign.pgpShortKeyId}")
)
throw e
}
file.forEachBlock { block, size -> sign.update(block, 0, size) }
return sign.verify()
}
Expand All @@ -343,7 +355,7 @@ class ChecksumDependency(
append(" ").append(violation).appendPlatformLine(":")
artifacts
.asSequence()
.map { "${it.id.dependencyNotation} (pgp=${it.pgpKeys.hexKeys}, sha512=${it.sha512.ifEmpty { "[computation skipped]" }})" }
.map { "${it.id.dependencyNotation} (pgp=${it.pgpKeys}, sha512=${it.sha512.ifEmpty { "[computation skipped]" }})" }
.sorted()
.forEach {
append(" ").appendPlatformLine(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ open class ChecksumDependencyPlugin : Plugin<Settings> {
settings.property("checksum.xml", "checksum.xml")
val checksums = File(settings.rootDir, checksumFileName)
val buildDir = settings.property("checksumBuildDir", "build/checksum")
val cachedKeysTempRoot =
File(
settings.rootDir,
settings.property("checksumCachedPgpKeysTempDir", "build/checksum/key-cache-temp")
)
val buildFolder = File(settings.rootDir, buildDir)
val cachedKeysRoot =
settings.property("checksumCachedPgpKeysDir", "%{ROOT_DIR}/gradle/checksum-dependency-plugin/cached-pgp-keys")
Expand Down Expand Up @@ -137,10 +142,10 @@ open class ChecksumDependencyPlugin : Plugin<Settings> {
readTimeout = Duration.ofSeconds(pgpReadTimeout)
)
)
val keyStore = KeyStore(cachedKeysRoot, keyDownloader)
val keyStore = KeyStore(cachedKeysRoot, cachedKeysTempRoot, keyDownloader)
val verification =
if (checksums.exists()) {
DependencyVerificationStore.load(checksums)
DependencyVerificationStore.load(checksums, skipUnparseable = checksumUpdate)
} else {
DependencyVerification(VerificationConfig(PgpLevel.GROUP, ChecksumLevel.NONE))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ internal fun File.sha512(): String {
md.update(buffer, 0, bytesRead)
}
return BigInteger(1, md.digest()).toString(16).toUpperCase()
.padStart(128, '0')
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
*/
package com.github.vlsi.gradle.checksum

import com.github.vlsi.gradle.checksum.pgp.PgpKeyId
import org.bouncycastle.bcpg.ArmoredOutputStream
import java.io.File
import java.io.InputStream
import org.bouncycastle.openpgp.*
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.nio.ByteBuffer

fun InputStream.toSignatureList() =
buffered()
Expand All @@ -38,10 +43,56 @@ fun File.toSignatureList() = inputStream().toSignatureList()

val PGPSignature.hexKey: String get() = keyID.hexKey

val Iterable<Long>.hexKeys: String get() = sorted().joinToString(prefix = "[", postfix = "]") { it.hexKey }
val PGPSignature.pgpShortKeyId: PgpKeyId.Short get() =
PgpKeyId.Short(ByteBuffer.allocate(8).putLong(keyID).array())

val PGPPublicKey.pgpFullKeyId: PgpKeyId.Full get() =
PgpKeyId.Full(fingerprint)

val PGPPublicKey.pgpShortKeyId: PgpKeyId.Short get() =
PgpKeyId.Short(ByteBuffer.allocate(8).putLong(keyID).array())

// `java.lang`.Long.toHexString(this) does not generate leading 0
val Long.hexKey: String get() = "%016x".format(this)

fun InputStream.readPgpPublicKeys() =
PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(this), BcKeyFingerprintCalculator())

fun PGPPublicKeyRingCollection.publicKeysWithId(keyId: PgpKeyId.Short) =
keyRings.asSequence()
.flatMap { keyRing ->
keyRing.asSequence().filter { it.keyID == keyId.keyId }
}

/**
* Remove all UserIDs and Signatures to avoid storing personally identifiable information.
*/
fun PGPPublicKeyRingCollection.strip() =
PGPPublicKeyRingCollection(
toList()
.map { pgpPublicKeyRing ->
PGPPublicKeyRing(
pgpPublicKeyRing
.map {
it.signatures.asSequence()
.fold(it) { key, signature ->
PGPPublicKey.removeCertification(key, signature)
}
}
.map {
it.rawUserIDs.asSequence()
.fold(it) { key, userId ->
PGPPublicKey.removeCertification(key, userId)
}
}
)
}
)

fun armourEncode(body: (OutputStream) -> Unit) =
ByteArrayOutputStream().apply {
ArmoredOutputStream(this).use {
it.clearHeaders()
body(it)
}
}.toByteArray()
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.github.vlsi.gradle.checksum.model

import com.github.vlsi.gradle.checksum.debug
import com.github.vlsi.gradle.checksum.hexKeys
import com.github.vlsi.gradle.checksum.pgp.PgpKeyId
import org.gradle.api.artifacts.DependencyArtifact
import org.gradle.api.logging.Logging

Expand Down Expand Up @@ -56,7 +56,7 @@ class DependencyChecksum(
val id: Id
) {
val sha512 = mutableSetOf<String>()
val pgpKeys = mutableSetOf<Long>()
val pgpKeys = mutableSetOf<PgpKeyId.Full>()
val verificationConfig: VerificationConfig
get() =
VerificationConfig(
Expand All @@ -71,19 +71,19 @@ class DependencyChecksum(
}

override fun toString(): String {
return "DependencyChecksum(sha512=$sha512, pgpKeys=${pgpKeys.hexKeys}"
return "DependencyChecksum(sha512=$sha512, pgpKeys=$pgpKeys"
}
}

class DependencyVerification(val defaultVerificationConfig: VerificationConfig) {
val ignoredKeys = mutableSetOf<Long>()
val ignoredKeys = mutableSetOf<PgpKeyId>()

val groupKeys = mutableMapOf<String, MutableSet<Long>>()
val groupKeys = mutableMapOf<String, MutableSet<PgpKeyId.Full>>()

fun add(group: String, key: Long): Boolean =
fun add(group: String, key: PgpKeyId.Full): Boolean =
groupKeys.getOrPut(group) { mutableSetOf() }.add(key)

fun groupKeys(group: String): Set<Long>? = groupKeys[group]
fun groupKeys(group: String): Set<PgpKeyId.Full>? = groupKeys[group]

val dependencies = mutableMapOf<Id, DependencyChecksum>()

Expand All @@ -100,7 +100,7 @@ class DependencyVerification(val defaultVerificationConfig: VerificationConfig)
}

override fun toString(): String {
return "DependencyVerification(ignoredKeys=${ignoredKeys.hexKeys}, trustedKeys=${groupKeys.mapValues { it.value.hexKeys }}, dependencies=$dependencies)"
return "DependencyVerification(ignoredKeys=$ignoredKeys, trustedKeys=${groupKeys.mapValues { it.value.toString() }}, dependencies=$dependencies)"
}
}

Expand All @@ -123,9 +123,9 @@ class DependencyVerificationDb(
fun getConfigFor(id: Id): VerificationConfig =
verification.dependencies[id]?.verificationConfig ?: verification.defaultVerificationConfig

fun isIgnored(key: Long) = verification.ignoredKeys.contains(key)
fun isIgnored(key: PgpKeyId) = verification.ignoredKeys.contains(key)

fun ignoreKey(key: Long) {
fun ignoreKey(key: PgpKeyId) {
updatedVerification.ignoredKeys += key
hasUpdates = true
}
Expand Down Expand Up @@ -153,18 +153,18 @@ class DependencyVerificationDb(
val pass = groupKeys.any { dependencyChecksum.pgpKeys.contains(it) }
logger.debug {
"${if (pass) "OK" else "KO"} PGP group verification for $id." +
" The file was signed via ${dependencyChecksum.pgpKeys.hexKeys}," +
" trusted keys for group ${id.group} are ${groupKeys.hexKeys}"
" The file was signed via ${dependencyChecksum.pgpKeys}," +
" trusted keys for group ${id.group} are $groupKeys"
}
if (pass) {
pgpResult = PgpLevel.GROUP
} else if (expected == null && verificationConfig.pgp == PgpLevel.GROUP) {
details +=
"Trusted PGP keys for group ${id.group} are ${groupKeys.hexKeys}, " +
"Trusted PGP keys for group ${id.group} are $groupKeys, " +
if (dependencyChecksum.pgpKeys.isEmpty()) {
"however no signature found"
} else {
"however artifact is signed by ${dependencyChecksum.pgpKeys.hexKeys} only"
"however artifact is signed by ${dependencyChecksum.pgpKeys} only"
}
}
}
Expand All @@ -186,13 +186,13 @@ class DependencyVerificationDb(
val pass = expected.pgpKeys.any { dependencyChecksum.pgpKeys.contains(it) }
logger.debug {
"${if (pass) "OK" else "KO"} PGP module verification for $id." +
" The file was signed via ${dependencyChecksum.pgpKeys.hexKeys}," +
" trusted keys for module are ${expected.pgpKeys.hexKeys}"
" The file was signed via ${dependencyChecksum.pgpKeys}," +
" trusted keys for module are ${expected.pgpKeys}"
}
if (pass) {
pgpResult = PgpLevel.MODULE
} else {
details += "Expecting one of the following PGP signatures: ${expected.pgpKeys.hexKeys}, but artifact is signed by ${dependencyChecksum.pgpKeys.hexKeys} only"
details += "Expecting one of the following PGP signatures: ${expected.pgpKeys}, but artifact is signed by ${dependencyChecksum.pgpKeys} only"
}
}
if (expected.sha512.isNotEmpty()) {
Expand Down
Loading