Skip to content

Commit 3490c7e

Browse files
authored
Merge pull request #5427 from square/jwilson.0905.decode_pems
Make it easier to decode PEM files
2 parents ba2c676 + 93c5bcc commit 3490c7e

File tree

2 files changed

+373
-1
lines changed

2 files changed

+373
-1
lines changed

okhttp-tls/src/main/java/okhttp3/tls/HeldCertificate.kt

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
package okhttp3.tls
1717

1818
import okhttp3.internal.canParseAsIpAddress
19+
import okio.Buffer
1920
import okio.ByteString
21+
import okio.ByteString.Companion.decodeBase64
2022
import okio.ByteString.Companion.toByteString
2123
import org.bouncycastle.asn1.ASN1Encodable
2224
import org.bouncycastle.asn1.DERSequence
@@ -27,14 +29,20 @@ import org.bouncycastle.asn1.x509.X509Extensions
2729
import org.bouncycastle.jce.provider.BouncyCastleProvider
2830
import org.bouncycastle.x509.X509V3CertificateGenerator
2931
import java.math.BigInteger
32+
import java.security.GeneralSecurityException
33+
import java.security.KeyFactory
3034
import java.security.KeyPair
3135
import java.security.KeyPairGenerator
3236
import java.security.PrivateKey
3337
import java.security.PublicKey
3438
import java.security.SecureRandom
3539
import java.security.Security
40+
import java.security.cert.CertificateFactory
3641
import java.security.cert.X509Certificate
42+
import java.security.interfaces.ECPublicKey
3743
import java.security.interfaces.RSAPrivateKey
44+
import java.security.interfaces.RSAPublicKey
45+
import java.security.spec.PKCS8EncodedKeySpec
3846
import java.util.Date
3947
import java.util.UUID
4048
import java.util.concurrent.TimeUnit
@@ -353,7 +361,7 @@ class HeldCertificate(
353361
BigInteger.ONE
354362
}
355363
val signatureAlgorithm = if (signedByKeyPair.private is RSAPrivateKey) {
356-
"SHA256WithRSAEncryption"
364+
"SHA256WithRSA"
357365
} else {
358366
"SHA256withECDSA"
359367
}
@@ -419,4 +427,103 @@ class HeldCertificate(
419427
}
420428
}
421429
}
430+
431+
companion object {
432+
private val PEM_REGEX = Regex("""-----BEGIN ([!-,.-~ ]*)-----([^-]*)-----END \1-----""")
433+
434+
/**
435+
* Decodes a multiline string that contains both a [certificate][certificatePem] and a
436+
* [private key][privateKeyPkcs8Pem], both [PEM-encoded][rfc_7468]. A typical input string looks
437+
* like this:
438+
*
439+
* ```
440+
* -----BEGIN CERTIFICATE-----
441+
* MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl
442+
* cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx
443+
* MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h
444+
* cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD
445+
* ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw
446+
* HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF
447+
* AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT
448+
* yyaoEufLKVXhrTQhRfodTeigi4RX
449+
* -----END CERTIFICATE-----
450+
* -----BEGIN PRIVATE KEY-----
451+
* MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J
452+
* lu/GJQZoU9lDrCPeUcQ28tzOWw==
453+
* -----END PRIVATE KEY-----
454+
* ```
455+
*
456+
* The string should contain exactly one certificate and one private key in [PKCS #8][rfc_5208]
457+
* format. It should not contain any other PEM-encoded blocks, but it may contain other text
458+
* which will be ignored.
459+
*
460+
* Encode a held certificate into this format by concatenating the results of
461+
* [certificatePem()][certificatePem] and [privateKeyPkcs8Pem()][privateKeyPkcs8Pem].
462+
*
463+
* [rfc_7468]: https://tools.ietf.org/html/rfc7468
464+
* [rfc_5208]: https://tools.ietf.org/html/rfc5208
465+
*/
466+
@JvmStatic
467+
fun decode(certificateAndPrivateKeyPem: String): HeldCertificate {
468+
var certificatePem: String? = null
469+
var pkcs8Base64: String? = null
470+
for (match in PEM_REGEX.findAll(certificateAndPrivateKeyPem)) {
471+
when (val label = match.groups[1]!!.value) {
472+
"CERTIFICATE" -> {
473+
require(certificatePem == null) { "string includes multiple certificates" }
474+
certificatePem = match.groups[0]!!.value // Keep --BEGIN-- and --END-- for certificates.
475+
}
476+
"PRIVATE KEY" -> {
477+
require(pkcs8Base64 == null) { "string includes multiple private keys" }
478+
pkcs8Base64 = match.groups[2]!!.value // Include the contents only for PKCS8.
479+
}
480+
else -> {
481+
throw IllegalArgumentException("unexpected type: $label")
482+
}
483+
}
484+
}
485+
require(certificatePem != null) { "string does not include a certificate" }
486+
require(pkcs8Base64 != null) { "string does not include a private key" }
487+
488+
return decode(certificatePem, pkcs8Base64)
489+
}
490+
491+
private fun decode(certificatePem: String, pkcs8Base64Text: String): HeldCertificate {
492+
val certificate = try {
493+
decodePem(certificatePem)
494+
} catch (e: GeneralSecurityException) {
495+
throw IllegalArgumentException("failed to decode certificate", e)
496+
}
497+
498+
val privateKey = try {
499+
val pkcs8Bytes = pkcs8Base64Text.decodeBase64()
500+
?: throw IllegalArgumentException("failed to decode private key")
501+
502+
// The private key doesn't tell us its type but it's okay because the certificate knows!
503+
val keyType = when (certificate.publicKey) {
504+
is ECPublicKey -> "EC"
505+
is RSAPublicKey -> "RSA"
506+
else -> throw IllegalArgumentException("unexpected key type: ${certificate.publicKey}")
507+
}
508+
509+
decodePkcs8(pkcs8Bytes, keyType)
510+
} catch (e: GeneralSecurityException) {
511+
throw IllegalArgumentException("failed to decode private key", e)
512+
}
513+
514+
val keyPair = KeyPair(certificate.publicKey, privateKey)
515+
return HeldCertificate(keyPair, certificate)
516+
}
517+
518+
private fun decodePem(pem: String): X509Certificate {
519+
val certificates = CertificateFactory.getInstance("X.509")
520+
.generateCertificates(Buffer().writeUtf8(pem).inputStream())
521+
return certificates.iterator().next() as X509Certificate
522+
}
523+
524+
private fun decodePkcs8(data: ByteString, keyAlgorithm: String): PrivateKey {
525+
val keyFactory = KeyFactory.getInstance(keyAlgorithm)
526+
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(data.toByteArray()))
527+
}
528+
}
422529
}

0 commit comments

Comments
 (0)