International-Data-Spaces-Association / omejdn-daps

Open Source implementation of the Dynamic Attribute Provisioning Service based on http://github.com/Fraunhofer-AISEC/omejdn-server
Apache License 2.0
5 stars 10 forks source link

Unable to request a DAT with dockered version on windows #28

Closed spetrac closed 1 year ago

spetrac commented 1 year ago

The Problem

My Setup

Microsoft Windows 10 Enterprise 10.0.19042, Build 19042 Docker version 20.10.17, build 100c701

Because I use docker the rest is irrelevant as it is not used anyway.

What I have done up front

The goal was to launch the daps with docker. Because port 80 is blocked on my machine, I configured different nginx ports in the compose.yml:

...
  nginx:
    image: nginx:latest
    restart: unless-stopped
    ports:
      - 4567:80
      - 8082:443
...

A omedjn.cert is setup correctly. To add a client, I used the _registerconnector.sh script. Because it obviously does not work on windows, I used WSL with Ubuntu-20.04 (also I had to replace all \r\n with \n in the script). I used a custom certificate and it worked properly: A cert file with the NAME was added under the keys folder and a cert file with the _CLIENTID was added in the clients folder.

The entry in the clients.yml was also added:

- client_id: D3:64:3F:3B:D0:3A:0B:01:FE:8E:5D:C5:F3:97:B3:E2:8D:40:3D:25:keyid:B2:86:93:B9:34:0F:6F:CA:D4:1A:C0:3E:C6:BF:E1:A0:A0:D0:ED:5E
  client_name: RC Alice
  grant_types: client_credentials
  token_endpoint_auth_method: private_key_jwt
  scope: idsc:IDS_CONNECTOR_ATTRIBUTES_ALL
  attributes:
  - key: idsc
    value: IDS_CONNECTOR_ATTRIBUTES_ALL
  - key: securityProfile
    value: idsc:BASE_SECURITY_PROFILE
  - key: referringConnector
    value: http://RC Alice.demo
  - key: "@type"
    value: ids:DatPayload
  - key: "@context"
    value: https://w3id.org/idsa/contexts/context.jsonld
  - key: transportCertsSha256
    value: 5aab5b7cb396dc49835f1b6f7c842c4e8f37976df3628d87a4d268f71a9d0f44

What I expected to happen

I had two tests:

  1. Get the JWKS with GET http://localhost:4567/auth/jwks.json
  2. Get a DAT with POST http://localhost:4567/auth/token

I expected the first to give my the regular JWKS object and I expected the second to give my a DAT. The DAT-request must obviously have the correct format and signed with the clients private key. I did all of that and tried different variations from the IDSA daps documentation and formats that worked on previous implementations. Because that is a little bit too much, here is a request I tried that I aligned to the test.sh script:

DAT request payload:

{
  "iss": "D3:64:3F:3B:D0:3A:0B:01:FE:8E:5D:C5:F3:97:B3:E2:8D:40:3D:25:keyid:B2:86:93:B9:34:0F:6F:CA:D4:1A:C0:3E:C6:BF:E1:A0:A0:D0:ED:5E",
  "sub": "D3:64:3F:3B:D0:3A:0B:01:FE:8E:5D:C5:F3:97:B3:E2:8D:40:3D:25:keyid:B2:86:93:B9:34:0F:6F:CA:D4:1A:C0:3E:C6:BF:E1:A0:A0:D0:ED:5E",
  "aud": "idsc:IDS_CONNECTORS_ALL",
  "iat": 1663669179, // current posix-time
  "nbf": 1663669179, // current posix-time
  "exp": 1663672779 // current posix-time + 3600 seconds
}

DAT request:

{
  "url": "http://localhost:4567/auth/token",
  "method": "POST",
  "body": "grant_type=client_credentials" +
    "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" +
    "&client_assertion=eyJhbGciOiJSUzI1NiJ9....OjqeT6Ka323z2I" +
    "&scope=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"
}

What actually happened

The first test was no problem, I got the jwks with the 1 key I added. The second test did not work with anything I tried. Instead I got a [400] Bad Request. The omejdn-server console was not very helpful, it only stated: POST /token HTTP/1.1" 400 63 0.0412. The response to the failed request was:

{
  "error": "invalid_client",
  "error_description": "Error decoding JWT: No verification key available"
}

The quest of trying to solve it

What were the results of searching for the error on the internet?

As you can see, the filename for the certificate is really wierd, because it replaced the forbidden windows symbol : with a placeholder () In previous versions of the omejdn-daps a base64 encoded _CLIENTID was used as the filename, so later I also tried that with the filename RDM6N...6NUU=.cert and an entry in the clients.yml for the test client:

...
  certfile: RDM6NjQ6M0Y6M0I6RDA6M0E6MEI6MDE6RkU6OEU6NUQ6QzU6RjM6OTc6QjM6RTI6OEQ6NDA6M0Q6MjU6a2V5aWQ6QjI6ODY6OTM6Qjk6MzQ6MEY6NkY6Q0E6RDQ6MUE6QzA6M0U6QzY6QkY6RTE6QTA6QTA6RDA6RUQ6NUU=.cert

This did not work either.

As I said, I tried different variations that did not work either and I may have suggestions that could improve the API overall. For example proper headers for the request with Content-Type: application/x-www-form-urlencoded;charset=UTF-8, a properly formatted query that is url-encoded (...client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer... instead of ...client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer...), a support for JSON as Content-Type of the request and inclusion of json-ld attributes in the DAT-Request like @context and @type.

I also tried to use the test.sh script after I modified it to use a different port, but I got the same results.

What is your best guess as to what might have happened?

