試運転ブログ

技術的なあれこれ

cosign実行時のやりとりを覗いてみた

cosign のコマンドを実行した時に、sigstore関連のサーバにどういったリクエストが飛ぶのかが気になり調べてみました。

$ cat test-result.json
{"passed": true}
$ COSIGN_EXPERIMENTAL=1 cosign attest  --type 'https://example.com/TestResult/v1' --predicate ./test-result.json otms61/test-custom-attest

cosignをある程度知っている前提で記事を書いているので、あまり馴染みがない場合には以下のブログなどを参考にしてみてください。

knqyf263.hatenablog.com

sil.hatenablog.com

blog.flatt.tech

扱うリクエス

  • oauth2
    • GET https://oauth2.sigstore.dev/auth/.well-known/openid-configuration
    • GET https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=AVDpMwBa9ulXDqR3M45vmV9jOd1dzAjMYGviS0KJJII&code_challenge_method=S256&nonce=2Ev62T7F7w5FJ1oDf6fdpm13LoI&redirect_uri=http%3A%2F%2Flocalhost%3A51217%2Fauth%2Fcallback&response_type=code&scope=openid+email&state=2Ev62TAtN2xv5qOnmoU6jIvrV1r
      • redirect_uri: http://localhost:51217/auth/callback
      • scope: openid+email
    • GET http://localhost:51217/auth/callback?code=ek2umzn4x5ox4twmr4mu5x6y5&state=2Ev62TAtN2xv5qOnmoU6jIvrV1r
    • GET https://oauth2.sigstore.dev/auth/keys
  • fulcio
    • POST https://fulcio.sigstore.dev/api/v1/signingCert
  • rekor
    • POST https://rekor.sigstore.dev/api/v1/log/entries

docker hubとのやりとりは対象外としています。

リクエスト内に含まれている署名に関しては検証コードを書いてみたりしました。Goだとほぼコピペで終わりそうだったので、あえてpythonで書いています。

記事の中のTokenは一部変更していたり、期限も過ぎているので使用はできなくはなっているはずですが、何か問題ありそうなところがあったら教えていただけると助かります。

oauth2.sigstore.devとのやりとり

OpenIDプロバイダーの情報

まずはOpenIDプロバイダーの情報を取得します。

> GET /auth/.well-known/openid-configuration HTTP/1.1
> Host: oauth2.sigstore.dev


< HTTP/2.0 200 OK
< Content-Length: 1140
< Content-Type: application/json
< Date: Sun, 18 Sep 2022 01:11:43 GMT
< Strict-Transport-Security: max-age=15724800; includeSubDomains

{
    "issuer": "https://oauth2.sigstore.dev/auth",
    "authorization_endpoint": "https://oauth2.sigstore.dev/auth/auth",
    "token_endpoint": "https://oauth2.sigstore.dev/auth/token",
    "jwks_uri": "https://oauth2.sigstore.dev/auth/keys",
    "userinfo_endpoint": "https://oauth2.sigstore.dev/auth/userinfo",
    "device_authorization_endpoint": "https://oauth2.sigstore.dev/auth/device/code",
    "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
    "response_types_supported": ["code"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "code_challenge_methods_supported": ["S256", "plain"],
    "scopes_supported": ["openid", "email", "groups", "profile", "offline_access"],
    "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
    "claims_supported": ["iss", "sub", "aud", "iat", "exp", "email", "email_verified", "locale", "name", "preferred_username", "at_hash"]
}

.well-known/openid-configuration ではOpenIDプロバイダーの情報を取得しています。ここで Authorization Endpointを取得しています。

.well-known/openid-configuration は、OpenID Connect Discovery 1.0 という仕様で定義されていて、以下のサイトの情報なども参考にしてください。 qiita.com

Authorization Endpointへの問い合わせ

ブラウザが開き以下のURLにアクセスします。

https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=Sb0BAM0SKuusKmwGOUKD1jzyM4X5Q_Oba7j8V6bKvjI&code_challenge_method=S256&nonce=2EzYGdY34ebABazSvPKBSf9gKa3&redirect_uri=http://localhost:57543/auth/callback&response_type=code&scope=openid+email&state=2EzYGkmktp31kSIeTrRyPr3FFpp

このURLのパスは、.well-known/openid-configurationauthorization_endpoint です。

"authorization_endpoint": "https://oauth2.sigstore.dev/auth/auth",

パラメータから、code grant flowで、openid と emailのスコープを要求していることがわかります。

最終的にredirect_uriに指定されたアドレスに以下のようにcodeをつけてリダイレクトされます。

http://localhost:57543/auth/callback?code=jjbbyyoo5kff6ohrzlkciet6e&state=2EzYGkmktp31kSIeTrRyPr3FFpp

ブラウザで開かれてから、localhostにリダイレクトされるまで他にもいくつかリクエストが飛んでいます。

  • ブラウザで開かれるURL
    • https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=Sb0BAM0SKuusKmwGOUKD1jzyM4X5Q_Oba7j8V6bKvjI&code_challenge_method=S256&nonce=2EzYGdY34ebABazSvPKBSf9gKa3&redirect_uri=http://localhost:57543/auth/callback&response_type=code&scope=openid+email&state=2EzYGkmktp31kSIeTrRyPr3FFpp
  • Googleを選択したときにリダイレクトされるURL
    • https://accounts.google.com/o/oauth2/v2/auth?client_id=237800849078-hri2ndt7gdafpf34kq8crd5sik9pe3so.apps.googleusercontent.com&redirect_uri=https://oauth2.sigstore.dev/auth/callback&response_type=code&scope=openid+email&state=aabb112233vxe2wr72yb2a22o
  • Googleから受け取ったcode( 4/00AAbbJrqZwRWjh0IBC19_8DIab-a2r6EHZXIYNpIcHWEGwpYCgly9NSbLcw1eK7XG0JFmg )を渡している
    • https://oauth2.sigstore.dev/auth/callback?state=aabb112233vxe2wr72yb2a22o&code=4/00AAbbJrqZwRWjh0IBC19_8DIab-a2r6EHZXIYNpIcHWEGwpYCgly9NSbLcw1eK7XG0JFmg&scope=email+https://www.googleapis.com/auth/userinfo.email+openid&authuser=0&prompt=none
  • 承認完了画面の表示
    • https://oauth2.sigstore.dev/auth/approval?req=aabb112233vxe2wr72yb2a22o&hmac=rZaSah1VGH47Dd8jZFdCUqCh-95J4jhk8jYTC72iVoM
  • localhostoauth2.sigstore.dev のcode( jjbbyyoo5kff6ohrzlkciet6e )を渡すリダイレクト
    • http://localhost:57543/auth/callback?code=jjbbyyoo5kff6ohrzlkciet6e&state=2EzYGkmktp31kSIeTrRyPr3FFpp

code が2度出てきて紛らわしいですが、最初の方がGoogleが発行し oauth2.sigstore.dev に渡すcodeで、oauth2.sigstore.dev のサーバ側でGoogleからトークンを取得するのに使っているものと思われます。最後の locahostへ伝えられるcodeは oauth2.sigstore.dev で発行されたcodeで、oauth2.sigstore.devtoken_endpointトークンを取得するのに使われます。

Id Tokenの取得

callbackで受け取ったcodeを oauth2.sigstore.dev に伝え、 oauth2.sigstore.dev のId Tokenを取得しています。

> POST /auth/token HTTP/1.1
> Host: oauth2.sigstore.dev
> Authorization: Basic c2lnc3RvcmU6
> Content-Length: 225
> Content-Type: application/x-www-form-urlencoded

code=jjbbyyoo5kff6ohrzlkciet6e&code_verifier=2Ev62YX6tFyMfI1sesgYp6ZRX4o2Ev62XJyVdyusyd32WyA2qbqGMF&grant_type=authorization_code&nonce=2EzYGdY34ebABazSvPKBSf9gKa3&redirect_uri=http%3A%2F%2Flocalhost%3A51217%2Fauth%2Fcallback

< HTTP/2.0 200 OK
< Cache-Control: no-store
< Content-Length: 2060
< Content-Type: application/json
< Date: Sun, 18 Sep 2022 01:11:49 GMT
< Pragma: no-cache
< Strict-Transport-Security: max-age=15724800; includeSubDomains

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImZhYjMwMThlZThmZTM1NjdlY2JhODMwN2RlOWI0OWM5YWYzYTE5NmIifQ.eyJpc3MiOiJodHRwczovL29hdXRoMi5zaWdzdG9yZS5kZXYvYXV0aCIsInN1YiI6IkNoVXhNREU0TnprMk1qYzFORE13TnpVM056ZzRNVGdTSDJoMGRIQnpPaVV5UmlVeVJtRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMCIsImF1ZCI6InNpZ3N0b3JlIiwiZXhwIjoxNjYzNDYzNTY4LCJpYXQiOjE2NjM0NjM1MDgsIm5vbmNlIjoiMkV2NjJUN0Y3dzVGSjFvRGY2ZmRwbTEzTG9JIiwiYXRfaGFzaCI6IlVRajBIc3RrR2dhWkZRSWNVRGFYZVEiLCJlbWFpbCI6InNhc29ha2lyYTYxMTRAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZlZGVyYXRlZF9jbGFpbXMiOnsiY29ubmVjdG9yX2lkIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwidXNlcl9pZCI6IjEwMTg3OTYyNzU0MzA3NTc3ODgxOCJ9fQ.hlqM-fgzkaEjqN1EpOLS2ui5BcrD-f3ljm6R5Us_stwP1FoLERNoh38-oM79F_FKXJLbCeeBQV2vYrianeKo7ZeRFlkBj604nUyhWxrnTyaRyEVb3t09egEPe8wjrIR15y8to2I1emrn3GOZLTmLH71hrRN4H-2yse_1QQgtdoqwFJ6iANSyoq2XiCKsGFpEdgguQYUG_o2nE1Ur7WQ5Z6yzo_NFsrcfKqA19gOJl1nKOpeudxjGwVP0Q6rePMAnFgLN04cNFE2jeDD4_apcb2eLCO7FeQOeuF-85FoXsF9gNUIkUgyTEM0dAWQFqHH6vL4dtCVQ1jS0MveU4VTS00",
    "token_type": "bearer",
    "expires_in": 59,
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImZhYjMwMThlZThmZTM1NjdlY2JhODMwN2RlOWI0OWM5YWYzYTE5NmIifQ.eyJpc3MiOiJodHRwczovL29hdXRoMi5zaWdzdG9yZS5kZXYvYXV0aCIsInN1YiI6IkNoVXhNREU0TnprMk1qYzFORE13TnpVM056ZzRNVGdTSDJoMGRIQnpPaVV5UmlVeVJtRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMCIsImF1ZCI6InNpZ3N0b3JlIiwiZXhwIjoxNjYzNDYzNTY4LCJpYXQiOjE2NjM0NjM1MDgsIm5vbmNlIjoiMkV2NjJUN0Y3dzVGSjFvRGY2ZmRwbTEzTG9JIiwiYXRfaGFzaCI6InIzWlVid3d5a2RHTVlwT0tUUEVjQ0EiLCJjX2hhc2giOiJlMVJLVXhHWlZPdnZacS1VbERQRFpBIiwiZW1haWwiOiJzYXNvYWtpcmE2MTE0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmZWRlcmF0ZWRfY2xhaW1zIjp7ImNvbm5lY3Rvcl9pZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInVzZXJfaWQiOiIxMDE4Nzk2Mjc1NDMwNzU3Nzg4MTgifX0.ir6acPSd58j9TSVEDTpc1zuIq9YSJvCb-S314uFFEtaJ01Iv8AdhRjRVjuO6EJPx75OwxqDvQOb6SgeCw9y5H0Emy75mjWPj4sCQXIuSRhLrJhJ13CaJ9g-Pdy1xAsk3OOd4U5VrSRqh6tvmRenrD6rMY5ee_m5NTFaFOpj9D133xND87o_HIxDI7EP-9rau9S_E6uv_zUAn4LEQRasXj-F9s1Yn33BuHkNlQS5Kln62qp9Rj8leAR6GW9xJNyMNN_VdQeIva7lHOCaK68d-GVPqKVjyzyL1PLJiNBIUKc4EsbNtI6phpAGj3OOGWn1UFN7MnDYLhESDrDntvoGF00"
}

