From b9b820d8af774e1f1b73d582775cc04b8fdef301 Mon Sep 17 00:00:00 2001 From: Chris Rebert Date: Sat, 21 Jan 2017 22:37:12 -0800 Subject: Implement SHA1-RSA signature verification logic using JCA & Bouncy Castle (#53) To be used for Travis's new authentication mechanism Refs #43 --- build.sbt | 3 ++ .../scala/com/getbootstrap/savage/crypto/Pem.scala | 41 ++++++++++++++++++++++ .../getbootstrap/savage/crypto/RsaPublicKey.scala | 16 +++++++++ .../getbootstrap/savage/crypto/Sha1WithRsa.scala | 27 ++++++++++++++ .../crypto/SignatureVerificationStatus.scala | 9 +++++ 5 files changed, 96 insertions(+) create mode 100644 src/main/scala/com/getbootstrap/savage/crypto/Pem.scala create mode 100644 src/main/scala/com/getbootstrap/savage/crypto/RsaPublicKey.scala create mode 100644 src/main/scala/com/getbootstrap/savage/crypto/Sha1WithRsa.scala create mode 100644 src/main/scala/com/getbootstrap/savage/crypto/SignatureVerificationStatus.scala diff --git a/build.sbt b/build.sbt index 0eea957..fecc1a8 100644 --- a/build.sbt +++ b/build.sbt @@ -19,6 +19,9 @@ libraryDependencies += "com.google.code.gson" % "gson" % "2.8.0" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.1.9" +// For reading PEM ("-----BEGIN PUBLIC KEY-----"), which Travis's API uses for its public key. +libraryDependencies += "org.bouncycastle" % "bcpkix-jdk15on" % "1.56" + libraryDependencies ++= { val akkaV = "2.3.16" val sprayV = "1.3.4" diff --git a/src/main/scala/com/getbootstrap/savage/crypto/Pem.scala b/src/main/scala/com/getbootstrap/savage/crypto/Pem.scala new file mode 100644 index 0000000..47496f1 --- /dev/null +++ b/src/main/scala/com/getbootstrap/savage/crypto/Pem.scala @@ -0,0 +1,41 @@ +package com.getbootstrap.savage.crypto + +import scala.util.{Try,Success,Failure} +import java.io.StringReader +import java.security.spec.X509EncodedKeySpec +import org.bouncycastle.util.io.pem.PemReader +import org.bouncycastle.util.io.pem.PemObject + + +sealed class MalformedPemException(cause: Throwable) extends RuntimeException("The given data did not conform to the PEM format!", cause) + +sealed class UnexpectedPemDataTypeException(expectedType: String, pemObj: PemObject) + extends RuntimeException(s"PEM contained data of unexpected type! Expected: ${expectedType} Actual: ${pemObj.getType}") + +// PEM is the name for the format that involves "-----BEGIN PUBLIC KEY-----" etc. +object Pem { + private val PublicKeyPemType = "PUBLIC KEY" + + @throws[MalformedPemException]("if there is a problem decoding the PEM data") + private def decode(pem: String): PemObject = { + val pemReader = new PemReader(new StringReader(pem)) + val pemObjTry = Try { pemReader.readPemObject() } + val closeTry = Try { pemReader.close() } + (pemObjTry, closeTry) match { + case (Failure(readExc), _) => throw new MalformedPemException(readExc) + case (_, Failure(closeExc)) => throw new MalformedPemException(closeExc) + case (Success(pemObj), Success(_)) => pemObj + } + } + + // Decodes PKCS8 data in PEM format into a X509EncodedKeySpec + // which can be handled by sun.security.rsa.RSAKeyFactory + @throws[UnexpectedPemDataTypeException]("if the PEM contains non-public-key data") + def decodePublicKeyIntoSpec(publicKeyInPem: String): X509EncodedKeySpec = { + val pemObj = decode(publicKeyInPem) + pemObj.getType match { + case PublicKeyPemType => new X509EncodedKeySpec(pemObj.getContent) + case unexpectedType => throw new UnexpectedPemDataTypeException(PublicKeyPemType, pemObj) + } + } +} diff --git a/src/main/scala/com/getbootstrap/savage/crypto/RsaPublicKey.scala b/src/main/scala/com/getbootstrap/savage/crypto/RsaPublicKey.scala new file mode 100644 index 0000000..a91c6ed --- /dev/null +++ b/src/main/scala/com/getbootstrap/savage/crypto/RsaPublicKey.scala @@ -0,0 +1,16 @@ +package com.getbootstrap.savage.crypto + +import scala.util.Try +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.X509EncodedKeySpec + + +sealed case class RsaPublicKey private(publicKey: PublicKey) + +object RsaPublicKey { + private val rsaKeyFactory = KeyFactory.getInstance("RSA") // Supported in all spec-compliant JVMs + + def fromX509Spec(keySpec: X509EncodedKeySpec): Try[RsaPublicKey] = Try{ rsaKeyFactory.generatePublic(keySpec) }.map{ new RsaPublicKey(_) } + def fromPem(pem: String): Try[RsaPublicKey] = Try{ Pem.decodePublicKeyIntoSpec(pem) }.flatMap{ fromX509Spec(_) } +} diff --git a/src/main/scala/com/getbootstrap/savage/crypto/Sha1WithRsa.scala b/src/main/scala/com/getbootstrap/savage/crypto/Sha1WithRsa.scala new file mode 100644 index 0000000..c01afee --- /dev/null +++ b/src/main/scala/com/getbootstrap/savage/crypto/Sha1WithRsa.scala @@ -0,0 +1,27 @@ +package com.getbootstrap.savage.crypto + +import java.security.Signature +import java.security.SignatureException +import java.security.InvalidKeyException + + +object Sha1WithRsa { + private val signatureAlgorithmName = "SHA1withRSA" // Supported in all spec-compliant JVMs + private def newSignatureVerifier(): Signature = Signature.getInstance(signatureAlgorithmName) + + def verifySignature(signature: Array[Byte], publicKey: RsaPublicKey, signedData: Array[Byte]): SignatureVerificationStatus = { + val verifier = newSignatureVerifier() + try { + verifier.initVerify(publicKey.publicKey) + verifier.update(signedData) + verifier.verify(signature) match { + case true => SuccessfullyVerified + case false => FailedVerification + } + } + catch { + case keyExc:InvalidKeyException => ExceptionDuringVerification(keyExc) + case sigExc:SignatureException => ExceptionDuringVerification(sigExc) + } + } +} diff --git a/src/main/scala/com/getbootstrap/savage/crypto/SignatureVerificationStatus.scala b/src/main/scala/com/getbootstrap/savage/crypto/SignatureVerificationStatus.scala new file mode 100644 index 0000000..2697335 --- /dev/null +++ b/src/main/scala/com/getbootstrap/savage/crypto/SignatureVerificationStatus.scala @@ -0,0 +1,9 @@ +package com.getbootstrap.savage.crypto + +sealed trait SignatureVerificationStatus + +object SuccessfullyVerified extends SignatureVerificationStatus + +trait FailedVerification extends SignatureVerificationStatus +object FailedVerification extends SignatureVerificationStatus +case class ExceptionDuringVerification(error: Throwable) extends FailedVerification -- cgit v1.2.3