Auth token server: token signed by untrusted key with ID

I am attempting to set up an external auth token server for registry:2. My process is as follows:

Generate a self signed certificate:

openssl req -x509 -days 365 -nodes -newkey rsa:4096 -keyout registry-auth-key.pem -out registry-auth-cert.pem

Export to pfx (for .NET code later):

openssl pkcs12 -export -out registry-auth.pfx -inkey registry-auth-key.pem -in registry-auth-cert.pem

Then, I spin up a registry container:

docker run -d -p 5000:5000 \
-e REGISTRY_LOG_LEVEL=debug \
-e REGISTRY_AUTH=token \
-e REGISTRY_AUTH_TOKEN_REALM=http://internal/token \
-e REGISTRY_AUTH_TOKEN_SERVICE="Docker registry" \
-e REGISTRY_AUTH_TOKEN_ISSUER="issuer" \
-e REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/ssl/registry-auth-cert.pem \
-v /auth_certs:/ssl \
--restart=always \
--name registry registry:2

The container spins up and everything looks fine. I then attempt to construct an auth token in .NET Core:

private static string CreateToken()
{
	//Load up the pulic / private key
	var bytes = File.ReadAllBytes("registry-auth.pfx");

	X509Certificate2 cert = new X509Certificate2(bytes, new SecureString());

	var key = new X509SecurityKey(cert);
	var credentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);

	var header = new JwtHeader(credentials);

	var claims = new Claim[]
	{
	};

	DateTime expires = DateTime.UtcNow.Add(TimeSpan.FromDays(100));
	DateTime notBefore = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(30));
	DateTime issuedAt = DateTime.UtcNow;

	var payload = new JwtPayload("issuer", "Docker registry", claims, notBefore, expires, issuedAt);

	var secToken = new JwtSecurityToken(header, payload);

	var handler = new JwtSecurityTokenHandler();

	var token = handler.WriteToken(secToken);

	return token;
}

And make a request using the following:

var token = CreateToken(algorithmn);

using (var httpClient = new HttpClient())
{
      var request = new HttpRequestMessage(HttpMethod.Get, uri);

      request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

      var response = await httpClient.SendAsync(request);
}

The response is always “401 Unauthorized” with invalid token.

Here is the debug log:

[28/Feb/2018:16:37:46 +0000] "GET /v2/ HTTP/1.1" 401 87 "" ""
time="2018-02-28T16:37:46.537066857Z" level=debug msg="authorizing request" go.version=go1.7.6 http.request.host="172.22.5.74:5000" http.request.id=1e965937-fc12-4845-be32-d5e4191c8e45 http.request.method=GET http.request.remoteaddr="172.22.5.65:53586" http.request.uri="/v2/" http.request.useragent= instance.id=c344acf5-53e9-4fc9-9a25-da74aa0b0a0e service=registry version=v2.6.2 
time="2018-02-28T16:37:46.537132358Z" level=info msg="token signed by untrusted key with ID: \"CCA6245062A3AB465851BE88FA5DD890D87ABB74\"" 
time="2018-02-28T16:37:46.537165859Z" level=warning msg="error authorizing context: invalid token" go.version=go1.7.6 http.request.host="172.22.5.74:5000" http.request.id=1e965937-fc12-4845-be32-d5e4191c8e45 http.request.method=GET http.request.remoteaddr="172.22.5.65:53586" http.request.uri="/v2/" http.request.useragent= instance.id=c344acf5-53e9-4fc9-9a25-da74aa0b0a0e service=registry version=v2.6.2 
172.22.5.65 - - 

Figured it out. By default, the MS provided nuget package for JWT uses the fingerprint of the public key. Docker registry expects a different format (libtrust fingerprint):

{
    "typ": "JWT",
    "alg": "ES256",
    "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
}

So, to come up with this format, ended up using BouncyCastle to do the following:

private static string CreateToken()
{
	//Load up the pulic / private key
	var bytes = File.ReadAllBytes("registry-auth.pfx");

	X509Certificate2 cert = new X509Certificate2(bytes, new SecureString());

	var key = new X509SecurityKey(cert)
	{
		KeyId = GetKid(cert)
	};

	var credentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
	
	var header = new JwtHeader(credentials);

	var claims = new Claim[]
	{
		//new Claim("scope", "agent:"),
	};

	DateTime expires = DateTime.UtcNow.Add(TimeSpan.FromDays(100));
	DateTime notBefore = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(30));
	DateTime issuedAt = DateTime.UtcNow;

	var payload = new JwtPayload("issuer", "Docker registry", claims, notBefore, expires, issuedAt);

	var secToken = new JwtSecurityToken(header, payload);

	var handler = new JwtSecurityTokenHandler();

	var token = handler.WriteToken(secToken);

	return token;
}

/// <summary>
/// Gets the kid for docker registry.
/// </summary>
/// <param name="certificate"></param>
/// <returns></returns>
private static string GetKid(X509Certificate2 certificate)
{
	X509Certificate bouncyCert = DotNetUtilities.FromX509Certificate(certificate);

	AsymmetricKeyParameter bouncyPublicKey = bouncyCert.GetPublicKey();

	SubjectPublicKeyInfo info = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(bouncyPublicKey);

	var encoded = info.GetDerEncoded();

	using (SHA256Managed sha256 = new SHA256Managed())
	{
		byte[] hash = sha256.ComputeHash(encoded);

		//Take the first 30 bytes
		byte[] sub = hash
			.Take(30)
			.ToArray();

		string base32 = Base32.Encode(sub);

		return FormatKid(base32);
	}
}

private static string FormatKid(string raw)
{
	const int RawKidLength = 48;

	if (raw.Length != RawKidLength)
		throw new Exception($"Raw kid was {raw.Length} characters instead of {RawKidLength}.");

	string[] parts =
	{
		raw.Substring(0, 4),
		raw.Substring(4, 4),
		raw.Substring(8, 4),
		raw.Substring(12, 4),
		raw.Substring(16, 4),
		raw.Substring(20, 4),
		raw.Substring(24, 4),
		raw.Substring(28, 4),
		raw.Substring(32, 4),
		raw.Substring(36, 4),
		raw.Substring(40 , 4),
		raw.Substring(44, 4),
	};

	return string.Join(':', parts);

}

Reference:

2 Likes