Sapient Bundle

Sapient bundle help you to secure data exchange. It encrypt and sign response of an API. A client can then verify signature and decrypt content.

This bundle include Guzzle middleware to decrypt and verify signature when calling a Sapient API

Content

Installation

Before starting installation, your application must have PHP 7.2 minimum. This bundle use Sapient library which require libsodium extension. Symfony 3.4 and Synfony 4 are supported.

Symfony 4 with recipe

A recipe has been created for this bundle.

Enable contrib repository in composer.

composer config extra.symfony.allow-contrib true
composer install lepiaf/sapient-bundle

By default, recipe will enable and generate a minimal config file. You have to run a command to initialize configuration.

bin/console sapient:configure

It will output configuration. Copy and paste it to config/packages/sapient.yml

sapient:
    sign:
        public: 'signing-key-public'
        private: 'signing-key-private'
        name: 'signer-name'
    seal:
        public: 'seal-key-public'
        private: 'seal-key-private'
    sealing_public_keys: ~
    verifying_public_keys: ~

Now your api is ready. Repeat this process with a client.

Symfony 4 without recipe

As usual, install it via composer

composer install lepiaf/sapient-bundle

Enable it in config/bundles.yml

<?php

return [
    lepiaf\SapientBundle\SapientBundle::class => ['all' => true],
];

Now bundle is registered. You can run command to generate default configuration.

bin/console sapient:configure

It will output configuration. Copy and paste it to config/packages/sapient.yml

sapient:
    sign:
        public: 'signing-key-public'
        private: 'signing-key-private'
        name: 'signer-name'
    seal:
        public: 'seal-key-public'
        private: 'seal-key-private'
    sealing_public_keys: ~
    verifying_public_keys: ~

Now your api is ready. Repeat this process with a client.

Symfony 3.4 without recipe

PHP 7.2 is the only requirement, it can work with symfony 3.4 and below.

Install it via composer

composer install lepiaf/sapient-bundle

Enable bundle in app/AppKernel.php

<?php

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new lepiaf\SapientBundle\SapientBundle()
        );

        return $bundles;
    }
}

Now bundle is registered. You can run command to generate default configuration.

bin/console sapient:configure

It will output configuration. Copy and paste it to app/config/config.yml

sapient:
    sign:
        public: 'signing-key-public'
        private: 'signing-key-private'
        name: 'signer-name'
    seal:
        public: 'seal-key-public'
        private: 'seal-key-private'
    sealing_public_keys: ~
    verifying_public_keys: ~

Now your api is ready. Repeat this process with a client.

Configuration

We will see 3 differents uses case:

  • Sign only response of your API. Each client are free to check signature.
  • Sign and seal response. Only the client who did request can unseal content of response and can verify the signature.
  • Sign and seal request. Only API can unseal content of request and can verify the signature.

Before diving deep, let assume we have an API Alice and a client Bob. API Alice and client Bob have a key pair to sign and seal (see Installation part). Client Bob will do request to API Alice in order to get some information.

Note

To use this bundle, you need to have basic understand of cryptography and asymmetric encryption.

Sign response only

Go to configuration file and open it. After installation, you only have a key pair for sign and seal.

sapient:
    sign:
        public: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
        private: 'giP81DlS_R3JL4-UnSVbn2I5lm9abv8vA7aLuEdOUB4bfOjlm5vaj57Kn6DcZhv0lcTN20iqYV0M69Tk9XqEGQ=='
        host: 'api-alice'
        response: true
    seal:
        public: 'tquhje8C_hNdd85R-CzVq7n7MOLqc5h11GJv7Vo7fgc='
        private: 'NoxnlCvhxl8NRfCgIhuxm95IE1Y9QFUHMuvDkrWrnQ4='
        response: true
    sealing_public_keys: ~
    verifying_public_keys: ~

Do a request and you will see your response in clear text. Then check headers of response.

HTTP/1.1 200 OK
Host: localhost:8000
Connection: close
X-Powered-By: PHP/7.2.4-1+ubuntu16.04.1+deb.sury.org+1
Cache-Control: no-cache, private
Date: Sat, 12 May 2018 19:00:19 +0200, Sat, 12 May 2018 17:00:19 GMT
content-type: application/json
Body-Signature-Ed25519: 6sHYDSKwx05QNDe-s2a1tBXxKw2JZxLZwUBpLojEQpqzcGEU1XcaqdaG9_FQTbVkeSa_25vSak8MJcZ8RaoaAg==
Sapient-Signer: api-alice

