試運転ブログ

技術的なあれこれ

in-toto attestation の仕様とcosignでの使われ方

はじめに

anchoreのsyftが最近対応したattestationの出力 に使われている in-toto attestation format の仕様を調査をしたので、備忘録としてまとめておきます。

調べながらの記述となっており、不正確な情報も含まれているかもしれないです。何か気づいたことがあればコメントやtwitterなどで教えていただけると助かります。

TL; DL

例えば、あるDockerImageの作者は {"author": "saso"} というメタデータのattestationを作りたい時に、

  • メタデータの型を http://my.example.com/author とする
  • メタデータ{"author": "saso"}
  • 対象のDockerImageのdigestは sha256:20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062
  • 署名はcosignで生成した鍵を使用する

の場合に生成される、attestaionは以下のようになります。

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwOi8vbXkuZXhhbXBsZS5jb20vYXV0aG9yIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9vdG1zNjEvaGVsbG8tMSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIyMGQzZjY5M2RjZmZhNDRkNmIyNGVhZTg4NzgzMzI0ZDI1Y2MxMzJjMjIwODlmNzBlNGZiZmI4NTg2MjViMDYyIn19XSwicHJlZGljYXRlIjp7ImF1dGhvciI6InNhc28ifX0=",
  "signatures": [
    {
      "keyid": "",
      "sig": "MEQCIG+na8kNMK4u9j2jc2Db94aR0rglNqbHZcscHH9QqP6zAiAtBfLFuNLhHNh/uPkaD++c7F1czPrCKdBdjq+If/g67Q=="
    }
  ]
}

payloadのbase64をデコードすると以下のようなデーが含まれます。

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "http://my.example.com/author",
  "subject": [
    {
      "name": "index.docker.io/otms61/hello-1",
      "digest": {
        "sha256": "20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062"
      }
    }
  ],
  "predicate": {
    "author": "saso"
  }
}

in-toto attestation の概要

in-toto attestation format は、ソフトウェアのアーティファクトのセットに対するメタデータへの署名のフォーマットを提供します。

仕様は in-toto/attestation で定義されています。現在(2022/5/22)でのバージョンは 0.1.0 で、開発段階とのことです。

アーティファクトはハッシュで特定できる必要があり、複数のアーティファクトが同じメタデータを持つこともあります。 仕様の中で例としてあげられているものとしては、

などがあります。

構成要素

attestationの大まかな構成要素は、以下の3つです。

Envelope

Envelopeは、attestationの出力して得られるjsonの形式を定義しています。Evelopeの形式は Dead Simple Signing Envelope(DSSE) というフォーマットでもとにしています。 payloadType,payload,signatures の3つのキーをもちます。

Envelopeは以下のようなjson形式です。

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "<Base64(Statement)>",
  "signatures": [{"sig": "<Base64(Signature)>"}]
}
  • payloadType は application/vnd.in-toto+json で固定
  • payload に次に紹介するStatementとPredicateがbase64エンコードされて入っている
  • signatureには DSSE て定義されている payloadTypeとpayloadに対するひとつ以上の署名
    • signatureはpayloadTypeとpayloadに対して署名を作る
    • 作り方は、 SIGNATURE = Sign(PAE(UTF8(payloadType), SERIALIZED_BODY))
    • Signは署名する関数
    • SERIALIZED_BODY は、payloadのbase64される前のもの。 payload=<Base64(SERIALIZED_BODY)>
    • PAEは PAE(payloadType, payload) = "DSSEv1" + SP + LEN(payloadType) + SP + payloadType + SP + LEN(SERIALIZED_BODY) + SP + SERIALIZED_BODY
      • payloadTypeが http://example.com/HelloWorld で、SERIALIZED_BODYが hello world (この時にpayloadは aGVsbG8gd29ybGQ= となる) の場合 PAEは、 DSSEv1 29 http://example.com/HelloWorld 11 hello world

Statement

Statementは対象のソフトウェアのアーティファクトを管理します。

StatementとPredicateは以下のようなjson形式です。Envelopeのpayloadに、このjsonbase64エンコードされて埋め込まれています。

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "subject": [
    {
      "name": "<NAME>",
      "digest": {"<ALGORITHM>": "<HEX_VALUE>"}
    },
    ...
  ],
  "predicateType": "<URI>",
  "predicate": {
    // arbitary object
  }
}
  • _type はv0.1では https://in-toto.io/Statement/v0.1 で固定
  • subject は対象のアーティファクトのセット
  • subject[*].digest は アルゴリズムハッシュ値が入っている
    • subject[*].digest は DigestSet という型で定義されている。sha256以上の強度のハッシュアルゴリズムが推奨されている。
  • subject[*].name はアーティファクトの名前
  • ビルドされた環境」のメタデータをバイナリの形式で区別する場合は以下のようになる
    • { "name": "curl-7.72.0.zip", "digest": { "sha256": "e363cc5b4e500bfc727106434a2578b38440aa18e105d57576f3d8f2abebf888" }}
    • { "name": "curl-7.72.0.tar.gz", "digest": { "sha256": "d4d5899a3868fbb6ae1856c3e55a32ce35913de3956d1973caccd37bd0174fa2" }}

