Skip to content

Commit

Permalink
Merge branch 'feature/accept-more-private-key-formats' into 'main'
Browse files Browse the repository at this point in the history
accept more private key formats

See merge request products/hello-http!26
  • Loading branch information
Sunny Chung committed Nov 15, 2024
2 parents 62f2556 + 4cf8e31 commit c20fea4
Show file tree
Hide file tree
Showing 24 changed files with 1,017 additions and 194 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Mouse hovering variable placeholders in Body Editor to show a tooltip for its value (if exists)
- Number badges in Request Parameter Type tabs to indicate the number of active entries declared in the selected example, e.g. the number of active key-value pairs of a multipart request body declared in the selected Request Example.
- Certificates in P7B (PKCS#7) format can now be imported
- Private keys in PEM or PKCS#1 formats can now be imported, and does not limit to RSA keys anymore.
- PKCS#12 (known as p12) and PFX files can now be imported as client certificates

### Changed
- The main monospace font has been changed to Pitagon Sans Mono and unified among all platforms
Expand All @@ -29,7 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- The copy button overlapped with the search bar in the response body viewer.

### Optimized
- Request body editor, payload body editor and response body viewer are now able to handle bodies with a size of megabytes without noticeable performance issues.
- Request body editor, payload body editor and response body viewer are now able to handle bodies with a size of megabytes without significant performance issues.
- Clicking the "Send" button now never freeze for a short while.

## [1.6.0] - 2024-07-22
Expand Down
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ kotlin {
implementation("io.github.tree-sitter:ktreesitter:0.23.0")
implementation("io.github.sunny-chung:ktreesitter-json:0.23.0.1")
implementation("io.github.sunny-chung:ktreesitter-graphql:1.0.0.0")

// public/private key decoding
implementation("org.bouncycastle:bcpkix-jdk18on:1.79")
}

resources.srcDir("$buildDir/resources")
Expand All @@ -97,6 +100,7 @@ kotlin {
implementation(kotlin("test"))
implementation("org.junit.jupiter:junit-jupiter-params")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.15.2")
implementation(project(":test-common"))
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions doc/features/ssl-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Currently, the accepted formats are:
- PEM (also known as CER or CRT)
- P7B

The formats are detected automatically.

Imported certificates can be disabled by unchecking the corresponding green tick box, or deleted. Changes to the
original file would not affect imported ones.

Expand All @@ -44,16 +46,14 @@ Accepted formats for a client certificate are:
Accepted formats for a private key are:
- Unencrypted PKCS #8 DER
- Password-encrypted PKCS #8 DER
- PKCS #1 DER
- Unencrypted PKCS #8 PEM
- Password-encrypted PKCS #8 PEM
- PKCS #1 PEM

An unencrypted PKCS #8 DER key file can be converted from a PEM file using OpenSSL.
```
openssl pkcs8 -topk8 -in clientKey.pem -out clientKey.pkcs8.der -outform DER -nocrypt
```
Alternatively, a PKCS#12 or P12 or PFX bundle containing exactly one certificate and one private key can be imported. If multiple entries of the same kind are found, only the first one would be imported.

A password-encrypted PKCS #8 DER key file can be converted from a PEM file using OpenSSL.
```
openssl pkcs8 -topk8 -in clientKey.pem -out clientKey.pkcs8.encrypted.der -outform DER
```
The formats are detected automatically. Files with the `.key` file extension are usually in PEM or DER formats.


## Disable SSL Verification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,9 @@ import com.sunnychung.application.multiplatform.hellohttp.annotation.Persisted
import com.sunnychung.application.multiplatform.hellohttp.document.Identifiable
import com.sunnychung.application.multiplatform.hellohttp.util.uuidString
import com.sunnychung.application.multiplatform.hellohttp.ux.DropDownable
import com.sunnychung.lib.multiplatform.kdatetime.KDateTimeFormat
import com.sunnychung.lib.multiplatform.kdatetime.KInstant
import com.sunnychung.lib.multiplatform.kdatetime.KZoneOffset
import com.sunnychung.lib.multiplatform.kdatetime.KZonedInstant
import com.sunnychung.lib.multiplatform.kdatetime.extension.seconds
import kotlinx.serialization.Serializable
import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStream
import java.io.InputStreamReader
import java.security.KeyFactory
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.EncryptedPrivateKeyInfo
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.security.auth.x500.X500Principal

@Persisted
@Serializable
Expand Down Expand Up @@ -127,124 +109,3 @@ data class ClientCertificateKeyPair(

companion object
}

fun ClientCertificateKeyPair.Companion.importFrom(certFile: File, keyFile: File, keyPassword: String): ClientCertificateKeyPair {
listOf(certFile, keyFile).forEach { file ->
if (!file.canRead()) {
throw IllegalArgumentException("File ${file.name} cannot be read.")
}
}

val cert: X509Certificate = try {
val certBytes = certFile.readBytes()
parseCaCertificates(certBytes).also {
if (it.size > 1) {
throw RuntimeException("There should be only one certificate but ${it.size} were found.")
} else if (it.isEmpty()) {
throw RuntimeException("No certificate was found.")
}
}.single()
} catch (e: Throwable) {
throw RuntimeException("Error while parsing the certificate file -- ${e.message}", e)
}

fun decryptAsRsaKeySpec(keyBytes: ByteArray, password: String): PKCS8EncodedKeySpec {
return EncryptedPrivateKeyInfo(keyBytes)
.let {
val secretKey = SecretKeyFactory.getInstance(it.algName).generateSecret(PBEKeySpec(password.toCharArray()))
val cipher = Cipher.getInstance(it.algName)
cipher.init(Cipher.DECRYPT_MODE, secretKey, it.algParameters)
val keySpec = it.getKeySpec(cipher)

// try if all these work
KeyFactory.getInstance("RSA").generatePrivate(keySpec)

keySpec
}
}

val keyBytes = keyFile.readBytes()
val keySpec = try {
if (keyPassword.isEmpty()) {
// try without password. if it's fail, try with empty password
try {
val keySpec = PKCS8EncodedKeySpec(keyBytes)
KeyFactory.getInstance("RSA").generatePrivate(keySpec)
keySpec
} catch (e: InvalidKeySpecException) {
decryptAsRsaKeySpec(keyBytes = keyBytes, keyPassword)
}
} else {
decryptAsRsaKeySpec(keyBytes = keyBytes, keyPassword)
}
} catch (e: Throwable) {
throw RuntimeException("Error while parsing the private key file -- ${e.message}", e)
}

val now = KInstant.now()
return ClientCertificateKeyPair(
id = uuidString(),
certificate = ImportedFile(
id = uuidString(),
name = cert.subjectX500Principal.getName(X500Principal.RFC1779) +
"\nExpiry: ${KZonedInstant(cert.notAfter.time, KZoneOffset.local()).format(KDateTimeFormat.ISO8601_DATETIME.pattern)}",
originalFilename = certFile.name,
createdWhen = now,
isEnabled = true,
content = cert.encoded,
),
privateKey = ImportedFile(
id = uuidString(),
name = "Private Key",
originalFilename = keyFile.name,
createdWhen = now,
isEnabled = true,
content = keySpec.encoded, // store decrypted bytes
),
createdWhen = now,
isEnabled = true,
)
}

// ----------- TODO refactor to a separate file -----------

fun parseCaCertificates(bytes: ByteArray) : List<X509Certificate> {
InputStreamReader(ByteArrayInputStream(bytes)).buffered().use { reader ->
val firstLine = reader.readLine()
val certBytes = if (firstLine == "-----BEGIN PKCS7-----") { // p7b
val base64Encoded = buildString {
while (reader.ready()) {
val line = reader.readLine()
if (line != "-----END PKCS7-----") {
append(line)
} else {
break
}
}
}
Base64.getDecoder().decode(base64Encoded)
} else { // der / pem
bytes
}
return CertificateFactory.getInstance("X.509").generateCertificates(ByteArrayInputStream(certBytes)).map { it as X509Certificate }
}
}

fun importCaCertificates(file: File): List<ImportedFile> {
val content = file.readBytes()
val certs = parseCaCertificates(content)

return certs.map { cert ->
ImportedFile(
id = uuidString(),
name = cert.subjectX500Principal.getName(X500Principal.RFC1779) +
"\nExpiry: ${KZonedInstant(cert.notAfter.time, KZoneOffset.local()).format(KDateTimeFormat.ISO8601_DATETIME.pattern)}" +
if (cert.keyUsage?.get(5) != true || cert.basicConstraints < 0) "\n⚠️ Not a CA certificate!" else ""
,
originalFilename = file.name,
createdWhen = KInstant.now(),
isEnabled = true,
content = cert.encoded,
)
}
}
Loading

0 comments on commit c20fea4

Please sign in to comment.