Two new headers appear:

  • Body-Signature-Ed25519 is signature of response. It is used by Sapient library to verify response with public key.
  • Sapient-Signer: name of who sign this response. It is usefull when client call more than one API.

For now we have API Alice who sign all their response. It is good but not usefull for now. Let’s configure client Bob to verify signature.

API Alice must give her sign public key to client Bob. As shown in configuration above, it is G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk=. Do not give private key, hence the name, it is private.

Open client Bob configuration file and add API Alice public key.

sapient:
    sealing_public_keys: ~
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

I’ve added API Alice sign public key in verifying_public_keys configuration. It must have the key and name of signer. Here it is api-alice.

Client Bob use Guzzle to request API Alice. Sapient bundle comes with Guzzle middleware to make verification easier. You need to enable it.

guzzle_middleware:
    verify: true

Here is the final configuration of client Bob.

sapient:
    guzzle_middleware:
        verify: true
    sealing_public_keys: ~
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

Now, every time you will request API Alice, it will verify every signature. If signature cannot be verifyed, an exception will raise. It can be a misconfiguration or an man-in-the-middle.

Sign and seal response

This is the most usefull usecase. It sign and seal the response. Only the requester can unseal the content of the response. It use XChaCha20-Poly1305 algorithm to seal and ED25519 for signature.

Follow part Sign response only first. In this part, we will configure API Alice to seal response for client Bob.

In client Bob configuration file, generate a seal key pair. You can do it easily with bin/console sapient:configure. Copy and paste sign and seal part.

sapient:
    sign:
        public: 'aO8pIZYoGUrPOSJFC1UfH-XE7M19xC-LP-tZwukwFqI='
        private: 'nnr3sTDvLfDHtw6suup3LlNh2YYCCCcXvksDpIp5VHVo7ykhligZSs85IkULVR8f5cTszX3EL4s_61nC6TAWog=='
        host: 'client-bob'
        response: true
    seal:
        public: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
        private: 'FzyiZAbEuquHUXt-YNF6WOXFB6CVBpyz2ocMMaT0FK8='
        response: true
    guzzle_middleware:
        verify: true
    sealing_public_keys: ~
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

As mentioned in introduction of this part, API Alice will seal response. Client Bob use guzzle and Sapient bundle has a middlware to unseal response. Enable it.

sapient:
    sign:
        public: 'aO8pIZYoGUrPOSJFC1UfH-XE7M19xC-LP-tZwukwFqI='
        private: 'nnr3sTDvLfDHtw6suup3LlNh2YYCCCcXvksDpIp5VHVo7ykhligZSs85IkULVR8f5cTszX3EL4s_61nC6TAWog=='
        host: 'client-bob'
        response: true
    seal:
        public: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
        private: 'FzyiZAbEuquHUXt-YNF6WOXFB6CVBpyz2ocMMaT0FK8='
        response: true
    guzzle_middleware:
        verify: true
        unseal: true
    sealing_public_keys: ~
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

Then, you need to enable option guzzle_middleware.requester_host to add header Sapient-Requester. This header is used by API Alice to return a signed and sealed response.

sapient:
    sign:
        public: 'aO8pIZYoGUrPOSJFC1UfH-XE7M19xC-LP-tZwukwFqI='
        private: 'nnr3sTDvLfDHtw6suup3LlNh2YYCCCcXvksDpIp5VHVo7ykhligZSs85IkULVR8f5cTszX3EL4s_61nC6TAWog=='
        host: 'client-bob'
        response: true
    seal:
        public: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
        private: 'FzyiZAbEuquHUXt-YNF6WOXFB6CVBpyz2ocMMaT0FK8='
        response: true
    guzzle_middleware:
        verify: true
        unseal: true
        requester_host: 'client-bob'
    sealing_public_keys: ~
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

Now we are done in client Bob configuration. Before updating configuration of API Alice, copy seal public key of client Bob.

In API Alice, add seal public key of client Bob in sealing_public_keys configuration.

sapient:
    sign:
        public: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
        private: 'giP81DlS_R3JL4-UnSVbn2I5lm9abv8vA7aLuEdOUB4bfOjlm5vaj57Kn6DcZhv0lcTN20iqYV0M69Tk9XqEGQ=='
        host: 'api-alice'
        response: true
    seal:
        public: 'tquhje8C_hNdd85R-CzVq7n7MOLqc5h11GJv7Vo7fgc='
        private: 'NoxnlCvhxl8NRfCgIhuxm95IE1Y9QFUHMuvDkrWrnQ4='
        response: true
    sealing_public_keys:
        -
            host: 'client-bob'
            key: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
    verifying_public_keys: ~

