Harden JWT PEM normalization diagnostics
This commit is contained in:
@@ -109,10 +109,11 @@ class JwtAuthMiddleware
|
|||||||
throw new \RuntimeException('JWT public key is empty');
|
throw new \RuntimeException('JWT public key is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->flushOpenSslErrors();
|
||||||
$keyResource = openssl_pkey_get_public($publicKey);
|
$keyResource = openssl_pkey_get_public($publicKey);
|
||||||
|
|
||||||
if ($keyResource === false) {
|
if ($keyResource === false) {
|
||||||
throw new \RuntimeException(openssl_error_string() ?: 'OpenSSL could not read JWT public key');
|
throw new \RuntimeException($this->openSslErrorMessage('OpenSSL could not read JWT public key'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = openssl_verify(
|
$result = openssl_verify(
|
||||||
@@ -123,7 +124,7 @@ class JwtAuthMiddleware
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new \RuntimeException(openssl_error_string() ?: 'OpenSSL could not verify JWT signature');
|
throw new \RuntimeException($this->openSslErrorMessage('OpenSSL could not verify JWT signature'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result === 1;
|
return $result === 1;
|
||||||
@@ -131,6 +132,15 @@ class JwtAuthMiddleware
|
|||||||
|
|
||||||
private function normalizePublicKey(string $publicKey): string
|
private function normalizePublicKey(string $publicKey): string
|
||||||
{
|
{
|
||||||
|
$publicKey = trim($publicKey);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(str_starts_with($publicKey, '"') && str_ends_with($publicKey, '"')) ||
|
||||||
|
(str_starts_with($publicKey, "'") && str_ends_with($publicKey, "'"))
|
||||||
|
) {
|
||||||
|
$publicKey = substr($publicKey, 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
$publicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $publicKey));
|
$publicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $publicKey));
|
||||||
|
|
||||||
if ($publicKey === '') {
|
if ($publicKey === '') {
|
||||||
@@ -139,23 +149,52 @@ class JwtAuthMiddleware
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
preg_match(
|
preg_match(
|
||||||
'/-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----/s',
|
'/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s',
|
||||||
$publicKey,
|
$publicKey,
|
||||||
$matches
|
$matches
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
$body = preg_replace('/\s+/', '', $matches[1]);
|
$type = $matches[1];
|
||||||
|
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $matches[2]);
|
||||||
|
|
||||||
if ($body === '') {
|
if ($body === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "-----BEGIN {$type}-----\n"
|
||||||
|
. chunk_split($body, 64, "\n")
|
||||||
|
. "-----END {$type}-----\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_contains($publicKey, '-----BEGIN')) {
|
||||||
|
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $publicKey);
|
||||||
|
|
||||||
|
if (strlen($body) > 100) {
|
||||||
return "-----BEGIN PUBLIC KEY-----\n"
|
return "-----BEGIN PUBLIC KEY-----\n"
|
||||||
. chunk_split($body, 64, "\n")
|
. chunk_split($body, 64, "\n")
|
||||||
. "-----END PUBLIC KEY-----\n";
|
. "-----END PUBLIC KEY-----\n";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $publicKey;
|
return trim($publicKey) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flushOpenSslErrors(): void
|
||||||
|
{
|
||||||
|
while (openssl_error_string() !== false) {
|
||||||
|
// Clear stale OpenSSL errors before reading the next operation result.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function openSslErrorMessage(string $fallback): string
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
while (($error = openssl_error_string()) !== false) {
|
||||||
|
$errors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors === [] ? $fallback : implode(' | ', $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function tokenIsExpired(array $payload): bool
|
private function tokenIsExpired(array $payload): bool
|
||||||
|
|||||||
@@ -30,31 +30,61 @@ Route::get('/health', function () {
|
|||||||
|
|
||||||
Route::get('/health-check-key', function () {
|
Route::get('/health-check-key', function () {
|
||||||
$rawPublicKey = (string) config('jwt.public_key');
|
$rawPublicKey = (string) config('jwt.public_key');
|
||||||
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $rawPublicKey));
|
$formattedPublicKey = trim($rawPublicKey);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
preg_match(
|
(str_starts_with($formattedPublicKey, '"') && str_ends_with($formattedPublicKey, '"')) ||
|
||||||
'/-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----/s',
|
(str_starts_with($formattedPublicKey, "'") && str_ends_with($formattedPublicKey, "'"))
|
||||||
$formattedPublicKey,
|
|
||||||
$matches
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
$body = preg_replace('/\s+/', '', $matches[1]);
|
$formattedPublicKey = substr($formattedPublicKey, 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $formattedPublicKey));
|
||||||
|
$pemType = null;
|
||||||
|
$bodyLength = null;
|
||||||
|
|
||||||
|
if (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s', $formattedPublicKey, $matches)) {
|
||||||
|
$pemType = $matches[1];
|
||||||
|
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $matches[2]);
|
||||||
|
$bodyLength = strlen($body);
|
||||||
|
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
|
||||||
|
. chunk_split($body, 64, "\n")
|
||||||
|
. "-----END {$pemType}-----\n";
|
||||||
|
} elseif (!str_contains($formattedPublicKey, '-----BEGIN')) {
|
||||||
|
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $formattedPublicKey);
|
||||||
|
$bodyLength = strlen($body);
|
||||||
|
|
||||||
|
if ($bodyLength > 100) {
|
||||||
|
$pemType = 'PUBLIC KEY';
|
||||||
$formattedPublicKey = "-----BEGIN PUBLIC KEY-----\n"
|
$formattedPublicKey = "-----BEGIN PUBLIC KEY-----\n"
|
||||||
. chunk_split($body, 64, "\n")
|
. chunk_split($body, 64, "\n")
|
||||||
. "-----END PUBLIC KEY-----\n";
|
. "-----END PUBLIC KEY-----\n";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (openssl_error_string() !== false) {
|
||||||
|
// Clear stale OpenSSL errors before testing the current key.
|
||||||
|
}
|
||||||
|
|
||||||
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
|
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
|
||||||
|
$openSslErrors = [];
|
||||||
|
|
||||||
|
while (($error = openssl_error_string()) !== false) {
|
||||||
|
$openSslErrors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'raw_key_empty' => $rawPublicKey === '',
|
'raw_key_empty' => $rawPublicKey === '',
|
||||||
'raw_key_length' => strlen($rawPublicKey),
|
'raw_key_length' => strlen($rawPublicKey),
|
||||||
'formatted_key_length' => strlen($formattedPublicKey),
|
'formatted_key_length' => strlen($formattedPublicKey),
|
||||||
|
'pem_type' => $pemType,
|
||||||
|
'pem_body_length' => $bodyLength,
|
||||||
'has_begin_marker' => str_contains($rawPublicKey, '-----BEGIN PUBLIC KEY-----'),
|
'has_begin_marker' => str_contains($rawPublicKey, '-----BEGIN PUBLIC KEY-----'),
|
||||||
|
'has_rsa_begin_marker' => str_contains($rawPublicKey, '-----BEGIN RSA PUBLIC KEY-----'),
|
||||||
'has_end_marker' => str_contains($rawPublicKey, '-----END PUBLIC KEY-----'),
|
'has_end_marker' => str_contains($rawPublicKey, '-----END PUBLIC KEY-----'),
|
||||||
|
'has_rsa_end_marker' => str_contains($rawPublicKey, '-----END RSA PUBLIC KEY-----'),
|
||||||
'openssl_accepted' => $publicKeyResource !== false,
|
'openssl_accepted' => $publicKeyResource !== false,
|
||||||
'openssl_error' => openssl_error_string(),
|
'openssl_errors' => $openSslErrors,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,31 +17,61 @@ Route::get('/health', function () {
|
|||||||
|
|
||||||
Route::get('/health-check-key', function () {
|
Route::get('/health-check-key', function () {
|
||||||
$rawPublicKey = (string) config('jwt.public_key');
|
$rawPublicKey = (string) config('jwt.public_key');
|
||||||
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $rawPublicKey));
|
$formattedPublicKey = trim($rawPublicKey);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
preg_match(
|
(str_starts_with($formattedPublicKey, '"') && str_ends_with($formattedPublicKey, '"')) ||
|
||||||
'/-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----/s',
|
(str_starts_with($formattedPublicKey, "'") && str_ends_with($formattedPublicKey, "'"))
|
||||||
$formattedPublicKey,
|
|
||||||
$matches
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
$body = preg_replace('/\s+/', '', $matches[1]);
|
$formattedPublicKey = substr($formattedPublicKey, 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $formattedPublicKey));
|
||||||
|
$pemType = null;
|
||||||
|
$bodyLength = null;
|
||||||
|
|
||||||
|
if (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s', $formattedPublicKey, $matches)) {
|
||||||
|
$pemType = $matches[1];
|
||||||
|
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $matches[2]);
|
||||||
|
$bodyLength = strlen($body);
|
||||||
|
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
|
||||||
|
. chunk_split($body, 64, "\n")
|
||||||
|
. "-----END {$pemType}-----\n";
|
||||||
|
} elseif (!str_contains($formattedPublicKey, '-----BEGIN')) {
|
||||||
|
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $formattedPublicKey);
|
||||||
|
$bodyLength = strlen($body);
|
||||||
|
|
||||||
|
if ($bodyLength > 100) {
|
||||||
|
$pemType = 'PUBLIC KEY';
|
||||||
$formattedPublicKey = "-----BEGIN PUBLIC KEY-----\n"
|
$formattedPublicKey = "-----BEGIN PUBLIC KEY-----\n"
|
||||||
. chunk_split($body, 64, "\n")
|
. chunk_split($body, 64, "\n")
|
||||||
. "-----END PUBLIC KEY-----\n";
|
. "-----END PUBLIC KEY-----\n";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (openssl_error_string() !== false) {
|
||||||
|
// Clear stale OpenSSL errors before testing the current key.
|
||||||
|
}
|
||||||
|
|
||||||
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
|
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
|
||||||
|
$openSslErrors = [];
|
||||||
|
|
||||||
|
while (($error = openssl_error_string()) !== false) {
|
||||||
|
$openSslErrors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'raw_key_empty' => $rawPublicKey === '',
|
'raw_key_empty' => $rawPublicKey === '',
|
||||||
'raw_key_length' => strlen($rawPublicKey),
|
'raw_key_length' => strlen($rawPublicKey),
|
||||||
'formatted_key_length' => strlen($formattedPublicKey),
|
'formatted_key_length' => strlen($formattedPublicKey),
|
||||||
|
'pem_type' => $pemType,
|
||||||
|
'pem_body_length' => $bodyLength,
|
||||||
'has_begin_marker' => str_contains($rawPublicKey, '-----BEGIN PUBLIC KEY-----'),
|
'has_begin_marker' => str_contains($rawPublicKey, '-----BEGIN PUBLIC KEY-----'),
|
||||||
|
'has_rsa_begin_marker' => str_contains($rawPublicKey, '-----BEGIN RSA PUBLIC KEY-----'),
|
||||||
'has_end_marker' => str_contains($rawPublicKey, '-----END PUBLIC KEY-----'),
|
'has_end_marker' => str_contains($rawPublicKey, '-----END PUBLIC KEY-----'),
|
||||||
|
'has_rsa_end_marker' => str_contains($rawPublicKey, '-----END RSA PUBLIC KEY-----'),
|
||||||
'openssl_accepted' => $publicKeyResource !== false,
|
'openssl_accepted' => $publicKeyResource !== false,
|
||||||
'openssl_error' => openssl_error_string(),
|
'openssl_errors' => $openSslErrors,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user