For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Navegação principal
Theme

Descriptografar registros do PSP

Leia o conteúdo retido do cliente com sua chave gerenciada na nuvem

Com a descriptografia do PSP, você pode usar uma chave assimétrica sob seu controle para ler o conteúdo dos registros do PSP salvos no armazenamento em nuvem conectado.

Com o registro e os metadados da chave original em mãos, você pode descriptografar o conteúdo usando o serviço de gerenciamento de chaves do seu provedor de nuvem e código executado localmente. Você não precisa chamar uma API da OpenAI.

Este guia se aplica à ZDR com PSP. Ele não se aplica à retenção privada com PSP, que armazena os registros na infraestrutura gerenciada pela OpenAI.

O serviço de gerenciamento de chaves do seu provedor de nuvem descriptografa uma pequena cápsula de recuperação. Seu aplicativo usa o segredo recuperado para descriptografar o registro localmente. Sua chave privada permanece no serviço de gerenciamento de chaves do provedor.

O apêndice apresenta o pacote do registro e um exemplo em Python.

Antes de começar

  • Habilite a descriptografia do PSP, conecte o armazenamento e selecione uma chave de recuperação. Consulte Descriptografia do PSP e Configurar chaves assimétricas.
  • Conceda ao aplicativo permissão para ler o objeto armazenado e para descriptografar com a chave de recuperação RSA-3072 original do registro.
  • Mantenha o cadastro original, a chave pública e a chave do provedor de nuvem na versão exata indicada no cadastro enquanto houver registros que dependam deles.

Sua chave só pode descriptografar registros gravados enquanto a descriptografia do PSP estava habilitada para essa chave. Os registros existentes não são convertidos.

Descriptografar um registro

1. Leia o registro

Leia o registro no seu armazenamento em nuvem. Se ele tiver um envelope de armazenamento EKM, descriptografe primeiro essa camada usando a configuração EKM original.

Analise o pacote JSON resultante, que contém protected_record e customer_access, conforme descrito na seção Formato do pacote. Se customer_access for null, a descriptografia do PSP não está disponível para esse registro.

2. Selecione a chave original

Use customer_access.customer_key_id para localizar a chave original do provedor de nuvem e sua versão nos metadados de cadastro que você salvou. Você também pode consultar o cadastro pela API de administração com GET /v1/organization/external_keys/{customer_key_id}, usando uma chave da API de administração da organização. Consulte Consultar o cadastro da chave.

Confira a impressão digital da chave pública e use exatamente a chave e a versão indicadas no cadastro. Mantenha a chave privada no serviço de gerenciamento de chaves do seu provedor de nuvem. Nunca compartilhe a chave privada com a OpenAI.

3. Descriptografe a cápsula de recuperação

Envie o valor decodificado de customer_access.recovery_secret ao serviço de gerenciamento de chaves do seu provedor de nuvem para que o serviço o descriptografe com RSA-OAEP-SHA256.

4. Descriptografe e verifique o conteúdo

Use o exemplo em Python para verificar o vínculo entre a cápsula e o registro e descriptografar o conteúdo localmente.

Retorne o conteúdo somente depois que todas as verificações e a autenticação AES-GCM forem concluídas com sucesso.

Verificar o resultado

Quando a operação é bem-sucedida, ela retorna conteúdo autenticado em UTF-8. Os registros de solicitação contêm request_text e metadados que indicam se houve truncamento. Consulte Conteúdo recuperado.

Solução de problemas

Resultado O que verificar
customer_access é null Quando o registro foi gravado, a descriptografia do PSP não estava habilitada para uma chave de recuperação. Selecionar uma chave não habilita a descriptografia do PSP para esse registro.
Acesso ao KMS negado ou chave indisponível Verifique se você tem permissão para descriptografar e se a chave original e sua versão estão disponíveis no provedor de nuvem.
Divergência na impressão digital ou no vínculo Confira se você está usando o registro desejado e o cadastro original. Não modifique os campos autenticados nem substitua a chave por outra.
Falha na autenticação AES-GCM Confira os dados de entrada do exemplo em Python. Não retorne nenhum trecho do texto descriptografado.

Guias relacionados

Apêndice: referência de formato e descriptografia

Este apêndice descreve a versão 1 do pacote e a versão v4 do registro protegido. Use o exemplo em Python a seguir para implementar a descriptografia local sem usar bibliotecas da OpenAI.

