SPIRE x SigstoreによるKeyless署名

## はじめに

[前回](https://qiita.com/hiyosi/items/6ebf3f1748c600d688aa)の記事ではSigstoreによる基本的な署名・検証の動作確認をしました。
Fulcioによって発行された短命な証明書をベースとしてCosignで署名しそのメタデータはRekorに記録されていました。
また、証明書発行時にはOIDCで認証しており、基本的に人が介在するようなフローになっていました。

しかし成果物の署名などの場合、CIによるテスト〜ビルド〜署名といった人が介在しないフローもありえます。
FucioではOIDC TokenのIssuerを複数サポートしており、CIといった用途に向けてGitHub ActionsやK8s、SPIFFE(の実装)といったものが発行するTokenも設定により利用できます。

- https://docs.sigstore.dev/fulcio/oidc-in-fulcio

今回はFulcioの認証にSPIFFE実装であるSPIREが発行するJWT SVIDを利用することで、自動的に認証〜証明書発行〜署名まで行えることを確認したいと思います。

```mermaid
graph 
    Cosign[Cosgin] -- Fetch JWT SVID --> SA[SPIRE Agent]
    Cosign[Cosign] -- Authenticate --> Fulcio[Fulcio]
    Fulcio[Fulcio] -- Verify Token --> Fulcio[Fulcio]
    Fulcio[Fulcio] -- Issue Certificate --> Cosign[Cosign]
    Cosign[Cosign] -- Sign --> Cosign[Cosign]
    Fulcio[Flucio] -- Fetch Public Keys --> DP[OIDC Discovery Provider]
    DP[OIDC Discovery Provider] -- Fetch Bundles --> SA2[SPIRE Agent]
    SA[SPIRE Agent] --> SS[SPIRE Server]
    SA2[SPIRE Agent] --> SS[SPIRE Server]
    SA[SPIRE Agent] -- Attest Workload --> SA[SPIRE Agent]
```

## Fulcio

Fulcioでは有効なIssuerは設定ファイルで設定できるようです。

設定ファイルについてのドキュメントは見当たらなかったのでソースコードやexamplesなどを参考にしました。

- https://github.com/sigstore/fulcio/blob/v1.0.0/pkg/config/config.go
- https://github.com/sigstore/fulcio/blob/main/config/fulcio-config.yaml

今回は以下のようにデフォルトのIssuerとSPIREが使えるような設定にしました。
(public.example.orgは今回のテスト用ドメインです)

```json
{
  "OIDCIssuers": {
    "https://oauth2.sigstore.dev/auth": {
      "IssuerURL": "https://oauth2.sigstore.dev/auth",
      "ClientID": "sigstore",
      "Type": "email"
    },
    "https://public.example.org": {
      "IssuerURL": "https://public.example.org",
      "ClientID": "sigstore",
      "Type": "spiffe",
      "SPIFFETrustDomain": "example.org"
    }
  }
}
```

Fulcioを起動します。

```
# fulcio serve \
  --config=/etc/fulcio-config/config.json \
  --host=0.0.0.0 \
  --port=5555 \
  --grpc-port=5554 \
  --ca=ephemeralca \
  --ct-log-url=http://127.0.0.1:6962/test \
  --metrics-port=12112 > /tmp/fulcio.log &

# curl -s http://127.0.0.1:5555/api/v1/rootCert -o root.pem
# mv root.pem /etc/config/root.pem
```

その他コンポーネントは[前回](https://qiita.com/hiyosi/items/6ebf3f1748c600d688aa)の記事を参考にセットアップしてください。

## SPIRE 

インストールの手順は省略しますが、SPIREのバイナリはGitHubのReleaseページからダウンロード or コンテナイメージも提供されています。

- e.g. https://github.com/spiffe/spire/releases/download/v1.5.3/spire-1.5.3-linux-x86_64-glibc.tar.gz

K8s等へのデプロイのExamplesも提供されていますので参考にしてください。

- https://github.com/spiffe/spire-examples

### SPIRE Server

SPIREをFulcioの認証で利用するにはいくつか設定が必要となります。
FulcioではJWT(Token)には以下のclaimが必須となっています。

- https://docs.sigstore.dev/fulcio/oidc-in-fulcio#oidc-token-requirements-with-extracted-claims

| claims | desc | spec |
|:-------|:-----|:------------|
| `aud`    | audience | `sigstore`を含んでいること |
| `iss`    | issuer   | Fulcioで設定したURIを含んでいること |
| `exp`    | expiration | 値がセットされていること |
| `iat`    | issued at  | 値がセットされていること |

SPIRE Serverではデフォルトで発行するJWT SVIDに`iss`を含んでいないため追加の設定が必要になります。
`jwt_issuer`というパラメータを設定することで任意の値を`iss`にセットすることができます。

- https://github.com/spiffe/spire/blob/v1.5.3/doc/spire_server.md#server-configuration-file

Fulcioの設定(`IssuerURL`)に合わせて、今回は以下のように値を設定します。

```hcl
server {
    bind_address = "127.0.0.1"
    bind_port = "8081"
    trust_domain = "example.org"
    data_dir = "./data/server"
    log_level = "DEBUG"
    ca_ttl = "168h"
    default_svid_ttl = "48h"
    jwt_issuer = "https://public.example.org"  // <- ココ
}

// その他プラグインの設定など ...
```

設定ファイルが準備できたら起動します。

```shell
# spire-server run -config /path/to/server.conf > /tmp/spire-server.log &
```

### SPIRE Agent

今回の動作確認においてSPIRE Agentに特別に必要な設定は必要ありません。Server側で有効になっているプラグインに合わせて起動してください。
今回はテストなのでNode AttestationにJoin Tokenというワンタイムトークンを使います。

```shell
# spire-server token generate -spiffeID spiffe://example.org/test
Token: a5378c48-8471-402f-bc4d-4008f90cb329

# spire-agent run \
  -config /etc/spire/agent.conf \
  -joinToken a5378c48-8471-402f-bc4d-4008f90cb329 \
  > /tmp/spire-agent.log &
```

### OIDC Discovery Provider

FulcioではOIDCのフローに従ってJWT/ID Tokenを検証するため、Discoveryエンドポイントを用意する必要があります。
SPIREではこのエンドポイントは本体とは切り離されているため、別途用意が必要となっています。
バイナリは本体と同様にReleaseページからダウンロードできますし、コンテナイメージも用意されています。

- e.g. https://github.com/spiffe/spire/releases/download/v1.5.3/spire-extras-1.5.3-linux-x86_64-glibc.tar.gz

Fulcioの設定に合わせてdomainsを`public.example.org`に、その他SPIRE Agentの設定に合わせてTrust DomainやWorkload APIのSocket Pathを指定しています。

```
log_level = "debug"
domains = ["public.example.org"]
listen_socket_path = "/tmp/oidc-discovery-provider/server.sock"
workload_api {
    socket_path = "/tmp/spire-agent/public/api.sock"
    trust_domain = "example.org"
}
```
    
OIDC Discovery Providerは検証用の鍵を提供しますが、それをWorkload APIから取得するためのRegistration Entryを作成します。
ParentIDなどは前述で起動したSPIRE Agentに合わせています。

```shell
# spire-server entry create \
  -parentID spiffe://example.org/test \
  -spiffeID spiffe://example.org/oidc-discovery-provider \
  -selector unix:path:/usr/local/bin/oidc-discovery-provider 
```

OIDC Discovery Providerを起動します。

```shell
# oidc-discovery-provider
  -config /path/to/oidc-discovery-provider.conf \
  > /tmp/oidc-discovery-provider.log &
```

エンドポイントをHTTPSで提供するためのNginxを用意します。

```
daemon off;
events {}
http {
  access_log /dev/stdout;
  upstream oidc {
    server unix:/tmp/oidc-discovery-provider/server.sock;
  }
  server {
    listen 443 ssl;
    server_name public.example.org;
    ssl_certificate /etc/nginx/public.example.org.pem;
   ssl_certificate_key /etc/nginx/public.example.org-key.pem;
    location / {
      proxy_pass http://oidc;
     proxy_set_header Host public.example.org;
    }
  }
}
```

サーバ証明書を用意します。テスト用なので今回は`mkcert`を使いました。

```shell
# mkcert -install
# mkcert public.example.org
# cp public.example.org* /etc/nginx/.  
```

Nginxを起動します。

```
# nginx -c /path/to/nginx.conf &
```

`/etc/hosts`にDNS名を設定して動作確認してみます。

```shell
# echo "127.0.0.1 public.example.org" >>  /etc/hosts

# curl  https://public.example.org/.well-known/openid-configuration
{  "issuer": "https://public.example.org",
  "jwks_uri": "https://public.example.org/keys",
  "authorization_endpoint": "",
  "response_types_supported": [
    "id_token"
  ],
  "subject_types_supported": [],
  "id_token_signing_alg_values_supported": [
    "RS256",
    "ES256",
    "ES384"
  ]
}
```

期待するレスポンスが返ってきたことが確認できました。
これでFulcioがJWT SVIDを検証することができるようになりました。

## Cosign

クライアントであるCosignではデフォルトは `https://oauth2.sigstore.dev/auth` を使って認証するような振る舞いですが、`-oidc-provider`パラメータを指定することで別のProvider(filesystemだったりSPIFFE Workload APIだったり)からTokenを取得して認証に使うこともできます。(ちなみに何も指定しなくても有効なProviderがあるかどうか探すようになっています)
ただし、そのTokenがFulcioで有効なものかどうかはFulcioの設定によります。


今回は`-oidc-provier=spiffe`で動作確認したいわけですが、cosignがWorkload APIからJWT SVIDを取得するためにRegistration Entryの事前登録が必要になります。

```shell
# spire-server entry create \
  -parentID spiffe://example.org/test \
  -spiffeID spiffe://example.org/cosgin \
  -selector unix:path:/usr/local/bin/cosign
```

エントリの登録が完了したらJWT SVIDを使って認証〜署名ができることを確認します。

```shell
# cosign sign-blob \
  artifact \
  --oidc-provider spiffe \
  --fulcio-url "http://127.0.0.1:5555" \
  --rekor-url "http://127.0.0.1:3000" \
  --output-certificate cert.pem \
  --output-signature sig

Using payload from: artifact
Generating ephemeral keys...
Retrieving signed certificate...

Note that there may be personally identifiable information associated with this signed artifact.
This may include the email address associated with the account with which you authenticate.
his information will be used for signing this artifact and will be stored in public transparency logs and cannot be removed later.

**Warning** Using a non-standard public key for verifying SCT: /etc/config/pubkey.pem
Successfully verified SCT...
using ephemeral certificate:
-----BEGIN CERTIFICATE-----
MIICtzCCAl2gAwIBAgIUb3L2Q+AfBx/2dgTFuSH2u95TnQkwCgYIKoZIzj0EAwIw
aDEMMAoGA1UEBhMDVVNBMQswCQYDVQQIEwJXQTERMA8GA1UEBxMIS2lya2xhbmQx
FTATBgNVBAkTDDc2NyA2dGggU3QgUzEOMAwGA1UEERMFOTgwMzMxETAPBgNVBAoT
CHNpZ3N0b3JlMB4XDTIyMTIyMDAyMzY1MloXDTIyMTIyMDAyNDY1MlowADBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABHSC2X6zsartyEkBF83pPxoIyyCG0bI/VgLo
iMqPPOnFVl3LfppMDHiEgR19HDqWx56fEmdXSnuGIT0LiYcbBhOjggFLMIIBRzAO
BgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFNvo
0SN89r5jR9a5lU/dVAau4vMyMB8GA1UdIwQYMBaAFPVh99sSDWCnNT41T2P7vRET
OdENMCkGA1UdEQEB/wQfMB2GG3NwaWZmZTovL2V4YW1wbGUub3JnL2Nvc2dpbjAo
BgorBgEEAYO/MAEBBBpodHRwczovL3B1YmxpYy5leGFtcGxlLm9yZzCBigYKKwYB
BAHWeQIEAgR8BHoAeAB2APPzbmCu3uwDRMH+ATX1rJ2gX20AZWAX43f+yV0RpZL0
AAABhS1kFt0AAAQDAEcwRQIhAK8Tf2dQpuIMGycmM4M0/KisGq/kXaSnAVeGKLfv
YfQwAiBU5SrU+k100LtslBMjvT9vT/Z+Y4LOfKe6ril3SvKHiTAKBggqhkjOPQQD
AgNIADBFAiAp5W4EnhzMJWc9Vp0pMwM1g5fDX6xv6Vs2Bq5/DNrcTQIhAMirw7px
j8BUec6e+SP2Bz8HQ3P+T5STkt3HR8NfCQbb
-----END CERTIFICATE-----

tlog entry created with index: 3
Signature wrote in the file sig
Certificate wrote in the file cert.pem
```

※ Workload APIのパスがデフォルト値と異なっている場合は`SPIFFE_ENDPOINT_SOCKET`という環境変数で指定することができるようです。

識別子をみてみるとSPIFEF IDが埋め込まれていることを確認できます。
```shell
# cat cert.pem |base64 -d | openssl x509 -in /dev/stdin -noout -text |grep "URI"
                URI:spiffe://example.org/cosgin
```

署名の検証も成功することを確認します。

```shell
# cosign verify-blob \
  artifact \
  --signature sig \
  --cert cert.pem \
  --cert-chain /etc/config/root.pem \
  --rekor-url=""

Verified OK
```

`oidc-provider=spiffe`を無効にしたい場合には `--oidc-disable-ambient-providers spiffe`のように指定することもできます。
無効にした場合はその他providerは設定されていないのでデフォルトのdexで認証する挙動となります。

## まとめ

SigstoreとSPIREとの連携は前回の記事の最後に触れたようなWorkload Attestation時に署名検証を行うことの他に、今回のような署名時の認証プロバイダとして利用することもできます。

SPIREから取得したJWT SVIDを使うことで署名のフローで人の介在が不要になり、CI等へ署名フローを組み込めるようになります。
その他現在はGitHub ActionsのTokenやGoogle Workload Identity、k8sのtokenのようなFileからの読みこみなどもサポートされています。

ソフトウェアサプライチェーンの保護が注目されている現在、HTTPS対応のときのLet's Encryptのように簡単に利用できるものがあると普及が進むことがあります。Sigstoreによってソフトウェアへの署名・検証という行為がより普及していくことを期待したいです。今後も注目していきます。