The source of the error can be found in the omejdn-server repo: https://github.com/Fraunhofer-AISEC/omejdn-server/blob/c767a7a11e2776f1cbdb53f21ae74edd739ed138/lib/client.rb#L100-L112

The client must have been found at that point in time, because otherwise the error would have been Client unknown.

I have no further idea, why the key cannot be found. Maybe it is due to filenames and maybe the previous solution with the certfile in the clients.yml could make it work again.

bellebaum commented 1 year ago

Hey :)

You identified correctly that the filename missmatch is the cause for Omejdn not being able to find the certificate and key. If you want to dig deeper, here is a pointer:

https://github.com/Fraunhofer-AISEC/omejdn-server/blob/c767a7a11e2776f1cbdb53f21ae74edd739ed138/lib/client.rb#L171

Originally, the client's certificate was in a base64url encoded location, which was very unintuitive and did cause problems in Kubernetes setups. Hence I changed it to the more intuitive format, which is however causing problems in the IDS, where client ID's contain this windows-incompatible character. It was a trade-off I made based on the realization that no intuitive solution could ever be compatible with all systems (given that client IDs may contain arbitrary printable ASCII characters).

So here is how I envisioned a way out for systems where this problem remains:

Omejdn Plugin System

Note that to load the key, Omejdn calls this function: https://github.com/Fraunhofer-AISEC/omejdn-server/blob/c767a7a11e2776f1cbdb53f21ae74edd739ed138/lib/keys.rb#L17

This is calling a plugin listening for KEY events, and ultimately the default one registered in lib/keys.rb, which implements the actual loading and saving according to the interface described in the documentation.

If your setup is incompatible with the default configuration, you can just disable the default plugin and load your own. You would have to reimplement the three KEYS_* event listeners (to store, load, and enumerate cryptographic key material), but this would allow you to store the certificates and keys anywhere you like (not just files but also e.g. a database).

Speaking of which, there is also a plugin you can pick up to save everything in a PostgreSQL database. See the documentation. It does not provide a dedicated graphical frontend to insert values, so you will have to do this either manually or via Omejdn's UI or API.

spetrac commented 1 year ago

From this implementation I can understand, why a certfile options does not work. The event only gets the _CLIENTID! But I don't understand the argument against a base64 encoding of the filename. It really makes sense to encode the filename from a technical standpoint and any confusion with the filenames from a human readable perspective is irrelevant to me for an implementation.

It was quite some work, but I finally solved my problem by implementing my own _filesystembackend that uses base64 encoding for filenames.

I also had to change the compose.yml to integrate plugins at all:

  omejdn-server:
    image: ghcr.io/fraunhofer-aisec/omejdn-server:${OMEJDN_VERSION}
    restart: unless-stopped
    environment:
      - OMEJDN_ISSUER=${OMEJDN_ISSUER}
      - OMEJDN_FRONT_URL=${OMEJDN_ISSUER}
      - OMEJDN_OPENID=true
      - OMEJDN_ENVIRONMENT=${OMEJDN_ENVIRONMENT}
      - OMEJDN_ACCEPT_AUDIENCE=idsc:IDS_CONNECTORS_ALL
      - OMEJDN_DEFAULT_AUDIENCE=idsc:IDS_CONNECTORS_ALL
      - OMEJDN_ADMIN=${ADMIN_USERNAME}:${ADMIN_PASSWORD}
      - OMEJDN_PLUGINS=/opt/${OMEJDN_PLUGINS}
    volumes:
      - ./config:/opt/config
      - ./keys:/opt/keys
      - ./plugins:/opt/plugins

And addition to the .env file:

OMEJDN_VERSION="1.7.0"
OMEJDN_PLUGINS="config/plugins.yml"

The plugins.yml:

plugins:
  filesystem_backend:
    handlers:
      - keys
deactivate_defaults:
  - keys

Then I basically took the _postgresbackend class, stripped what I don't needed, replaced the _storagekeys.rb with the DefaultKeysDB and added the Base64.urlsafe_encode64.

Now the omejdn.key did not get loaded currectly, but anyway just renamed it to b21lamRu.key and everything works.

bellebaum commented 1 year ago

Well done :)

I think with that we can close this particular issue.

A bit more about the filenames: I agree that from a technical standpoint and especially for production deployments, human intuition does not play an important role. But I was seeing two very different groups of users (according to the issues raised here).

One group tried to use Omejdn in production settings. They were unhappy because information was not stored in a persistent database at the time, which is understandable, especially if you consider that many cloud deployments require a read-only filesystem and files with a limited character set. The other, seemingly larger group was trying to experiment with Omejdn within and outside the IDS, and was having trouble setting everything up. Explaining how base64url works regularly took too much time, and the import_certfile option did cause confusion for being a one-time import instruction. (There was also a certfile option to specify the filename explicitly, but that one was causing trouble and security problems with the Admin API.)

Combined with the need to connect other types of existing databases, the need for a plugin system was unavoidable. The only question was how the default should have been handled. I figured that the intuitive file-based option was uniquely fit, as this avoids having to explain too much stuff to average users while giving a distinct advantage over other hard-to-configure-in-files IDPs like Keycloak.

If you would like to further discuss this decision, you are welcome to do so in this issue: https://github.com/Fraunhofer-AISEC/omejdn-server/issues/37 Or you can contact me directly :)

spetrac commented 1 year ago

I know, the omejdn-server should be general purpose and not rely on client ids to have the SKI:AKI format. Otherwise a compromise between readability and usability could be to use dashes instead of colons for the filename:

filename = "#{KEYS_DIR}/#{target_type}/#{target.gsub ':', '-'}"