このURLのパスは、.well-known/openid-configurationtoken_endpoint です。

"token_endpoint": "https://oauth2.sigstore.dev/auth/token",

Id Tokenの中身は、

Header:
{
    "alg": "RS256",
    "kid": "fab3018ee8fe3567ecba8307de9b49c9af3a196b"
}
Claims:
{
    "at_hash": "r3ZUbwwykdGMYpOKTPEcCA",
    "aud": "sigstore",
    "c_hash": "e1RKUxGZVOvvZq-UlDPDZA",
    "email": "sasoakira6114@gmail.com",
    "email_verified": true,
    "exp": 1663463568,
    "federated_claims": {
        "connector_id": "https://accounts.google.com",
        "user_id": "101879627543075778818"
    },
    "iat": 1663463508,
    "iss": "https://oauth2.sigstore.dev/auth",
    "nonce": "2Ev62T7F7w5FJ1oDf6fdpm13LoI",
    "sub": "ChUxMDE4Nzk2Mjc1NDMwNzU3Nzg4MTgSH2h0dHBzOiUyRiUyRmFjY291bnRzLmdvb2dsZS5jb20"
}

有効期限は59秒と短い値が採用されています。

"expires_in": 59

レスポンスヘッダーの時間より、Id Tokenが発行された時刻は 01:11:49

< Date: Sun, 18 Sep 2022 01:11:49 GMT

Id Token の clameの exp1663463568 なので、失効するのは 01:12:48 となっています。

> date -u -r "1663463568"
Sun Sep 18 01:12:48 UTC 2022

有効期限は59秒、発行時刻が 01:11:49で、IdTokenの失効時刻が 01:12:48 となっており、整合性が取れていることがわかります。

Id Token検証のための鍵の取得

Id Tokenを検証するために鍵を取得します。

> GET /auth/keys HTTP/1.1
> Host: oauth2.sigstore.dev

< HTTP/2.0 200 OK
< Cache-Control: max-age=8930, must-revalidate
< Content-Length: 1032
< Content-Type: application/json
< Date: Sun, 18 Sep 2022 01:11:49 GMT
< Strict-Transport-Security: max-age=15724800; includeSubDomains

{
    "keys": [
        {
            "use": "sig",
            "kty": "RSA",
            "kid": "fab3018ee8fe3567ecba8307de9b49c9af3a196b",
            "alg": "RS256",
            "n": "sgYrM7Fmkez4ruXBTmwNovlo0pDpCRmWuHvLBlStsXpZoiypNdUQ-9foCkA2JSDJ2ZH6YRuGZlQkHesLVVumOMITxz53-zmBuHdRtLBe4RG8hKDwEXTND0JS43-Ew9nzqpZy5xcZlXQGngGptDA_dCgNLRdiX6QTnLCyHQna4qUzum3CExyOCpyldIFBXL7XvNECWarrFAsLZzeWCRPpeMqd5jQ27DDEgzsQfd6Z3F-fyDcp64KYNGIcj64wYnKErM2p7IsfpjqURD2hhEtXcxgd7_b31GUbDP1r24XXsAgSeGNRSw5XNhbzVn80x_EyY7VHAGcFWsnYjFfLJ4gaJQ",
            "e": "AQAB"
        },
        {
            "use": "sig",
            "kty": "RSA",
            "kid": "7f5cc041e09c5ca3df4f88eff9f75149f973250a",
            "alg": "RS256",
            "n": "li7QSTuaFhmtFU2Td12ju7dD9Q-_igOhSd5yXQxHXLpZInbcyL8R7MdiYIHOG6t2mLpD8QKwEL5qwnEjCGqx-REUARyRT6pWqHDtlGBvekabYNZIc4bCWMxkpcvDNpLvIhr-6C8LC7ocpZiZtcKuAtYWW62lzEtAhQEC2WdFeBONBwHthh-M0edRfMlxciKEH89q75AGG-4JeSWPUW6eYEoOw4NxupKGEBu4C7rSBJb1jJxDZq2vfMBHyCmlADYGaw_hbD-HN6Yny2-CXrJpv1FkKI4bea_NnWG85SEE9COQzhsiKvFJDWW2ZPoxlVif4OcVBz9K74IrKxXtfBxVuQ",
            "e": "AQAB"
        }
    ]
}

このURLのパスは、.well-known/openid-configurationjwks_uri です。

"jwks_uri": "https://oauth2.sigstore.dev/auth/keys",

Id Tokenの検証については以下のブログが参考になります。

www.sambaiz.net

fulcioとのやりとり

oauth2.sigstore.dev で発行したIdTokenをもとに証明書を発行します。

> POST /api/v1/signingCert HTTP/1.1
> Host: fulcio.sigstore.dev
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImZhYjMwMThlZThmZTM1NjdlY2JhODMwN2RlOWI0OWM5YWYzYTE5NmIifQ.eyJpc3MiOiJodHRwczovL29hdXRoMi5zaWdzdG9yZS5kZXYvYXV0aCIsInN1YiI6IkNoVXhNREU0TnprMk1qYzFORE13TnpVM056ZzRNVGdTSDJoMGRIQnpPaVV5UmlVeVJtRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMCIsImF1ZCI6InNpZ3N0b3JlIiwiZXhwIjoxNjYzNDYzNTY4LCJpYXQiOjE2NjM0NjM1MDgsIm5vbmNlIjoiMkV2NjJUN0Y3dzVGSjFvRGY2ZmRwbTEzTG9JIiwiYXRfaGFzaCI6InIzWlVid3d5a2RHTVlwT0tUUEVjQ0EiLCJjX2hhc2giOiJlMVJLVXhHWlZPdnZacS1VbERQRFpBIiwiZW1haWwiOiJzYXNvYWtpcmE2MTE0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmZWRlcmF0ZWRfY2xhaW1zIjp7ImNvbm5lY3Rvcl9pZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInVzZXJfaWQiOiIxMDE4Nzk2Mjc1NDMwNzU3Nzg4MTgifX0.ir6acPSd58j9TSVEDTpc1zuIq9YSJvCb-S314uFFEtaJ01Iv8AdhRjRVjuO6EJPx75OwxqDvQOb6SgeCw9y5H0Emy75mjWPj4sCQXIuSRhLrJhJ13CaJ9g-Pdy1xAsk3OOd4U5VrSRqh6tvmRenrD6rMY5ee_m5NTFaFOpj9D133xND87o_HIxDI7EP-9rau9S_E6uv_zUAn4LEQRasXj-F9s1Yn33BuHkNlQS5Kln62qp9Rj8leAR6GW9xJNyMNN_VdQeIva7lHOCaK68d-GVPqKVjyzyL1PLJiNBIUKc4EsbNtI6phpAGj3OOGWn1UFN7MnDYLhESDrDntvoGF00
> Content-Length: 325
> Content-Type: application/json

{
    "publicKey": {
        "content": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi2pKkJdJxrDZrCuDT+JDN/63RS9Hcz1yr4YBYVYLnI4hnFqnAMvx68xbjWXW3O8jEWIsgkMGcGa+giYIDV0bFQ==",
        "algorithm": "ecdsa"
    },
    "signedEmailAddress": "MEQCIEme0ouHErG7JnhFx94DtQuUGOJ38N+Uj1cVp8P/fM2MAiBv95ZfvLi1U9taWbA1p/T9FTrKZFmkqQF7XhZq3h+e2g==",
    "certificateSigningRequest": null
}



< HTTP/2.0 201 Created
< Content-Type: application/pem-certificate-chain
< Date: Sun, 18 Sep 2022 01:11:49 GMT
< Strict-Transport-Security: max-age=15724800; includeSubDomains
< Vary: Origin

-----BEGIN CERTIFICATE-----
MIICpDCCAimgAwIBAgIUOrq8rexC31ayBokWjaOX28PUdEYwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIwOTE4MDExMTQ5WhcNMjIwOTE4MDEyMTQ5WjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEi2pKkJdJxrDZrCuDT+JDN/63RS9Hcz1yr4YB
YVYLnI4hnFqnAMvx68xbjWXW3O8jEWIsgkMGcGa+giYIDV0bFaOCAUgwggFEMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUOIuA
oH2TmkY/ULd1symjM3KMmVswHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJQYDVR0RAQH/BBswGYEXc2Fzb2FraXJhNjExNEBnbWFpbC5jb20wKQYKKwYB
BAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5
AgQCBHwEegB4AHYACGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3IAAAGD
TibPoAAABAMARzBFAiBBYUxFpEdyvAakCjn+oeSVjXHACO5cy3wbdnG1yla+8wIh
AP/M0EOBYQeM0UC/PBbgkhH2qY3VZbRPoV/ZSLDre7uVMAoGCCqGSM49BAMDA2kA
MGYCMQDpGJ4wxnLlNKT1lzpbwCdUAmcftAoZohq62z7NkmTgXJqET8VdiMCTagO4
4Jkb4GICMQDz0lHSw5SYgc1JF2RP0yScTIlkQ2Io4SG8efZwtJw77vlNhAYxBMx5
RX2TKy4AeO0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7
7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS
0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB
BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp
KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI
zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR
nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP
mygUY7Ii2zbdCdliiow=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7
XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex
X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j
YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY
wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ
KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM
WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9
TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ
-----END CERTIFICATE-----

リクエス

keyless sigingの時のcosign attest コマンド内では、コマンド実行中に秘密鍵を生成します。この秘密鍵に対応する公開鍵と、この秘密鍵で署名したメールアドレスがリクエストには含まれています。

cosign/fulcio.go at 7ba521444f9fcfdf2e1e5936c05834597674e6c9 · sigstore/cosign · GitHub

リクエストに含まれるpublicKeyは、ASN.1 DER 形式にエンコードされています。

cosign/fulcio.go at 7ba521444f9fcfdf2e1e5936c05834597674e6c9 · sigstore/cosign · GitHub

signedEmailAddress は、Id Tokenに含まれているメールアドレス( sasoakira6114@gmail.com )を秘密鍵で署名したシグネチャをDER形式にしたものです。

import base64
import hashlib

import ecdsa
from ecdsa.util import sigdecode_der

c = {
    "publicKey": {
        "content": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi2pKkJdJxrDZrCuDT+JDN/63RS9Hcz1yr4YBYVYLnI4hnFqnAMvx68xbjWXW3O8jEWIsgkMGcGa+giYIDV0bFQ==",
        "algorithm": "ecdsa",
    },
    "signedEmailAddress": "MEQCIEme0ouHErG7JnhFx94DtQuUGOJ38N+Uj1cVp8P/fM2MAiBv95ZfvLi1U9taWbA1p/T9FTrKZFmkqQF7XhZq3h+e2g==",
    "certificateSigningRequest": None,
}

v = ecdsa.VerifyingKey.from_der(base64.b64decode(c["publicKey"]["content"]))
# verifyに失敗するとFalseがかえる
print(
    v.verify(
        signature=base64.b64decode(c["signedEmailAddress"]),
        data=b"sasoakira6114@gmail.com",
        hashfunc=hashlib.sha256,
        sigdecode=sigdecode_der,
    )
)
"""
$ python x.py
True
"""