Configuration is done for API Alice.

Every time client Bob will request API Alice, API Alice will seal and sign response. Then, client Bob receive response and pass to Guzzle middleware. It unseal and verify signature. If everything is ok, your controller/service will use data as usual. Else it will raise an exception.

To get more information, check library documentation. Sapient is available in container and you can use more functionality.

Sign and seal request

To complete our usecase above, we can sign and seal request to api. Then, we have a full confidentiality on request made to api.

Before continuing, you must follow step Sign and seal response part.

Note: for now, it is not possible to sign/seal request without signing and sealing response. It could be possible in future version.

Client Bob want to seal and sign all request to API Alice. Only API Alice can read request from Client Bob.

As we use Guzzle, you can enable an option to automatically sign and seal all request.

sapient:
    sign:
        public: 'aO8pIZYoGUrPOSJFC1UfH-XE7M19xC-LP-tZwukwFqI='
        private: 'nnr3sTDvLfDHtw6suup3LlNh2YYCCCcXvksDpIp5VHVo7ykhligZSs85IkULVR8f5cTszX3EL4s_61nC6TAWog=='
        host: 'client-bob'
        response: true
    seal:
        public: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
        private: 'FzyiZAbEuquHUXt-YNF6WOXFB6CVBpyz2ocMMaT0FK8='
        response: true
    guzzle_middleware:
        verify: true
        unseal: true
        sign_request: true
        seal_request: true
        requester_host: 'client-bob'
    sealing_public_keys: ~
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

Now we have request signed and sealed. But API Alice will not understand it. We need to enable options in API Alice configuration and exchange keys.

There are 2 options: verify_request and unseal_request. Enable it.

sapient:
    sign:
        public: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
        private: 'giP81DlS_R3JL4-UnSVbn2I5lm9abv8vA7aLuEdOUB4bfOjlm5vaj57Kn6DcZhv0lcTN20iqYV0M69Tk9XqEGQ=='
        host: 'api-alice'
        response: true
    seal:
        public: 'tquhje8C_hNdd85R-CzVq7n7MOLqc5h11GJv7Vo7fgc='
        private: 'NoxnlCvhxl8NRfCgIhuxm95IE1Y9QFUHMuvDkrWrnQ4='
        response: true
    sealing_public_keys:
        -
            host: 'client-bob'
            key: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
    verifying_public_keys: ~
    verify_request: true
    unseal_request: true

Then, we have to exchange public key. API Alice must send his seal public key to Client Bob. And Client Bob must send his sign public key to API Alice.

In Client Bob configuration, we must have:

sapient:
    sign:
        public: 'aO8pIZYoGUrPOSJFC1UfH-XE7M19xC-LP-tZwukwFqI='
        private: 'nnr3sTDvLfDHtw6suup3LlNh2YYCCCcXvksDpIp5VHVo7ykhligZSs85IkULVR8f5cTszX3EL4s_61nC6TAWog=='
        host: 'client-bob'
        response: true
    seal:
        public: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
        private: 'FzyiZAbEuquHUXt-YNF6WOXFB6CVBpyz2ocMMaT0FK8='
        response: true
    guzzle_middleware:
        verify: true
        unseal: true
        sign_request: true
        seal_request: true
        requester_host: 'client-bob'
    sealing_public_keys:
        -
            key: 'tquhje8C_hNdd85R-CzVq7n7MOLqc5h11GJv7Vo7fgc='
            host: 'api-alice'
    verifying_public_keys:
        -
            key: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
            host: 'api-alice'

In API Alice configuration, we must have:

sapient:
    sign:
        public: 'G3zo5Zub2o-eyp-g3GYb9JXEzdtIqmFdDOvU5PV6hBk='
        private: 'giP81DlS_R3JL4-UnSVbn2I5lm9abv8vA7aLuEdOUB4bfOjlm5vaj57Kn6DcZhv0lcTN20iqYV0M69Tk9XqEGQ=='
        host: 'api-alice'
        response: true
    seal:
        public: 'tquhje8C_hNdd85R-CzVq7n7MOLqc5h11GJv7Vo7fgc='
        private: 'NoxnlCvhxl8NRfCgIhuxm95IE1Y9QFUHMuvDkrWrnQ4='
        response: true
    sealing_public_keys:
        -
            host: 'client-bob'
            key: 'M2SMMPHg9NOXoX3NgzlWY8iTheyu8qSovnTZpAlIGB0='
    verifying_public_keys:
        -
            host: 'client-bob'
            key: 'aO8pIZYoGUrPOSJFC1UfH-XE7M19xC-LP-tZwukwFqI='
    verify_request: true
    unseal_request: true

