Skip to content

Commit

Permalink
Merge pull request #5427 from square/jwilson.0905.decode_pems
Browse files Browse the repository at this point in the history
Make it easier to decode PEM files
  • Loading branch information
swankjesse committed Sep 7, 2019
2 parents ba2c676 + 93c5bcc commit 3490c7e
Show file tree
Hide file tree
Showing 2 changed files with 373 additions and 1 deletion.
109 changes: 108 additions & 1 deletion okhttp-tls/src/main/java/okhttp3/tls/HeldCertificate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package okhttp3.tls

import okhttp3.internal.canParseAsIpAddress
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.toByteString
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.DERSequence
Expand All @@ -27,14 +29,20 @@ import org.bouncycastle.asn1.x509.X509Extensions
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.x509.X509V3CertificateGenerator
import java.math.BigInteger
import java.security.GeneralSecurityException
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.SecureRandom
import java.security.Security
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Date
import java.util.UUID
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -353,7 +361,7 @@ class HeldCertificate(
BigInteger.ONE
}
val signatureAlgorithm = if (signedByKeyPair.private is RSAPrivateKey) {
"SHA256WithRSAEncryption"
"SHA256WithRSA"
} else {
"SHA256withECDSA"
}
Expand Down Expand Up @@ -419,4 +427,103 @@ class HeldCertificate(
}
}
}

companion object {
private val PEM_REGEX = Regex("""-----BEGIN ([!-,.-~ ]*)-----([^-]*)-----END \1-----""")

/**
* Decodes a multiline string that contains both a [certificate][certificatePem] and a
* [private key][privateKeyPkcs8Pem], both [PEM-encoded][rfc_7468]. A typical input string looks
* like this:
*
* ```
* -----BEGIN CERTIFICATE-----
* MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl
* cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx
* MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h
* cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD
* ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw
* HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF
* AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT
* yyaoEufLKVXhrTQhRfodTeigi4RX
* -----END CERTIFICATE-----
* -----BEGIN PRIVATE KEY-----
* MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J
* lu/GJQZoU9lDrCPeUcQ28tzOWw==
* -----END PRIVATE KEY-----
* ```
*
* The string should contain exactly one certificate and one private key in [PKCS #8][rfc_5208]
* format. It should not contain any other PEM-encoded blocks, but it may contain other text
* which will be ignored.
*
* Encode a held certificate into this format by concatenating the results of
* [certificatePem()][certificatePem] and [privateKeyPkcs8Pem()][privateKeyPkcs8Pem].
*
* [rfc_7468]: https://tools.ietf.org/html/rfc7468
* [rfc_5208]: https://tools.ietf.org/html/rfc5208
*/
@JvmStatic
fun decode(certificateAndPrivateKeyPem: String): HeldCertificate {
var certificatePem: String? = null
var pkcs8Base64: String? = null
for (match in PEM_REGEX.findAll(certificateAndPrivateKeyPem)) {
when (val label = match.groups[1]!!.value) {
"CERTIFICATE" -> {
require(certificatePem == null) { "string includes multiple certificates" }
certificatePem = match.groups[0]!!.value // Keep --BEGIN-- and --END-- for certificates.
}
"PRIVATE KEY" -> {
require(pkcs8Base64 == null) { "string includes multiple private keys" }
pkcs8Base64 = match.groups[2]!!.value // Include the contents only for PKCS8.
}
else -> {
throw IllegalArgumentException("unexpected type: $label")
}
}
}
require(certificatePem != null) { "string does not include a certificate" }
require(pkcs8Base64 != null) { "string does not include a private key" }

return decode(certificatePem, pkcs8Base64)
}

private fun decode(certificatePem: String, pkcs8Base64Text: String): HeldCertificate {
val certificate = try {
decodePem(certificatePem)
} catch (e: GeneralSecurityException) {
throw IllegalArgumentException("failed to decode certificate", e)
}

val privateKey = try {
val pkcs8Bytes = pkcs8Base64Text.decodeBase64()
?: throw IllegalArgumentException("failed to decode private key")

// The private key doesn't tell us its type but it's okay because the certificate knows!
val keyType = when (certificate.publicKey) {
is ECPublicKey -> "EC"
is RSAPublicKey -> "RSA"
else -> throw IllegalArgumentException("unexpected key type: ${certificate.publicKey}")
}

decodePkcs8(pkcs8Bytes, keyType)
} catch (e: GeneralSecurityException) {
throw IllegalArgumentException("failed to decode private key", e)
}

val keyPair = KeyPair(certificate.publicKey, privateKey)
return HeldCertificate(keyPair, certificate)
}

private fun decodePem(pem: String): X509Certificate {
val certificates = CertificateFactory.getInstance("X.509")
.generateCertificates(Buffer().writeUtf8(pem).inputStream())
return certificates.iterator().next() as X509Certificate
}

private fun decodePkcs8(data: ByteString, keyAlgorithm: String): PrivateKey {
val keyFactory = KeyFactory.getInstance(keyAlgorithm)
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(data.toByteArray()))
}
}
}
Loading

0 comments on commit 3490c7e

Please sign in to comment.