Skip to content

fix(jans-auth-server): ES512 potentially invalid. x/y length doesn't match curve size. #7951

Open
@saernz

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions