Skip to content

Commit

Permalink
checksum-dependency: use full pgp fingerprints for verification
Browse files Browse the repository at this point in the history
  • Loading branch information
vlsi committed Jan 10, 2023
1 parent 9ae7fdd commit c1e972d
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 98 deletions.
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,7 +142,7 @@ 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)
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

0 comments on commit c1e972d

Please sign in to comment.