<!-- markdownlint-disable MD013 -->

<!-- wiki-title: External Account Binding -->

# External Account Binding

External Account Binding (EAB) allows an ACME account to use authorizations granted to an external, non-ACME account. This enables `acme2certifier` to handle issuance scenarios that cannot yet be fully automated, such as issuing Extended Validation (EV) certificates.

To enable EAB, the Certificate Authority (CA) operator must provide both the ACME client and `acme2certifier` with a Key Identifier (`kid`) and a MAC key (`mac_key`). These credentials authenticate `NewAccount` requests.

`kid` and `mac_key` are loaded into `acme2certifier` via a plugin-based mechanism. By default, two plugins are available in the `example/eab_handler` directory.

Key identifiers are included in reports generated by the [Housekeeping](housekeeping.md) class.

By default `acme2certifier` validates, during each ACME transaction, whether the EAB credentials used to create the ACME account remain valid. If this check fails, `acme2certifier` stops processing the transaction. This check can be disabled by the configuration option `eabkid_check_disable` in `acme_srv.cfg`.

```ini
[EABhandler]
...
eabkid_check_disable: True
```

## File Handler

The `eab_file_handler.py` script allows `kid` and `mac_key` to be loaded from a CSV file. To activate this handler, configure the `EABhandler` section in `acme_srv.cfg` as follows:

```ini
[EABhandler]
eab_handler_file: examples/eab_handler/file_handler.py
key_file: examples/eab_handler/key_file.csv
```

The `key_file` must be in CSV format, with `kid` in the first column and `mac_key` (Base64 encoded) in the second column:

```csv
eab_kid,eab_mac
keyid_00,bWFjXz...Aw
keyid_01,bWFjXz...Ax
keyid_02,bWFjXz...Ay
keyid_03,bWFjXz...Az
```

## JSON Handler

The `eab_json_handler.py` script allows `kid` and `mac_key` (Base64 encoded) to be loaded from a JSON file. To activate this handler, configure the `EABhandler` section in `acme_srv.cfg` as follows:

```ini
[EABhandler]
eab_handler_file: examples/eab_handler/json_handler.py
key_file: examples/eab_handler/key_file.json
```

The `key_file` should contain key-value pairs in JSON format:

```json
{
  "keyid_00": "bWFjXz...Aw",
  "keyid_01": "bWFjXz...Ax",
  "keyid_02": "bWFjXz...Ay",
  "keyid_03": "bWFjXz...Az"
}
```

## SQL Handler

The `sql_handler.py` script allows `kid` and `mac_key` (Base64 encoded) to be loaded from a database. SQL Handler supports PostgreSQL and SQL Server database systems.

Using a database for storing EAB credentials is useful in use cases where many `acme2certifier` instances use the same credentials, for example in data centers using load balancing. Instead of maintaining multiple JSON files, credentials can be maintained using a single database instance.

Using a database is beneficial also when rotating and managing `kid/mac_key` pairs in more complex scenarios, with the addition of an `account` table that can maintain information about the account related to each key.

It is recommended to use the same [external database](external_database_support.md) for both `acme2certifier` and EAB.

### Database Schema

The database schema includes two tables, one for actual credentials used by `acme2certifier` and another for managing accounts and credentials. Any number of credentials can be related to a single account. Only the credentials table is actually used by `acme2certifier`.

Schemas for the database systems differ in relation to fields that are used to maintain JSON data. SQL Server has `NVARCHAR` type for that, whereas PostgreSQL has `JSONB`. Schemas also have a status column to indicate if the credentials are active or not (value is 0 or 1).

#### When Using PostgreSQL

