보호된 리소스에 대한 액세스가 Google CloudIAM에서 관리되지 않는 경우(예: 리소스가 다른 클라우드 서비스, 온프레미스 또는 휴대전화와 같은 로컬 기기에 저장된 경우)에도 이러한 리소스에 대한 액세스를 제공하는 기기(신뢰 당사자라고도 함)에 Confidential Space 워크로드를 인증할 수 있습니다.
이를 위해 신뢰 당사자는 맞춤 잠재고객 및 선택적 nonce를 사용하여 Confidential Space 증명 서비스에서 증명 토큰을 요청해야 합니다. 이와 같이 증명 토큰을 요청할 때는 리소스에 대한 액세스 권한을 부여하기 전에 자체 토큰 유효성 검사를 실행해야 합니다.
다음 문서에서는 Google Cloud외부의 리소스와 함께 Confidential Space를 사용하는 것과 관련된 개념을 다룹니다. 전체 과정을 살펴보려면 codelab을 참고하세요.
증명 토큰 흐름
증명 토큰은 신뢰 당사자를 대신하여 워크로드에서 요청하고 증명 서비스에서 반환합니다. 필요에 따라 맞춤 잠재고객을 정의하고 원하는 경우 nonce를 제공할 수 있습니다.
암호화되지 않음
토큰 검색 프로세스를 쉽게 이해할 수 있도록 여기에 표시된 흐름은 암호화를 사용하지 않습니다. 실제로는 TLS로 통신을 암호화하는 것이 좋습니다.
다음 다이어그램은 흐름을 보여줍니다.
신뢰 당사자는 생성한 nonce(선택사항)와 함께 워크로드에 토큰 요청을 전송합니다.
워크로드는 대상을 결정하고 요청에 대상을 추가한 후 기밀 정보 공간 런처에 요청을 전송합니다.
런처가 증명 서비스에 요청을 전송합니다.
증명 서비스는 지정된 대상 및 선택적 nonce가 포함된 토큰을 생성합니다.
증명 서비스는 토큰을 런처에 반환합니다.
런처는 워크로드에 토큰을 반환합니다.
워크로드는 토큰을 신뢰 당사자에게 반환합니다.
신뢰 당사자는 대상 및 선택적 nonce를 비롯한 클레임을 확인합니다.
TLS로 암호화됨
암호화되지 않은 흐름은 요청을 미디어 중간자 공격에 취약하게 만듭니다. nonce는 데이터 출력 또는 TLS 세션에 바인딩되지 않으므로 공격자가 요청을 가로채고 워크로드를 명의 도용할 수 있습니다.
이러한 유형의 공격을 방지하려면 신뢰 당사자와 워크로드 간에 TLS 세션을 설정하고 TLS 내보낸 키 자료 (EKM)를 nonce로 사용할 수 있습니다. TLS 내보낸 키 재료는 증명을 TLS 세션에 바인딩하고 증명 요청이 보안 채널을 통해 전송되었음을 확인합니다. 이 프로세스를 채널 결합이라고도 합니다.
다음 다이어그램은 채널 바인딩을 사용하는 흐름을 보여줍니다.
신뢰 당사자는 워크로드를 실행하는 컨피덴셜 VM과 보안 TLS 세션을 설정합니다.
신뢰 당사자는 안전한 TLS 세션을 사용하여 토큰 요청을 전송합니다.
워크로드는 대상을 결정하고 TLS 내보낸 키 자료를 사용하여 nonce를 생성합니다.
워크로드는 기밀 공간 런처에 요청을 전송합니다.
런처가 증명 서비스에 요청을 전송합니다.
증명 서비스는 지정된 대상 및 nonce가 포함된 토큰을 생성합니다.
증명 서비스는 토큰을 런처에 반환합니다.
런처는 워크로드에 토큰을 반환합니다.
워크로드는 토큰을 신뢰 당사자에게 반환합니다.
신뢰 당사자는 TLS로 내보낸 키 자료를 사용하여 nonce를 다시 생성합니다.
신뢰 당사자는 대상 및 nonce를 비롯한 클레임을 확인합니다. 토큰의 nonce는 신뢰 당사자가 재생성한 nonce와 일치해야 합니다.
증명 토큰 구조
증명 토큰은 다음과 같은 구조의 JSON 웹 토큰입니다.
헤더: 서명 알고리즘을 설명합니다. PKI 토큰은
x5c
필드의 헤더에 인증서 체인도 저장합니다.서명된 JSON 데이터 페이로드: 대상, 발급자, 대상, nonce, 만료 시간과 같은 신뢰 당사자의 워크로드에 관한 요구사항이 포함됩니다.
서명: 토큰이 전송 중에 변경되지 않았음을 확인합니다. 서명 사용에 관한 자세한 내용은 OpenID Connect ID 토큰을 검증하는 방법을 참고하세요.
다음 코드 샘플은 Confidential Space 240500 이미지에서 생성된 인코딩된 증명 토큰의 예입니다. 최신 이미지에는 추가 필드가 포함될 수 있습니다. https://jwt.io/를 사용하여 디코딩할 수 있습니다 (서명은 삭제됨).
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk<SIGNATURE>
다음은 이전 샘플의 디코딩된 버전입니다.
{
"alg": "HS256",
"kid": "12345",
"typ": "JWT"
}.
{
"aud": "AUDIENCE_NAME",
"dbgstat": "disabled-since-boot",
"eat_nonce": [
"NONCE_1",
"NONCE_2"
],
"eat_profile": "https://cloud.google.com/confidential-computing/confidential-space/docs/reference/token-claims",
"exp": 1721330075,
"google_service_accounts": [
"PROJECT_ID-compute@developer.gserviceaccount.com"
],
"hwmodel": "GCP_AMD_SEV",
"iat": 1721326475,
"iss": "https://confidentialcomputing.googleapis.com",
"nbf": 1721326475,
"oemid": 11129,
"secboot": true,
"sub": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/us-central1-a/instances/INSTANCE_NAME",
"submods": {
"confidential_space": {
"monitoring_enabled": {
"memory": false
},
"support_attributes": [
"LATEST",
"STABLE",
"USABLE"
]
},
"container": {
"args": [
"/customnonce",
"/docker-entrypoint.sh",
"nginx",
"-g",
"daemon off;"
],
"env": {
"HOSTNAME": "HOST_NAME",
"NGINX_VERSION": "1.27.0",
"NJS_RELEASE": "2~bookworm",
"NJS_VERSION": "0.8.4",
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"PKG_RELEASE": "2~bookworm"
},
"image_digest": "sha256:67682bda769fae1ccf5183192b8daf37b64cae99c6c3302650f6f8bf5f0f95df",
"image_id": "sha256:fffffc90d343cbcb01a5032edac86db5998c536cd0a366514121a45c6723765c",
"image_reference": "docker.io/library/nginx:latest",
"image_signatures": [
{
"key_id": "<hexadecimal-sha256-fingerprint-public-key1>",
"signature": "<base64-encoded-signature>",
"signature_algorithm": "RSASSA_PSS_SHA256"
},
{
"key_id": "<hexadecimal-sha256-fingerprint-public-key2>",
"signature": "<base64-encoded-signature>",
"signature_algorithm": "RSASSA_PSS_SHA256"
},
{
"key_id": "<hexadecimal-sha256-fingerprint-public-key3>",
"signature": "<base64-encoded-signature>",
"signature_algorithm": "ECDSA_P256_SHA256"
}
],
"restart_policy": "Never"
},
"gce": {
"instance_id": "INSTANCE_ID",
"instance_name": "INSTANCE_NAME",
"project_id": "PROJECT_ID",
"project_number": "PROJECT_NUMBER",
"zone": "us-central1-a"
}
},
"swname": "CONFIDENTIAL_SPACE",
"swversion": [
"240500"
]
}
증명 토큰 필드에 관한 자세한 설명은 증명 토큰 클레임을 참고하세요.
증명 토큰 검색
Confidential Space 환경에서 증명 토큰을 구현하려면 다음 단계를 완료하세요.
워크로드에서 HTTP 클라이언트를 설정합니다.
워크로드에서 HTTP 클라이언트를 사용하여 Unix 도메인 소켓을 통해 리슨 URL
http://localhost/v1/token
에 HTTP 요청을 전송합니다. 소켓 파일은/run/container_launcher/teeserver.sock
에 있습니다.
수신 대기 URL에 요청이 접수되면 Confidential Space 런처가 증명 증거 수집을 관리하고 증명 서비스에서 증명 토큰을 요청한 후 (맞춤 매개변수 전달) 생성된 토큰을 워크로드로 반환합니다.
다음 Go 코드 샘플은 IPC를 통해 실행기의 HTTP 서버와 통신하는 방법을 보여줍니다.
func getCustomTokenBytes(body string) ([]byte, error) {
httpClient := http.Client{
Transport: &http.Transport{
// Set the DialContext field to a function that creates
// a new network connection to a Unix domain socket
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", "/run/container_launcher/teeserver.sock")
},
},
}
// Get the token from the IPC endpoint
url := "http://localhost/v1/token"
resp, err := httpClient.Post(url, "application/json", strings.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to get raw token response: %w", err)
}
tokenbytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token body: %w", err)
}
fmt.Println(string(tokenbytes))
return tokenbytes, nil
}
맞춤 잠재고객으로 증명 토큰 요청
HTTP 메서드 및 URL:
POST http://localhost/v1/token
JSON 요청 본문:
{
"audience": "AUDIENCE_NAME",
"token_type": "TOKEN_TYPE",
"nonces": [
"NONCE_1",
"NONCE_2",
...
]
}
다음 값을 제공합니다.
AUDIENCE_NAME
: (필수사항) 신뢰 당사자에게 지정한 이름인 잠재고객 값입니다. 이는 워크로드에서 설정합니다.맞춤 잠재고객이 없는 토큰의 경우 이 필드의 기본값은
https://sts.google.com
입니다. 커스텀 잠재고객을 설정할 때는https://sts.google.com
값을 사용할 수 없습니다. 최대 길이는 512바이트입니다.토큰에 맞춤 잠재고객을 포함하려면 신뢰 당사자가 아닌 워크로드가 비공개 스페이스 증명 서비스에 요청을 보내기 전에 증명 토큰 요청에 맞춤 잠재고객을 추가해야 합니다. 이렇게 하면 신뢰 당사자가 액세스할 수 없는 보호된 리소스에 대한 토큰을 요청하지 못하도록 할 수 있습니다.
TOKEN_TYPE
: (필수사항) 반환할 토큰 유형입니다. 다음 유형 중 하나를 선택합니다.OIDC
: 이러한 토큰은 OIDC 토큰 검증 엔드포인트의jwks_uri
필드에 지정된 공개 키를 기준으로 검증됩니다. 공개 키는 정기적으로 순환됩니다.PKI
: 이러한 토큰은 PKI 토큰 유효성 검사 엔드포인트의root_ca_uri
필드에 지정된 루트 인증서에 대해 유효성 검사됩니다. 이 인증서는 직접 저장해야 합니다. 인증서는 10년마다 순환됩니다.
토큰 유효성 검사에 단기 만료 공개 키 대신 만료일이 긴 인증서가 사용되므로 IP 주소가 Google 서버에 자주 노출되지 않습니다. 즉, PKI 토큰은 OIDC 토큰보다 더 높은 수준의 개인 정보 보호를 제공합니다.
OpenSSL을 사용하여 인증서의 지문을 확인할 수 있습니다.
openssl x509 -fingerprint -in confidential_space_root.crt
디지털 지문은 다음 SHA-1 다이제스트와 일치해야 합니다.
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
NONCE
: 선택사항. 토큰을 한 번만 사용할 수 있도록 하는 고유하고 무작위이며 불투명한 값입니다. 이 값은 신뢰 당사자가 설정합니다. nonce는 최대 6개까지 허용됩니다. 각 nonce는 10~74바이트(양 끝값 포함)여야 합니다.nonce를 포함하는 경우 신뢰 당사자는 증명 토큰 요청에서 전송된 nonce가 반환된 토큰의 nonce와 동일한지 확인해야 합니다. 일치하지 않으면 신뢰 당사자는 토큰을 거부해야 합니다.
증명 토큰 파싱 및 유효성 검사
다음 Go 코드 샘플은 증명 토큰을 확인하는 방법을 보여줍니다.
OIDC 증명 토큰
package main
import (
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
)
const (
socketPath = "/run/container_launcher/teeserver.sock"
expectedIssuer = "https://confidentialcomputing.googleapis.com"
wellKnownPath = "/.well-known/openid-configuration"
)
type jwksFile struct {
Keys []jwk `json:"keys"`
}
type jwk struct {
N string `json:"n"` // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
E string `json:"e"` // "AQAB" or 65537 as an int
Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",
// Unused fields:
// Alg string `json:"alg"` // "RS256",
// Kty string `json:"kty"` // "RSA",
// Use string `json:"use"` // "sig",
}
type wellKnown struct {
JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"
// Unused fields:
// Iss string `json:"issuer"` // "https://confidentialcomputing.googleapis.com"
// Subject_types_supported string `json:"subject_types_supported"` // [ "public" ]
// Response_types_supported string `json:"response_types_supported"` // [ "id_token" ]
// Claims_supported string `json:"claims_supported"` // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
// Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
// Scopes_supported string `json:"scopes_supported"` // [ "openid" ]
}
func getWellKnownFile() (wellKnown, error) {
httpClient := http.Client{}
resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
}
wellKnownJSON, err := io.ReadAll(resp.Body)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
}
wk := wellKnown{}
json.Unmarshal(wellKnownJSON, &wk)
return wk, nil
}
func getJWKFile() (jwksFile, error) {
wk, err := getWellKnownFile()
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
}
// Get JWK URI from .wellknown
uri := wk.JwksURI
fmt.Printf("jwks URI: %v\n", uri)
httpClient := http.Client{}
resp, err := httpClient.Get(uri)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
}
jwkbytes, err := io.ReadAll(resp.Body)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
}
file := jwksFile{}
err = json.Unmarshal(jwkbytes, &file)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
}
return file, nil
}
// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return nil, err
}
z := new(big.Int)
z.SetBytes(b)
return z, nil
}
func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
keysfile, err := getJWKFile()
if err != nil {
return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
}
// Multiple keys are present in this endpoint to allow for key rotation.
// This method finds the key that was used for signing to pass to the validator.
kid := t.Header["kid"]
for _, key := range keysfile.Keys {
if key.Kid != kid {
continue // Select the key used for signing
}
n, err := base64urlUIntDecode(key.N)
if err != nil {
return nil, fmt.Errorf("failed to decode key.N %w", err)
}
e, err := base64urlUIntDecode(key.E)
if err != nil {
return nil, fmt.Errorf("failed to decode key.E %w", err)
}
// The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
// or an array of keys. We chose to show passing a single key in this example as its possible
// not all validators accept multiple keys for validation.
return &rsa.PublicKey{
N: n,
E: int(e.Int64()),
}, nil
}
return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}
func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
var err error
fmt.Println("Unmarshalling token and checking its validity...")
token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)
fmt.Printf("Token valid: %v", token.Valid)
if token.Valid {
return token, nil
}
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
}
if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
// If device time is not synchronized with the Attestation Service you may need to account for that here.
return nil, errors.New("token is not active yet")
}
if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
return nil, fmt.Errorf("token is expired")
}
return nil, fmt.Errorf("unknown validation error: %v", err)
}
return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}
func main() {
// Get a token from a workload running in Confidential Space
tokenbytes, err := getTokenBytesFromWorkload()
// Write a method to return a public key from the well-known endpoint
keyFunc := getRSAPublicKeyFromJWKsFile
// Verify properties of the original Confidential Space workload that generated the attestation
// using the token claims.
token, err := decodeAndValidateToken(tokenbytes, keyFunc)
if err != nil {
panic(err)
}
claimsString, err := json.MarshalIndent(token.Claims, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(claimsString))
}
PKI 증명 토큰
토큰을 확인하려면 신뢰 당사자가 다음 단계를 완료해야 합니다.
토큰의 헤더를 파싱하여 인증서 체인을 가져옵니다.
저장된 루트에 대해 인증서 체인을 검증합니다. 이전에 PKI 토큰 유효성 검사 엔드포인트에서 반환된
root_ca_uri
필드에 지정된 URL에서 루트 인증서를 다운로드해야 합니다.리프 인증서의 유효성을 확인합니다.
리프 인증서를 사용하여 헤더의
alg
키에 지정된 알고리즘을 사용하여 토큰 서명을 확인합니다.
토큰이 확인되면 신뢰 당사자는 토큰의 소유권 주장을 파싱할 수 있습니다.
// This code is an example of how to validate a PKI token. This library is not an official library,
// nor is it endorsed by Google.
// ValidatePKIToken validates the PKI token returned from the attestation service is valid.
// Returns a valid jwt.Token or returns an error if invalid.
func ValidatePKIToken(storedRootCertificate x509.Certificate, attestationToken string) (jwt.Token, error) {
// IMPORTANT: The attestation token should be considered untrusted until the certificate chain and
// the signature is verified.
jwtHeaders, err := ExtractJWTHeaders(attestationToken)
if err != nil {
return jwt.Token{}, fmt.Errorf("ExtractJWTHeaders(token) returned error: %v", err)
}
if jwtHeaders["alg"] != "RS256" {
return jwt.Token{}, fmt.Errorf("ValidatePKIToken(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - got Alg: %v, want: %v", jwtHeaders["alg"], "RS256")
}
// Additional Check: Validate the ALG in the header matches the certificate SPKI.
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7
// This is included in golangs jwt.Parse function
x5cHeaders := jwtHeaders["x5c"].([]any)
certificates, err := ExtractCertificatesFromX5CHeader(x5cHeaders)
if err != nil {
return jwt.Token{}, fmt.Errorf("ExtractCertificatesFromX5CHeader(x5cHeaders) returned error: %v", err)
}
// Verify the leaf certificate signature algorithm is an RSA key
if certificates.LeafCert.SignatureAlgorithm != x509.SHA256WithRSA {
return jwt.Token{}, fmt.Errorf("leaf certificate signature algorithm is not SHA256WithRSA")
}
// Verify the leaf certificate public key algorithm is RSA
if certificates.LeafCert.PublicKeyAlgorithm != x509.RSA {
return jwt.Token{}, fmt.Errorf("leaf certificate public key algorithm is not RSA")
}
// Verify the storedRootCertificate is the same as the root certificate returned in the token.
// storedRootCertificate is downloaded from the confidential computing well known endpoint
// https://confidentialcomputing.googleapis.com/.well-known/attestation-pki-root
err = CompareCertificates(storedRootCertificate, *certificates.RootCert)
if err != nil {
return jwt.Token{}, fmt.Errorf("failed to verify certificate chain: %v", err)
}
err = VerifyCertificateChain(certificates)
if err != nil {
return jwt.Token{}, fmt.Errorf("VerifyCertificateChain(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - error verifying x5c chain: %v", err)
}
keyFunc := func(token *jwt.Token) (any, error) {
return certificates.LeafCert.PublicKey, nil
}
verifiedJWT, err := jwt.Parse(attestationToken, keyFunc)
return *verifiedJWT, err
}
// ExtractJWTHeaders parses the JWT and returns the headers.
func ExtractJWTHeaders(token string) (map[string]any, error) {
parser := &jwt.Parser{}
// The claims returned from the token are unverified at this point
// Do not use the claims until the algorithm, certificate chain verification and root certificate
// comparison is successful
unverifiedClaims := &jwt.MapClaims{}
parsedToken, _, err := parser.ParseUnverified(token, unverifiedClaims)
if err != nil {
return nil, fmt.Errorf("Failed to parse claims token: %v", err)
}
return parsedToken.Header, nil
}
// PKICertificates contains the certificates extracted from the x5c header.
type PKICertificates struct {
LeafCert *x509.Certificate
IntermediateCert *x509.Certificate
RootCert *x509.Certificate
}
// ExtractCertificatesFromX5CHeader extracts the certificates from the given x5c header.
func ExtractCertificatesFromX5CHeader(x5cHeaders []any) (PKICertificates, error) {
if x5cHeaders == nil {
return PKICertificates{}, fmt.Errorf("VerifyAttestation(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - x5c header not set")
}
x5c := []string{}
for _, header := range x5cHeaders {
x5c = append(x5c, header.(string))
}
// The PKI token x5c header should have 3 certificates - leaf, intermediate and root
if len(x5c) != 3 {
return PKICertificates{}, fmt.Errorf("incorrect number of certificates in x5c header, expected 3 certificates, but got %v", len(x5c))
}
leafCert, err := DecodeAndParseDERCertificate(x5c[0])
if err != nil {
return PKICertificates{}, fmt.Errorf("cannot parse leaf certificate: %v", err)
}
intermediateCert, err := DecodeAndParseDERCertificate(x5c[1])
if err != nil {
return PKICertificates{}, fmt.Errorf("cannot parse intermediate certificate: %v", err)
}
rootCert, err := DecodeAndParseDERCertificate(x5c[2])
if err != nil {
return PKICertificates{}, fmt.Errorf("cannot parse root certificate: %v", err)
}
certificates := PKICertificates{
LeafCert: leafCert,
IntermediateCert: intermediateCert,
RootCert: rootCert,
}
return certificates, nil
}
// DecodeAndParseDERCertificate decodes the given DER certificate string and parses it into an x509 certificate.
func DecodeAndParseDERCertificate(certificate string) (*x509.Certificate, error) {
bytes, _ := base64.StdEncoding.DecodeString(certificate)
cert, err := x509.ParseCertificate(bytes)
if err != nil {
return nil, fmt.Errorf("cannot parse certificate: %v", err)
}
return cert, nil
}
// DecodeAndParsePEMCertificate decodes the given PEM certificate string and parses it into an x509 certificate.
func DecodeAndParsePEMCertificate(certificate string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(certificate))
if block == nil {
return nil, fmt.Errorf("cannot decode certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("cannot parse certificate: %v", err)
}
return cert, nil
}
// VerifyCertificateChain verifies the certificate chain from leaf to root.
// It also checks that all certificate lifetimes are valid.
func VerifyCertificateChain(certificates PKICertificates) error {
if isCertificateLifetimeValid(certificates.LeafCert) {
return fmt.Errorf("leaf certificate is not valid")
}
if isCertificateLifetimeValid(certificates.IntermediateCert) {
return fmt.Errorf("intermediate certificate is not valid")
}
interPool := x509.NewCertPool()
interPool.AddCert(certificates.IntermediateCert)
if isCertificateLifetimeValid(certificates.RootCert) {
return fmt.Errorf("root certificate is not valid")
}
rootPool := x509.NewCertPool()
rootPool.AddCert(certificates.RootCert)
_, err := certificates.LeafCert.Verify(x509.VerifyOptions{
Intermediates: interPool,
Roots: rootPool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
})
if err != nil {
return fmt.Errorf("failed to verify certificate chain: %v", err)
}
return nil
}
func isCertificateLifetimeValid(certificate *x509.Certificate) bool {
currentTime := time.Now()
// check the current time is after the certificate NotBefore time
if !currentTime.After(certificate.NotBefore) {
return false
}
// check the current time is before the certificate NotAfter time
if currentTime.Before(certificate.NotAfter) {
return false
}
return true
}
// CompareCertificates compares two certificate fingerprints.
func CompareCertificates(cert1 x509.Certificate, cert2 x509.Certificate) error {
fingerprint1 := sha256.Sum256(cert1.Raw)
fingerprint2 := sha256.Sum256(cert2.Raw)
if fingerprint1 != fingerprint2 {
return fmt.Errorf("certificate fingerprint mismatch")
}
return nil
}