元のメールアドレスは、リクエストボディには含まず、AuthorizationヘッダーにId Tokenが含まれているためサーバサイドで検証することが可能です。

レスポンス

レスポンスの証明書に含まれる情報を見ていきます。 証明書の仕様 や、fulcioが証明書発行のリクエストを受けた時の挙動を説明したhow-certificate-issuing-works.md を知ると分かりやすいです。

how-certificate-issuing-works.md2 | Authentication は、Id Token検証のための鍵の取得 でクライアント側でもId Tokenの検証をしたのと同様のことをfulcioでも行います。 3 | Verifying the challengefulcioとのやりとりリクエスト の検証で確かめたことと同様のことをしています。challengeと呼んでいるものは、signedEmailAddressのことです。

以降は発行された証明書を見ながら確認していきます。

レスポンスには3つの証明書チェインで返ってきますが、3番目がfulcioのルート証明書で、2番目がfulcioの中間証明書で、利用者のリクエストをもとに発行されたのは1番目の証明書です。

fulcioの証明書チェーン

まずは、中間証明書とfulcioのルート証明書をみていきます。

中間証明書の中身

$ openssl x509 -text -in intermediate.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            b9:d5:89:57:e7:53:46:eb:25:ab:26:46:41:eb:9f:f5:27:7d:a4
    Signature Algorithm: ecdsa-with-SHA384
        Issuer: O=sigstore.dev, CN=sigstore
        Validity
            Not Before: Apr 13 20:06:15 2022 GMT
            Not After : Oct  5 13:56:58 2031 GMT
        Subject: O=sigstore.dev, CN=sigstore-intermediate
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:f1:15:52:ff:2b:07:f8:d3:af:b8:36:72:3c:86:
                    6d:8a:58:14:17:d3:65:6a:b6:29:01:df:47:3f:5b:
                    c1:04:7d:54:e4:25:7b:ec:b4:92:ee:cd:19:88:7e:
                    27:13:b1:ef:ee:9b:52:e8:bb:ef:47:f4:93:93:bf:
                    7c:2d:58:0c:cc:b9:49:e0:77:88:7c:5d:ed:1d:26:
                    9e:c4:b7:18:a5:20:12:af:59:12:d0:df:d1:80:12:
                    73:ff:d8:d6:0a:25:e7
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Extended Key Usage:
                Code Signing
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Subject Key Identifier:
                DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
            X509v3 Authority Key Identifier:
                keyid:58:C0:1E:5F:91:45:A5:66:A9:7A:CC:90:A1:93:22:D0:2A:C5:C5:FA

    Signature Algorithm: ecdsa-with-SHA384
         30:64:02:30:3c:2b:10:2b:80:d8:89:96:03:3c:86:83:8b:91:
         c5:2a:77:f1:5f:1e:80:49:25:66:11:17:ec:ca:76:01:89:7d:
         97:e9:22:51:9d:95:3c:e3:ff:43:65:d9:c5:be:fc:66:02:30:
         4e:b7:a4:29:06:57:38:27:fd:03:c6:f9:13:0a:aa:5c:96:fc:
         e2:2f:a0:42:08:f9:e3:76:52:01:dc:fb:b7:07:1b:0f:9b:28:
         14:63:b2:22:db:36:dd:09:d9:62:8a:8c

ルート証明書の中身

openssl x509 -text -in root.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            b6:4d:00:f1:5d:c4:73:f0:8d:e0:e5:a0:3c:32:60:28:40:3b:fe
    Signature Algorithm: ecdsa-with-SHA384
        Issuer: O=sigstore.dev, CN=sigstore
        Validity
            Not Before: Oct  7 13:56:59 2021 GMT
            Not After : Oct  5 13:56:58 2031 GMT
        Subject: O=sigstore.dev, CN=sigstore
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:fb:5d:e1:53:e2:b6:f7:3d:01:b0:4b:82:1a:8e:
                    d2:e4:df:f3:a5:9e:98:1a:9e:06:81:72:56:29:b1:
                    80:6b:e6:2f:b8:ca:70:74:ed:c7:9b:dc:b3:f4:38:
                    83:99:77:17:b1:5f:af:5c:e6:25:6e:c8:94:50:f8:
                    7c:f4:e7:28:be:50:5d:ee:05:60:25:1e:98:92:e6:
                    c8:74:f8:7d:86:1c:4e:d2:5e:b9:35:10:2e:66:d5:
                    3a:f5:f4:bf:60:83:dd
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                58:C0:1E:5F:91:45:A5:66:A9:7A:CC:90:A1:93:22:D0:2A:C5:C5:FA
            X509v3 Authority Key Identifier:
                keyid:58:C0:1E:5F:91:45:A5:66:A9:7A:CC:90:A1:93:22:D0:2A:C5:C5:FA

    Signature Algorithm: ecdsa-with-SHA384
         30:66:02:31:00:8f:59:c7:79:76:69:fb:5d:cd:58:13:5a:f8:
         40:ec:0c:ff:06:d5:65:a0:d6:d0:8c:58:ff:d6:1c:fa:a9:69:
         5a:34:8e:1b:30:78:d1:59:81:2b:34:78:4e:f0:60:8e:2a:02:
         31:00:d9:60:7d:a2:df:7c:b0:89:28:17:7b:d9:61:d7:77:fd:
         5b:56:07:96:fd:4c:d3:1e:6b:b2:31:fe:cb:49:e5:37:dc:2c:
         b7:80:04:b1:38:04:d2:4e:b1:0e:2f:9c:11:c9

証明書チェーンになっていることも確認しておきます。

$ openssl verify -verbose -CAfile root.crt.pem intermediate.crt.pem
intermediate.crt: OK

$ cat intermediate.crt.pem root.crt.pem > p.crt.pem
$ openssl verify -verbose -CAfile  p.crt.pem user.crt.pem
user.crt:
error 10 at 0 depth lookup:certificate has expired
OK

証明書の有効期限が10分なので、検証した時には期限切れになってしまっていますが、証明書のチェーンはできていることがわかります。

TUFによる証明書の管理

このfulcioのルート証明書sigstore/sigstore という、sigstoreのライブラリがまとまっているレポジトリ内にも置かれています。一方で、中間証明書はこのレポジトリには置かれていないようでした。

github.com

コードを見るとTUF(The Update Framework) という仕組みを使って証明書を管理し、アップデートの対応をおこなっているようです。

github.com

DefaultRemoteRootにアクセスするとファイルのリストを閲覧可能です。

const (
    // DefaultRemoteRoot is the default remote TUF root location.
    DefaultRemoteRoot = "https://sigstore-tuf-root.storage.googleapis.com"

)

sigstore/client.go at 181eb26ad2d04c11a387c35efdecef307c941541 · sigstore/sigstore · GitHub

このファイルのリスト内に、先ほど確認した証明書と同様のルート証明書targets/fulcio_v1.crt.pem )と週刊証明書( targets/fulcio_intermediate_v1.crt.pem )というファイルが存在します。また、後ほど出てくるSCTの署名検証に使う公開鍵( targets/ctfe.pub )も含まれています。

TUFのrootパスにアクセス時

それぞれ、RootのURLにパスを追加するとファイルをダウロードすることが可能です。中身も先ほどと同様であることがわかります。

> curl https://sigstore-tuf-root.storage.googleapis.com/targets/fulcio_v1.crt.pem
-----BEGIN CERTIFICATE-----
MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7
XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex
X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j
YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY
wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ
KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM
WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9
TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ
-----END CERTIFICATE-----

> curl https://sigstore-tuf-root.storage.googleapis.com/targets/fulcio_intermediate_v1.crt.pem
-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7
7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS
0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB
BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp
KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI
zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR
nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP
mygUY7Ii2zbdCdliiow=
-----END CERTIFICATE-----

TUFについて詳しいことは分かっていませんが、コード実行時にremoteのファイルリストを確認し、証明書を取得していると思われます。

ユーザの証明書

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            3a:ba:bc:ad:ec:42:df:56:b2:06:89:16:8d:a3:97:db:c3:d4:74:46
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: O = sigstore.dev, CN = sigstore-intermediate
        Validity
            Not Before: Sep 18 01:11:49 2022 GMT
            Not After : Sep 18 01:21:49 2022 GMT
        Subject:
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:8b:6a:4a:90:97:49:c6:b0:d9:ac:2b:83:4f:e2:
                    43:37:fe:b7:45:2f:47:73:3d:72:af:86:01:61:56:
                    0b:9c:8e:21:9c:5a:a7:00:cb:f1:eb:cc:5b:8d:65:
                    d6:dc:ef:23:11:62:2c:82:43:06:70:66:be:82:26:
                    08:0d:5d:1b:15
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                Code Signing
            X509v3 Subject Key Identifier:
                38:8B:80:A0:7D:93:9A:46:3F:50:B7:75:B3:29:A3:33:72:8C:99:5B
            X509v3 Authority Key Identifier:
                DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
            X509v3 Subject Alternative Name: critical
                email:sasoakira6114@gmail.com
            1.3.6.1.4.1.57264.1.1:
                https://accounts.google.com
            CT Precertificate SCTs:
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : 08:60:92:F0:28:52:FF:68:45:D1:D1:6B:27:84:9C:45:
                                67:18:AC:16:3D:C3:38:D2:6D:E6:BC:22:06:36:6F:72
                    Timestamp : Sep 18 01:11:49.920 2022 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:45:02:20:41:61:4C:45:A4:47:72:BC:06:A4:0A:39:
                                FE:A1:E4:95:8D:71:C0:08:EE:5C:CB:7C:1B:76:71:B5:
                                CA:56:BE:F3:02:21:00:FF:CC:D0:43:81:61:07:8C:D1:
                                40:BF:3C:16:E0:92:11:F6:A9:8D:D5:65:B4:4F:A1:5F:
                                D9:48:B0:EB:7B:BB:95
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:66:02:31:00:e9:18:9e:30:c6:72:e5:34:a4:f5:97:3a:5b:
        c0:27:54:02:67:1f:b4:0a:19:a2:1a:ba:db:3e:cd:92:64:e0:
        5c:9a:84:4f:c5:5d:88:c0:93:6a:03:b8:e0:99:1b:e0:62:02:
        31:00:f3:d2:51:d2:c3:94:98:81:cd:49:17:64:4f:d3:24:9c:
        4c:89:64:43:62:28:e1:21:bc:79:f6:70:b4:9c:3b:ee:f9:4d:
        84:06:31:04:cc:79:45:7d:93:2b:2e:00:78:ed

この証明書はリクエストで送った公開鍵に対する証明書といくつかの拡張フィールドに値が含まれています。 how-certificate-issuing-works.md4 | Constructing a certificate の画像からもわかるかと思います。

https://raw.githubusercontent.com/sigstore/fulcio/main/docs/img/create-certificate.png

https://raw.githubusercontent.com/sigstore/fulcio/main/docs/img/create-certificate.png:image=https://raw.githubusercontent.com/sigstore/fulcio/main/docs/img/create-certificate.png

        Validity
            Not Before: Sep 18 01:11:49 2022 GMT
            Not After : Sep 18 01:21:49 2022 GMT

この証明書の有効期限は10分間となっています。

この証明書に含まれている公開鍵を使って、先ほどのsignedEmailAddressできることを以下のようなコードで検証することができます。

import base64
from cryptography.x509 import load_pem_x509_certificate

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes


