For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Navegación principal
Theme

Descifra registros de PSP

Lee el contenido retenido de los clientes con tu clave administrada en la nube

El descifrado de PSP te permite usar una clave asimétrica que tú controlas para leer el contenido de los registros de PSP guardados en el almacenamiento en la nube que conectaste.

Una vez que tengas el registro y los metadatos de la clave original, puedes descifrarlo con tu servicio de administración de claves en la nube y código local. No necesitas llamar a ninguna API de OpenAI.

Esta guía se aplica a ZDR con PSP. No se aplica a la retención privada con PSP, que almacena los registros en infraestructura administrada por OpenAI.

Tu servicio de administración de claves en la nube descifra una pequeña cápsula de recuperación. Tu aplicación usa el secreto recuperado para descifrar el registro de forma local. La clave privada permanece en tu servicio de administración de claves en la nube.

En el apéndice encontrarás el paquete del registro y un ejemplo en Python.

Antes de empezar

  • Habilita el descifrado de PSP, conecta el almacenamiento y selecciona una clave de recuperación. Consulta Descifrado de PSP y Configura claves asimétricas.
  • Dale a tu aplicación permiso para leer el objeto almacenado y para usar la clave de recuperación RSA-3072 original del registro en operaciones de descifrado.
  • Conserva la información de registro original de la clave, la clave pública y la versión exacta de la clave en la nube mientras haya registros que dependan de ellas.

Con tu clave solo puedes descifrar los registros que se guardaron cuando el descifrado de PSP estaba habilitado para esa clave. Los registros existentes no se convierten.

Descifra un registro

1. Lee el registro

Lee el registro desde tu almacenamiento en la nube. Si tiene una envoltura de almacenamiento EKM, descifra primero esa capa con la configuración EKM original.

Analiza el paquete JSON resultante, que contiene protected_record y customer_access, como se muestra en Formato del paquete. Si customer_access es null, el descifrado de PSP no está disponible para ese registro.

2. Selecciona la clave original

Usa customer_access.customer_key_id para buscar la clave original en la nube y su versión en los metadatos de registro que guardaste. También puedes consultar el registro de la clave a través de la API de administración con GET /v1/organization/external_keys/{customer_key_id} y una clave de la API de administración de la organización. Consulta Obtén el registro de la clave.

Verifica la huella digital de la clave pública y usa la versión exacta de la clave en la nube. Conserva la clave privada en tu servicio de administración de claves en la nube. Nunca la compartas con OpenAI.

3. Descifra la cápsula de recuperación

Envía el valor decodificado de customer_access.recovery_secret a tu servicio de administración de claves en la nube para que lo descifre con RSA-OAEP-SHA256.

4. Descifra y verifica el contenido

Usa el ejemplo en Python para verificar que la cápsula esté vinculada al registro y descifrar el contenido de forma local.

Devuelve el contenido solo si se superan todas las verificaciones y la autenticación con AES-GCM se completa correctamente.

Verifica el resultado

Si la operación se completa correctamente, devuelve contenido autenticado en UTF-8. Los registros de solicitudes contienen request_text y metadatos que indican si se truncó el texto. Consulta Contenido recuperado.

Solución de problemas

Resultado Qué verificar
customer_access es null El registro se guardó sin que el descifrado de PSP estuviera habilitado para una clave de recuperación. Seleccionar una clave no habilita el descifrado de PSP para ese registro.
Se denegó el acceso al KMS o la clave no está disponible Verifica que tengas permiso para descifrar y que la clave original en la nube y su versión estén disponibles.
Falla la verificación de la huella digital o del vínculo con el registro Verifica que tengas el registro que quieres descifrar y la información de registro original de la clave. No modifiques los campos autenticados ni uses otra clave.
Falla la autenticación con AES-GCM Verifica los datos de entrada del ejemplo en Python. No devuelvas texto en claro parcial.

Guías relacionadas

Apéndice: referencia de formato y descifrado

Este apéndice abarca la versión 1 del paquete y la versión v4 del registro protegido. Usa el siguiente ejemplo en Python para implementar el descifrado local sin bibliotecas de OpenAI.

Formato del paquete

El objeto está en formato JSON, con codificación UTF-8, y tiene la siguiente estructura. Los valores entre signos de menor y mayor que son marcadores de posición.

{
  "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 almacena el contenido cifrado de la cadena de pensamiento. No puedes descifrar ese contenido con tu clave de recuperación de cliente. El arreglo puede estar vacío.

Ejemplo en Python

Usa el siguiente ejemplo después de descifrar la envoltura de almacenamiento EKM por separado, si la hay, y de descifrar la cápsula de recuperación con la clave original en la nube. El ejemplo verifica la huella digital de la clave, el vínculo entre la cápsula y el registro, el escalar recuperado y la huella digital de la clave del entorno de ejecución antes de autenticar y descifrar la carga útil.

Instala pyhpke, cryptography y rfc8785 en tu entorno de Python. Pasa estos argumentos a recover_record:

  • bundle_json: el paquete interno del registro como bytes de un documento JSON codificado en UTF-8.
  • capsule: los bytes que devuelve tu servicio de administración de claves en la nube.
  • original_key_id: el ID de registro de la clave original del paso 2.
  • original_public_key_pem: la clave pública RSA correspondiente a esa versión de la clave en la nube, en formato PEM y expresada en bytes.

La función devuelve el contenido descifrado como una cadena UTF-8. Si falla alguna verificación, la recuperación se detiene.

Descifra un registro de 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)

Contenido recuperado

En los registros de solicitudes cuyo JSON recuperado contiene format: "psp_request_v1", el campo request_text contiene el texto retenido de la solicitud. Los demás campos son reasoning_summaries (arreglo de cadenas), request_text_truncated (booleano) y reasoning_summary_parts_omitted (entero no negativo). Revisa los campos que indican si hubo truncamiento al interpretar el resultado. Otros tipos de contenido protegido pueden usar formatos de texto en claro diferentes.

Puedes seguir recuperando contenido después de que venza la clave del entorno de ejecución de OpenAI, siempre que sigan disponibles tu clave de recuperación de cliente original y la instantánea. Conserva el valor autenticado de expires_at sin cambios. Los bloques de datos de encrypted_cot se cifran por separado y permanecen cifrados durante la recuperación con tu clave de cliente.