The worst thing a specification can do is not specify something
The recent retirement of OB Legacy certificates has caused trouble in the Open Banking UK world. With a hard deadline of June 30, 2021, some institutions were still working in the last three weeks to complete the change.
After having finished their transfer, others turned off the old certificate support weeks ago, essentially cutting off any integration that hadn’t been updated.
However, the remaining number struggled with the lack of specs for handling secure APIs and communicating information in a standardized way.
Some Background – OpenID FAPI JWT
Using Financial-grade API (FAPI) with OpenID to authenticate gives you some options on how you want to authenticate:
You can find out what is supported from the value of token_endpoint_auth_methods_supported in the OpenID Connect discovery document.
For example, Google publishes theirs here: https://accounts.google.com/.well-known/openid-configuration, and our own Auth service is published here https://auth.apimetrics.io/.well-known/openid-configuration.
Both say client_secret_post and client_secret_basic. These are the most common approaches – “classic OAuth 2.”
Banks with robust security need to use:
- private_key_jwt – instead of passing a client id and secret to the auth server, you pass a client_assertion, a JWT. This has a small payload, the subject “sub” and issuer “iss” values are set to the client_id, and the audience “aud” is the URL of the auth service (usually – the value is specified in the OpenID Connect discovery document as the “issuer”).
- tls_client_auth – instead of relying on Application-layer parameters to identify the API caller, we use the transport layer. We use the SSL certificate in the TLS handshake.
Most of the time, you can just think of certificates as a clever kind of username and password with some complex math involved. You have a public key – the “username,” and a private key – the “password.”
The one wrinkle to add to this is that they also contain some metadata – a key ID and some other information about the person or organization presenting it.
For either of these methods to work, the certificates you use have to be known and trusted. For Open Banking UK, TPPs can generate certificates via the Open Banking Directory. The public keys and their IDs are accessible from it via API.
There is a standard format for publishing information about your keys – JSON Web Key Sets “JWKS.” This way, a bank, when receiving a request using either of the auth methods above, can validate the request is from a trusted party.
Open Banking uses two certificates:
- A transport certificate, for mutual-TLS (mTLS) (this is used everywhere even if not using tls_client_auth)
- A signing certificate, for signing JWTs (this is used in the authorization step, even if not using private_key_jwt on the token endpoint)
There are different kinds of these certificates as well:
- OB Legacy
- OBSeal (signing) and OBWAC (transport)
- For EU entities – eIDAS certificates – QSEAL (signing) and QWAC (transport)
As you may guess from the name, OB Legacy certs were phased out as of July 1, 2021. This leads to a problem – if you’ve been using them, how do you migrate to use the new certificates.
Dynamic Client Registration (DCR)
The OB has specified endpoints to allow a TPP to register itself with a bank. No form to fill out, just a nice API. They have even established a PUT endpoint to allow you to update values you’ve previously registered.
Now, not all banks have implemented this endpoint, but that’s a story for another time.
How to migrate from OB Legacy to OBSeal / OBWAC
When using private_key_jwt:
You initially registered with the bank with the DCR. All you had to do was specify the token_endpoint_auth_method as private_key_jwt, the token_endpoint_auth_signing_alg as the algorithm you’re using to sign (e.g., PS256), and include your software_statement – the “software statement assertion” – a JWT-encoded description of your TPP from the OB Directory.
The software statement includes a URL, the software_jwks_endpoint, that points to all the valid certificates we can use.
So, to migrate, the only step is to make sure you have the new certificates in your JWKS and then use them. The bank will see the new Key ID , find it in your JWKS, and validate the JWT.
When using tls_client_auth:
You initially registered with the bank using DCR, and you specified as before: token_endpoint_auth_method as “tls_client_auth” and the software_statement
The other parameter you need is tls_client_auth_subject_dn, which tells the bank what certificate you’re going to use.
Let’s see what the spec says about this (https://openbanking.atlassian.net/wiki/spaces/DZ/pages/1078034771/Dynamic+Client+Registration+-+v3.2):
This value must be set if token_endpoint_auth_method is set to tls_client_auth
The tls_client_auth_subject_dn claim MUST contain the DN of the certificate that the TPP will present to the ASPSP token endpoint.
The ASPSP may decide to match only a part of the DN so that the match is based only on the part of the DN that will be immutable for the TPP across all EIDAS certificates issued to it.
The DN of a certificate is the “distinguished name.” They’re unique to a specific certificate so that you can identify them.
Let’s see what the MTLS spec they reference (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-mtls-13#section-2.1.2) says:
An [RFC4514] string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication.
That spec says (https://datatracker.ietf.org/doc/html/rfc4514 it’s a comma-separated list of keys and values, such as:
To get really in the weeds, it specifies some other things:
- You can use whitespace around the commas for presentation and readability
- There is a specific list of keys you can use, but you MAY use other strings
- Keys can also be specified as a numerical string (e.g., 126.96.36.199)
- Values can be ASCII strings, or they could be in a hexadecimal form (e.g., #04024869)
So, how do we find out the DN of the certificate we want to use?
There are multiple ways, but as a Professional Software Engineer who knows enough to be dangerous, I Googled and found this handy OpenSSL command:
OpenSSL x509 -in transport.pem -noout -subject
Which gave me this value:
C = GB, O = OpenBanking, OU = 0015800001ZEZ1sAAH, CN = mTxvBFQd99jOaqgn7TgAr2
Note, added whitespace, and the order goes: C, O, OU, then CN.
Now, for OBWAC and OBSeal (and indeed QWAC and QSEAL), there’s an additional value included: C = GB, O = PYXIS VENTURES LIMITED, organizationIdentifier = PSDGB-OB-Unknown0015800001ZEZ1sAAH, CN = 0015800001ZEZ1sAAH
Right, now we’re ready!
We PUT an update to the DCR endpoint, including our DN value, and existing calls using our old certificate stop working, and we have to use this new certificate.
Not ideal, but at least it works. We’ve migrated from OB Legacy to OBSeal/OBWAC certificates.
Now the problems start…
The trouble is, there are different implementations for this.
Some banks appear to accept most inputs and probably (I assume) just take any certificate that’s in your JWKS, so the value is ignored. Strictly speaking, this is perhaps not recommended and may be against the OB spec.
Some banks expect there to be no whitespace and the keys to be in a specific order.
Some banks do not accept organizationIdentifier as a key and only accept the numeric representation of it: 188.8.131.52
Some banks do not accept the string representation of the organization identifier value and only accept the hexadecimal form:
Section 5.2 of the spec https://datatracker.ietf.org/doc/html/rfc4514#section-5.2 does seem to hint that perhaps this last format is the correct one to use, but this is strictly a SHOULD and not a MUST section, and quite frankly, some of that section is well beyond my ken.
The biggest problem for us was the bank that accepted the string representation of the organization identifier. So, we updated our client successfully, but then the actual endpoint only accepted it if it was in a numeric format.
We were locked out and could not call any API, including the DCR PUT endpoint until they could fix our client registration manually.
In conclusion… it’s not the specification that caused the problem, it was the lack of specification
A good principle for API design is to be permissive with your inputs but very strict with your outputs. As the OB UK has not specified the value of the tls_client_auth_subject_dn beyond pointing at the spec, banks should be expected to handle any valid string passed to them. It would be up to them to then normalize the input to a format they use internally and output that in the response.
The alternative is for the OB UK to explicitly call out the expected format, including the keys, values, and order formats.
Or, why not just use the Key ID instead?
That’s good enough for private_key_jwt authentication and let the bank extract the value in whatever format they require from the ASPSP’s JWKS file. As I showed above, you can pull the DN with an OpenSSL command from the public key!
The only downside to this is that it ignores the MTLS specification. https://datatracker.ietf.org/doc/html/draft-ietf-oauth-mtls-13#section-2.1.2
But as this is still a draft spec, there must be time to raise this issue with the IETF.
Next time on technical talks: OpenAPI spec and JWTs
To see an example of what this means, visit API.expert‘s Production Open Banking Tracker