cert = """-----BEGIN CERTIFICATE-----
MIICpDCCAimgAwIBAgIUOrq8rexC31ayBokWjaOX28PUdEYwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIwOTE4MDExMTQ5WhcNMjIwOTE4MDEyMTQ5WjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEi2pKkJdJxrDZrCuDT+JDN/63RS9Hcz1yr4YB
YVYLnI4hnFqnAMvx68xbjWXW3O8jEWIsgkMGcGa+giYIDV0bFaOCAUgwggFEMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUOIuA
oH2TmkY/ULd1symjM3KMmVswHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJQYDVR0RAQH/BBswGYEXc2Fzb2FraXJhNjExNEBnbWFpbC5jb20wKQYKKwYB
BAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5
AgQCBHwEegB4AHYACGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3IAAAGD
TibPoAAABAMARzBFAiBBYUxFpEdyvAakCjn+oeSVjXHACO5cy3wbdnG1yla+8wIh
AP/M0EOBYQeM0UC/PBbgkhH2qY3VZbRPoV/ZSLDre7uVMAoGCCqGSM49BAMDA2kA
MGYCMQDpGJ4wxnLlNKT1lzpbwCdUAmcftAoZohq62z7NkmTgXJqET8VdiMCTagO4
4Jkb4GICMQDz0lHSw5SYgc1JF2RP0yScTIlkQ2Io4SG8efZwtJw77vlNhAYxBMx5
RX2TKy4AeO0=
-----END CERTIFICATE-----"""


c = {
    "signedEmailAddress": "MEQCIEme0ouHErG7JnhFx94DtQuUGOJ38N+Uj1cVp8P/fM2MAiBv95ZfvLi1U9taWbA1p/T9FTrKZFmkqQF7XhZq3h+e2g==",
    "certificateSigningRequest": None,
}


z = load_pem_x509_certificate(cert.encode("utf-8"))

# verifyに失敗すると例外があがる
print(
    z.public_key().verify(
        signature=base64.b64decode(c["signedEmailAddress"]),
        data=b"sasoakira6114@gmail.com",
        signature_algorithm=ec.ECDSA(hashes.SHA256()),
    )
)

"""
$ python x.py
None
"""

X509v3 extensionsについて

X509v3 Subject Alternative Name: critical
    email:sasoakira6114@gmail.com

Subject Alternative Name にはメールアドレスが含まれています。

/docs/man1.0.2/man5/x509v3_config.html

1.3.6.1.4.1.57264.1.1:
    https://accounts.google.com

Object Identifier(OID)が 1.3.6.1.4.1.57264 は、sigstoreによって定義されているフィールドのようです。 fulcio/oid-info.md at main · sigstore/fulcio · GitHub

1.3.6.1.4.1.57264.1.1 は、 Issuerの情報らしく、Googleアカウントと紐づけた場合には https://accounts.google.com になりました。 また、Github アカウントと紐づけた場合には、 https://github.com/login/oauth となりました。

1.3.6.1.4.1.57264 には、他にもGithub ActionのWorkflowで作られた場合にはworkflowの情報が含まれるようです。

X509v3 extensionsのSCTについて

CT Precertificate SCTs:
    Signed Certificate Timestamp:
        Version   : v1 (0x0)
        Log ID    : 08:60:92:F0:28:52:FF:68:45:D1:D1:6B:27:84:9C:45:
                    67:18:AC:16:3D:C3:38:D2:6D:E6:BC:22:06:36:6F:72
        Timestamp : Sep 18 01:11:49.920 2022 GMT
        Extensions: none
        Signature : ecdsa-with-SHA256
                    30:45:02:20:41:61:4C:45:A4:47:72:BC:06:A4:0A:39:
                    FE:A1:E4:95:8D:71:C0:08:EE:5C:CB:7C:1B:76:71:B5:
                    CA:56:BE:F3:02:21:00:FF:CC:D0:43:81:61:07:8C:D1:
                    40:BF:3C:16:E0:92:11:F6:A9:8D:D5:65:B4:4F:A1:5F:
                    D9:48:B0:EB:7B:BB:95

CT Precertificate SCTsのOIDは 1.3.6.1.4.1.11129.2.4.2 で、説明は以下となっています。

One or more RFC 6962 Signed Certificate Timestamps

RFC 6962 とはCertificate Transparencyに関するものです。 6 | Certificate Transparency log inclusion にも Certificate Transparency に証明書発行のログをアップロードして、その情報を埋め込んだ証明書を作っていることが分かります。

certificate.transparency.dev

SCTの仕組みについては、Googleのサイト や、以下のような資料が詳しいです。 https://www.jnsa.org/seminar/pki-day/2016/data/1-2_oosumi.pdf

Log IDは、署名に使われた公開鍵のDER形式のsha256の値です。公開鍵はTUFのファイルリストから取得可能です。 計算方法は以下のブログが参考になります。

zkat.hatenablog.com

Log ID    : 08:60:92:F0:28:52:FF:68:45:D1:D1:6B:27:84:9C:45:67:18:AC:16:3D:C3:38:D2:6D:E6:BC:22:06:36:6F:72

LogIDを公開鍵から求めてみます。

$ curl -sO https://sigstore-tuf-root.storage.googleapis.com/targets/ctfe.pub

$ cat ctfe.pub
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu
dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==
-----END PUBLIC KEY-----⏎

$ openssl ec -outform der -pubin -in ctfe.pub | shasum -a 256
read EC key
writing EC key
086092f02852ff6845d1d16b27849c456718ac163dc338d26de6bc2206366f72  -

フォーマットの違いはありますが、LogIDが一致することが確認できました。

SCTを検証しているコードはこの関数。

cosign/verify.go at 31e665415f2da47356e4657e751443fcf5f394ed · sigstore/cosign · GitHub

rekorとのやりとり

rekorに、attestationと証明書を登録します。

> POST /api/v1/log/entries HTTP/1.1
> Host: rekor.sigstore.dev
> Accept: application/json
> Content-Length: 2080
> Content-Type: application/json

{
    "apiVersion": "0.0.1",
    "spec": {
        "content": {
            "envelope": "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ==\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIAs21tx59kbSkqA9kRE/b3De51SN62cbJSCWG6x9PDO7AiEAquGbnjchwI4D0P4m4njMnH2AsnR/zpzosRogQugDkJ8=\"}]}",
            "hash": {
                "algorithm": "sha256",
                "value": "505eaf34d3fd4b40b3d60734b1f14d1792d4dc91b0b2677993fd37b8d493a466"
            }
        },
        "publicKey": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwRENDQWltZ0F3SUJBZ0lVT3JxOHJleEMzMWF5Qm9rV2phT1gyOFBVZEVZd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09URTRNREV4TVRRNVdoY05Nakl3T1RFNE1ERXlNVFE1V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpMnBLa0pkSnhyRFpyQ3VEVCtKRE4vNjNSUzlIY3oxeXI0WUIKWVZZTG5JNGhuRnFuQU12eDY4eGJqV1hXM084akVXSXNna01HY0dhK2dpWUlEVjBiRmFPQ0FVZ3dnZ0ZFTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVPSXVBCm9IMlRta1kvVUxkMXN5bWpNM0tNbVZzd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0pRWURWUjBSQVFIL0JCc3dHWUVYYzJGemIyRnJhWEpoTmpFeE5FQm5iV0ZwYkM1amIyMHdLUVlLS3dZQgpCQUdEdnpBQkFRUWJhSFIwY0hNNkx5OWhZMk52ZFc1MGN5NW5iMjluYkdVdVkyOXRNSUdLQmdvckJnRUVBZFo1CkFnUUNCSHdFZWdCNEFIWUFDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJQUFBR0QKVGliUG9BQUFCQU1BUnpCRkFpQkJZVXhGcEVkeXZBYWtDam4rb2VTVmpYSEFDTzVjeTN3YmRuRzF5bGErOHdJaApBUC9NMEVPQllRZU0wVUMvUEJiZ2toSDJxWTNWWmJSUG9WL1pTTERyZTd1Vk1Bb0dDQ3FHU000OUJBTURBMmtBCk1HWUNNUURwR0o0d3huTGxOS1QxbHpwYndDZFVBbWNmdEFvWm9ocTYyejdOa21UZ1hKcUVUOFZkaU1DVGFnTzQKNEprYjRHSUNNUUR6MGxIU3c1U1lnYzFKRjJSUDB5U2NUSWxrUTJJbzRTRzhlZlp3dEp3Nzd2bE5oQVl4Qk14NQpSWDJUS3k0QWVPMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
    },
    "kind": "intoto"
}

< HTTP/2.0 201 Created
< Content-Type: application/json
< Date: Sun, 18 Sep 2022 01:11:53 GMT
< Location: /api/v1/log/entries/4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1
< Strict-Transport-Security: max-age=15724800; includeSubDomains
< Vary: Origin

{
    "4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1": {
        "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1MDVlYWYzNGQzZmQ0YjQwYjNkNjA3MzRiMWYxNGQxNzkyZDRkYzkxYjBiMjY3Nzk5M2ZkMzdiOGQ0OTNhNDY2In0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZDRjZjI2YWUwZmQ3OWRhMTU2ZjAzYzgwNjY2ZTFjZWFmZTljNjQ4ZWU2MGQwYjM2ODBlNGU4ZTQzZDNiZGNiMCJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdsdFowRjNTVUpCWjBsVlQzSnhPSEpsZUVNek1XRjVRbTlyVjJwaFQxZ3lPRkJWWkVWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVUlRSTlJFVjRUVlJSTlZkb1kwNU5ha2wzVDFSRk5FMUVSWGxOVkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZwTW5CTGEwcGtTbmh5UkZweVEzVkVWQ3RLUkU0dk5qTlNVemxJWTNveGVYSTBXVUlLV1ZaWlRHNUpOR2h1Um5GdVFVMTJlRFk0ZUdKcVYxaFhNMDg0YWtWWFNYTm5hMDFIWTBkaEsyZHBXVWxFVmpCaVJtRlBRMEZWWjNkblowWkZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZQU1hWQkNtOUlNbFJ0YTFrdlZVeGtNWE41YldwTk0wdE5iVlp6ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZExRbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTSGRGWldkQ05FRklXVUZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBRS1ZHbGlVRzlCUVVGQ1FVMUJVbnBDUmtGcFFrSlpWWGhHY0VWa2VYWkJZV3REYW00cmIyVlRWbXBZU0VGRFR6VmplVE4zWW1SdVJ6RjViR0VyT0hkSmFBcEJVQzlOTUVWUFFsbFJaVTB3VlVNdlVFSmlaMnRvU0RKeFdUTldXbUpTVUc5V0wxcFRURVJ5WlRkMVZrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tdEJDazFIV1VOTlVVUndSMG8wZDNodVRHeE9TMVF4Ykhwd1luZERaRlZCYldObWRFRnZXbTlvY1RZeWVqZE9hMjFVWjFoS2NVVlVPRlprYVUxRFZHRm5UelFLTkVwcllqUkhTVU5OVVVSNk1HeElVM2MxVTFsbll6RktSakpTVURCNVUyTlVTV3hyVVRKSmJ6UlRSemhsWmxwM2RFcDNOemQyYkU1b1FWbDRRazE0TlFwU1dESlVTM2swUVdWUE1EMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=",
        "integratedTime": 1663463513,
        "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
        "logIndex": 3527741,
        "verification": {
            "signedEntryTimestamp": "MEUCIQDDPOjpA4pKVf0go1NclpptEBALyqqmtJlhKi13w5CyxgIgB4lxM6RktSfMtoaeO8DffoN51ymoAZWAmJjvL6UrNz0="
        }
    }
}

リクエス

まず、spec.publicKey を見ていきます。ここには、fulcioから取得したユーザの証明書が入っています。

import base64

