Verificação Dupla

A Verificação Dupla (TSSV — Two-Step Security Verification) é o processo de custódia segura de alunos, onde um responsável é verificado antes de retirar um aluno da escola. A TSSV suporta dois canais de check-in: o totem biométrico instalado na escola (reconhecimento facial físico) e o app móvel (verificação criptográfica vinculada à biometria do dispositivo).

Como funciona

A TSSV funciona por dois canais independentes. O resultado final é o mesmo: o responsável é verificado e os alunos disponíveis para custódia são retornados.

Via totem biométrico (reconhecimento facial)

  1. 1

    O responsável se aproxima do totem biométrico instalado na escola e realiza o reconhecimento facial.

  2. 2

    O totem envia o CPF do responsável e o CNPJ da escola via POST /api/v1/tssv/responsible/check/in usando o token de parceiro.

  3. 3

    A API retorna a lista de alunos vinculados ao responsável presentes na escola naquele dia.

Via app móvel (assinatura criptográfica)

  1. 1

    O responsável abre o app e inicia o check-in. O app solicita um desafio (nonce) ao servidor.

  2. 2

    O app assina o nonce com a chave privada do dispositivo — o SO exibe Face ID ou digital ao responsável. Só com biometria aprovada a assinatura é gerada.

  3. 3

    O app envia o check-in com device_id, challenge e signature. O backend verifica a assinatura e libera os alunos.

Status possíveis na resposta
students (array)

Sucesso — retorna lista de alunos disponíveis para custódia.

ALREADY_WAITING_FOR_CUSTODY

O responsável já está aguardando liberação.

RESPONSIBLE_NOT_FOUND

O responsável não foi encontrado no sistema.

NO_STUDENTS_FOR_CUSTODY

O responsável não possui alunos para custódia hoje.

Totem biométrico POST /v1/tssv/responsible/check/in

Check-in do responsável com facial

Usado exclusivamente pelo totem biométrico instalado na escola. Após o reconhecimento facial do responsável, o totem chama este endpoint informando o CPF do responsável e o CNPJ da escola. A API registra a chegada e retorna os alunos disponíveis para custódia.

Este endpoint usa autenticação por token de parceiro (X-Authorization, X-Partner, X-Client), Para check-in via app móvel, veja Check-in com app móvel.

Atributos obrigatórios (body JSON)

school_cnpj string obrigatório

CNPJ da escola (deve existir no sistema).

responsible_cpf string obrigatório

CPF do responsável (deve estar cadastrado no sistema).

checked_in_at datetime obrigatório

Data e hora da chegada no formato Y-m-d H:i:s.

Códigos de resposta

200

Sucesso.

400

Dados inválidos.

401

Autenticação inválida.

404

Recurso de referência não encontrado.

Requisição POST
curl https://toakiescola.com.br/api/v1/tssv/responsible/check/in \
  -H "Content-Type: application/json" \
  -H "X-Authorization: {api_token}" \
  -H "X-Partner: {partner_token}" \
  -H "X-Client: {client_slug}" \
  -d '{"school_cnpj": "26019466000122", "responsible_cpf": "123.456.789-00", "checked_in_at": "2026-04-29 17:10:00"}'
Resposta
{
  "success": true,
  "students": [
    {
      "enrollment_number": "7.553",
      "name": "João da Silva",
      "photo": "iVBORw0KGgoAAAANS..."
    }
  ]
}
Resposta
{
  "success": true,
  "message": "O responsável não possui alunos para custódia no dia de hoje."
}
App móvel Segurança biométrica Verificação biométrica por chave de dispositivo

Como funciona a verificação criptográfica

