Cofre e auditoria
Como cifrar o dossiê, assinar a atestação e responder a uma auditoria.
Tudo usa primitivas padrão (AES-256-GCM, Ed25519, RSA-OAEP), disponíveis em qualquer linguagem e em qualquer KMS.
1. Registre a chave de assinatura
Uma vez. O parceiro gera um par Ed25519, guarda a chave privada no próprio KMS e registra só a pública.
POST/v1/kyc/signing-keyscurl -X POST $OXUS_API/v1/kyc/signing-keys \
-H "Authorization: Bearer $OXUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keyId": "parceiro-kyc-2026",
"algorithm": "Ed25519",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA…\n-----END PUBLIC KEY-----"
}'Para rotacionar, registre uma nova chave com outro keyId. Atestações antigas continuam válidas.
2. Monte a atestação de cada cliente
- Cifre o dossiê com uma chave de dados (DEK) exclusiva do cliente, gerada no KMS do parceiro.
- Calcule o SHA-256 do dossiê em claro.
- Assine os campos da atestação com Ed25519.
A DEK nunca sai do parceiro nesta etapa: a Oxus recebe só uma referência (dataKeyReference).
import { createCipheriv, createHash, randomBytes, sign, type KeyObject } from 'node:crypto'
/** RFC 8785 canonical JSON: keys sorted at every level, no whitespace. */
function canonicalize(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`
if (value && typeof value === 'object') {
const sortedEntries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
return `{${sortedEntries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalize(entry)}`).join(',')}}`
}
return JSON.stringify(value)
}
export function buildKycAttestation(params: {
customerId: string
dossier: object
dataKey: Buffer // 32 bytes from the partner's KMS (GenerateDataKey)
dataKeyReference: string
signingKey: KeyObject // Ed25519 private key, ideally a KMS/HSM signing call
signingKeyId: string
}) {
const dossierBytes = Buffer.from(JSON.stringify(params.dossier))
const iv = randomBytes(12)
const cipher = createCipheriv('aes-256-gcm', params.dataKey, iv)
const ciphertext = Buffer.concat([cipher.update(dossierBytes), cipher.final()])
const dossierSha256 = createHash('sha256').update(dossierBytes).digest('hex')
const attestedFields = {
customerId: params.customerId,
level: 'STANDARD',
verifiedAt: '2026-09-30T18:44:00.000Z',
riskRating: 'LOW',
politicallyExposedPerson: false,
sanctionsScreening: { status: 'CLEAR', screenedAt: new Date().toISOString(), lists: ['OFAC', 'UN', 'COAF'] },
dossierSha256,
dataKeyReference: params.dataKeyReference,
}
const signature = sign(null, Buffer.from(canonicalize(attestedFields)), params.signingKey)
return {
level: attestedFields.level,
verifiedAt: attestedFields.verifiedAt,
riskRating: attestedFields.riskRating,
politicallyExposedPerson: attestedFields.politicallyExposedPerson,
sanctionsScreening: attestedFields.sanctionsScreening,
dossier: {
algorithm: 'AES-256-GCM',
ciphertext: ciphertext.toString('base64'),
iv: iv.toString('base64'),
authTag: cipher.getAuthTag().toString('base64'),
sha256: dossierSha256,
dataKeyReference: params.dataKeyReference,
},
signature: { algorithm: 'Ed25519', keyId: params.signingKeyId, value: signature.toString('base64') },
}
}3. Envie a atestação
POST/v1/customers/{customerId}/kyc-attestationsA Oxus verifica a assinatura com a chave do keyId e guarda o ciphertext. Assinatura inválida retorna 422 INVALID_ATTESTATION_SIGNATURE e o cliente não é ativado.
4. Responda a uma auditoria
A Oxus abre uma solicitação e o parceiro recebe o webhook kyc.audit_requested.
reason | Exemplo |
|---|---|
REGULATORY_EXAMINATION | Fiscalização de um regulador |
TRANSACTION_MONITORING_ALERT | Alerta de PLD/FT fora do perfil do cliente |
PERIODIC_SAMPLE_REVIEW | Revisão amostral prevista em contrato |
LAW_ENFORCEMENT_REQUEST | Ordem judicial ou requisição de autoridade |
Cada solicitação traz o escopo (quais clientes), o prazo de acesso pedido e o prazo de resposta.
Para aprovar, o parceiro decifra a DEK no próprio KMS e a recifra para a chave pública de auditoria da Oxus. A DEK nunca trafega em claro.
import { constants, publicEncrypt } from 'node:crypto'
export async function approveAuditRequest(auditRequestId: string, attestationIds: string[]) {
const auditPublicKey = await oxusApi.get('/v1/kyc/audit-public-key')
const releasedDataKeys = await Promise.all(
attestationIds.map(async attestationId => {
const dataKey = await partnerKms.decryptDataKey(attestationId)
const wrappedDataKey = publicEncrypt(
{ key: auditPublicKey.publicKeyPem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
dataKey
)
return { attestationId, wrappedDataKey: wrappedDataKey.toString('base64'), auditKeyId: auditPublicKey.keyId }
})
)
return oxusApi.post(`/v1/kyc/audit-requests/${auditRequestId}/approve`, {
approvedBy: 'compliance@parceiro.com.br',
releasedDataKeys,
})
}Para recusar, use POST /v1/kyc/audit-requests/{auditRequestId}/decline com a justificativa.
Depois da aprovação:
- O dossiê é lido em ambiente isolado e conferido contra o hash do cadastro.
- O acesso expira sozinho em
decision.accessExpiresAt, e o parceiro recebekyc.audit_access_expired. - Cada evento (chave recebida, dossiê decifrado, visualizado, chave destruída) fica em
GET /v1/kyc/audit-requests/{auditRequestId}/access-log.
No sandbox, aud_3c9f1e2b7a44 é uma solicitação pendente e aud_91ab0c7de210 tem a trilha completa.