r = {
    "apiVersion": "0.0.1",
    "spec": {
        "content": {
            "envelope": '{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ==","signatures":[{"keyid":"","sig":"MEUCIAs21tx59kbSkqA9kRE/b3De51SN62cbJSCWG6x9PDO7AiEAquGbnjchwI4D0P4m4njMnH2AsnR/zpzosRogQugDkJ8="}]}',
            "hash": {
                "algorithm": "sha256",
                "value": "505eaf34d3fd4b40b3d60734b1f14d1792d4dc91b0b2677993fd37b8d493a466",
            },
        },
        "publicKey": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwRENDQWltZ0F3SUJBZ0lVT3JxOHJleEMzMWF5Qm9rV2phT1gyOFBVZEVZd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09URTRNREV4TVRRNVdoY05Nakl3T1RFNE1ERXlNVFE1V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpMnBLa0pkSnhyRFpyQ3VEVCtKRE4vNjNSUzlIY3oxeXI0WUIKWVZZTG5JNGhuRnFuQU12eDY4eGJqV1hXM084akVXSXNna01HY0dhK2dpWUlEVjBiRmFPQ0FVZ3dnZ0ZFTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVPSXVBCm9IMlRta1kvVUxkMXN5bWpNM0tNbVZzd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0pRWURWUjBSQVFIL0JCc3dHWUVYYzJGemIyRnJhWEpoTmpFeE5FQm5iV0ZwYkM1amIyMHdLUVlLS3dZQgpCQUdEdnpBQkFRUWJhSFIwY0hNNkx5OWhZMk52ZFc1MGN5NW5iMjluYkdVdVkyOXRNSUdLQmdvckJnRUVBZFo1CkFnUUNCSHdFZWdCNEFIWUFDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJQUFBR0QKVGliUG9BQUFCQU1BUnpCRkFpQkJZVXhGcEVkeXZBYWtDam4rb2VTVmpYSEFDTzVjeTN3YmRuRzF5bGErOHdJaApBUC9NMEVPQllRZU0wVUMvUEJiZ2toSDJxWTNWWmJSUG9WL1pTTERyZTd1Vk1Bb0dDQ3FHU000OUJBTURBMmtBCk1HWUNNUURwR0o0d3huTGxOS1QxbHpwYndDZFVBbWNmdEFvWm9ocTYyejdOa21UZ1hKcUVUOFZkaU1DVGFnTzQKNEprYjRHSUNNUUR6MGxIU3c1U1lnYzFKRjJSUDB5U2NUSWxrUTJJbzRTRzhlZlp3dEp3Nzd2bE5oQVl4Qk14NQpSWDJUS3k0QWVPMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
    },
    "kind": "intoto",
}

print(base64.b64decode(r["spec"]["publicKey"]).decode("utf-8"))

"""
> python b.py
-----BEGIN CERTIFICATE-----
MIICpDCCAimgAwIBAgIUOrq8rexC31ayBokWjaOX28PUdEYwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIwOTE4MDExMTQ5WhcNMjIwOTE4MDEyMTQ5WjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEi2pKkJdJxrDZrCuDT+JDN/63RS9Hcz1yr4YB
YVYLnI4hnFqnAMvx68xbjWXW3O8jEWIsgkMGcGa+giYIDV0bFaOCAUgwggFEMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUOIuA
oH2TmkY/ULd1symjM3KMmVswHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJQYDVR0RAQH/BBswGYEXc2Fzb2FraXJhNjExNEBnbWFpbC5jb20wKQYKKwYB
BAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5
AgQCBHwEegB4AHYACGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3IAAAGD
TibPoAAABAMARzBFAiBBYUxFpEdyvAakCjn+oeSVjXHACO5cy3wbdnG1yla+8wIh
AP/M0EOBYQeM0UC/PBbgkhH2qY3VZbRPoV/ZSLDre7uVMAoGCCqGSM49BAMDA2kA
MGYCMQDpGJ4wxnLlNKT1lzpbwCdUAmcftAoZohq62z7NkmTgXJqET8VdiMCTagO4
4Jkb4GICMQDz0lHSw5SYgc1JF2RP0yScTIlkQ2Io4SG8efZwtJw77vlNhAYxBMx5
RX2TKy4AeO0=
-----END CERTIFICATE-----
"""

リクエストのin-toto attestationを検証してみる

次に、spec.content.envelope を見ていきます。 おさらいとしてcosiginのコマンドを貼っておきます。

$ cat test-result.json
{"passed": true}

$ COSIGN_EXPERIMENTAL=1 ./cosign attest  --type 'https://example.com/TestResult/v1' --predicate ./test-result.json otms61/test-custom-attest

spec.content.envelope にはin-toto attestationの形式で、指定した test-result.json の値がpredicateとして、対象のレポジトリ( otms61/test-custom-attest ) がsubjectとして入っています。

in-toto attestationについては以前に記事を書いたので参考にしてみてください。 otameshi61.hatenablog.com

$ cat envelop.intoto.jsonl | jq .
{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ==",
  "signatures": [
    {
      "keyid": "",
      "sig": "MEUCIAs21tx59kbSkqA9kRE/b3De51SN62cbJSCWG6x9PDO7AiEAquGbnjchwI4D0P4m4njMnH2AsnR/zpzosRogQugDkJ8="
    }
  ]
}

sigにある署名を検証してみます。 in-toto attestaionはDSSE Envelopeという形式をとっていて、payloadTypeとpayloadをもとにPAEという文字列を生成し、この文字列に対してsigを生成しています。

github.com

公開鍵は証明書から取り出したものを使用しています。

import base64
from cryptography.x509 import load_pem_x509_certificate

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes


cert = """-----BEGIN CERTIFICATE-----
MIICpDCCAimgAwIBAgIUOrq8rexC31ayBokWjaOX28PUdEYwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIwOTE4MDExMTQ5WhcNMjIwOTE4MDEyMTQ5WjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEi2pKkJdJxrDZrCuDT+JDN/63RS9Hcz1yr4YB
YVYLnI4hnFqnAMvx68xbjWXW3O8jEWIsgkMGcGa+giYIDV0bFaOCAUgwggFEMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUOIuA
oH2TmkY/ULd1symjM3KMmVswHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJQYDVR0RAQH/BBswGYEXc2Fzb2FraXJhNjExNEBnbWFpbC5jb20wKQYKKwYB
BAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5
AgQCBHwEegB4AHYACGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3IAAAGD
TibPoAAABAMARzBFAiBBYUxFpEdyvAakCjn+oeSVjXHACO5cy3wbdnG1yla+8wIh
AP/M0EOBYQeM0UC/PBbgkhH2qY3VZbRPoV/ZSLDre7uVMAoGCCqGSM49BAMDA2kA
MGYCMQDpGJ4wxnLlNKT1lzpbwCdUAmcftAoZohq62z7NkmTgXJqET8VdiMCTagO4
4Jkb4GICMQDz0lHSw5SYgc1JF2RP0yScTIlkQ2Io4SG8efZwtJw77vlNhAYxBMx5
RX2TKy4AeO0=
-----END CERTIFICATE-----"""


p = {
    "payloadType": "application/vnd.in-toto+json",
    "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ==",
    "signatures": [
        {
            "keyid": "",
            "sig": "MEUCIAs21tx59kbSkqA9kRE/b3De51SN62cbJSCWG6x9PDO7AiEAquGbnjchwI4D0P4m4njMnH2AsnR/zpzosRogQugDkJ8=",
        }
    ],
}


decoded = base64.b64decode(p["payload"]).decode("utf-8")
pae = f"DSSEv1 {len(p['payloadType'])} {p['payloadType']} {len(decoded)} {decoded}".encode(
    "utf-8"
)


z = load_pem_x509_certificate(cert.encode("utf-8"))
# 検証に失敗すると例外があがる
print(
    z.public_key().verify(
        signature=base64.b64decode(p["signatures"][0]["sig"]),
        data=pae,
        signature_algorithm=ec.ECDSA(hashes.SHA256()),
    )
)

"""
$ python z.py
None
"""

in-toto attestationのstatementのsubjectについて

payloadをデコードすると以下の情報が入っています。

$ cat envelop.intoto.jsonl | jq ".payload | @base64d | fromjson"
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://example.com/TestResult/v1",
  "subject": [
    {
      "name": "index.docker.io/otms61/test-custom-attest",
      "digest": {
        "sha256": "92251458088c638061cda8fd8b403b76d661a4dc6b7ee71b6affcf1872557b2b"
      }
    }
  ],
  "predicate": {
    "passed": true
  }
}

predicate には --predicateで指定した test-result.json の内容が入っており、predicateTypeには --type で指定した https://example.com/TestResult/v1 が入っていることがわかります。

$ cat test-result.json
{"passed": true}

$ COSIGN_EXPERIMENTAL=1 ./cosign attest  --type 'https://example.com/TestResult/v1' --predicate ./test-result.json otms61/test-custom-attest

digest の sha256は、対象のレポジトリのマニフェストのsha256の値が入っています。

$ crane manifest otms61/test-custom-attest | shasum -a 256
92251458088c638061cda8fd8b403b76d661a4dc6b7ee71b6affcf1872557b2b  -

sigstoreがなぜsubjectに対照イメージのマニフェストハッシュ値を使っているかは以下のブログの 署名フォーマット に詳しいです。

knqyf263.hatenablog.com

リクエスト内のハッシュ値

残りのリクエストの値も見ていきます。

spec.content.hash の値は、 spec.content.envelopeハッシュ値です。

from hashlib import sha256

r = {
    "apiVersion": "0.0.1",
    "spec": {
        "content": {
            "envelope": '{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ==","signatures":[{"keyid":"","sig":"MEUCIAs21tx59kbSkqA9kRE/b3De51SN62cbJSCWG6x9PDO7AiEAquGbnjchwI4D0P4m4njMnH2AsnR/zpzosRogQugDkJ8="}]}',
            "hash": {
                "algorithm": "sha256",
                "value": "505eaf34d3fd4b40b3d60734b1f14d1792d4dc91b0b2677993fd37b8d493a466",
            },
        },
        "publicKey": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwRENDQWltZ0F3SUJBZ0lVT3JxOHJleEMzMWF5Qm9rV2phT1gyOFBVZEVZd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09URTRNREV4TVRRNVdoY05Nakl3T1RFNE1ERXlNVFE1V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpMnBLa0pkSnhyRFpyQ3VEVCtKRE4vNjNSUzlIY3oxeXI0WUIKWVZZTG5JNGhuRnFuQU12eDY4eGJqV1hXM084akVXSXNna01HY0dhK2dpWUlEVjBiRmFPQ0FVZ3dnZ0ZFTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVPSXVBCm9IMlRta1kvVUxkMXN5bWpNM0tNbVZzd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0pRWURWUjBSQVFIL0JCc3dHWUVYYzJGemIyRnJhWEpoTmpFeE5FQm5iV0ZwYkM1amIyMHdLUVlLS3dZQgpCQUdEdnpBQkFRUWJhSFIwY0hNNkx5OWhZMk52ZFc1MGN5NW5iMjluYkdVdVkyOXRNSUdLQmdvckJnRUVBZFo1CkFnUUNCSHdFZWdCNEFIWUFDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJQUFBR0QKVGliUG9BQUFCQU1BUnpCRkFpQkJZVXhGcEVkeXZBYWtDam4rb2VTVmpYSEFDTzVjeTN3YmRuRzF5bGErOHdJaApBUC9NMEVPQllRZU0wVUMvUEJiZ2toSDJxWTNWWmJSUG9WL1pTTERyZTd1Vk1Bb0dDQ3FHU000OUJBTURBMmtBCk1HWUNNUURwR0o0d3huTGxOS1QxbHpwYndDZFVBbWNmdEFvWm9ocTYyejdOa21UZ1hKcUVUOFZkaU1DVGFnTzQKNEprYjRHSUNNUUR6MGxIU3c1U1lnYzFKRjJSUDB5U2NUSWxrUTJJbzRTRzhlZlp3dEp3Nzd2bE5oQVl4Qk14NQpSWDJUS3k0QWVPMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
    },
    "kind": "intoto",
}