O check-in do responsável via app móvel usa criptografia assimétrica vinculada à biometria do dispositivo (Face ID / digital). Em vez de transmitir dados biométricos — o que é arquiteturalmente impossível e ilegal — o dispositivo assina um desafio emitido pelo servidor. O backend verifica a assinatura com a chave pública previamente registrada.

  1. 1

    Geração do par de chaves no dispositivo

    O app gera um par de chaves EC (P-256) dentro do Secure Enclave (iOS) ou Android Keystore com a flag userPresence = true. Isso garante que a chave privada só pode ser utilizada após biometria bem-sucedida (Face ID ou digital). A chave privada jamais sai do chip.

  2. 2

    Registro da chave pública no servidor

    A chave pública (PEM) é enviada uma única vez para POST .../device/register e armazenada vinculada ao responsável e ao device_id.

  3. 3

    Obtenção do desafio (nonce) — antes de cada check-in

    O app solicita um nonce em GET .../device/challenge. O servidor gera um valor aleatório de 32 bytes (base64), armazena em cache com TTL de 5 minutos e o retorna. Esse valor é de uso único.

  4. 4

    Assinatura do desafio (biometria exigida pelo SO)

    O app tenta assinar o nonce com a chave privada. O SO apresenta Face ID / digital ao usuário. Só com biometria aprovada a assinatura é gerada. O app envia device_id, challenge e signature no corpo do check-in.

  5. 5

    Verificação no servidor

    O backend busca a chave pública registrada para o par responsible + device_id, confirma que o nonce bate com o que foi emitido e chama openssl_verify(). Se a verificação falhar, o check-in é recusado com 403.

Retrocompatibilidade: enquanto nenhum dispositivo for registrado para um responsável, o check-in funciona sem assinatura. Assim que o primeiro dispositivo for registrado, o servidor passa a exigir assinatura válida em todos os check-ins desse responsável.

Exemplo — geração de chave no iOS (Swift)
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.privateKeyUsage, .userPresence], nil
)!

let attributes: [String: Any] = [
    kSecAttrKeyType       as String: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeySizeInBits as String: 256,
    kSecAttrTokenID       as String: kSecAttrTokenIDSecureEnclave,
    kSecPrivateKeyAttrs   as String: [
        kSecAttrIsPermanent        as String: true,
        kSecAttrApplicationTag    as String: "br.com.toakiescola.tssv",
        kSecAttrAccessControl     as String: access,
    ],
]
var error: Unmanaged<CFError>?
let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)!
let publicKey  = SecKeyCopyPublicKey(privateKey)!
Exemplo — geração de chave no Android (Kotlin)
val keyPairGenerator = KeyPairGenerator
    .getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")

keyPairGenerator.initialize(
    KeyGenParameterSpec.Builder(
        "tssv_key",
        KeyProperties.PURPOSE_SIGN
    )
    .setDigests(KeyProperties.DIGEST_SHA256)
    .setUserAuthenticationRequired(true)
    .setInvalidatedByBiometricEnrollment(true)
    .build()
)
val keyPair = keyPairGenerator.generateKeyPair()
// keyPair.public → enviar como PEM para o servidor
Exemplo — geração de chave no React Native (Expo)
// expo install expo-crypto expo-secure-store expo-local-authentication
import * as Crypto from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';
import * as LocalAuthentication from 'expo-local-authentication';

// 1. Gerar par de chaves EC P-256 e persistir a chave privada
const keyPair = await Crypto.generateKeyPairAsync(
  'EC',
  { namedCurve: 'P-256', extractable: false },
);

// 2. Exportar chave pública como PEM para enviar ao servidor
const publicKeyPem = await Crypto.exportKeyAsync('spki', keyPair.publicKey);

// 3. Persistir a chave privada de forma segura
await SecureStore.setItemAsync('tssv_private_key', keyPair.privateKey, {
  requireAuthentication: true,
});

// 4. Na hora do check-in: solicitar autenticação biométrica e assinar o challenge
const biometricResult = await LocalAuthentication.authenticateAsync({
  promptMessage: 'Confirme sua identidade para o check-in',
});

if (!biometricResult.success) throw new Error('Autenticação biométrica cancelada');

const privateKey = await SecureStore.getItemAsync('tssv_private_key', {
  requireAuthentication: true,
});

const signature = await Crypto.signAsync(
  'SHA-256',
  challenge, // string base64 obtida de /device/challenge
  privateKey,
);
// signature → enviar como base64 no campo "signature"
App móvel POST /api/v1/pedagogic/attendance/responsible/device/register

Registrar chave pública do dispositivo

Registra (ou atualiza) a chave pública EC de um dispositivo móvel para o responsável autenticado. Após o registro, o check-in desse responsável passará a exigir assinatura biométrica. Requer bearer token do responsável.

Atributos obrigatórios (body JSON)