Formato do pacote

O objeto é um documento JSON codificado em UTF-8 com a estrutura a seguir. Os trechos entre os sinais de menor e maior representam valores a preencher.

{
  "bundle_version": 1,
  "protected_record": {
    "version": "v4",
    "algorithm": "HPKE-v1-DHKEM_P256_HKDF_SHA256-EXPORT_ONLY-AES256_GCM",
    "scope": "global",
    "key_thumbprint": "<runtime public-key thumbprint>",
    "expires_at": "2026-11-20",
    "content_id": "<content ID>",
    "encrypted_dek": "<E: base64url P-256 public point>",
    "safety_review_runtime_public_key": "<R: base64url P-256 public point>",
    "nonce": "<base64url 12-byte nonce>",
    "encrypted_cot": ["<opaque encrypted CoT string>"],
    "ciphertext": "<base64url ciphertext followed by GCM tag>"
  },
  "customer_access": {
    "version": 1,
    "algorithm": "RSA-OAEP-256",
    "customer_key_id": "<original registration ID>",
    "customer_public_key_fingerprint": "<RSA public-key fingerprint>",
    "recovery_secret": "<base64url RSA ciphertext>"
  }
}

encrypted_cot contém o conteúdo criptografado da cadeia de pensamento. A chave de recuperação do cliente não pode descriptografar esse conteúdo. O array pode estar vazio.

Exemplo em Python

Use o exemplo a seguir depois de descriptografar o envelope de armazenamento EKM separado, se houver, e de descriptografar a cápsula de recuperação com a chave original do provedor de nuvem. O exemplo verifica a impressão digital da chave, o vínculo da cápsula com o registro, o escalar recuperado e a impressão digital da chave do ambiente de execução antes de autenticar e descriptografar os dados.

Instale pyhpke, cryptography e rfc8785 no seu ambiente Python. Passe os seguintes argumentos para recover_record:

  • bundle_json: o pacote interno do registro em JSON, como uma sequência de bytes em UTF-8.
  • capsule: os bytes retornados pelo serviço de gerenciamento de chaves do seu provedor de nuvem.
  • original_key_id: o ID do cadastro original da chave, obtido na etapa 2.
  • original_public_key_pem: a chave pública RSA correspondente àquela versão da chave do provedor de nuvem, em formato PEM, como bytes.

A função retorna o conteúdo descriptografado como uma string UTF-8. Se alguma verificação falhar, a recuperação é interrompida.

Descriptografar um registro do PSP
import base64
import hashlib
import hmac
import json

import rfc8785
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from pyhpke import AEADId, CipherSuite, KDFId, KEMId, KEMKey, KEMKeyPair


def recover_record(bundle_json, capsule, original_key_id, original_public_key_pem):
    """Return plaintext from exact inner bundle bytes and a decrypted capsule.

    Use the documented schema and trusted original key ID/RSA public PEM.
    No cloud calls occur here. The RSA private key stays in the cloud.
    """
    record, access, runtime, nonce, ciphertext = validate_bundle(
        bundle_json, original_key_id, original_public_key_pem
    )
    e = recover_scalar(record, access, capsule)

    ephemeral = ec.derive_private_key(e, ec.SECP256R1())
    pair = KEMKeyPair(
        KEMKey.from_pyca_cryptography_key(ephemeral),
        KEMKey.from_pyca_cryptography_key(ephemeral.public_key()),
    )
    suite = CipherSuite.new(
        KEMId.DHKEM_P256_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.EXPORT_ONLY
    )
    enc, sender = suite.create_sender_context(
        KEMKey.from_pyca_cryptography_key(runtime),
        info=b"OpenAI-PSP-v4/HPKE-export",
        eks=pair,
    )
    require(enc == decode64(record["encrypted_dek"]), "Wrong recovered scalar")
    aad = rfc8785.dumps({k: v for k, v in record.items() if k != "ciphertext"})
    key = sender.export(b"OpenAI-PSP-v4/payload-key" + hashlib.sha256(aad).digest(), 32)
    return AESGCM(key).decrypt(nonce, ciphertext, aad).decode("utf-8")