hash = sha256(r["spec"]["content"]["envelope"].encode("utf-8"))
print(hash.hexdigest() == r["spec"]["content"]["hash"]["value"])

"""
> python a.py
True
"""

レスポンス

< HTTP/2.0 201 Created
< Content-Type: application/json
< Date: Sun, 18 Sep 2022 01:11:53 GMT
< Location: /api/v1/log/entries/4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1
< Strict-Transport-Security: max-age=15724800; includeSubDomains
< Vary: Origin

{
    "4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1": {
        "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1MDVlYWYzNGQzZmQ0YjQwYjNkNjA3MzRiMWYxNGQxNzkyZDRkYzkxYjBiMjY3Nzk5M2ZkMzdiOGQ0OTNhNDY2In0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZDRjZjI2YWUwZmQ3OWRhMTU2ZjAzYzgwNjY2ZTFjZWFmZTljNjQ4ZWU2MGQwYjM2ODBlNGU4ZTQzZDNiZGNiMCJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdsdFowRjNTVUpCWjBsVlQzSnhPSEpsZUVNek1XRjVRbTlyVjJwaFQxZ3lPRkJWWkVWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVUlRSTlJFVjRUVlJSTlZkb1kwNU5ha2wzVDFSRk5FMUVSWGxOVkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZwTW5CTGEwcGtTbmh5UkZweVEzVkVWQ3RLUkU0dk5qTlNVemxJWTNveGVYSTBXVUlLV1ZaWlRHNUpOR2h1Um5GdVFVMTJlRFk0ZUdKcVYxaFhNMDg0YWtWWFNYTm5hMDFIWTBkaEsyZHBXVWxFVmpCaVJtRlBRMEZWWjNkblowWkZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZQU1hWQkNtOUlNbFJ0YTFrdlZVeGtNWE41YldwTk0wdE5iVlp6ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZExRbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTSGRGWldkQ05FRklXVUZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBRS1ZHbGlVRzlCUVVGQ1FVMUJVbnBDUmtGcFFrSlpWWGhHY0VWa2VYWkJZV3REYW00cmIyVlRWbXBZU0VGRFR6VmplVE4zWW1SdVJ6RjViR0VyT0hkSmFBcEJVQzlOTUVWUFFsbFJaVTB3VlVNdlVFSmlaMnRvU0RKeFdUTldXbUpTVUc5V0wxcFRURVJ5WlRkMVZrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tdEJDazFIV1VOTlVVUndSMG8wZDNodVRHeE9TMVF4Ykhwd1luZERaRlZCYldObWRFRnZXbTlvY1RZeWVqZE9hMjFVWjFoS2NVVlVPRlprYVUxRFZHRm5UelFLTkVwcllqUkhTVU5OVVVSNk1HeElVM2MxVTFsbll6RktSakpTVURCNVUyTlVTV3hyVVRKSmJ6UlRSemhsWmxwM2RFcDNOemQyYkU1b1FWbDRRazE0TlFwU1dESlVTM2swUVdWUE1EMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=",
        "integratedTime": 1663463513,
        "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
        "logIndex": 3527741,
        "verification": {
            "signedEntryTimestamp": "MEUCIQDDPOjpA4pKVf0go1NclpptEBALyqqmtJlhKi13w5CyxgIgB4lxM6RktSfMtoaeO8DffoN51ymoAZWAmJjvL6UrNz0="
        }
    }
}

ステータスコード201 Created で、Locationに作成されたリソースのパスが記載されています。 GETで取得した値の方が情報が多いため、こちらの値を見ていきます。

$ curl -s --request GET --url https://rekor.sigstore.dev/api/v1/log/entries/4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1 | jq .
{
  "4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1": {
    "attestation": {
      "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ=="
    },
    "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1MDVlYWYzNGQzZmQ0YjQwYjNkNjA3MzRiMWYxNGQxNzkyZDRkYzkxYjBiMjY3Nzk5M2ZkMzdiOGQ0OTNhNDY2In0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZDRjZjI2YWUwZmQ3OWRhMTU2ZjAzYzgwNjY2ZTFjZWFmZTljNjQ4ZWU2MGQwYjM2ODBlNGU4ZTQzZDNiZGNiMCJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdsdFowRjNTVUpCWjBsVlQzSnhPSEpsZUVNek1XRjVRbTlyVjJwaFQxZ3lPRkJWWkVWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVUlRSTlJFVjRUVlJSTlZkb1kwNU5ha2wzVDFSRk5FMUVSWGxOVkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZwTW5CTGEwcGtTbmh5UkZweVEzVkVWQ3RLUkU0dk5qTlNVemxJWTNveGVYSTBXVUlLV1ZaWlRHNUpOR2h1Um5GdVFVMTJlRFk0ZUdKcVYxaFhNMDg0YWtWWFNYTm5hMDFIWTBkaEsyZHBXVWxFVmpCaVJtRlBRMEZWWjNkblowWkZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZQU1hWQkNtOUlNbFJ0YTFrdlZVeGtNWE41YldwTk0wdE5iVlp6ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZExRbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTSGRGWldkQ05FRklXVUZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBRS1ZHbGlVRzlCUVVGQ1FVMUJVbnBDUmtGcFFrSlpWWGhHY0VWa2VYWkJZV3REYW00cmIyVlRWbXBZU0VGRFR6VmplVE4zWW1SdVJ6RjViR0VyT0hkSmFBcEJVQzlOTUVWUFFsbFJaVTB3VlVNdlVFSmlaMnRvU0RKeFdUTldXbUpTVUc5V0wxcFRURVJ5WlRkMVZrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tdEJDazFIV1VOTlVVUndSMG8wZDNodVRHeE9TMVF4Ykhwd1luZERaRlZCYldObWRFRnZXbTlvY1RZeWVqZE9hMjFVWjFoS2NVVlVPRlprYVUxRFZHRm5UelFLTkVwcllqUkhTVU5OVVVSNk1HeElVM2MxVTFsbll6RktSakpTVURCNVUyTlVTV3hyVVRKSmJ6UlRSemhsWmxwM2RFcDNOemQyYkU1b1FWbDRRazE0TlFwU1dESlVTM2swUVdWUE1EMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=",
    "integratedTime": 1663463513,
    "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
    "logIndex": 3527741,
    "verification": {
      "inclusionProof": {
        "hashes": [
          "c9758c506f78253208cfb417acf808db1c7f845bc7d84426041be88da141de78",
          "f1f4310c743e29c84c5d0668eb710d1b6647a11d3b4ecc080105eac8941ed858",
          "7ad323f32d80ac4800c4d305e97e4082ada7545c7ce18e7e760f2a254fc37545",
          "7900dfe673792ae932e9003fc6219f7608413ae36e2ea8cd82522324dae23d7d",
          "f97d5b3cec12ff3607a3b25fa51e36c6fbb13553ec93d5cdc1ff3cdf509e17a6",
          "19cdf066e43cdde3b8782e2177b02b47891bae7b523238064c12a3e576f359bd",
          "ae95a5a30dc3f4f4aaa324f7f7d028a61b4c752efc2c5e680e4cb2b4bc2c616d",
          "5de43cdc0fc444eff6cdec0f8e36ff3dc02cd672fc03e4a06b51cf2022d2e575",
          "c9509918f560d8d4a7c688a236af36fde5e15e21e1548cf130c98695810970b8",
          "cdf24d1833eeef52de4c31b84a2f481930e6c2167ca1c9820e5311dc01287fd5",
          "81e8c50dd54ea567e80fc4427d61d26a9ad16e29cdaea6ac87e69aaed32a54f4",
          "fe79312498d289bcb6bc0d4226b21cabb71bb260f987bb711b548a5fe1f83b6e",
          "b06b1025bd35f834f3c39786ef4e554bf06c76f61e497fd1765d0689480d3107",
          "bca0b68bd01bcbedb77d5ef7760d79854383eb1359263884cee78eb7955766f9",
          "7ae973cb7963f4c8609ee238ba06d9d84f1f73be8aa145279e2110e59c7862a3",
          "f8b4461eb78dadbd7cffd9a281185b7b1f2dfee61c17688016dc75c40b728eb2",
          "4e659e217261878d89a86f48b5a2f1a8bf2e47aad22d077ad45d0bcec969c383",
          "72ffc75e39e3042b31f3ee5e6f6315726d1263c7ccc6465bfca688cc2f612247",
          "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b",
          "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b"
        ],
        "logIndex": 3527741,
        "rootHash": "5f1fbe1b5cb5b9daff35778d78ef02a2f11584539a59c62835afac3ef3198da1",
        "treeSize": 3531728
      },
      "signedEntryTimestamp": "MEYCIQCLfobPJEVNQ7//arswv62/R1PgQMAelCl5zHa+tr0dFwIhAMGNKi77UKqKrFeTeFlaLQzPs5pTwFf6+uHCj56fn1TB"
    }
  }
}

まず、1番上の 4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1 はrekorの中でUUIDと呼ばれるIDで、エントリに一意に紐づくID。また、logIndex の3527741も同様にエントリに紐づく値になっています。

logIDは、rekorの公開鍵のハッシュ値となっています。logIDという名前はSCTの公開鍵のハッシュ値の時に使われていた名称から持ってきていると思われますが、rekorのレスポンスに並べられると、エントリのindexのlogIndexなどがありかなり分かりにくいですね。。

# TUFから取得
> curl -s https://sigstore-tuf-root.storage.googleapis.com/targets/rekor.pub | openssl ec -outform der -pubin | shasum -a 256
read EC key
writing EC key
c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d  -

# rekorから取得
> curl -s --request GET  --url https://rekor.sigstore.dev/api/v1/log/publicKey  | openssl ec -outform der -pubin | shasum -a 256
read EC key
writing EC key
c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d  -

attestation.data には、アップロードしたattestaionのpayloadが入っています。

bodyの中身

body は デコードすると以下の情報が入っています。

{
  "apiVersion": "0.0.1",
  "kind": "intoto",
  "spec": {
    "content": {
      "hash": {
        "algorithm": "sha256",
        "value": "505eaf34d3fd4b40b3d60734b1f14d1792d4dc91b0b2677993fd37b8d493a466"
      },
      "payloadHash": {
        "algorithm": "sha256",
        "value": "d4cf26ae0fd79da156f03c80666e1ceafe9c648ee60d0b3680e4e8e43d3bdcb0"
      }
    },
    "publicKey": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwRENDQWltZ0F3SUJBZ0lVT3JxOHJleEMzMWF5Qm9rV2phT1gyOFBVZEVZd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09URTRNREV4TVRRNVdoY05Nakl3T1RFNE1ERXlNVFE1V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpMnBLa0pkSnhyRFpyQ3VEVCtKRE4vNjNSUzlIY3oxeXI0WUIKWVZZTG5JNGhuRnFuQU12eDY4eGJqV1hXM084akVXSXNna01HY0dhK2dpWUlEVjBiRmFPQ0FVZ3dnZ0ZFTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVPSXVBCm9IMlRta1kvVUxkMXN5bWpNM0tNbVZzd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0pRWURWUjBSQVFIL0JCc3dHWUVYYzJGemIyRnJhWEpoTmpFeE5FQm5iV0ZwYkM1amIyMHdLUVlLS3dZQgpCQUdEdnpBQkFRUWJhSFIwY0hNNkx5OWhZMk52ZFc1MGN5NW5iMjluYkdVdVkyOXRNSUdLQmdvckJnRUVBZFo1CkFnUUNCSHdFZWdCNEFIWUFDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJQUFBR0QKVGliUG9BQUFCQU1BUnpCRkFpQkJZVXhGcEVkeXZBYWtDam4rb2VTVmpYSEFDTzVjeTN3YmRuRzF5bGErOHdJaApBUC9NMEVPQllRZU0wVUMvUEJiZ2toSDJxWTNWWmJSUG9WL1pTTERyZTd1Vk1Bb0dDQ3FHU000OUJBTURBMmtBCk1HWUNNUURwR0o0d3huTGxOS1QxbHpwYndDZFVBbWNmdEFvWm9ocTYyejdOa21UZ1hKcUVUOFZkaU1DVGFnTzQKNEprYjRHSUNNUUR6MGxIU3c1U1lnYzFKRjJSUDB5U2NUSWxrUTJJbzRTRzhlZlp3dEp3Nzd2bE5oQVl4Qk14NQpSWDJUS3k0QWVPMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
  }
}

