OxusAPI

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-keys
curl -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

  1. Cifre o dossiê com uma chave de dados (DEK) exclusiva do cliente, gerada no KMS do parceiro.
  2. Calcule o SHA-256 do dossiê em claro.
  3. Assine os campos da atestação com Ed25519.

A DEK nunca sai do parceiro nesta etapa: a Oxus recebe só uma referência (dataKeyReference).

attest-customer.ts
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-attestations

A 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.

reasonExemplo
REGULATORY_EXAMINATIONFiscalização de um regulador
TRANSACTION_MONITORING_ALERTAlerta de PLD/FT fora do perfil do cliente
PERIODIC_SAMPLE_REVIEWRevisão amostral prevista em contrato
LAW_ENFORCEMENT_REQUESTOrdem 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.

approve-audit.ts
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 recebe kyc.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.

Nesta página