Predicate

Prdicateはメタデータの型とメタデータの実体を持ちます。

  • predicateType はPredicateの型を定義しているURI
    • TypeURIRFC3986 のフォーマット
    • predicateTypeの例
      • SLSA によって定義されているアーティファクトのどのように生成されたかを記述する predicateType は https://slsa.dev/provenance/v0.1
      • anchoreでcyclonedx-jsonのフォーマットでattestの出力で使われているpredicateTypeは、 https://cyclonedx.org/bom
      • cosign の定義する脆弱性情報のattestのpredicateTypeは cosign.sigstore.dev/attestation/vuln/v1となっている
        • RFC3986の URIのBFN を見る感じでも、URIにはschemaの指定が必須なので、このpredicateTypeはshemaを持たないのであまりよくないようにはみえる。
  • predicate はpredicateTypeで定義されたデータが入る
    • syftのSBOMのattest生成の場合にはSBOMのデータそのままpredicateに含まれている

attestation の具体例

githubに上がっている例です。一部省略しています。

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "ewogICJzdWJqZWN0IjogWwogICAg...",
  "signatures": [{"sig": "MeQyap6MyFyc9Y..."}]
}

payloadをbase64でデコードしたものは以下になります。

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "subject": [
    { "name": "curl-7.72.0.tar.gz",
      "digest": { "sha256": "d4d5899a3868fbb6ae1856c3e55a32ce35913de3956d1973caccd37bd0174fa2" }},
    { "name": "curl-7.72.0.zip",
      "digest": { "sha256": "e363cc5b4e500bfc727106434a2578b38440aa18e105d57576f3d8f2abebf888" }}
  ],
  "predicateType": "https://slsa.dev/provenance/v0.1",
  "predicate": {
    "builder": { "id": "https://github.com/Attestations/GitHubHostedActions@v1" },
    "recipe": {
      "type": "https://github.com/Attestations/GitHubActionsWorkflow@v1",
      "definedInMaterial": 0,
      "entryPoint": "build.yaml:maketgz"
    },
    "metadata": {
      "buildStartedOn": "2020-08-19T08:38:00Z"
    },
    "materials": [
      {
        "uri": "git+https://github.com/curl/curl-docker@master",
        "digest": { "sha1": "d6525c840a62b398424a78d792f457477135d0cf" }
      }, {
        "uri": "github_hosted_vm:ubuntu-18.04:20210123.1"
      }, {
        "uri": "git+https://github.com/actions/checkout@v2",
        "digest": {"sha1": "5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f"}
      }, {
        "uri": "git+https://github.com/actions/upload-artifact@v2",
        "digest": { "sha1": "e448a9b857ee2131e752b06002bf0e093c65e571" }
      }, {
        "uri": "pkg:deb/debian/stunnel4@5.50-3?arch=amd64",
        "digest": { "sha256": "e1731ae217fcbc64d4c00d707dcead45c828c5f762bcf8cc56d87de511e096fa" }
      }, {
        "uri": "pkg:deb/debian/libbrotli-dev@1.0.7-2+deb10u1?arch=amd64",
        "digest": { "sha256": "05b6e467173c451b6211945de47ac0eda2a3dccb3cc7203e800c633f74de8b4f" }
      }
    ]
  }
}

cosign のattestation

cosign を使ってDocker Imageに対してattestationを作成し、DockerHubのようなOCI Registryにattestationを保存することができます。

試しに適当なpredicateTypeとpredicateを自分で作成したotms61/hello-1登録してみます。

$ cosign generate-key-pair
Enter password for private key:
Enter password for private key again:
Private key written to cosign.key
Public key written to cosign.pub

$ cat predicate.json |jq .
{
  "author": "saso"
}

$ cosign attest  --key cosign.key --type http://my.example.com/author --predicate predicate.json otms61/hello-1

verify-attestationコマンドで生成されたattestationの確認と、署名が正しいかの確認ができます。

$ cosign verify-attestation --key cosign.pub otms61/hello-1