spec.content.hash には、リクエストでも送った in-toto attestationのハッシュ値が入っています。 spec.publicKey には、リクエストでも送った fulcioから受け取ったユーザの証明書が含まれています。

spec.content.payloadHash には、attestation.data をデコードした値のsha256が入っています。

import base64
import json
import hashlib

r = {
    "4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1": {
        "attestation": {
            "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ=="
        },
        "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1MDVlYWYzNGQzZmQ0YjQwYjNkNjA3MzRiMWYxNGQxNzkyZDRkYzkxYjBiMjY3Nzk5M2ZkMzdiOGQ0OTNhNDY2In0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZDRjZjI2YWUwZmQ3OWRhMTU2ZjAzYzgwNjY2ZTFjZWFmZTljNjQ4ZWU2MGQwYjM2ODBlNGU4ZTQzZDNiZGNiMCJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdsdFowRjNTVUpCWjBsVlQzSnhPSEpsZUVNek1XRjVRbTlyVjJwaFQxZ3lPRkJWWkVWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVUlRSTlJFVjRUVlJSTlZkb1kwNU5ha2wzVDFSRk5FMUVSWGxOVkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZwTW5CTGEwcGtTbmh5UkZweVEzVkVWQ3RLUkU0dk5qTlNVemxJWTNveGVYSTBXVUlLV1ZaWlRHNUpOR2h1Um5GdVFVMTJlRFk0ZUdKcVYxaFhNMDg0YWtWWFNYTm5hMDFIWTBkaEsyZHBXVWxFVmpCaVJtRlBRMEZWWjNkblowWkZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZQU1hWQkNtOUlNbFJ0YTFrdlZVeGtNWE41YldwTk0wdE5iVlp6ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZExRbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTSGRGWldkQ05FRklXVUZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBRS1ZHbGlVRzlCUVVGQ1FVMUJVbnBDUmtGcFFrSlpWWGhHY0VWa2VYWkJZV3REYW00cmIyVlRWbXBZU0VGRFR6VmplVE4zWW1SdVJ6RjViR0VyT0hkSmFBcEJVQzlOTUVWUFFsbFJaVTB3VlVNdlVFSmlaMnRvU0RKeFdUTldXbUpTVUc5V0wxcFRURVJ5WlRkMVZrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tdEJDazFIV1VOTlVVUndSMG8wZDNodVRHeE9TMVF4Ykhwd1luZERaRlZCYldObWRFRnZXbTlvY1RZeWVqZE9hMjFVWjFoS2NVVlVPRlprYVUxRFZHRm5UelFLTkVwcllqUkhTVU5OVVVSNk1HeElVM2MxVTFsbll6RktSakpTVURCNVUyTlVTV3hyVVRKSmJ6UlRSemhsWmxwM2RFcDNOemQyYkU1b1FWbDRRazE0TlFwU1dESlVTM2swUVdWUE1EMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=",
        "integratedTime": 1663463513,
        "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
        "logIndex": 3527741,
        "verification": {
            "inclusionProof": {
                "hashes": [
                    "c9758c506f78253208cfb417acf808db1c7f845bc7d84426041be88da141de78",
                    "f1f4310c743e29c84c5d0668eb710d1b6647a11d3b4ecc080105eac8941ed858",
                    "7ad323f32d80ac4800c4d305e97e4082ada7545c7ce18e7e760f2a254fc37545",
                    "7900dfe673792ae932e9003fc6219f7608413ae36e2ea8cd82522324dae23d7d",
                    "f97d5b3cec12ff3607a3b25fa51e36c6fbb13553ec93d5cdc1ff3cdf509e17a6",
                    "19cdf066e43cdde3b8782e2177b02b47891bae7b523238064c12a3e576f359bd",
                    "ae95a5a30dc3f4f4aaa324f7f7d028a61b4c752efc2c5e680e4cb2b4bc2c616d",
                    "5de43cdc0fc444eff6cdec0f8e36ff3dc02cd672fc03e4a06b51cf2022d2e575",
                    "c9509918f560d8d4a7c688a236af36fde5e15e21e1548cf130c98695810970b8",
                    "cdf24d1833eeef52de4c31b84a2f481930e6c2167ca1c9820e5311dc01287fd5",
                    "81e8c50dd54ea567e80fc4427d61d26a9ad16e29cdaea6ac87e69aaed32a54f4",
                    "fe79312498d289bcb6bc0d4226b21cabb71bb260f987bb711b548a5fe1f83b6e",
                    "b06b1025bd35f834f3c39786ef4e554bf06c76f61e497fd1765d0689480d3107",
                    "bca0b68bd01bcbedb77d5ef7760d79854383eb1359263884cee78eb7955766f9",
                    "7ae973cb7963f4c8609ee238ba06d9d84f1f73be8aa145279e2110e59c7862a3",
                    "f8b4461eb78dadbd7cffd9a281185b7b1f2dfee61c17688016dc75c40b728eb2",
                    "4e659e217261878d89a86f48b5a2f1a8bf2e47aad22d077ad45d0bcec969c383",
                    "72ffc75e39e3042b31f3ee5e6f6315726d1263c7ccc6465bfca688cc2f612247",
                    "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b",
                    "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b",
                ],
                "logIndex": 3527741,
                "rootHash": "5f1fbe1b5cb5b9daff35778d78ef02a2f11584539a59c62835afac3ef3198da1",
                "treeSize": 3531728,
            },
            "signedEntryTimestamp": "MEYCIQCLfobPJEVNQ7//arswv62/R1PgQMAelCl5zHa+tr0dFwIhAMGNKi77UKqKrFeTeFlaLQzPs5pTwFf6+uHCj56fn1TB",
        },
    }
}

hashed = hashlib.sha256(
    base64.b64decode(
        r["4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1"][
            "attestation"
        ]["data"].encode("utf-8")
    )
).hexdigest()
body = json.loads(
    base64.b64decode(
        r["4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1"]["body"]
    ).decode("utf-8")
)
print(hashed == body["spec"]["content"]["payloadHash"]["value"])

"""
> python c.py
True
"""

verification について - inclusionProof

verification.inclusionProof は、trillian のデータ構造の値が埋め込んであるようですが、rekor のクライアントの検証にも使われていないため、どう活用するかはまだ確認中です

2022/10/3 追記 @knqyf263 さんに VerifyTLogEntryの中でinclusionProofが使われていることを教えていただきました。ありがとうございます!

cosign/tlog.go at b3b6ae25362dc2c92c78abf2370ba0342ee86b2f · sigstore/cosign · GitHub

inclusionProof は、trillianが使っているデータ構造のMerkle Treeの検証に必要な情報が含まれています。 検証の流れを理解するには以下のブログを読んでもらうのが1番良いと思います。

transparency.dev

簡単に流れを説明すると、rekorのレスポンスに含まれるbodyから得られる値(下図の②)からleaf(B)を計算し、BとAからCを計算します。その後にCとDからrootのEまで計算します。

https://transparency.dev/images/merkle-trees/merkle-tree-a-b-d.svg

https://transparency.dev/images/merkle-trees/merkle-tree-a-b-d.svg

ここで、AとDについては今まで全く出てこない値となります。rekorでは、これらの値はサーバのレスポンスから受け取ります。

したがって、rekorのレスポンスに含まれるbodyと、inclusionProofのhashesの値からrootのハッシュまで計算していきます。

rootのハッシュは別途inclusionProofにrootHashとして与えられます。また、このrootのハッシュ値はrekorに署名された形で取得も可能のようです。

以下のようなコードでrekorからのレスポンスからrootハッシュを計算することが可能です。

import base64
import hashlib


res = {
    "362f8ecba72f43264c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1": {
        "attestation": {
            "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ=="
        },
        "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1MDVlYWYzNGQzZmQ0YjQwYjNkNjA3MzRiMWYxNGQxNzkyZDRkYzkxYjBiMjY3Nzk5M2ZkMzdiOGQ0OTNhNDY2In0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZDRjZjI2YWUwZmQ3OWRhMTU2ZjAzYzgwNjY2ZTFjZWFmZTljNjQ4ZWU2MGQwYjM2ODBlNGU4ZTQzZDNiZGNiMCJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdsdFowRjNTVUpCWjBsVlQzSnhPSEpsZUVNek1XRjVRbTlyVjJwaFQxZ3lPRkJWWkVWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVUlRSTlJFVjRUVlJSTlZkb1kwNU5ha2wzVDFSRk5FMUVSWGxOVkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZwTW5CTGEwcGtTbmh5UkZweVEzVkVWQ3RLUkU0dk5qTlNVemxJWTNveGVYSTBXVUlLV1ZaWlRHNUpOR2h1Um5GdVFVMTJlRFk0ZUdKcVYxaFhNMDg0YWtWWFNYTm5hMDFIWTBkaEsyZHBXVWxFVmpCaVJtRlBRMEZWWjNkblowWkZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZQU1hWQkNtOUlNbFJ0YTFrdlZVeGtNWE41YldwTk0wdE5iVlp6ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZExRbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTSGRGWldkQ05FRklXVUZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBRS1ZHbGlVRzlCUVVGQ1FVMUJVbnBDUmtGcFFrSlpWWGhHY0VWa2VYWkJZV3REYW00cmIyVlRWbXBZU0VGRFR6VmplVE4zWW1SdVJ6RjViR0VyT0hkSmFBcEJVQzlOTUVWUFFsbFJaVTB3VlVNdlVFSmlaMnRvU0RKeFdUTldXbUpTVUc5V0wxcFRURVJ5WlRkMVZrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tdEJDazFIV1VOTlVVUndSMG8wZDNodVRHeE9TMVF4Ykhwd1luZERaRlZCYldObWRFRnZXbTlvY1RZeWVqZE9hMjFVWjFoS2NVVlVPRlprYVUxRFZHRm5UelFLTkVwcllqUkhTVU5OVVVSNk1HeElVM2MxVTFsbll6RktSakpTVURCNVUyTlVTV3hyVVRKSmJ6UlRSemhsWmxwM2RFcDNOemQyYkU1b1FWbDRRazE0TlFwU1dESlVTM2swUVdWUE1EMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=",
        "integratedTime": 1663463513,
        "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
        "logIndex": 3527741,
        "verification": {
            "inclusionProof": {
                "checkpoint": "rekor.sigstore.dev - 3904496407287907110\n4163431\nTQBqpG78tgfdUdkAsSE3VMUMySUcNAXGwlYdnWovMjk=\nTimestamp: 1664726649849955098\n\n— rekor.sigstore.dev wNI9ajBFAiEA2fSWL7E94qWhPUcYBULuLtRWe7gFy0yyIConYkLkp6wCIHQ6ObwfZPcn28sCLe9DoDb7mdVXAo/a5qtX5q5N1JIR\n",
                "hashes": [
                    "c9758c506f78253208cfb417acf808db1c7f845bc7d84426041be88da141de78",
                    "f1f4310c743e29c84c5d0668eb710d1b6647a11d3b4ecc080105eac8941ed858",
                    "7ad323f32d80ac4800c4d305e97e4082ada7545c7ce18e7e760f2a254fc37545",
                    "7900dfe673792ae932e9003fc6219f7608413ae36e2ea8cd82522324dae23d7d",
                    "f97d5b3cec12ff3607a3b25fa51e36c6fbb13553ec93d5cdc1ff3cdf509e17a6",
                    "19cdf066e43cdde3b8782e2177b02b47891bae7b523238064c12a3e576f359bd",
                    "ae95a5a30dc3f4f4aaa324f7f7d028a61b4c752efc2c5e680e4cb2b4bc2c616d",
                    "5de43cdc0fc444eff6cdec0f8e36ff3dc02cd672fc03e4a06b51cf2022d2e575",
                    "c9509918f560d8d4a7c688a236af36fde5e15e21e1548cf130c98695810970b8",
                    "cdf24d1833eeef52de4c31b84a2f481930e6c2167ca1c9820e5311dc01287fd5",
                    "81e8c50dd54ea567e80fc4427d61d26a9ad16e29cdaea6ac87e69aaed32a54f4",
                    "fe79312498d289bcb6bc0d4226b21cabb71bb260f987bb711b548a5fe1f83b6e",
                    "b06b1025bd35f834f3c39786ef4e554bf06c76f61e497fd1765d0689480d3107",
                    "628304d1503a60c71421b080a8e63f9f412e5434084dbfb66dfdef1a785e2034",
                    "7ae973cb7963f4c8609ee238ba06d9d84f1f73be8aa145279e2110e59c7862a3",
                    "f8b4461eb78dadbd7cffd9a281185b7b1f2dfee61c17688016dc75c40b728eb2",
                    "4e659e217261878d89a86f48b5a2f1a8bf2e47aad22d077ad45d0bcec969c383",
                    "3e7b39f35274294419372997dc29033a0cdb7c1f628a44df62eadcaa9a487e35",
                    "72ffc75e39e3042b31f3ee5e6f6315726d1263c7ccc6465bfca688cc2f612247",
                    "b387d94cf186d8be69e18e15422d3c85a6146d6527a8653c0fa07423d45d2dcb",
                    "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b",
                    "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b",
                ],
                "logIndex": 3527741,
                "rootHash": "4d006aa46efcb607dd51d900b1213754c50cc9251c3405c6c2561d9d6a2f3239",
                "treeSize": 4163431,
            },
            "signedEntryTimestamp": "MEUCICqfXqsxO8GiismNP4xykravxTrmGBF550HELnzDBumcAiEAl8FeAWT3Hk7BBx6Dgol/Uzy+GaLP347Djf3jM7dxQSs=",
        },
    }
}
r = res[
    "362f8ecba72f43264c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1"
]