device_id string (UUID) obrigatório

UUID gerado pelo app na primeira instalação. Identifica o dispositivo de forma única por responsável.

public_key string (PEM) obrigatório

Chave pública EC P-256 exportada no formato PEM (-----BEGIN PUBLIC KEY-----).

platform string obrigatório

Plataforma do dispositivo: ios ou android.

device_name string obrigatório

Nome legível do dispositivo (ex: "iPhone 15 de Maria"). Exibido para o usuário na tela de gerenciamento de dispositivos.

Códigos de resposta

200

Sucesso.

400

Dados inválidos.

401

Autenticação inválida.

404

Recurso de referência não encontrado.

Requisição POST
curl .../api/v1/pedagogic/attendance/responsible/device/register \
  -H "Authorization: Bearer {bearer_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYH...\n-----END PUBLIC KEY-----",
    "platform": "ios",
    "device_name": "iPhone 15 de Maria"
  }'
Resposta
{
  "message": "Dispositivo registrado com sucesso."
}
App móvel GET /api/v1/pedagogic/attendance/responsible/device/challenge

Obter desafio para assinatura

Retorna um nonce aleatório (32 bytes, base64) vinculado ao responsável e ao dispositivo. O nonce expira em 5 minutos e é consumido na primeira utilização — não pode ser reutilizado. Chame este endpoint imediatamente antes de solicitar a assinatura biométrica ao usuário.

Parâmetros de query

device_id string (UUID) obrigatório

O mesmo device_id usado no cadastro do dispositivo.

Códigos de resposta

200

Sucesso.

401

Autenticação inválida.

404

Recurso não encontrado.

Requisição GET
curl .../api/v1/pedagogic/attendance/responsible/device/challenge?device_id=550e8400-... \
  -H "Authorization: Bearer {bearer_token}"
Resposta
{
  "challenge": "dGhpcyBpcyBhIHJhbmRvbSBub25jZQ==",
  "expires_at": "2026-05-01T17:15:00+00:00"
}
App móvel POST /api/v1/pedagogic/attendance/responsible/check/in

Check-in do responsável com app móvel

Endpoint de check-in do responsável para o app móvel. Requer autenticação por Bearer Token. Quando o responsável possui um dispositivo registrado, os campos device_id, challenge e signature são obrigatórios. O servidor verifica a assinatura ECDSA via openssl_verify() com SHA-256 antes de processar o check-in.

Este endpoint usa autenticação por Bearer Token. Para check-in via totem biométrico, veja Check-in com facial.

Campos adicionais no body JSON

Aviso de descontinuação — obrigatoriedade a partir de 1º de julho de 2026

Os campos device_id, challenge e signature são condicionais até 30 de junho de 2026. A partir de 1º de julho de 2026, o envio desses campos passará a ser obrigatório para todos os responsáveis, independentemente de possuírem dispositivo registrado. Utilize este período de transição para implementar o fluxo de registro biométrico e assinatura de challenge no seu aplicativo.

device_id string condicional

Obrigatório se o responsável tiver dispositivo registrado.

challenge string (base64) condicional

O nonce obtido em /device/challenge imediatamente antes.

signature string (base64) condicional

Assinatura SHA-256 do challenge, gerada pela chave privada biométrica do dispositivo, codificada em base64.

Dica para o app móvel: assine a string do challenge diretamente (não em UTF-8 nem re-encode). No iOS, use SecKeyCreateSignature com ecdsaSignatureMessageX962SHA256. No Android, use Signature.getInstance("SHA256withECDSA") e converta o resultado para base64.

Códigos de resposta

200

Sucesso.

400

Dados inválidos.

401

Autenticação inválida.

404

Recurso de referência não encontrado.

Requisição com assinatura POST
curl .../api/v1/pedagogic/attendance/responsible/check/in \
  -H "Authorization: Bearer {bearer_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "challenge": "dGhpcyBpcyBhIHJhbmRvbSBub25jZQ==",
    "signature": "MEUCIQD3...base64-encoded-ECDSA-signature..."
  }'
Resposta
{
  "success": true,
  "students": [
    {
      "enrollment_number": "7.553",
      "name": "João da Silva",
      "photo": "iVBORw0KGgoAAAANS..."
    }
  ]
}