in-toto attestation の仕様とcosignでの使われ方
はじめに
anchoreのsyftが最近対応したattestationの出力 に使われている in-toto attestation format
の仕様を調査をしたので、備忘録としてまとめておきます。
調べながらの記述となっており、不正確な情報も含まれているかもしれないです。何か気づいたことがあればコメントやtwitterなどで教えていただけると助かります。
TL; DL
in-toto attestation format
はソフトウェアのアーティファクトのメタデータ向けの署名の仕方とデータ形式- メタデータはデータ型を指定する
- アーティファクトの生成方法に関する情報のProvenanceや 脆弱性スキャンの結果 などの公開されているデータ型がある。また、自分で定義したものなどでもよい。
- メタデータ自身もattestationには含まれる
- 対象のアーティファクトはハッシュで指定できる必要がある
例えば、ある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
で、開発段階とのことです。
アーティファクトはハッシュで特定できる必要があり、複数のアーティファクトが同じメタデータを持つこともあります。 仕様の中で例としてあげられているものとしては、
- コンテナイメージ(digestがsha256: 87f7fe...)が作られた「gitコミットのブランチ名、コミットハッシュ」
- git commit "f0c93d…" の「テスト結果」
- コンテナイメージ(digestがsha256: 87f7fe...)の「脆弱性スキャンの結果」
- 配布するバイナリ(zip形式のdigestがsha256:e363cc... で、tar.gz形式のdigestがsha256:d4d589...)の「ビルドされた環境」
などがあります。
構成要素
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
- payloadTypeが
Statement
Statementは対象のソフトウェアのアーティファクトを管理します。
StatementとPredicateは以下のようなjson形式です。Envelopeのpayloadに、このjsonがbase64にエンコードされて埋め込まれています。
{ "_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 は対象のアーティファクトのセット
- アーティファクトによってdigestの種類(Jarファイルの場合はファイルのハッシュか、JAR signingのハッシュか)や、content type(Docker Imageか、ZIP Fileか)の情報を追加した方が良いかなどは議論されていた。しかし、これらを追加するのは複雑性が増すわりにメリットは少なそうということで対応はされなそうな感じになってそう。
- Consider adding a "digest kind" and/or "content type" to subject · Issue #28 · in-toto/attestation · GitHub
- subject[*].digest は アルゴリズムとハッシュ値が入っている
- 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
- TypeURI は RFC3986 のフォーマット
- predicateTypeの例
- 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が対応したことにより、今後いろいろと使われていく可能性などなど感じられて興味深かったです。