leaf = hashlib.sha256(b"\x00" + base64.b64decode(r["body"])).hexdigest()
proof = r["verification"]["inclusionProof"]
index = proof["logIndex"]
size = proof["treeSize"]

inner = len(bin(index ^ (size - 1)).lstrip("0b"))
border = len(bin(index >> inner).lstrip("0b"))


def hash_children(l, r):
    return hashlib.sha256(b"\x01" + bytes.fromhex(l) + bytes.fromhex(r)).hexdigest()


seed = leaf

for i, h in enumerate(proof["hashes"][:inner]):
    if (index >> i) & 1 == 0:
        seed = hash_children(seed, h)
    else:
        seed = hash_children(h, seed)

for i, h in enumerate(proof["hashes"][inner:]):
    seed = hash_children(h, seed)

print(seed == proof["rootHash"])

"""
> python x.py
True
"""

verification について - signedEntryTimestamp

verification.signedEntryTimestamp を見ていきます。

"signedEntryTimestamp": "MEYCIQCLfobPJEVNQ7//arswv62/R1PgQMAelCl5zHa+tr0dFwIhAMGNKi77UKqKrFeTeFlaLQzPs5pTwFf6+uHCj56fn1TB",

これはレスポンスに含まれる integratedTime, logIndex, body, logID をrekorが署名した値になっています。 検証に使うための鍵はAPI経由で取得することができます。

> curl --request GET  --url https://rekor.sigstore.dev/api/v1/log/publicKey

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
-----END PUBLIC KEY-----

データは JSON Canonicalization Scheme という、jsonの形式が一意に定まるようにフォーマットを使っています。 datatracker.ietf.org

import base64
import hashlib

import ecdsa
from ecdsa.util import sigdecode_der
import canonicaljson


res = {
    "4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1": {
        "attestation": {
            "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2V4YW1wbGUuY29tL1Rlc3RSZXN1bHQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL290bXM2MS90ZXN0LWN1c3RvbS1hdHRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiOTIyNTE0NTgwODhjNjM4MDYxY2RhOGZkOGI0MDNiNzZkNjYxYTRkYzZiN2VlNzFiNmFmZmNmMTg3MjU1N2IyYiJ9fV0sInByZWRpY2F0ZSI6eyJwYXNzZWQiOnRydWV9fQ=="
        },
        "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1MDVlYWYzNGQzZmQ0YjQwYjNkNjA3MzRiMWYxNGQxNzkyZDRkYzkxYjBiMjY3Nzk5M2ZkMzdiOGQ0OTNhNDY2In0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZDRjZjI2YWUwZmQ3OWRhMTU2ZjAzYzgwNjY2ZTFjZWFmZTljNjQ4ZWU2MGQwYjM2ODBlNGU4ZTQzZDNiZGNiMCJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdsdFowRjNTVUpCWjBsVlQzSnhPSEpsZUVNek1XRjVRbTlyVjJwaFQxZ3lPRkJWWkVWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVUlRSTlJFVjRUVlJSTlZkb1kwNU5ha2wzVDFSRk5FMUVSWGxOVkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZwTW5CTGEwcGtTbmh5UkZweVEzVkVWQ3RLUkU0dk5qTlNVemxJWTNveGVYSTBXVUlLV1ZaWlRHNUpOR2h1Um5GdVFVMTJlRFk0ZUdKcVYxaFhNMDg0YWtWWFNYTm5hMDFIWTBkaEsyZHBXVWxFVmpCaVJtRlBRMEZWWjNkblowWkZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZQU1hWQkNtOUlNbFJ0YTFrdlZVeGtNWE41YldwTk0wdE5iVlp6ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZExRbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTSGRGWldkQ05FRklXVUZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBRS1ZHbGlVRzlCUVVGQ1FVMUJVbnBDUmtGcFFrSlpWWGhHY0VWa2VYWkJZV3REYW00cmIyVlRWbXBZU0VGRFR6VmplVE4zWW1SdVJ6RjViR0VyT0hkSmFBcEJVQzlOTUVWUFFsbFJaVTB3VlVNdlVFSmlaMnRvU0RKeFdUTldXbUpTVUc5V0wxcFRURVJ5WlRkMVZrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tdEJDazFIV1VOTlVVUndSMG8wZDNodVRHeE9TMVF4Ykhwd1luZERaRlZCYldObWRFRnZXbTlvY1RZeWVqZE9hMjFVWjFoS2NVVlVPRlprYVUxRFZHRm5UelFLTkVwcllqUkhTVU5OVVVSNk1HeElVM2MxVTFsbll6RktSakpTVURCNVUyTlVTV3hyVVRKSmJ6UlRSemhsWmxwM2RFcDNOemQyYkU1b1FWbDRRazE0TlFwU1dESlVTM2swUVdWUE1EMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=",
        "integratedTime": 1663463513,
        "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
        "logIndex": 3527741,
        "verification": {
            "inclusionProof": {
                "hashes": [
                    "c9758c506f78253208cfb417acf808db1c7f845bc7d84426041be88da141de78",
                    "f1f4310c743e29c84c5d0668eb710d1b6647a11d3b4ecc080105eac8941ed858",
                    "7ad323f32d80ac4800c4d305e97e4082ada7545c7ce18e7e760f2a254fc37545",
                    "7900dfe673792ae932e9003fc6219f7608413ae36e2ea8cd82522324dae23d7d",
                    "f97d5b3cec12ff3607a3b25fa51e36c6fbb13553ec93d5cdc1ff3cdf509e17a6",
                    "19cdf066e43cdde3b8782e2177b02b47891bae7b523238064c12a3e576f359bd",
                    "ae95a5a30dc3f4f4aaa324f7f7d028a61b4c752efc2c5e680e4cb2b4bc2c616d",
                    "5de43cdc0fc444eff6cdec0f8e36ff3dc02cd672fc03e4a06b51cf2022d2e575",
                    "c9509918f560d8d4a7c688a236af36fde5e15e21e1548cf130c98695810970b8",
                    "cdf24d1833eeef52de4c31b84a2f481930e6c2167ca1c9820e5311dc01287fd5",
                    "81e8c50dd54ea567e80fc4427d61d26a9ad16e29cdaea6ac87e69aaed32a54f4",
                    "fe79312498d289bcb6bc0d4226b21cabb71bb260f987bb711b548a5fe1f83b6e",
                    "b06b1025bd35f834f3c39786ef4e554bf06c76f61e497fd1765d0689480d3107",
                    "bca0b68bd01bcbedb77d5ef7760d79854383eb1359263884cee78eb7955766f9",
                    "7ae973cb7963f4c8609ee238ba06d9d84f1f73be8aa145279e2110e59c7862a3",
                    "f8b4461eb78dadbd7cffd9a281185b7b1f2dfee61c17688016dc75c40b728eb2",
                    "4e659e217261878d89a86f48b5a2f1a8bf2e47aad22d077ad45d0bcec969c383",
                    "72ffc75e39e3042b31f3ee5e6f6315726d1263c7ccc6465bfca688cc2f612247",
                    "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b",
                    "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b",
                ],
                "logIndex": 3527741,
                "rootHash": "5f1fbe1b5cb5b9daff35778d78ef02a2f11584539a59c62835afac3ef3198da1",
                "treeSize": 3531728,
            },
            "signedEntryTimestamp": "MEYCIQCLfobPJEVNQ7//arswv62/R1PgQMAelCl5zHa+tr0dFwIhAMGNKi77UKqKrFeTeFlaLQzPs5pTwFf6+uHCj56fn1TB",
        },
    }
}
r = res["4c43de72686ceb257f6641ca1d62069f7f891822bca772d166dd88941e31c5f1"]

pub = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
-----END PUBLIC KEY-----"""


data = canonicaljson.encode_canonical_json(
    {
        "integratedTime": r["integratedTime"],
        "logIndex": r["logIndex"],
        "body": r["body"],
        "logID": r["logID"],
    }
)

v = ecdsa.VerifyingKey.from_pem(pub)
print(
    v.verify(
        signature=base64.b64decode(r["verification"]["signedEntryTimestamp"]),
        data=data,
        hashfunc=hashlib.sha256,
        sigdecode=sigdecode_der,
    )
)


"""
> python d.py
True
"""

verification.signedEntryTimestamp の検証には integratedTime, logIndex, body, logID を使っているため、attestationのデータの attestation.data は直接含まれていません。ただし、spec.content.payloadHash には attestation.data をデコードした値のsha256が入っています。そのため、verification.signedEntryTimestamp の検証後に、spec.content.payloadHash を確認することで、attestation.data が改ざんされていないことがわかります。

最後に

ひとつのコマンドでやり取りされる内容をまとめていたらだいぶ長くなってしまった。リクエスト内でhashやsignatureなどのキーで値がやり取りされているが、一体どこの値のハッシュを取ったのかや、どこの値をどこの鍵でsignatureを作っているのかなど曖昧な点が多かった。今回記事を書きながら手を動かせたことで、だいぶ理解が深まったと思う。

sigstore関連の話題は、関連する技術も同時に知ることができて結構楽しい。SCTの検証とinclusionProofは調べきれなかったのは心残りではある。