Create a database and then these two tables. See [Usage](#Usage) for entering some data in the tables. Then, [configure acme_srv.cfg](#Activate) with the database credentials that you have.

```sql
CREATE TABLE account (
    id SERIAL PRIMARY KEY,
    name VARCHAR(127) NOT NULL,
    contact VARCHAR(127)
);

CREATE TABLE credentials (
    id SERIAL PRIMARY KEY,
    account_id INT NOT NULL REFERENCES account (id),
    key_id VARCHAR(63) NOT NULL,
    profile JSONB,
    description VARCHAR(255),
    status SMALLINT NOT NULL
);
```

#### When Using SQL Server

Create a database and then these two tables. See [Usage](#Usage) for entering some data in the tables. Then, [configure acme_srv.cfg](#Activate) with the database credentials that you have.

```sql
CREATE TABLE account (
    id INT IDENTITY(1,1) PRIMARY KEY,
    name NVARCHAR(127) NOT NULL,
    contact NVARCHAR(127)
);

CREATE TABLE credentials (
    id INT IDENTITY(1,1) PRIMARY KEY,
    account_id INT NOT NULL REFERENCES account (id),
    key_id NVARCHAR(63) NOT NULL,
    profile NVARCHAR(MAX),
    description NVARCHAR(255),
    status TINYINT NOT NULL
);
```

### Usage

In the simplest scenario, the database will have one account that all the keys are related to.

```sql
INSERT INTO account (name, contact)
  VALUES ('myaccount', 'contact@myaccount.com');
```

The `profile` column in `credentials` table should contain JSON data in the same format as it is used in JSON Handler.

Example:

```sql
INSERT INTO credentials (account_id, key_id, description, profile, status)
  VALUES (
    (SELECT id FROM account WHERE account.name = 'myaccount'),
    'keyid_03',
    'mykey',
    '{
        "hmac": "YW5kX2ZpbmFsbHlfdGhlX2xhc3RfaG1hY19rZXlfd2hpY2hfaXNfbG9uZ2VyX3RoYW5fMjU2X2JpdHNfYW5kX3Nob3VsZF93b3Jr",
        "authorization": {
            "prevalidated_domainlist": ["www.example.com"]
        }
    }',
    1
  );
```

### Activate Handler

To activate this handler, configure the `EABhandler` section in `acme_srv.cfg` as follows. For `db_system`, enter either `mssql` or `postgres`.

```ini
[EABhandler]
eab_profiling: True
eab_handler_file: examples/eab_handler/sql_handler.py
db_system: mssql, postgres
db_host:
db_name:
db_user:
db_password:
```

## Keyfile Verification

To check the consistency of the keyfile, use the `tools/eab_chk.py` utility:

```bash
usage: eab_chk.py [-h] -c CONFIGFILE [-d] [-v] [-vv] [-k KEYID | -s]

eab_chk.py - verify eab keyfile

options:
  -h, --help            show this help message and exit
  -c CONFIGFILE, --configfile CONFIGFILE
                        configfile
  -d, --debug           debug mode
  -v, --verbose         verbose
  -vv, --veryverbose    show enrollment profile
  -k KEYID, --keyid KEYID
                        keyid to filter
  -s, --summary         summary
```

Example usage:

```bash
python /var/www/acme2certifier/tools/eab_chk.py -c /var/www/acme2certifier/acme_srv/acme_srv.cfg -v
```

Example output:

```bash
Summary: 4 entries in key_file
keyid_00: bWFjXz...Aw
keyid_01: bWFjXz...Ax
keyid_02: bWFjXz...Ay
keyid_03: bWFjXz...Az
```

## Creating a Custom EAB Handler

Creating a custom EAB handler is straightforward. You need to create a `handler.py` file containing an `EABhandler` class with a `mac_key_get` method to look up the `mac_key` based on a given `kid`.

The `allowed_domains_check` method is optional and can be used to customize the [`allowed_domainlist_check()` function](https://github.com/grindsa/acme2certifier/blob/master/acme_srv/helper.py#L1641).

The [skeleton_eab_handler.py](../examples/eab_handler/skeleton_eab_handler.py) provides a template for creating a custom handler.

Below is an example of the class structure:

```python
class EABhandler(object):
    """EAB file handler"""

    def __init__(self, logger=None):
        self.logger = logger
        self.key = None

    def __enter__(self):
        """Makes EABhandler a Context Manager"""
        if not self.key_file:
            self._config_load()
        return self

    def __exit__(self, *args):
        """Close the connection at the end of the context"""

    def _config_load(self):
        """Load additional configuration parameters from acme_srv.cfg"""
        self.logger.debug("EABhandler._config_load()")
        config_dic = load_config(self.logger, "EABhandler")
        if "key" in config_dic["EABhandler"]:
            self.key = config_dic["EABhandler"]["key"]
        self.logger.debug("EABhandler._config_load() ended")

    def allowed_domains_check(self, csr, value) -> str:
        """Check allowed domains"""
        self.logger.debug("EABhandler.allowed_domains_check(%s, %s)", csr, value)
        error = None  # Return an error message if applicable
        return error

    def mac_key_get(self, kid=None):
        """Check external account binding"""
        self.logger.debug("EABhandler.mac_key_get({})".format(kid))
        mac_key = None  # Implement logic to look up the mac_key
        return mac_key
```
