OpenLDAPのバックエンドデータベースをブロックチェーンにした話

## はじめに
[OpenLDAP](https://www.openldap.org/)は[LDAP](https://datatracker.ietf.org/doc/html/rfc4511)を実装したオープンソースソフトウェアです。LDAPはディレクトリサービスを提供するプロトコルで、いろいろなソフトウェアで認証・認可の基盤として利用できます。会社だとMicrosoft Active Directoryが使われているケースが多いですが、それと似たようなことができます。

LDAPは人・モノに関する情報を木構造で管理します。この情報は何らかのデータベースに保存しますが、OpenLDAPではデータベース部分が変更できるような設計になっています。

そこで今回は[flowブロックチェーン](https://ja.onflow.org/)をデータベースとして利用する実験をしてみます。

## なぜ LDAP * ブロックチェーン？
ブロックチェーンの応用分野としてDID (Decentralized IDentity)がホットです。例えば、MiscoroftはBitcoinをバックエンドとする[ION](https://github.com/decentralized-identity/ion)というDIDを開発しています（ホワイトペーパーは[こちら](https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE2DjfY)）。

Bitcoinを使用する場合、データサイズを極限まで削るための工夫が欠かせません。つまり、相当数の生データのハッシュをまとめたマークルツリーのルートをBitcoinに書き込むことになります（プライバシーの観点からも生データは書き込めません）。また、アプリケーションで認証・認可に使用するためにはその生データをどこに保存し、どうやって受け渡すかと言った取り決めが必要です。IONは理想的なDIDではありますが、普及させるには技術的な観点以外にも様々な障壁があると考えられます。

これに対し、LDAPはすでに世の中で広く使われている認証・認可（にもつかえる）技術です。ここにDID的な観点を入れられたら面白いんじゃないか？というのが動機です。また、プライバシーの観点を無視すればflowを使うことでデータをそのままオンチェーンで管理する事もできます。



## バックエンド
OpenLDAPがサポートするデータベースには[Shell backend](https://linux.die.net/man/5/slapd-shell)と呼ばれるものがあります。これは、`ldapadd`, `ldapsearch`のようなコマンドの解析結果をそのまま外部のプログラムに渡します。つまり、flowにアクセスする外部プログラムを作成すればOpenLDAPのデータベースとして使うことができるということです。

毎回プロセスを起動するのでパフォーマンスはよくないです。が、OpenLDAPがC言語 v.s. flow SDKがGo言語 なのでOpenLDAPに直接組み込むのは辛いということで、今回はShellで実装しました。

## 詳細
### flowで管理するもの
ブロックチェーンを使用する醍醐味は「分散環境で自分のデータは自分で管理する」です。なので、LDAPエントリそのものをflowのオンチェーンで管理することにしました。今回作成したコントラクトは[こちら](https://github.com/OttyLab/openldap-shell/blob/flow-db/cadence/LdapDb.cdc)です。

LDAPエントリを表すリソースの定義が以下です。各アカウントが自身のストレージに保存し、`Reader`インターフェースをCapabilityとして公開します。

```swift
    pub resource Entry: Reader {
        pub var attributes: {String: [String]} 

        init() {
            self.attributes = {}
        }

        pub fun setAttributes(attributes: {String: [String]}) {
            self.attributes = attributes
        }
    }
```

Capabilityはコントラクト本体の`entries`にDicionaryとして保存します。

```swift
pub contract LdapDb {
    pub let EntryStoragePath: StoragePath
    pub let EntryPublicPath: PublicPath

    pub let entries: {Address: Capability<&Entry{Reader}>}
...
    pub fun setCapability(address: Address, capability: Capability<&Entry{Reader}>) {
        self.entries[address] = capability
    }
...
```

Go言語で開発するbackendは`entries`を参照することで現在保存されているエントリをすべて参照することができます。

また、flow/CadenceはCapabilityをいつでも削除できるという素晴らしい特徴があります。なので、エントリを作ってCapabilityを`entries`に格納しても、あとでCapabilityを削除すれば参照されなくなります。なんともDID的じゃないですか！

### バックエンド

[ここ](https://github.com/OttyLab/openldap-shell/tree/flow-db/backend_go)に実装があります。基本的にはOpenLDAPから標準入力で渡されたコマンドを解析して実行します。ただし、今回の実験の要件として、エントリの作成・管理は各個人が自身の秘密鍵でトランザクションを署名する必要があります。もし、OpenLDAPのバックエンドとしてエントリの作成を行うとこれが実現できません。（バックエンドがエントリを作ったら自分で管理できないですからね）

そこで、エントリの作成・管理は別アプリケーションとして実装します。バックエンドはLDAPクライアントからの検索クエリ(`ldapsearch`)のみに応答するよう実装しました。[ここ](https://github.com/OttyLab/openldap-shell/blob/flow-db/backend_go/db/flow_db.go#L78-L91)を見ると、`entries`にアクセスするためのCadence scriptが確認できます。



### エントリの作成

[ここ](https://github.com/OttyLab/openldap-shell/tree/flow-db/client)に実装があります。[アカウントの作成](https://github.com/OttyLab/openldap-shell/blob/flow-db/client/main.go#L207)、[エントリの作成](https://github.com/OttyLab/openldap-shell/blob/flow-db/client/main.go#L216)、[Capabilityの登録](https://github.com/OttyLab/openldap-shell/blob/flow-db/client/main.go#L223)を行っています。

## 実験
Thunderbirdの住所録、Apache2のBasic認証、sshのログイン認証を試しました。まるっと[Docker](https://github.com/OttyLab/openldap-shell/tree/flow-db/docker)で環境を作ります。

### ディレクトリ構造

![directory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/e171723a-2b09-3b28-ebae-9a6bf8a50915.png)

イメージとしては複数の会社が参加するディレクトリです。rootの下に複数の会社(org1, org2)がぶら下がり、その下に従業員(taro.yamada, jiro.sato)が所属しています。

### Thunderbird
アドレス帳でLDAPを検索できます。以下のように設定すると org1 の従業員を検索できます。

![thunderbird01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/2704a1bb-2657-cb3a-2ed7-1248f1729764.png)

`taro` で検索すると表示されました。

![thunderbird02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/4843ebad-3448-75b0-aeba-4e14c96b71dd.png)

`jiro.sato` さんは org2 の従業員なので、検索しても引っかかりません。

### Apache2

[authnz_ldap_moduleモジュール](https://github.com/OttyLab/openldap-shell/blob/flow-db/docker/apache2/configs/httpd.conf#L82)を読み込み、Basic認証でLDAPを使用できるようにします。

[`/internal`の`.htaccess`](https://github.com/OttyLab/openldap-shell/blob/flow-db/docker/apache2/configs/pages/internal/.htaccess#L3-L5)に以下のように設定しています。これにより、Apacheは`ou=org1,dc=root`以下を検索し、`cn=taro.yamada`がアクセスできます。

```
AuthName "Internal site authentication"
AuthType Basic
AuthBasicProvider ldap
AuthLDAPUrl "ldap://openldap-shell/ou=org1,dc=root?uid"
Require ldap-attribute cn=taro.yamada
```

参考資料:
https://httpd.apache.org/docs/2.4/mod/mod_authnz_ldap.html

#### 結果
`localhost:8081/internal` にアクセスすると認証を要求されます。

![apache01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/1b9331f2-a90f-0515-0caf-a3e9bb73efb3.png)

正しく入力すると表示できます

![apache02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/2915ea0a-e19d-00ac-17e4-5ef0a6d3d2f1.png)

ちなみに、jiro.sato さんは権限を持っていないためアクセスできません。


### SSH

SSHログインにLDAPを使用するには、sssdを利用します。今回の設定は[こちら](https://github.com/OttyLab/openldap-shell/blob/flow-db/docker/sshd/configs/sssd.conf)です。sssdについては[こちらの記事](https://qiita.com/mypaceshun/items/9c3b3f0ef9580c6d60ea)で詳細に解説されています。

以下の設定により、sssdはorg2を参照します。

```
ldap_search_base = ou=employee,ou=org2,dc=root
```

#### 結果

SSHに jiro.sato さんでアクセスすると
```bash
$ ssh -p 8022 jiro.sato@localhost
```

以下のようにログインできます。

![ssh01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/06a9c61e-ccad-8a53-d150-c00245b56e49.png)

ちなみに、taro.yamada さんは権限を持っていないためアクセスできません。

参考資料:
https://linux.die.net/man/5/sssd-ldap


## 考察
バックエンドをブロックチェーンにしても正しく動くことがわかりました。また、ブロックチェーンを使用することで「自分の情報は自分で管理する」というDID的世界観に一歩近づきました。

今回は簡単な検証でしたが、以下のようにするとより良いと考えています。

1. 従業員のエントリは `ou=person,dc=root`以下に置き、org1, org2内部ではエイリアスとしてこれを参照
2. org1, org2等、各社を頂点とするツリー内部にエントリを追加できるのは、各社の(flow)アドレスに限定するようにコントラクトを記述


現状では任意のdnを持つエントリを作成できます。これでは自分が所属していない会社の従業員として勝手に登録できてしまうため、少々具合が悪いです。そこで、コントラクトがエントリを作成したアドレスを管理し、そのエントリの子エントリを作成できるのはそのアドレスに限定する処理を行う必要があります。ただし、`ou=person,dc=root`だけは例外とし、誰でも自分のエントリを作成できるようにします。

つまり、各個人は`ou=person,dc=root`以下に自身のエントリを作成・管理し、org1に就職したらorg1の担当者が自社のツリーにその個人エントリへのエイリアスを作成します。

本当は今回の検証でエイリアスも試したかったのですが、[検索フィルタを適当に実装した](https://github.com/OttyLab/openldap-shell/blob/flow-db/backend_go/command.go#L98)ために動きませんでした。また、「エントリを作成したアドレスを管理」部分も、現在のコントラクトでは一アドレス一エントリしか保持できないので改良の必要があります。

ちなみに、Thunderbirdのアドレス帳はエイリアスを参照した検索をしてくれないのでちょっと悩ましいです。

参考資料:
https://docs.oracle.com/cd/E24001_01/oid.1111/b55919/oid_alias_entries.htm