Verification for otms61/hello-1 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwOi8vbXkuZXhhbXBsZS5jb20vYXV0aG9yIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9vdG1zNjEvaGVsbG8tMSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIyMGQzZjY5M2RjZmZhNDRkNmIyNGVhZTg4NzgzMzI0ZDI1Y2MxMzJjMjIwODlmNzBlNGZiZmI4NTg2MjViMDYyIn19XSwicHJlZGljYXRlIjp7ImF1dGhvciI6InNhc28ifX0=","signatures":[{"keyid":"","sig":"MEQCIG+na8kNMK4u9j2jc2Db94aR0rglNqbHZcscHH9QqP6zAiAtBfLFuNLhHNh/uPkaD++c7F1czPrCKdBdjq+If/g67Q=="}]}

最後の1行に生成されたattestationが出力されています。payloadのbase64をデコードすると登録したpredicateが確認できます。

$ pbpaste | jq '.payload | @base64d | fromjson'
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "http://my.example.com/author",
  "subject": [
    {
      "name": "index.docker.io/otms61/hello-1",
      "digest": {
        "sha256": "20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062"
      }
    }
  ],
  "predicate": {
    "author": "saso"
  }
}

間違った鍵で公開鍵で検証をすると以下のような出力になります。

$ cosign verify-attestation --key cosign.pub otms61/hello-1
Error: no matching attestations:
Accepted signatures do not match threshold, Found: 0, Expected 1
main.go:46: error during command execution: no matching attestations:
Accepted signatures do not match threshold, Found: 0, Expected 1

OCI Registryにどう保管されるか

cosign attestコマンドはattestationを生成して、DockerHubの対象イメージに ${イメージのダイジェスト}.att というタグでアーティファクトは保存されています。

$ crane digest otms61/hello-1:latest
sha256:20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062

$ crane ls otms61/hello-1
latest
sha256-20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062.att

今回の場合は イメージのダイジェストが sha256:20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062 なので、 sha256-20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062.att というタグでアーティファクトが保存されています。

マニフェストを確認すると以下の通りです。

> crane manifest otms61/hello-1:sha256-20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062.att | jq .
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 233,
    "digest": "sha256:e06a8292130008eb64770bafe3b2c6d2e22f0b5e0b2125ee95f131c61e719a64"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dsse.envelope.v1+json",
      "size": 544,
      "digest": "sha256:60f9b1fbaf99f54daee933b6a8920b0dde05f5d8dd7b5b7e466f23b2f96f3cb4",
      "annotations": {
        "dev.cosignproject.cosign/signature": ""
      }
    }
  ]
}

configの中身 (crane blob otms61/hello-1@sha256:e06a8292130008eb64770bafe3b2c6d2e22f0b5e0b2125ee95f131c61e719a64の結果)

{
  "architecture": "",
  "created": "0001-01-01T00:00:00Z",
  "history": [
    {
      "created": "0001-01-01T00:00:00Z"
    }
  ],
  "os": "",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:60f9b1fbaf99f54daee933b6a8920b0dde05f5d8dd7b5b7e466f23b2f96f3cb4"
    ]
  },
  "config": {}
}

layerの中身( crane blob otms61/hello-1@sha256:60f9b1fbaf99f54daee933b6a8920b0dde05f5d8dd7b5b7e466f23b2f96f3cb4 の結果)は、 cosign verify-attestation --key cosign.pub otms61/hello-1 コマンドの時に出力されていたものと同じものが取得できることがわかります。

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwOi8vbXkuZXhhbXBsZS5jb20vYXV0aG9yIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9vdG1zNjEvaGVsbG8tMSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIyMGQzZjY5M2RjZmZhNDRkNmIyNGVhZTg4NzgzMzI0ZDI1Y2MxMzJjMjIwODlmNzBlNGZiZmI4NTg2MjViMDYyIn19XSwicHJlZGljYXRlIjp7ImF1dGhvciI6InNhc28ifX0=",
  "signatures": [
    {
      "keyid": "",
      "sig": "MEQCIG+na8kNMK4u9j2jc2Db94aR0rglNqbHZcscHH9QqP6zAiAtBfLFuNLhHNh/uPkaD++c7F1czPrCKdBdjq+If/g67Q=="
    }
  ]
}

payloadの中身のbaes64を解いてみると。

> crane blob otms61/hello-1@sha256:60f9b1fbaf99f54daee933b6a8920b0dde05f5d8dd7b5b7e466f23b2f96f3cb4| jq -r '.payload | @base64d' | jq .
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "http://my.example.com/author",
  "subject": [
    {
      "name": "index.docker.io/otms61/hello-1",
      "digest": {
        "sha256": "20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062"
      }
    }
  ],
  "predicate": {
    "author": "saso"
  }
}

subject に対象にしたDockerImageに関する情報が入っていて、 predicateTypeとpredicateに指定した情報が入っていることがわかります。

複数のattestationを保持した場合の挙動

cosign attest を使って複数登録した場合にはレイヤーが追加されます。

$ cat predicate.json
{"author": "saso", "github": "otms61"}

