Adds support to read PEM keys (#29039)

Co-Authored-By: James Roper <james@jazzy.id.au>
This commit is contained in:
Ignasi Marimon-Clos 2020-05-17 00:09:24 +02:00 committed by GitHub
parent ac3065bfad
commit 54a9b3189a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 478 additions and 4 deletions

View file

@ -0,0 +1,130 @@
/*
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.pki.pem
import java.math.BigInteger
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAMultiPrimePrivateCrtKeySpec
import java.security.spec.RSAOtherPrimeInfo
import java.security.spec.RSAPrivateCrtKeySpec
import akka.annotation.ApiMayChange
import akka.pki.pem.PEMDecoder.DERData
import com.hierynomus.asn1.ASN1InputStream
import com.hierynomus.asn1.encodingrules.der.DERDecoder
import com.hierynomus.asn1.types.constructed.ASN1Sequence
import com.hierynomus.asn1.types.primitive.ASN1Integer
final class PEMLoadingException(message: String, cause: Throwable) extends RuntimeException(message, cause) {
def this(msg: String) = this(msg, null)
}
object DERPrivateKeyLoader {
/**
* Converts the DER payload in [[PEMDecoder.DERData]] into a [[java.security.PrivateKey]]. The received DER
* data must be a valid PKCS#1 (identified in PEM as "RSA PRIVATE KEY") or non-ecnrypted PKCS#8 (identified
* in PEM as "PRIVATE KEY").
* @throws PEMLoadingException when the `derData` is for an unsupported format
*/
@ApiMayChange
@throws[PEMLoadingException]("when the `derData` is for an unsupported format")
def load(derData: DERData): PrivateKey = {
derData.label match {
case "RSA PRIVATE KEY" =>
loadPkcs1PrivateKey(derData.bytes)
case "PRIVATE KEY" =>
loadPkcs8PrivateKey(derData.bytes)
case unknown =>
throw new PEMLoadingException(s"Don't know how to read a private key from PEM data with label [$unknown]")
}
}
private def loadPkcs1PrivateKey(bytes: Array[Byte]) = {
val derInputStream = new ASN1InputStream(new DERDecoder, bytes)
// Here's the specification: https://tools.ietf.org/html/rfc3447#appendix-A.1.2
val sequence = {
try {
derInputStream.readObject[ASN1Sequence]()
} finally {
derInputStream.close()
}
}
val version = getInteger(sequence, 0, "version").intValueExact()
if (version < 0 || version > 1) {
throw new IllegalArgumentException(s"Unsupported PKCS1 version: $version")
}
val modulus = getInteger(sequence, 1, "modulus")
val publicExponent = getInteger(sequence, 2, "publicExponent")
val privateExponent = getInteger(sequence, 3, "privateExponent")
val prime1 = getInteger(sequence, 4, "prime1")
val prime2 = getInteger(sequence, 5, "prime2")
val exponent1 = getInteger(sequence, 6, "exponent1")
val exponent2 = getInteger(sequence, 7, "exponent2")
val coefficient = getInteger(sequence, 8, "coefficient")
val keySpec = if (version == 0) {
new RSAPrivateCrtKeySpec(
modulus,
publicExponent,
privateExponent,
prime1,
prime2,
exponent1,
exponent2,
coefficient)
} else {
// Does anyone even use multi-primes? Who knows, maybe this code will never be used. Anyway, I guess it will work,
// the spec isn't exactly complicated.
val otherPrimeInfosSequence = getSequence(sequence, 9, "otherPrimeInfos")
val otherPrimeInfos = (for (i <- 0 until otherPrimeInfosSequence.size()) yield {
val name = s"otherPrimeInfos[$i]"
val seq = getSequence(otherPrimeInfosSequence, i, name)
val prime = getInteger(seq, 0, s"$name.prime")
val exponent = getInteger(seq, 1, s"$name.exponent")
val coefficient = getInteger(seq, 2, s"$name.coefficient")
new RSAOtherPrimeInfo(prime, exponent, coefficient)
}).toArray
new RSAMultiPrimePrivateCrtKeySpec(
modulus,
publicExponent,
privateExponent,
prime1,
prime2,
exponent1,
exponent2,
coefficient,
otherPrimeInfos)
}
val keyFactory = KeyFactory.getInstance("RSA")
keyFactory.generatePrivate(keySpec)
}
private def getInteger(sequence: ASN1Sequence, index: Int, name: String): BigInteger = {
sequence.get(index) match {
case integer: ASN1Integer => integer.getValue
case other =>
throw new IllegalArgumentException(s"Expected integer tag for $name at index $index, but got: ${other.getTag}")
}
}
private def getSequence(sequence: ASN1Sequence, index: Int, name: String): ASN1Sequence = {
sequence.get(index) match {
case seq: ASN1Sequence => seq
case other =>
throw new IllegalArgumentException(s"Expected sequence tag for $name at index $index, but got: ${other.getTag}")
}
}
private def loadPkcs8PrivateKey(bytes: Array[Byte]) = {
val keySpec = new PKCS8EncodedKeySpec(bytes)
val keyFactory = KeyFactory.getInstance("RSA")
keyFactory.generatePrivate(keySpec)
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.pki.pem
import java.util.Base64
import akka.annotation.ApiMayChange
/**
* Decodes lax PEM encoded data, according to
*
* https://tools.ietf.org/html/rfc7468
*/
object PEMDecoder {
// I believe this regex matches the RFC7468 Lax ABNF semantics jkhdft exactly.
private val PEMRegex = {
// Luckily, Java Pattern's \s matches the RFCs W ABNF expression perfectly
// (space, tab, carriage return, line feed, form feed, vertical tab)
// The variables here are named to match the expressions in the RFC7468 ABNF
// description. The content of the regex may not match the structure of the
// expression because sometimes there are nicer way to do things in regexes.
// All printable ASCII characters minus hyphen
val labelchar = """[\p{Print}&&[^-]]"""
// Starts and finishes with a labelchar, with as many label chars and hyphens or
// spaces in between, but no double spaces or hyphens, also may be empty.
val label = raw"""(?:$labelchar(?:[\- ]?$labelchar)*)?"""
// capturing group so we can extract the label
val preeb = raw"""-----BEGIN ($label)-----"""
// we don't extract the end label because the RFC says we can ignore it (it
// doesn't have to match the begin label)
val posteb = raw"""-----END $label-----"""
// Any of the base64 chars (alphanum, +, /) and whitespace, followed by at most 2
// padding characters, separated by zero to many whitespace characters
val laxbase64text = """[A-Za-z0-9\+/\s]*(?:=\s*){0,2}"""
val laxtextualmessage = raw"""\s*$preeb($laxbase64text)$posteb\s*"""
laxtextualmessage.r
}
/**
* Decodes a PEM String into an identifier and the DER bytes of the content.
*
* See https://tools.ietf.org/html/rfc7468 and https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
*
* @param pemData the PEM data (pre-eb, base64-MIME data and ponst-eb)
* @return the decoded bytes and the content type.
*/
@throws[PEMLoadingException](
"If the `pemData` is not valid PEM format (according to https://tools.ietf.org/html/rfc7468).")
@ApiMayChange
def decode(pemData: String): DERData = {
pemData match {
case PEMRegex(label, base64) =>
try {
new DERData(label, Base64.getMimeDecoder.decode(base64))
} catch {
case iae: IllegalArgumentException =>
throw new PEMLoadingException(
s"Error decoding base64 data from PEM data (note: expected MIME-formatted Base64)",
iae)
}
case _ => throw new PEMLoadingException("Not a PEM encoded data.")
}
}
@ApiMayChange
final class DERData(val label: String, val bytes: Array[Byte])
}

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAv
MS0wKwYDVQQDEyQwZDIwN2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgw
HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIw
N2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDhD0BxlDzEOzcp45lPHL60lnM6k3puEGb2lKHL5/nR
F94FCnZL0FH8EdxWzzAYgys+kUwSdo4QMuWuvjY2Km4Wob6k4uAeYEFTCfBdi4/z
r4kpWzu8xLz+uZWimLQrjqVytNNK3DMv6ebWUJ/92VTDS4yzWk4YV0MVr2b2OgMK
SgMvaFQ8L/CwyML72PBWIqU67+MMvvcTLxQdyEgQTTjP0bbiXMLDvfZDarLJojsW
SNBz7AIkznhGkzIGGdhAa41PnPu9XaBFhaqx9Qe3+MG2/k1l/46eHtmxCqhOUde1
i0vy6ZfgcGifua1tg1UBI/oT4S0dsq24dq7K1MYLyHTrAgMBAAGjIzAhMA4GA1Ud
DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa
5YOlvob4wqps3HLaOIi7VzDihNHciP+BI0mzxHa7D3bGaecRPSeG3xEoD/Uxs9o4
8cByPpYx1Wl8LLRx14wDcK0H+UPpo4gCI6h6q92cJj0YRjcSUUt8EIu3qnFMjtM+
sl/uc21fGlq6cvMRZXqtVYENoSNTDHi5a5eEXRa2eZ8XntjvOanOhIKWmxvr8r4a
Voz4WdnXx1C8/BzB62UBoMu4QqMGMLk5wXP0D6hECUuespMest+BeoJAVhTq7wZs
rSP9q08n1stZFF4+bEBaxcqIqdhOLQdHcYELN+a5v0Mcwdsy7jJMagmNPfsKoOKC
hLOsmNYKHdmWg37Jib5o
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN RSA PRIVATE KEY-----
MIIE2AIBAQKCAQEAqFD3rwpvkqgkxSCIKyO2M///6CnJz/YqYycSxeUXJSeC+sf0
Svu4mzSNyx9mglH6ubVJ1x01XlK7GDCAOuqi7Dgay23m+qRq+MF89gYZY9YuVPBr
jsFPK3XguIOLTIV2VCdskBLbW0G6b6VVDOVLCffD6fRuy/H43BI2l9+nuTAdYpCE
sX7IliRi2HR09nv1THSMrdrcq48EdLK+Qzj3f+9gedqeJP7ABY29eIBAOasUY356
So/2dqwhfmMbXswHqIFVQIBd+y1F4Gwf4HxTN06bHhs5rOg4fAREmr1SU25CnhDA
SBw8c5zRFNGgekkzIxSZLFw27ezYGxZyIjnKawIDAQABAoIBAFrcKniRV52BqyfG
4fr3sjnr7gcz17+tkUApLZcqjg3+gFREcHmx3PvbqNeHwdyDyKdLV+sJ129tlZX/
SJmFZCHEP6KlV1TiQOS7/msI69fbHPO5PTaCjTzkbA4WOAgv+M4XjR82RM1EusO1
tPegWfLhj5dvdAfgTpodW1XDFs3QHoQbKkLLMREOb3j+LuK38npxN1UmtDNSRlII
xv77KywOij0LMG4CjIeXmjGdL9BWzlbHv5Zk0wuWHtGFvkoQO6EPn3NVb1uY1ZdX
IekdvA71qssliVFi30/3ZiFTwt3fXfNBawN7t1s0pJlsPAOz7iiucHWrUCvtaXD+
miFFN4ECVgez3BQqe6o3lmHG7LQKy/RWBfDXLCc1JziipTeu+e1piNd7WnJcpsbl
31kTA3JBp7VU41EEXEBtWz8cW3I9y6AhukAyeWBnww9FSet8bKGAuYj1siCXAlYH
L+pEeS8NuGO1iigEEZzZrOMC3Yg9/evwhjXxbtwst9NewE22UdJGB7tm/2v7osaE
/DVQk4xrNe/AEDRcQf73XXEEq/wsoJjHp/Be8lvH6bLHpx0rBQJWA6Lxy3+HJQTb
dcwx4oqaYf/fDCa6TMR2gPGg6RouGt1RYtJ1EDsX7h2HBgnI/b9ri1vJem2BlFVM
2A3uSpBMO4zQ3xA1Z8N1PxdqX8g9jT412iiNcX8CVWC3atQH8iuwKhnSEqyuVw7s
e/kTMVcIsQMNENzn+/3JwaBQNXZFnGLJqhdhIMAkuVbcopiu9+/E4462gecCNRSX
XUyBHANxJIWicAtDYdXaDP6u3NUCVgSPOp+63cxSzMo7FLsYIAFH14VFEXKaw9s9
JbMI4+dIUMfgs2fvCK6ibFpIrhESv4kaQ+uRjkTjyGv+OkKvP5jxoDUjKRA4WcLU
OHH2wrajfrFJ0sfhMIIBDDCCAQgCVgMKWFv+kAwf55zhJjiP/+dSyL7wnEFw5gcw
F8tBg1M7LR4BgTn+j/p/8qEg4scL0eDdDcxGNPaMNlYAtFjO56Qv/oAHJ7EdwfYR
knMUDxYZV2LU+aGpAlYCMhBIrnWbK9b3xOby5ZnolDF/IQXVhA+4lRQ5pR+OlScp
ifClzpxuSsMNdFAPaQuwlDEImJJakDoUtQGHODKysC3ailAxaMnORjY5f/y8+qPO
LPnvsQJWAYm7meCYk9a89TGWQfFl4l1W1dYlI/4FDYOirOvFd+/LICMk+9E43Qff
lqeAL45/QoXNEDpSVNy507ggtBbxqcEehjspYGqp76NRTvRMKgK9/XH4u2E=
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA0IQFs9KpS6fpm7Bq5JcAzRzdZnYub7qGBJ9+QZX8F6qUYiXf
jbVPAZSIksNg6G5yMkzBLZ8r0UOm5WJs6sAHKGJOQk9DcwZEt3XpGbYXAnM5V1sb
xd5oNcbXLcHouU8jrEj+O2KvmgCzDDCUOf3SjGnF4dWqhsTT9tMJZvWVB0OjpKnT
zcoxFKO/XCz8Spb1+FgKUgt3afA+JTRpQWtaZJ41fuTg0rm0qtUSeZXPUEYkqqK4
msoSe7dbu7uiEOkUPoeiP7wzSrwihQSCxOzwdphV2XtF5Xfs24/Ad4WKXTqRVUK/
yldcQy2orFSsX41KBzS8PI1hnOC6uRA0PiGd/QIDAQABAoIBAQCDbxShGuK326m3
B2b5m+1XXSB5m3j92FbtxxMwiDgVOuK5UyItEuIwHs5PpHQLTsMQzaze8vwNtlUX
Ngltl4lrfTvTNF9Ru9vIwLwkBtFOLA8y7yz8doq9iw7LuvTVCft0d7Y4/KWvr00t
G9nzC/mRpIKlLaeFt7/cT34XtikwH+Ez8uWGidYnrqkKZ0bsIZvD+e7gae+veohN
BnTcyDIk8P5nXG0vM4hxtjLo3KstemwOt6vCtiKzL2Vq/JAVD3nlec8WPBzft79I
k5tb3Qm/OnxIQaWF5AhAVkXgMsLL3ddJoAn/K6NZ6NtRGZozkwdP+m4nacrKFJVJ
6ew7OdAJAoGBAO65GvteHLXQ3d1NfU+X6RSBatJdpuf2S4Hc7P+sTkPjLDlQPDKL
ZFLVigPlCehMvgGASPOxL8UqPZugwEo83ywp5t/iOUccXZCPSISreSzWJPfOJ+dl
aKP2cSHHPNC/mISDpp/NF+xfgEAUQQ6AdOKwHGlsFWBvkkw810d4zmXvAoGBAN+b
QYv32wNa2IHC1Ri3VgtfHpQ20fMz+UO6TMefBbgkcgBk4L+O7gSCpThaqy9R7UnX
IEH0QyaBEXzQxB7qvCo1pczWiEUcG+vAyyz9U9oO9Hee/UTMOWL4Sj7qoavau2Be
5PFOO6qA+19JTnStuNb3swNrMmxDQpyNDvUkYAbTAoGBALuYkSCJ84vZaBBZrajX
mt13WieYWuocPXf+0euVTyfAJOehKr0ZlywVDNFEssVvUT1Cv5FpYz3QlPtwlsuA
DGzbPMghMZu1Kb3JK1a+nYnjeseVpPwNT+7RYlQGCr+MYOF5x336oNsqrVEt2XX4
8mGVva4GtsHCy7fHc/GBeMjXAoGBALhEYkytER//okG0xBUdKFwwo6tyTavEndpx
UUqDwpvP9N5cQ1W4vG6dFviMx1s0gX4DOQMA/sFhRX79L1FnEW8bTKmz9RI2qs+p
zgUiMhKVlmJpc79ZKMVlZRHaGybbFuTA7pvoY4ULy5rndy7x5kvITg44LZJID0Gh
gL0Fn9ifAoGAEaWA7yxTA7phDIt0U91HZEVhNSM4cZDurE7614qWhKveSP8f0J4c
3d9y/re4RcCwmss/FtQWSkB+32WbD3qk+QB7hV1oFqJ5ObcfwevGYR8m6vOz1h2L
3pQNi4PcH3U8eeGG1laFKUQ295rBLNqIbOo2y+4hxMnC4tka1X118Ec=
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQhAWz0qlLp+mb
sGrklwDNHN1mdi5vuoYEn35BlfwXqpRiJd+NtU8BlIiSw2DobnIyTMEtnyvRQ6bl
YmzqwAcoYk5CT0NzBkS3dekZthcCczlXWxvF3mg1xtctwei5TyOsSP47Yq+aALMM
MJQ5/dKMacXh1aqGxNP20wlm9ZUHQ6OkqdPNyjEUo79cLPxKlvX4WApSC3dp8D4l
NGlBa1pknjV+5ODSubSq1RJ5lc9QRiSqoriayhJ7t1u7u6IQ6RQ+h6I/vDNKvCKF
BILE7PB2mFXZe0Xld+zbj8B3hYpdOpFVQr/KV1xDLaisVKxfjUoHNLw8jWGc4Lq5
EDQ+IZ39AgMBAAECggEBAINvFKEa4rfbqbcHZvmb7VddIHmbeP3YVu3HEzCIOBU6
4rlTIi0S4jAezk+kdAtOwxDNrN7y/A22VRc2CW2XiWt9O9M0X1G728jAvCQG0U4s
DzLvLPx2ir2LDsu69NUJ+3R3tjj8pa+vTS0b2fML+ZGkgqUtp4W3v9xPfhe2KTAf
4TPy5YaJ1ieuqQpnRuwhm8P57uBp7696iE0GdNzIMiTw/mdcbS8ziHG2Mujcqy16
bA63q8K2IrMvZWr8kBUPeeV5zxY8HN+3v0iTm1vdCb86fEhBpYXkCEBWReAywsvd
10mgCf8ro1no21EZmjOTB0/6bidpysoUlUnp7Ds50AkCgYEA7rka+14ctdDd3U19
T5fpFIFq0l2m5/ZLgdzs/6xOQ+MsOVA8MotkUtWKA+UJ6Ey+AYBI87EvxSo9m6DA
SjzfLCnm3+I5RxxdkI9IhKt5LNYk984n52Voo/ZxIcc80L+YhIOmn80X7F+AQBRB
DoB04rAcaWwVYG+STDzXR3jOZe8CgYEA35tBi/fbA1rYgcLVGLdWC18elDbR8zP5
Q7pMx58FuCRyAGTgv47uBIKlOFqrL1HtSdcgQfRDJoERfNDEHuq8KjWlzNaIRRwb
68DLLP1T2g70d579RMw5YvhKPuqhq9q7YF7k8U47qoD7X0lOdK241vezA2sybENC
nI0O9SRgBtMCgYEAu5iRIInzi9loEFmtqNea3XdaJ5ha6hw9d/7R65VPJ8Ak56Eq
vRmXLBUM0USyxW9RPUK/kWljPdCU+3CWy4AMbNs8yCExm7UpvckrVr6dieN6x5Wk
/A1P7tFiVAYKv4xg4XnHffqg2yqtUS3ZdfjyYZW9rga2wcLLt8dz8YF4yNcCgYEA
uERiTK0RH/+iQbTEFR0oXDCjq3JNq8Sd2nFRSoPCm8/03lxDVbi8bp0W+IzHWzSB
fgM5AwD+wWFFfv0vUWcRbxtMqbP1Ejaqz6nOBSIyEpWWYmlzv1koxWVlEdobJtsW
5MDum+hjhQvLmud3LvHmS8hODjgtkkgPQaGAvQWf2J8CgYARpYDvLFMDumEMi3RT
3UdkRWE1IzhxkO6sTvrXipaEq95I/x/Qnhzd33L+t7hFwLCayz8W1BZKQH7fZZsP
eqT5AHuFXWgWonk5tx/B68ZhHybq87PWHYvelA2Lg9wfdTx54YbWVoUpRDb3msEs
2ohs6jbL7iHEycLi2RrVfXXwRw==
-----END PRIVATE KEY-----

View file

@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.pki.pem
import java.io.File
import java.nio.charset.Charset
import java.nio.file.Files
import java.security.PrivateKey
import org.scalatest.EitherValues
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class DERPrivateKeyLoaderSpec extends AnyWordSpec with Matchers with EitherValues {
"The DER Private Key loader" should {
"decode the same key in PKCS#1 and PKCS#8 formats" in {
val pkcs1 = load("pkcs1.pem")
val pkcs8 = load("pkcs8.pem")
pkcs1 should ===(pkcs8)
}
"parse multi primes" in {
load("multi-prime-pkcs1.pem")
// Not much we can verify here - I actually think the default JDK security implementation ignores the extra
// primes, and it fails to parse a multi-prime PKCS#8 key.
}
"fail on unsupported PEM contents (Certificates are not private keys)" in {
assertThrows[PEMLoadingException] {
load("certificate.pem")
}
}
}
private def load(resource: String): PrivateKey = {
val derData: PEMDecoder.DERData = loadDerData(resource)
DERPrivateKeyLoader.load(derData)
}
private def loadDerData(resource: String) = {
val resourceUrl = getClass.getClassLoader.getResource(resource)
resourceUrl.getProtocol should ===("file")
val path = new File(resourceUrl.toURI).toPath
val bytes = Files.readAllBytes(path)
val str = new String(bytes, Charset.forName("UTF-8"))
val derData = PEMDecoder.decode(str)
derData
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.pki.pem
import java.util.Base64
import org.scalatest.EitherValues
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class PEMDecoderSpec extends AnyWordSpec with Matchers with EitherValues {
private val cert =
"""-----BEGIN CERTIFICATE-----
|MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAv
|MS0wKwYDVQQDEyQwZDIwN2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgw
|HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIw
|N2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgwggEiMA0GCSqGSIb3DQEB
|AQUAA4IBDwAwggEKAoIBAQDhD0BxlDzEOzcp45lPHL60lnM6k3puEGb2lKHL5/nR
|F94FCnZL0FH8EdxWzzAYgys+kUwSdo4QMuWuvjY2Km4Wob6k4uAeYEFTCfBdi4/z
|r4kpWzu8xLz+uZWimLQrjqVytNNK3DMv6ebWUJ/92VTDS4yzWk4YV0MVr2b2OgMK
|SgMvaFQ8L/CwyML72PBWIqU67+MMvvcTLxQdyEgQTTjP0bbiXMLDvfZDarLJojsW
|SNBz7AIkznhGkzIGGdhAa41PnPu9XaBFhaqx9Qe3+MG2/k1l/46eHtmxCqhOUde1
|i0vy6ZfgcGifua1tg1UBI/oT4S0dsq24dq7K1MYLyHTrAgMBAAGjIzAhMA4GA1Ud
|DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa
|5YOlvob4wqps3HLaOIi7VzDihNHciP+BI0mzxHa7D3bGaecRPSeG3xEoD/Uxs9o4
|8cByPpYx1Wl8LLRx14wDcK0H+UPpo4gCI6h6q92cJj0YRjcSUUt8EIu3qnFMjtM+
|sl/uc21fGlq6cvMRZXqtVYENoSNTDHi5a5eEXRa2eZ8XntjvOanOhIKWmxvr8r4a
|Voz4WdnXx1C8/BzB62UBoMu4QqMGMLk5wXP0D6hECUuespMest+BeoJAVhTq7wZs
|rSP9q08n1stZFF4+bEBaxcqIqdhOLQdHcYELN+a5v0Mcwdsy7jJMagmNPfsKoOKC
|hLOsmNYKHdmWg37Jib5o
|-----END CERTIFICATE-----""".stripMargin
"The PEM decoder" should {
"decode a real world certificate" in {
PEMDecoder.decode(cert).label should ===("CERTIFICATE")
}
"decode data with no spaces" in {
val result = PEMDecoder.decode(
"-----BEGIN FOO-----" + Base64.getEncoder.encodeToString("abc".getBytes()) + "-----END FOO-----")
result.label should ===("FOO")
new String(result.bytes) should ===("abc")
}
"decode data with lots of spaces" in {
val result = PEMDecoder.decode(
"\n \t \r -----BEGIN FOO-----\n" +
Base64.getEncoder.encodeToString("abc".getBytes()).flatMap(c => s"$c\n\r \t\n") + "-----END FOO-----\n \t \r ")
result.label should ===("FOO")
new String(result.bytes) should ===("abc")
}
"decode data with two padding characters" in {
// A 4 byte input results in a 6 character output with 2 padding characters
val result = PEMDecoder.decode(
"-----BEGIN FOO-----" + Base64.getEncoder.encodeToString("abcd".getBytes()) + "-----END FOO-----")
result.label should ===("FOO")
new String(result.bytes) should ===("abcd")
}
"decode data with one padding character" in {
// A 5 byte input results in a 7 character output with 1 padding character1
val result = PEMDecoder.decode(
"-----BEGIN FOO-----" + Base64.getEncoder.encodeToString("abcde".getBytes()) + "-----END FOO-----")
result.label should ===("FOO")
new String(result.bytes) should ===("abcde")
}
"fail decode when the format is wrong (not MIME BASE64, lines too long)" in {
val input = """-----BEGIN CERTIFICATE-----
|MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAviZjk2OTk1ODFjZjgw
|HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIwhLOsmNYKHdmWg37Jib5o
|-----END CERTIFICATE-----""".stripMargin
assertThrows[PEMLoadingException] {
PEMDecoder.decode(input)
}
}
"fail decode when the format is wrong (not PEM, invalid per/post-EB)" in {
val input = cert.replace("BEGIN", "BGN").replace("END ", "GLGLGL ")
assertThrows[PEMLoadingException] {
PEMDecoder.decode(input)
}
}
}
}

View file

@ -11,7 +11,8 @@ enablePlugins(
disablePlugins(MimaPlugin) disablePlugins(MimaPlugin)
addCommandAlias( addCommandAlias(
name = "fixall", name = "fixall",
value = ";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;scalafmtAll;test:compile;multi-jvm:compile;reload") value =
";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;scalafmtAll;test:compile;multi-jvm:compile;reload")
addCommandAlias( addCommandAlias(
name = "sortImports", name = "sortImports",
@ -67,6 +68,7 @@ lazy val aggregatedProjects: Seq[ProjectReference] = List[ProjectReference](
persistenceTestkit, persistenceTestkit,
protobuf, protobuf,
protobufV3, protobufV3,
pki,
remote, remote,
remoteTests, remoteTests,
slf4j, slf4j,
@ -313,6 +315,14 @@ lazy val protobufV3 = akkaModule("akka-protobuf-v3")
test in assembly := {}, // assembly runs tests for unknown reason which introduces another cyclic dependency to packageBin via exportedJars test in assembly := {}, // assembly runs tests for unknown reason which introduces another cyclic dependency to packageBin via exportedJars
description := "Akka Protobuf V3 is a shaded version of the protobuf runtime. Original POM: https://github.com/protocolbuffers/protobuf/blob/v3.9.0/java/pom.xml") description := "Akka Protobuf V3 is a shaded version of the protobuf runtime. Original POM: https://github.com/protocolbuffers/protobuf/blob/v3.9.0/java/pom.xml")
lazy val pki =
akkaModule("akka-pki")
.dependsOn(actor) // this dependency only exists for "@ApiMayChange"
.settings(Dependencies.pki)
.settings(AutomaticModuleName.settings("akka.pki"))
// The akka-pki artifact was added in Akka 2.6.2, no MiMa checks yet.
.disablePlugins(MimaPlugin)
lazy val remote = lazy val remote =
akkaModule("akka-remote") akkaModule("akka-remote")
.dependsOn( .dependsOn(

View file

@ -83,6 +83,8 @@ object Dependencies {
// Added explicitly for when artery tcp is used // Added explicitly for when artery tcp is used
val agrona = "org.agrona" % "agrona" % agronaVersion // ApacheV2 val agrona = "org.agrona" % "agrona" % agronaVersion // ApacheV2
val asnOne = "com.hierynomus" % "asn-one" % "0.4.0" // ApacheV2
val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion // ApacheV2 val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion // ApacheV2
val jacksonAnnotations = "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion // ApacheV2 val jacksonAnnotations = "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion // ApacheV2
val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion // ApacheV2 val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion // ApacheV2
@ -195,6 +197,13 @@ object Dependencies {
val actorTestkitTyped = l ++= Seq(Provided.logback, Provided.junit, Provided.scalatest, Test.scalatestJUnit) val actorTestkitTyped = l ++= Seq(Provided.logback, Provided.junit, Provided.scalatest, Test.scalatestJUnit)
val pki = l ++=
Seq(
asnOne,
// pull up slf4j version from the one provided transitively in asnOne to fix unidoc
Compile.slf4jApi % "provided",
Test.scalatest)
val remoteDependencies = Seq(netty, aeronDriver, aeronClient) val remoteDependencies = Seq(netty, aeronDriver, aeronClient)
val remoteOptionalDependencies = remoteDependencies.map(_ % "optional") val remoteOptionalDependencies = remoteDependencies.map(_ % "optional")