fix(jans-auth-server): ES512 potentially invalid. x/y length doesn't match curve size. #7951
Description
I have been using an oauth sidecar named oauth2-proxy (docker image "bitnami/oauth2-proxy:7"), and the container nearly always ends up producing errors when fetching my jwks. It has been complaining about x or y values being the wrong length for certain elliptic curve keys.
I'm not sure if this is an issue with oauth2-proxy, or the keys generated from this project. I have recreated their code for calculating what the encoded octect strings length should be for the curve size and created a tool to analyse the jwks, and I find whenever I am having these issues (usually after key rotation, though sometimes present in a fresh environment) that the key always generating the errors is the ES512 key, generally because the length should be 66 but I end up always getting 65 a lot of the times.
The library that oauth2-proxy uses is "go-jose", and the check is here: https://github.com/go-jose/go-jose/blob/main/jwk.go#L531.
It references a rfc here: https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2 which says about the x and y parameters:
The length of this octet string MUST
be the full size of a coordinate for the curve specified in the "crv"
parameter. For example, if the value of "crv" is "P-521", the octet
string must be 66 octets long.
To reproduce this behaviour please try building and running the following Dockerfile:
FROM ghcr.io/janssenproject/jans/certmanager:1.0.22-1
USER root
RUN apk add --no-cache php php-curl \
&& mkdir -p /srv/application
RUN cat <<'EOF' > /srv/application/curve_test.php
<?php
$curl = curl_init();
if (empty($argv[1])) {
$stdinData = file_get_contents('php://stdin');
echo "Reading from STDIN" . PHP_EOL;
}
if (!empty($argv[1])) {
$url = $argv[1];
curl_setopt_array($curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'Accept: application/json;'
),
));
$response = curl_exec($curl);
curl_close($curl);
$data = json_decode($response, true);
} else {
$data = json_decode($stdinData, true);
}
$keys = $invalidx = $invalidy = 0;
function decode($d) {
$ret = str_replace('-', '+', $d);
$ret = str_replace('_', '/', $ret);
if ((strlen($ret) % 4) !== 0) $ret .= str_repeat('=', 4 - (strlen($ret) % 4));
$ret = base64_decode($ret);
return $ret;
}
foreach ($data['keys'] as $d) {
if (empty($d['x']) || empty($d['y'])) continue;
$curveBitSize = (int) str_replace('P-', '', $d['crv']);
$div = intval($curveBitSize / 8);
$mod = $curveBitSize % 8;
$size = $div;
if ($mod !== 0) $size = $div + 1;
$xDecoded = decode($d['x']);
$xSize = strlen($xDecoded);
if ($xSize !== $size) {
echo "invalid EC public key, wrong length for x, kid: {$d['kid']}, expected $size got $xSize. crv: {$d['crv']}" . PHP_EOL;
$invalidx++;
}
$yDecoded = decode($d['y']);
$ySize = strlen($yDecoded);
if ($ySize !== $size) {
echo "invalid EC public key, wrong length for y, kid: {$d['kid']}, expected $size got $ySize. crv: {$d['crv']}" . PHP_EOL;
$invalidy++;
}
$keys++;
}
echo "$invalidx/$keys x keys invalid" . PHP_EOL;
echo "$invalidy/$keys y keys invalid" . PHP_EOL;
EOF
RUN cat <<'EOF' > /test.sh
for i in $(seq 1 10);
do
echo "---"
echo "Test $i"
KEYS=$(java -Dlog4j.defaultInitOverride=true -cp /app/javalibs/* io.jans.as.client.util.KeyGenerator -enc_keys RSA1_5 RSA-OAEP ECDH-ES -sig_keys RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 -dnname 'CN=Janssen Auth CA Certificates' -expiration_hours 3 -keystore /etc/certs/auth-keys.jks -keypasswd test123 -key_ops_type all)
echo "$KEYS" | php /srv/application/curve_test.php
echo
done
EOF
RUN chmod +x /test.sh
ENTRYPOINT /test.sh
This will create fresh jwk keys and run them through the curve test php function 10 times. Now I'm not sure if it's how the x & y lengths are being calculated, or if it's something I'm doing, but I nearly always get invalid x & y values for the ES512 key, and sometimes the ES256 one. Assuming I'm doing nothing wrong, I would expect the x and y lengths to always be 66 as per what the rfc7518 states.
In the mean time I have removed support for ES512 by altering the array of supported algorithms in the jwksAlgorithmsSupported
property using the below patch:
[
{
"op": "replace",
"path": "jwksAlgorithmsSupported",
"value": [
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"PS256",
"PS384",
"PS512",
"RSA1_5",
"RSA-OAEP"
]
}
]
Note the missing "ES512" value. This seems to have cleared up my issue for the oauth2-proxy.
After a bit of further investigation this was raised as an issue in go-jose repo: go-jose/go-jose#46, though later closed by the author who recognised it was per rfc. He also mentioned keycloak had a similar issue that they themselves also fixed: keycloak/keycloak#14933.
I believe the key generation code may need to be looked at, especially for P-521 curve, and also maybe P-256 curve. Possibly tests can be created to make sure the x & y lengths produced always equal the curve bit size.
Activity