diff options
author | Chris Rebert <github@chrisrebert.com> | 2017-01-22 09:37:12 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-01-22 09:37:12 +0300 |
commit | b9b820d8af774e1f1b73d582775cc04b8fdef301 (patch) | |
tree | c94f0fa722fbf85db2742f75c435939906349ca8 | |
parent | 266c7a5013c7f8b6f5b3201c2ea09b3dcef2dcd0 (diff) |
Implement SHA1-RSA signature verification logic using JCA & Bouncy Castle (#53)
To be used for Travis's new authentication mechanism
Refs #43
5 files changed, 96 insertions, 0 deletions
@@ -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 |