Digitally signing a payload in API Platform

September 24, 2019 | 4 minute read
Andy Knight
Principal Solution Architect
Text Size 100%:

The use-case

The Service Request RESTful endpoint requires that the POSTed payload be in so-called "Flattened" JWS JSON format and that the contents be digitally signed using the ES256 algorithm.

Method

This is a 2-step process as follows:-

  1. Acquire ECKey
  2. Utilise ECKey to sign the payload

Each of these steps will be executed in discrete Groovy Policies.

Let's start with a couple of caveats

There's a strong argument for saying that an API Gateway is not the place to be signing payloads. I would concur with this stance. However, the technique described here stems from an actual customer requirement so we'll proceed anyway.

Runtime requirements

This post specifically discusses use of the ES256 algorithm. There are many different algorithms for digital signing and the code and dependencies for each vary significantly. Whilst the code for ES256 signing isn't pretty, the good news is that API Platform can achieve the objective without any reliance on dependencies that are not available OOTB. Beyond the JDK there is a dependency on Nimbus JOSE+JWT libraries. But this not a dependency that requires any kind of gateway maintenance because these libraries are delivered with the installer (they're used by the OAuth Policy).

What is an ECKey?

An ECKey is an instance of com.nimbusds.jose.jwk.ECKey whose JSON representation would minimally have this structure:-

{

    "kty": "EC",

    "d": "rkFtMFZYu7WAqvcvmBmkViffxZt-lDrNVKzQgfdMB7E",

    "crv": "P-256",

    "kid": "123",

    "x": "4Re3UiDFQQBfdL8c0L5fi0sNp8qmI98XJg0E8AGDE4o",

    "y": "IfQ3siLFelhDYjRzvqy5oG_AOr-yfgDQzDv7PdXmDQU"

}

The kty and crv together define the Elliptic Curve style. Strictly speaking, the kid key is not mandatory in a minimal instance but it would be unusual not to have this value. kid is an arbitrary identifier (more on this later).

The x and y keys (so-called coordinates) are required and together define the public key.

The d key (another coordinate) defines the private key.

If one were to generate an ECKey using com.nimbusds.jose.jwk.gen.ECKeyGenerator, the resulting object will naturally contain the d (private) coordinate. In order to make the ECKey available as just a public key it suffices merely to remove the d coordinate.

Acquiring an ECKey at runtime

In a previous blog post I demonstrated how one might utilise a Service Callout that returns a usable payload. We're going to use that same technique. Here's the code:-

ExternalServiceCallout callout = context.newCallout()
ExternalServiceCallout.Callback callback = new ExternalServiceCallout.Callback() {
  boolean onCompleted(ServiceResponse response) throws PolicyProcessingException {
      String body = response.getBody().asString()
      context.setAttribute("ECKEY", com.nimbusds.jose.jwk.ECKey.parse(body))
      return true
  }
  boolean onThrowable(Throwable throwable) throws PolicyProcessingException {
    return false
  }
}
callout.setHeader("Accept", "application/json")
callout.setMethod("GET")
callout.setRequestURL("http://localhost:2222/ECKEY")
callout.sendAsync(callback)

What is this doing? We have a web service running locally to the gateway at http://localhost:2222/ECKEY that simply returns a JSON representation of an ECKey (including the private 'd' coordinate). We parse the WS response into an ECKey object and save that as a context attribute.

This is the point at which your security manager will be apoplectic because this isn't really secure. It might be if the gateway is running in a highly secure DMZ. It's also awkward to manage because you will typically have two or more gateways per logical gateway. Therefore, if you're going to access the WS (acquire the ECKey) via localhost, it will need to be deployed identically on all gateways. This leads to other discussions that are outside the scope of this blog so we'll move on...

Signing the payload

Let's just go with the code.

com.nimbusds.jose.jwk.ECKey ecJWK = (com.nimbusds.jose.jwk.ECKey)context.getAttribute("ECKEY")
com.nimbusds.jose.JWSSigner signer = new com.nimbusds.jose.crypto.ECDSASigner(ecJWK)
com.nimbusds.jose.JWSHeader header = new com.nimbusds.jose.JWSHeader.Builder(com.nimbusds.jose.JWSAlgorithm.ES256)
                    .keyID(ecJWK.getKeyID())
                    .contentType("jose+json")
                    .type(com.nimbusds.jose.JOSEObjectType.JOSE_JSON)
                    .build()
String body = context.getApiRequest().getBody().asString()
com.nimbusds.jose.Payload payload = new com.nimbusds.jose.Payload(body)
com.nimbusds.jose.JWSObject jwsObject = new com.nimbusds.jose.JWSObject(header, payload)
jwsObject.sign(signer)

// At this point, the payload has been appropriately signed. Now construct the flattened structure
String[] tokens = jwsObject.serialize().split("\.")
org.json.JSONObject head = new org.json.JSONObject().put("kid", ecJWK.getKeyID())
org.json.JSONObject flat = new org.json.JSONObject()
.put("header", head)
.put("payload", tokens[1])
.put("protected", tokens[0])
.put("signature", tokens[2])
context.getServiceRequest().setBody(new StringBodyImpl(flat.toString(), null))

The serialised JWSObject is in the so-called "compact" form. This format is comprised of 3 Base64URL encoded parts each separated by '.' (period). In sequence these are the protected, payload and signature parts. But what we need is the so-called "flattened" JSON structure to send to our Service Endpoint. Here's an example:-

{

"payload":"SW4gb3VyIHZpbGxhZ2UsIGZvbGtzIHNheSBHb2QgY3J1bWJsZXMgdXAgdGhlIG9sZCBtb29uIGludG8gc3RhcnMu",

"protected": "eyJhbGciOiJFUzI1NiJ9",

"header": {

"kid": "myEcKey"

},

"signature": "b7V2UpDPytr-kMnM_YjiQ3E0J2ucOI9LYA7mt57vccrK1rb84j9areqgQcJwOA00aWGoz4hf6sMTBfobdcJEGg"

}

This "flattened" structure fulfils the use-case requirements and is what will be sent as the payload to our Service Endpoint.

More on the kid key

This technology fits the traditional public/private key paradigm. In this example, the API Platform needs to have secure access to an ECKey that contains the 'd' coordinate - i.e. the private part. The server that we send the message to needs to know the public key in order to be able to decode the payload. However, that server may have a repository of several public keys because it's handling signed messages from various clients. So this is where the kid comes into play. It is used by the client to say "You will need to use the nominated public key in order to decipher this message". As you can see, the actual key identifier is sent in plain text because it is not the public key itself but merely a reference to it

 


 

 

Andy Knight

Principal Solution Architect


Previous Post

Installation and Configuring Recovery Manager Catalog on OCI DBaaS

Vivek Singh | 10 min read

Next Post


Making OCI Metrics Available in Oracle Management Cloud

Pulkit Sharma | 9 min read