Now you are fully configured !

Reference

sapient:
    sign:
        enabled: boolean
        public: string
        private: string
        host: string
        response: boolean
    seal:
        enabled: boolean
        public: string
        private: string
        response: boolean
    guzzle_middleware:
        unseal: boolean
        verify: boolean
        sign_request: boolean
        seal_request: boolean
        requester_host: string
    sealing_public_keys:
        -
            host: string
            key: string
    verifying_public_keys:
        -
            host: string
            key: string
    verify_request: boolean
    unseal_request: boolean

Above, you have full configuration reference.

sign

Enable signing response. If sign key is present, it must contain public, private and name property. If not present, feature is disabled.

sign.public

Required if sign key is present.

It is signing public key string. It is generated by \ParagonIE\Sapient\CryptographyKeys\SigningSecretKey::generate() function.

sign.private

Required if sign key is present.

It is signing private key string. It is generated by \ParagonIE\Sapient\CryptographyKeys\SigningSecretKey::generate() function. This key must never be revealed. If it is leaked, you must regenerate a new key pair.

sign.host

Required if sign key is present.

Host of who sign response. It is required if client want to verifying signature in response.

sign.response

Enable or disable subscriber that sign response.

seal

Enable sealing response. Sealing mean that response content is encrypted. Only receiver with sealing private key can decrypt and reveal response content in clear. If seal key is present, it must contain public, private and name property. If not present, feature is disabled.

seal.public

Required if seal key is present.

It is sealing public key string. It is generated by \ParagonIE\Sapient\CryptographyKeys\SealingSecretKey::generate() function.

seal.private

Required if seal key is present.

It is sealing private key string. It is generated by \ParagonIE\Sapient\CryptographyKeys\SealingSecretKey::generate() function. This key must never be revealed. If it is leaked, you must regenerate a new key pair.

seal.response

Enable or disable subscriber that seal response. To use this feature, you must enable sign feature. Without sign feature you will not able to use it. Sealing a response without signing is not secure. It mean your recipient will unseal the response but he will not be sure it was sent by the right sender.

guzzle_middleware

This bundle contain Guzzle middleware to decrypt and verify response.

guzzle_middleware.unseal

If enable, it will activate Guzzle middleware that decrypt response. By default it is disabled.

You must enable “seal” option and configure a “seal.private” key before using “guzzle_middleware.unseal” feature.

guzzle_middleware.verify

If enable, it will activate Guzzle middleware that verify signature in response. By default it is disabled.

Before enabling this option, you must configure verifying_public_keys array.

guzzle_middleware.requester_host

This Guzzle middleware will add a header Sapient-Requester automatically on each request. This header is used by recipient to choose the right key to encrypt response.

It is optional but highly recommended. If not enable, you must add header manually in Guzzle client configuration.

guzzle_middleware.sign_request

If enable, it will activate Guzzle middleware that sign all request. By default it is disabled.

You must enable “sign” option and configure a “sign.private” key before using “guzzle_middleware.sign_request” feature.

guzzle_middleware.seal_request

If enable, it will activate Guzzle middleware that seal all request. By default it is disabled.

It use hostname configured in Guzzle client in order to choose public key to seal request.

You must enable: - “seal” option and configure a “seal.private” key before using “guzzle_middleware.seal_request” feature. - “guzzle_middleware.sign_request” option before using “guzzle_middleware.seal_request” feature.

sealing_public_keys

List of all sealing public keys used to encrypt response. Your client must give you the value in sapient.seal.public. Each item must contain a key and a name. name must match header value Sapient-Signer.

sapient:
    sealing_public_keys:
        -
            name: "client-bob"
            key: "sealing public key of client-bob"

verifying_public_keys

List of all verifying public keys used to verify response. Your api must give you the value in sapient.sign.public. Each item must contain a key and a name. name must match header value Sapient-Requester.

sapient:
    verifying_public_keys:
        -
            name: "api-alice"
            key: "verifying public key of api-alice"

verify_request

Each request received by HttpKernel will enter in subscriber that verify signature. It check Sapient-Requester header and fetch the public key in verifying public keys array. If found, then it verify signature. If signature is invalid, an InvalidMessageException is raised.

unseal_request

Each request received by HttpKernel will enter in subscriber that unseal signature. It use it own private key to unseal request. If unseal process fail, an InvalidMessageException is raised.

You must enable “seal” option before using “unseal_request” feature.