def validate_bundle(bundle_json, original_key_id, original_public_key_pem):
    bundle = json.loads(bundle_json.decode("utf-8"), object_pairs_hook=unique_object)
    require(
        type(bundle["bundle_version"]) is int and bundle["bundle_version"] == 1,
        "Unsupported bundle version",
    )
    record, access = bundle["protected_record"], bundle["customer_access"]
    require(access is not None, "Customer recovery is unavailable")
    require(
        record["version"] == "v4"
        and record["algorithm"]
        == "HPKE-v1-DHKEM_P256_HKDF_SHA256-EXPORT_ONLY-AES256_GCM",
        "Unsupported record",
    )
    require(
        type(access["version"]) is int
        and access["version"] == 1
        and access["algorithm"] == "RSA-OAEP-256",
        "Unsupported recovery capsule",
    )
    require(access["customer_key_id"] == original_key_id, "Wrong key registration")
    require(len(decode64(access["recovery_secret"])) == 384, "Wrong RSA length")

    public_key = serialization.load_pem_public_key(original_public_key_pem)
    require(isinstance(public_key, rsa.RSAPublicKey), "Expected an RSA public key")
    require(public_key.key_size == 3072, "Expected RSA-3072")
    spki = public_key.public_bytes(
        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
    )
    fingerprint = decode64(access["customer_public_key_fingerprint"])
    require(
        hmac.compare_digest(fingerprint, hashlib.sha256(spki).digest()), "Wrong key"
    )
    runtime = point(record["safety_review_runtime_public_key"])
    coordinates = runtime.public_numbers()
    jwk = {
        "crv": "P-256",
        "kty": "EC",
        "x": b64url(coordinates.x.to_bytes(32, "big")),
        "y": b64url(coordinates.y.to_bytes(32, "big")),
    }
    thumbprint = b64url(hashlib.sha256(rfc8785.dumps(jwk)).digest())
    require(record["key_thumbprint"] == thumbprint, "Wrong runtime key")
    nonce, ciphertext = decode64(record["nonce"]), decode64(record["ciphertext"])
    require(len(nonce) == 12 and len(ciphertext) >= 16, "Invalid AES-GCM encoding")
    return record, access, runtime, nonce, ciphertext


def recover_scalar(record, access, capsule):
    header = {k: v for k, v in access.items() if k != "recovery_secret"}
    binding_input = {
        "purpose": "OpenAI-PSP-v4/customer-recovery-binding",
        "bundle_version": 1,
        "record": record,
        "recovery_header": header,
    }
    binding = hashlib.sha256(rfc8785.dumps(binding_input)).digest()
    require(
        isinstance(capsule, bytes)
        and len(capsule) == 72
        and capsule[:8] == b"PSPv4\x00\x00\x01"
        and hmac.compare_digest(capsule[40:], binding),
        "Invalid recovery capsule binding",
    )
    e = int.from_bytes(capsule[8:40], "big")
    order = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
    require(1 <= e < order, "Invalid P-256 private scalar")
    return e


def require(condition, message):
    if not condition:
        raise ValueError(message)


def unique_object(pairs):
    require(len(pairs) == len(dict(pairs)), "Duplicate JSON field")
    return dict(pairs)


def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")


def decode64(value):
    # Restore omitted "=" padding: -len(value) % 4 is how many characters
    # are needed to reach a multiple of four (e.g. length 10 needs "==").
    data = base64.b64decode(
        value + "=" * (-len(value) % 4), altchars=b"-_", validate=True
    )
    require(b64url(data) == value, "Noncanonical base64url")
    return data


def point(value):
    encoded = decode64(value)
    require(len(encoded) == 65 and encoded[0] == 4, "Invalid P-256 encoding")
    return ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), encoded)

Conteúdo recuperado

Nos registros de solicitação em que o JSON recuperado contém format: "psp_request_v1", o campo request_text contém o texto retido da solicitação. Os outros campos são reasoning_summaries (array de strings), request_text_truncated (booleano) e reasoning_summary_parts_omitted (inteiro não negativo). Ao interpretar o resultado, verifique os campos que indicam truncamento. Outros tipos de conteúdo protegido podem usar formatos diferentes para o texto descriptografado.

O cliente pode continuar a recuperar o conteúdo depois que a chave do ambiente de execução da OpenAI expirar, desde que a chave de recuperação original do cliente e o snapshot continuem disponíveis. Mantenha o valor autenticado de expires_at inalterado. Os blobs de encrypted_cot são criptografados separadamente, e seu conteúdo permanece inacessível ao cliente durante a recuperação.