From 32d8cc8ac4fc4c1f9417df6ec6da392291eb4759 Mon Sep 17 00:00:00 2001 From: Ryan O'Neill Date: Sat, 13 Jan 2018 01:37:38 +0000 Subject: [PATCH] util-security: Introduce X509CrlFile for Loading CRLs Summary: Problem / Solution CRL (Certificate Revocation List) files should be as easy to load as X.509v3 certificate files. This commit adds support to util-security for loading CRL files via `X509CrlFile`. JIRA Issues: CSL-5819 Differential Revision: https://phabricator.twitter.biz/D127700 --- CHANGES | 6 ++ .../util/security/X509CertificateFile.scala | 18 ++--- .../twitter/util/security/X509CrlFile.scala | 44 +++++++++++++ .../crl/csl-intermediate-garbage.crl | 14 ++++ .../test/resources/crl/csl-intermediate.crl | 19 ++++++ .../util/security/X509CrlFileTest.scala | 66 +++++++++++++++++++ 6 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 util-security/src/main/scala/com/twitter/util/security/X509CrlFile.scala create mode 100644 util-security/src/test/resources/crl/csl-intermediate-garbage.crl create mode 100644 util-security/src/test/resources/crl/csl-intermediate.crl create mode 100644 util-security/src/test/scala/com/twitter/util/security/X509CrlFileTest.scala diff --git a/CHANGES b/CHANGES index f82d98dcee..e16233422f 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,12 @@ Note that ``PHAB_ID=#`` and ``RB_ID=#`` correspond to associated messages in com Unreleased +New Features: + + * util-security: Added `c.t.util.security.X509CrlFile` for reading + Certificate Revocation List PEM formatted `X509CRL` files. + ``PHAB_ID=D127700`` + 17.12.0 2017-12-08 API Changes: diff --git a/util-security/src/main/scala/com/twitter/util/security/X509CertificateFile.scala b/util-security/src/main/scala/com/twitter/util/security/X509CertificateFile.scala index 3c1d5b5dbb..683be5439b 100644 --- a/util-security/src/main/scala/com/twitter/util/security/X509CertificateFile.scala +++ b/util-security/src/main/scala/com/twitter/util/security/X509CertificateFile.scala @@ -20,15 +20,6 @@ class X509CertificateFile(file: File) { private[this] def logException(ex: Throwable): Unit = log.warning(s"X509Certificate (${file.getName}) failed to load: ${ex.getMessage}.") - private[this] def generateX509Certificate(decodedMessage: Array[Byte]): X509Certificate = { - val certFactory = CertificateFactory.getInstance("X.509") - val certificate = certFactory - .generateCertificate(new ByteArrayInputStream(decodedMessage)) - .asInstanceOf[X509Certificate] - certificate.checkValidity() - certificate - } - /** * Attempts to read the contents of the X.509 Certificate from the file. */ @@ -57,4 +48,13 @@ private object X509CertificateFile { private val MessageType: String = "CERTIFICATE" private val log = Logger.get("com.twitter.util.security") + + private def generateX509Certificate(decodedMessage: Array[Byte]): X509Certificate = { + val certFactory = CertificateFactory.getInstance("X.509") + val certificate = certFactory + .generateCertificate(new ByteArrayInputStream(decodedMessage)) + .asInstanceOf[X509Certificate] + certificate.checkValidity() + certificate + } } diff --git a/util-security/src/main/scala/com/twitter/util/security/X509CrlFile.scala b/util-security/src/main/scala/com/twitter/util/security/X509CrlFile.scala new file mode 100644 index 0000000000..9816ffc8e6 --- /dev/null +++ b/util-security/src/main/scala/com/twitter/util/security/X509CrlFile.scala @@ -0,0 +1,44 @@ +package com.twitter.util.security + +import com.twitter.logging.Logger +import com.twitter.util.Try +import com.twitter.util.security.X509CrlFile._ +import java.io.{ByteArrayInputStream, File} +import java.security.cert.{CertificateFactory, X509CRL} + +/** + * A representation of an X.509 Certificate Revocation List (CRL) + * PEM-encoded and stored in a file. + * + * @example + * -----BEGIN X509 CRL----- + * base64encodedbytes + * -----END X509 CRL----- + */ +class X509CrlFile(file: File) { + + private[this] def logException(ex: Throwable): Unit = + log.warning(s"X509Crl (${file.getName}) failed to load: ${ex.getMessage}.") + + def readX509Crl(): Try[X509CRL] = { + val pemFile = new PemFile(file) + pemFile + .readMessage(MessageType) + .map(generateX509Crl) + .onFailure(logException) + } + +} + +private object X509CrlFile { + private val MessageType: String = "X509 CRL" + + private val log = Logger.get("com.twitter.util.security") + + private def generateX509Crl(decodedMessage: Array[Byte]): X509CRL = { + val certFactory = CertificateFactory.getInstance("X.509") + certFactory + .generateCRL(new ByteArrayInputStream(decodedMessage)) + .asInstanceOf[X509CRL] + } +} diff --git a/util-security/src/test/resources/crl/csl-intermediate-garbage.crl b/util-security/src/test/resources/crl/csl-intermediate-garbage.crl new file mode 100644 index 0000000000..020c9eccf3 --- /dev/null +++ b/util-security/src/test/resources/crl/csl-intermediate-garbage.crl @@ -0,0 +1,14 @@ +-----BEGIN X509 CRL----- +MIIDKTCCARECAQEwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApDYWxpZm9ybmlhMRAwDgYDVQQKDAdUd2l0dGVyMR8wHQYDVQQLDBZDb3Jl +IFN5c3RlbXMgTGlicmFyaWVzMRwwGgYDVQQDDBNDU0wgSW50ZXJtZWRpYXRlIENB +MSAwHgYJKoZIhvcNAQkBFhFyeWFub0B0d2l0dGVyLmNvbRcNMTgwMTEyMTkzMTQ0 +1jNNCn+9h0GfQniDxxE8Vd7zolky7uO5lLQHzLEuqXrCein9TqKTgE4X54XrJrZH +BMMKGYomoPqLhoocqHMSyJs0a0PYlbvL6VIFRpWdaczeZZrP5/wAsbUs6bNsprvZ +81Nx7XFPTo746aRyMFXUOOJQMfS7hu38sxSBk04+jTY/+3dn+VrGIcRCXB5i2HjZ +kdkHcBXPib/Hl5fKIWAIa0FDJ6Kq6+6iLqX+WjW35sQW3V81ZAi34vTagtPj9TcO +b9SdxkkHO0VBGXzKd0aBrdQNKe/yrNNAz7l1/RbUp8CrIG58GDoXs+wr3yubSUwU +WNoMe+STHFIQD6o1AQkmbXFqaRO71L/sTbTQzzlraqugo9bv1T4RZOIbENexNX50 +nF/B4RRNM38+YgcXEc7TegCh866WAsiIfO4ejtveGNigQqYbDfmsZknDlSqthRxi +Rdhk0MG4Vuo/wBo5/F5JIm3viNj2SLE2LxEKpFAEneEXhkWKhCDmRjas0F/5 +-----END X509 CRL----- diff --git a/util-security/src/test/resources/crl/csl-intermediate.crl b/util-security/src/test/resources/crl/csl-intermediate.crl new file mode 100644 index 0000000000..802886192a --- /dev/null +++ b/util-security/src/test/resources/crl/csl-intermediate.crl @@ -0,0 +1,19 @@ +-----BEGIN X509 CRL----- +MIIDKTCCARECAQEwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApDYWxpZm9ybmlhMRAwDgYDVQQKDAdUd2l0dGVyMR8wHQYDVQQLDBZDb3Jl +IFN5c3RlbXMgTGlicmFyaWVzMRwwGgYDVQQDDBNDU0wgSW50ZXJtZWRpYXRlIENB +MSAwHgYJKoZIhvcNAQkBFhFyeWFub0B0d2l0dGVyLmNvbRcNMTgwMTEyMTkzMTQ0 +WhcNMTgwMjExMTkzMTQ0WjAVMBMCAhAMFw0xODAxMTIxOTMxMjFaoDAwLjAfBgNV +HSMEGDAWgBRaEYAYDZMHjVdH+tBvLkOSFMKF8TALBgNVHRQEBAICEAEwDQYJKoZI +hvcNAQELBQADggIBAKzuybkOzirP09GhJpZ86gdkL/uB1TF8SlMLeFEZCr/Ng5sm +1jNNCn+9h0GfQniDxxE8Vd7zolky7uO5lLQHzLEuqXrCein9TqKTgE4X54XrJrZH +BMMKGYomoPqLhoocqHMSyJs0a0PYlbvL6VIFRpWdaczeZZrP5/wAsbUs6bNsprvZ +81Nx7XFPTo746aRyMFXUOOJQMfS7hu38sxSBk04+jTY/+3dn+VrGIcRCXB5i2HjZ +p82AdrUQQjnsOyIlUgGOjv6ayzMro7c/FWCfLqqbD6sViuAoXWU2ODKjDqeNlMhq +vVakF8iDSNLYXVbxOdnTk+/yw1h1SFmBnpF64xBGqWp4bda3mvwBzhD/pezQa2M7 +kdkHcBXPib/Hl5fKIWAIa0FDJ6Kq6+6iLqX+WjW35sQW3V81ZAi34vTagtPj9TcO +b9SdxkkHO0VBGXzKd0aBrdQNKe/yrNNAz7l1/RbUp8CrIG58GDoXs+wr3yubSUwU +WNoMe+STHFIQD6o1AQkmbXFqaRO71L/sTbTQzzlraqugo9bv1T4RZOIbENexNX50 +nF/B4RRNM38+YgcXEc7TegCh866WAsiIfO4ejtveGNigQqYbDfmsZknDlSqthRxi +Rdhk0MG4Vuo/wBo5/F5JIm3viNj2SLE2LxEKpFAEneEXhkWKhCDmRjas0F/5 +-----END X509 CRL----- diff --git a/util-security/src/test/scala/com/twitter/util/security/X509CrlFileTest.scala b/util-security/src/test/scala/com/twitter/util/security/X509CrlFileTest.scala new file mode 100644 index 0000000000..91ed6814e9 --- /dev/null +++ b/util-security/src/test/scala/com/twitter/util/security/X509CrlFileTest.scala @@ -0,0 +1,66 @@ +package com.twitter.util.security + +import com.twitter.io.TempFile +import com.twitter.util.Try +import java.io.File +import java.security.cert.{CRLException, X509CRL} +import org.scalatest.FunSuite + +class X509CrlFileTest extends FunSuite { + + private[this] val assertLogMessage = + PemFileTestUtils.assertLogMessage("X509Crl") _ + + private[this] def assertCrlException(tryCrl: Try[X509CRL]): Unit = + PemFileTestUtils.assertException[CRLException, X509CRL](tryCrl) + + private[this] val readCrlFromFile: File => Try[X509CRL] = + (tempFile) => { + val crlFile = new X509CrlFile(tempFile) + crlFile.readX509Crl() + } + + test("File path doesn't exist") { + PemFileTestUtils.testFileDoesntExist("X509Crl", readCrlFromFile) + } + + test("File path isn't a file") { + PemFileTestUtils.testFilePathIsntFile("X509Crl", readCrlFromFile) + } + + test("File isn't a crl") { + PemFileTestUtils.testEmptyFile[InvalidPemFormatException, X509CRL]( + "X509Crl", + readCrlFromFile + ) + } + + test("File is garbage") { + val handler = PemFileTestUtils.newHandler() + // Lines were manually deleted from a real crl file + val tempFile = TempFile.fromResourcePath("/crl/csl-intermediate-garbage.crl") + // deleteOnExit is handled by TempFile + + val crlFile = new X509CrlFile(tempFile) + val tryCrl = crlFile.readX509Crl() + + assertLogMessage(handler.get, tempFile.getName, "Incomplete BER/DER data.") + assertCrlException(tryCrl) + } + + test("File is a Certificate Revocation List") { + val tempFile = TempFile.fromResourcePath("/crl/csl-intermediate.crl") + // deleteOnExit is handled by TempFile + + val crlFile = new X509CrlFile(tempFile) + val tryCrl = crlFile.readX509Crl() + + assert(tryCrl.isReturn) + val crl = tryCrl.get() + + val principalInfo = new X500PrincipalInfo(crl.getIssuerX500Principal) + assert(principalInfo.organizationalUnitName.isDefined) + assert(principalInfo.organizationalUnitName.get == "Core Systems Libraries") + } + +}