Skip to content

Commit

Permalink
Compare 3rd-party attestations (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
raboof authored Jan 3, 2019
1 parent ce24e4c commit 86632af
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 40 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ certification from Travis.

### Checking certifications

Check your certification with other uploaded
You can check your certification against other uploaded
certifications with `reproducibleBuildsCheckCertification`

This currently does not work since we are evolving the way we are sharing
certifications.
Checking your certification with the 'official' published
certification is currently
[not yet implemented](https://github.com/raboof/sbt-reproducible-builds/issues/69).

## Drinking our own champagne

Expand Down
17 changes: 12 additions & 5 deletions src/main/scala/Certification.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import sbt.io.syntax.File

import scala.collection.mutable

case class Checksum(filename: String, length: Int, checksum: List[Byte])
case class Checksum(filename: String, length: Int, checksum: List[Byte]) {
def hexChecksum = checksum.map("%02x" format _).mkString
}
object Checksum {
def apply(file: File): Checksum = {
val bytes = Files.readAllBytes(file.toPath)
Expand All @@ -31,6 +33,11 @@ case class Certification(
checksums: List[Checksum],
date: Long,
) {
require(
checksums.map(_.filename).toSet.size == checksums.length,
"Checksum filenames should be unique"
)

def asPropertyString: String = {
val packageName = groupId + ":" + artifactId
val content = mutable.LinkedHashMap(
Expand All @@ -47,11 +54,11 @@ case class Certification(
"scala.binary-version" -> scalaBinaryVersion,
"date" -> date,
) ++ checksums.zipWithIndex.flatMap {
case (Checksum(filename, length, checksum), idx) =>
case (checksum @ Checksum(filename, length, _), idx) =>
Seq(
s"outputs.$idx.filename" -> filename,
s"outputs.$idx.length" -> length.toString,
s"outputs.$idx.checksums.sha512" -> checksum.map("%02x" format _).mkString,
s"outputs.$idx.checksums.sha512" -> checksum.hexChecksum,
)
} ++ classifier.map("classifier" -> _)

Expand Down Expand Up @@ -110,8 +117,8 @@ object Certification {
val filename = properties.getProperty(keys.find(_.endsWith(".filename")).get)
val length = Integer.parseInt(properties.getProperty(keys.find(_.endsWith(".length")).get))
val checksum = properties.getProperty(keys.find(_.endsWith(".checksums.sha512")).get)
val bs = new BigInteger(checksum, 16)
Checksum(filename, length, bs.toByteArray.toList)
val bs = new BigInteger(checksum, 16).toByteArray.toList.reverse.padTo[Byte, List[Byte]](64, Byte.box(0x00)).take(64).reverse
Checksum(filename, length, bs)
}

new Certification(
Expand Down
69 changes: 39 additions & 30 deletions src/main/scala/ReproducibleBuildsPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import java.net.InetAddress
import java.nio.charset.Charset
import java.nio.file.Files

import scala.concurrent.duration._

import gigahorse.GigahorseSupport
import sbt.{io => _, _}
import sbt.Keys._
Expand All @@ -14,6 +16,7 @@ import com.typesafe.sbt.pgp.PgpSigner
import com.typesafe.sbt.pgp.PgpKeys._
import com.typesafe.sbt.pgp.PgpSettings.{pgpPassphrase => _, pgpSecretRing => _, pgpSigningKey => _, useGpgAgent => _, _}
import io.github.zlika.reproducible._
import org.apache.ivy.core.IvyPatternHelper
import sbt.io.syntax.{URI, uri}
import sbt.librarymanagement.{Artifact, URLRepository}
import sbt.librarymanagement.Http.http
Expand All @@ -22,6 +25,7 @@ import scala.util.{Success, Try}
import spray.json._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future}

object ReproducibleBuildsPlugin extends AutoPlugin {
// To make sure we're loaded after the defaults
Expand All @@ -32,7 +36,7 @@ object ReproducibleBuildsPlugin extends AutoPlugin {

val ReproducibleBuilds = config("reproducible-builds")

val reproducibleBuildsPackageName = settingKey[String]("Package name of this build, including version")
val reproducibleBuildsPackageName = settingKey[String]("Module name of this build")
val publishCertification = settingKey[Boolean]("Include the certification when publishing")
val hostname = settingKey[String]("The hostname to include when publishing 3rd-party attestations")

Expand Down Expand Up @@ -111,22 +115,41 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
sbtVersion.value
)
val groupId = organization.value
// TODO resolve placeholders and trim file segment
val uploadPrefix = url((publishTo in ReproducibleBuilds).value.asInstanceOf[URLRepository].patterns.artifactPatterns.head).toURI
val uri = uploadPrefix.resolve(groupId + "/" + reproducibleBuildsPackageName.value + "/" + version.value + "/")
http.run(GigahorseSupport.url(uri.toASCIIString)).onComplete {
case Success(v) =>
v.bodyAsString
// TODO also check against the 'official' published buildinfo
val pattern = (publishTo in ReproducibleBuilds).value.getOrElse(bzztNetResolver).asInstanceOf[URLRepository].patterns.artifactPatterns.head
val prefixPattern = pattern.substring(0, pattern.lastIndexOf("/") + 1)
val prefix = IvyPatternHelper.substitute(
prefixPattern,
organization.value.replace('.', '/'),
ours.artifactId,
version.value,
ours.artifactId,
"buildinfo",
"buildinfo",
"compile"
)
// TODO add Accept header to request JSON-formatted
val done = http.run(GigahorseSupport.url(prefix)).flatMap { entity =>
val results = entity.bodyAsString
.parseJson
.asInstanceOf[JsArray]
.elements
.map(_.asInstanceOf[JsObject])
.filter(_.fields("type").asInstanceOf[JsString].value == "file")
.map(_.fields("name").asInstanceOf[JsString].value)
.foreach(name => {
checkVerification(ours, uploadPrefix.resolve(name))
})
.map(_.fields.get("name"))
.collect { case Some(JsString(objectname)) if objectname.endsWith(".buildinfo") => objectname }
.map(name => checkVerification(ours, uri(prefix).resolve(name)))
Future.sequence(results)
}.map { resultList =>
println(s"Processed ${resultList.size} results. ${resultList.count(_.ok)} matching attestations, ${resultList.filterNot(_.ok).size} mismatches");
resultList.foreach { result =>
println(s"${result.uri}:")
println("- " + (if (result.ok) "OK" else "NOT OK"))
result.verdicts.foreach {
case (filename, verdict) => println(s"- $filename: $verdict")
}
}
}
Await.result(done, 30.seconds)
},
ivyConfigurations += ReproducibleBuilds,
) ++ (
Expand Down Expand Up @@ -200,26 +223,12 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
out
}

private def checkVerification(ours: Certification, uri: URI): Unit = {
import scala.collection.JavaConverters._

private def checkVerification(ours: Certification, uri: URI): Future[VerificationResult] = {
val ourSums = ours.checksums

println("Checking remote builds:")

http.run(GigahorseSupport.url(uri.toASCIIString)).onComplete {
case Success(v) =>
println(s"Comparing against $uri (warning: signature not checked):")
val theirPropertyString = v.bodyAsString
val theirs = Certification(theirPropertyString)
val remoteSums = theirs.checksums
ourSums.foreach { ourSum =>
if (remoteSums.contains(ourSum)) {
println(s"Match: $ourSum")
} else {
println(s"Mismatch: our $ourSum not found in $remoteSums")
}
}
http.run(GigahorseSupport.url(uri.toASCIIString)).map { entity =>
val theirs = Certification(entity.bodyAsString)
VerificationResult(uri, ourSums, theirs.checksums)
}
}

Expand Down
41 changes: 41 additions & 0 deletions src/main/scala/VerificationResult.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package net.bzzt.reproduciblebuilds

import java.net.URI

case class VerificationResult(
uri: URI,
ourSums: Map[String, Checksum],
remoteSums: Map[String, Checksum],
) {
def verdicts: Seq[(String, String)] =
ourSums.map {
case (key, value) => (key, verdict(key, value))
}.toSeq ++
ourSums.keySet.diff(remoteSums.keySet).map { missingInTheirs => (missingInTheirs, "Missing in their checksums but present in ours") } ++
remoteSums.keySet.diff(ourSums.keySet).map { missingInOurs => (missingInOurs, "Missing in our checksums but present in theirs") }

def verdict(filename: String, ourSum: Checksum): String = remoteSums.get(filename) match {
case None => "Not found in remote attestation"
case Some(checksum) =>
if (checksum == ourSum) "Match"
else s"Mismatch: our ${ourSum.hexChecksum} did not match their ${checksum.hexChecksum}"
}

def ok = ourSums == remoteSums
}

object VerificationResult {
def apply(uri: URI, ourSums: Seq[Checksum], remoteSums: Seq[Checksum]) = {
new VerificationResult(
uri,
groupByUnique(ourSums)(_.filename),
groupByUnique(remoteSums)(_.filename),
)
}

private def groupByUnique[K, V](elements: Seq[V])(f: V => K): Map[K, V] =
elements.groupBy(f).map {
case (key, Seq(value)) => (key, value)
case (key, values) => throw new IllegalArgumentException(s"Found ${values.size} elements for key $key")
}
}
4 changes: 2 additions & 2 deletions src/test/scala/CertificationSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class CertificationSpec extends WordSpec with Matchers {
scalaBinaryVersion = "0.12",
sbtVersion = "1.2.7",
checksums = List(
Checksum("foo.jar", 42, Array[Byte](0x31, 0x23).toList),
Checksum("bar-with-weird-characters.xml" , 42, Array[Byte](0x31, 0x33).toList)
Checksum("foo.jar", 42, List(-94, 69, 0, -18, -102, 68, -59, 90, -69, 11, 68, 97, -44, -12, 5, -28, -62, -39, -120, -28, 52, 121, -96, 56, 89, 67, 34, 109, -46, 72, 127, -81, 101, -94, -114, 18, 27, 127, 83, -101, 118, 77, -14, 26, -46, 125, -21, -19, 91, -65, 127, -48, 125, -13, 77, 65, 58, -127, -34, -14, -81, 88, -97, 27)),
Checksum("bar-with-weird-characters.xml" , 42, Array[Byte](0x31, 0x33).toList.padTo[Byte, List[Byte]](64, 0x00))
),
42
)
Expand Down

0 comments on commit 86632af

Please sign in to comment.