$ cosign attest  --key cosign.key --type http://my.example.com/author --predicate predicate.json otms61/hello-1
Enter password for private key:
Using payload from: predicate.json

manifestをみるとlayerが追加されていることがわかります。

$ crane manifest otms61/hello-1:sha256-20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062.att | jq .
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 342,
    "digest": "sha256:10c33275bf4928e02c3364c1d9ee5c535ebf535995c808bcac9c1e628742acee"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dsse.envelope.v1+json",
      "size": 544,
      "digest": "sha256:60f9b1fbaf99f54daee933b6a8920b0dde05f5d8dd7b5b7e466f23b2f96f3cb4",
      "annotations": {
        "dev.cosignproject.cosign/signature": ""
      }
    },
    {
      "mediaType": "application/vnd.dsse.envelope.v1+json",
      "size": 568,
      "digest": "sha256:640d3bf02710f135ad453d3ba8fcc1afd60395a0a15d93e3354291fa8e4b20c0",
      "annotations": {
        "dev.cosignproject.cosign/signature": ""
      }
    }
  ]
}

cosign verify-attestation の結果でも二種類のjsonが取得できていることがわかります。

$ cosign verify-attestation --key cosign.pub otms61/hello-1

Verification for otms61/hello-1 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwOi8vbXkuZXhhbXBsZS5jb20vYXV0aG9yIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9vdG1zNjEvaGVsbG8tMSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIyMGQzZjY5M2RjZmZhNDRkNmIyNGVhZTg4NzgzMzI0ZDI1Y2MxMzJjMjIwODlmNzBlNGZiZmI4NTg2MjViMDYyIn19XSwicHJlZGljYXRlIjp7ImF1dGhvciI6InNhc28ifX0=","signatures":[{"keyid":"","sig":"MEQCIG+na8kNMK4u9j2jc2Db94aR0rglNqbHZcscHH9QqP6zAiAtBfLFuNLhHNh/uPkaD++c7F1czPrCKdBdjq+If/g67Q=="}]}
{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwOi8vbXkuZXhhbXBsZS5jb20vYXV0aG9yIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9vdG1zNjEvaGVsbG8tMSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIyMGQzZjY5M2RjZmZhNDRkNmIyNGVhZTg4NzgzMzI0ZDI1Y2MxMzJjMjIwODlmNzBlNGZiZmI4NTg2MjViMDYyIn19XSwicHJlZGljYXRlIjp7ImF1dGhvciI6InNhc28iLCJnaXRodWIiOiJvdG1zNjEifX0=","signatures":[{"keyid":"","sig":"MEUCIQDkqqPx6TTA383kBr5ZTQuAZfUKDBH7SS4eAyk8DRvasgIgMp9+dQMCzSBlV81tEATIGg/J1YGGLHOBE2atKhe91dk="}]}

新しく別の証明書を作成してattestを追加した時の挙動は確認してみたのですが、少し不可解な挙動をしているのでもう少し様子をみています。

syftのattestコマンド

alpine:3.15に対して cyclonedx-json のフォーマットでSBOMを作成し、attestationを作成しようとすると以下のようなコマンドになります。

$ syft attest --key ./cosign.key alpine:3.15 -o cyclonedx-json > sbom.att.json

sbom.att.jsonのpayloadをbase64でデコードすると以下のようなデータが入っています。

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://cyclonedx.org/bom",
  "subject": [
    {
      "name": "",
      "digest": {
        "sha256": "4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454"
      }
    }
  ],
  "predicate": {
    "bomFormat": "CycloneDX",
    // 省略
  }
}

subjectのハッシュはalpine:3.15のダイジェストになっています。

$ crane digest alpine:3.15
sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454

predicateも含めすべて確認したい場合にはgistに公開鍵も含めてあげています。

alpine:3.15 sbom attestation · GitHub

syftにはverifyする機能は含まれておらず、cosignコマンドもOCI Registryに保管されているattestを検証する機能しかないようなので、attestationと公開鍵のセットがあっても簡単に検証できるようなコマンドは現状なさそうな雰囲気はあります。 syftでは生成したattestationをcosignコマンドでOCIに登録して、 cosign verify-attestation をする例が記載されています。 github.com

おわりに

in-toto/attestation の仕様と、cosignとsyftでの使われ方をざっとみてみました。 最初にattestationの形式を見た時に、署名ということで、jwtに近い話なのかなと感じていました。in-toto/attestation は、predicateTypeを明示しなくてはいけなかったり、複数のsubjectを持つことができたり、複数のsignatureを持つことができたり、jwtに比べるとより幅広いユースケースを想定しているように感じました。

また、subjectが対象のアーティファクトのダイジェストだと分からなかったり、署名方法やpredicateあたりがわかりにくく感じました。

cosignが対応したことにより、今後いろいろと使われていく可能性などなど感じられて興味深かったです。