Autenticador headless do protocolo PJeOffice (CNJ). Servico escrito em Go que expoe um endpoint HTTP local na porta 8800, recebe desafios de assinatura do fluxo PJeOffice, assina com o certificado configurado e envia a resposta ao tribunal.
Implementa o protocolo de forma independente, com base na codebase do PJeOffice Pro (CNJ) e na observacao do protocolo: a aplicacao Java oficial foi descompilada com o Luyten (https://github.com/deathmarine/Luyten) para estudo do fluxo. Projeto nao-oficial, sem vinculo com o CNJ.
O servico cobre a camada de autenticacao por certificado digital do PJeOffice:
- Recebe o desafio (
mensagem) via HTTP. - Assina com o certificado ativo usando
MD5withRSA,SHA1withRSAouSHA256withRSA. - Envia
{uuid, mensagem, assinatura, certChain}ao endpoint do tribunal (servidor + enviarPara). - Expoe
GET /pjeOffice/como health-check.
Assinatura dual: o servico suporta backends de assinatura por ordem de
prioridade (PJE_SIGNER_PRIORITY) com fallback automatico:
pkcs11— token A3 (hardware, nao exportavel) via modulo PKCS#11 (ex.: SafeSignlibaetpkss.so). Assina dentro do token.pfx— certificado A1 em arquivo.pfx/.p12(PKCS#12).remote—RemoteSigner, que nao detem credencial local: delega a assinatura, via gRPC, a umSignerServicerodando em modosigner-onlyno host que tem o token. Habilitado quandoremoteaparece emPJE_SIGNER_PRIORITYePJE_SIGNER_REMOTE_ADDRaponta para o endpoint do servico.
Quando o backend de maior prioridade esta indisponivel, o servico cai para o
proximo e emite log WARN com a identidade ativa. Nenhum backend disponivel
resulta em erro explicito.
O binario tem quatro modos, selecionados por PJE_MODE:
full(padrao) — servidor HTTP PJeOffice na porta8800(descrito acima).signer-only— expoe oSignerServicegRPC (padrao:9090) sobre oSignerlocal. Faz login eager no token/PFX na inicializacao (fail-fast: PIN errado ou token ausente quebra na partida, nao no meio do fluxo), de modo que oHealthja reportaready. E o lado servidor da topologia de assinatura remota: roda no host que detem o token A3.login— driver end-to-end. Executa UM login headless completo no jus.br (requer Chrome) e imprimeLOGIN_OK bearer_len=N. Usa oSignerconfigurado, inclusiveremotepara delegar o handshake do certificado ao host do token. Serve para validar o fluxo dual contra o SSO real. O bearer nunca e logado por inteiro (apenas mascarado).login-service— expoe oLoginServicegRPC (padrao127.0.0.1:9091) comGetBearereHealth. Reusa a sessao viaLoginManager(cache do bearer com expiracao e coalescencia), disparando o login headless somente quando nao ha token fresco em cache.totp— imprime no stdout, a cada 1s, o codigo TOTP atual e o tempo restante da janela, a partir dePJE_2FA_TOTP_SECRET. Serve para captura MANUAL do 2FA viadocker logsde um container dedicado; nao requer Chrome nem assina nada.
O login (login / login-service) roda onde ha Chrome instalado. Com
PJE_SIGNER_PRIORITY=remote e PJE_SIGNER_REMOTE_ADDR=host-do-token:9090, o
handshake do certificado e delegado, por gRPC, ao SignerService (signer-only)
que roda no host com o token A3. Assim o token fica isolado num host e o browser
roda em outro: cada chamada de assinatura do fluxo de login viaja ate o host do
token e volta.
cmd/pjeheadless/ # main: bootstrap, leitura de config, selecao do modo (PJE_MODE)
internal/config/ # carrega env vars; tipo Config
internal/audit/ # logging estruturado JSON (log/slog)
internal/signer/ # interface Signer; PFXSigner; PKCS11Signer; DualSigner; RemoteSigner
internal/pjeoffice/ # servidor HTTP :8800; implementacao do protocolo PJeOffice
internal/browser/ # login headless no jus.br via chromedp/CDP; SSO + 2FA TOTP + captura do bearer
internal/loginsvc/ # LoginManager: cache do bearer, expiracao e coalescencia (single-flight)
internal/grpcsigner/ # SignerService gRPC (lado servidor da assinatura remota)
internal/grpclogin/ # LoginService gRPC (entrega o bearer com reuso de sessao)
internal/signerpb/ # stubs gerados de proto/signer.proto
internal/loginpb/ # stubs gerados de proto/login.proto
proto/ # contratos gRPC: signer.proto (SignerService) e login.proto (LoginService)
Requer Go >= 1.26 e CGO habilitado (necessario para o miekg/pkcs11, que
compila o binding C do PKCS#11).
# Instalar dependencias de compilacao (Debian/Ubuntu)
apt-get install -y gcc libc6-dev
# Compilar o binario
go build -o bin/pjeheadless ./cmd/pjeheadless
# Ou via Makefile
make buildPara verificar sem gerar binario:
go vet ./...O servico e configurado exclusivamente por variaveis de ambiente. Nenhum segredo deve ser passado por argumento de linha de comando ou versionado.
| Variavel | Padrao | Descricao |
|---|---|---|
PJE_MODE |
full |
Modo do servico: full, signer-only, login, login-service ou totp (ver "Modos de operacao"). |
PJE_SIGNER_PRIORITY |
pkcs11,pfx |
Ordem de backends (pkcs11, pfx, remote). Primeiro disponivel e usado. |
PJE_PKCS11_MODULE |
/usr/lib/libaetpkss.so |
Caminho do modulo PKCS#11 do token A3. |
PJE_PKCS11_PIN |
(sem padrao) | PIN do token A3. |
PJE_PKCS11_SLOT |
(sem padrao) | Slot do token (opcional; usa primeiro disponivel se vazio). |
PJE_PKCS11_TOKEN_LABEL |
(sem padrao) | Label do token (opcional). |
PJE_PFX_PATH |
(sem padrao) | Caminho do arquivo .pfx/.p12. |
PJE_PFX_PASS |
(sem padrao) | Senha do arquivo PFX. |
PJE_PJEOFFICE_PORT |
8800 |
Porta do servidor HTTP PJeOffice. |
PJE_BIND_ADDR |
127.0.0.1 |
Interface de bind do servidor HTTP. Padrao loopback (somente local). |
PJE_CHAIN_DIR |
(sem padrao) | Diretorio com certificados intermediarios/raiz (opcional). |
PJE_GRPC_ADDR |
:9090 |
Bind do SignerService gRPC (modo signer-only). Padrao todas as interfaces. |
PJE_SIGNER_REMOTE_ADDR |
(sem padrao) | host:porta do SignerService remoto, usado quando remote esta em PJE_SIGNER_PRIORITY. |
PJE_LOGIN_GRPC_ADDR |
127.0.0.1:9091 |
Bind do LoginService gRPC (modo login-service). Padrao loopback. |
PJE_2FA_TOTP_SECRET |
(sem padrao) | Segredo TOTP base32 do 2FA, usado nos modos de login. Nunca hardcoded. |
PJE_CHROME_PATH |
(sem padrao) | Caminho do binario Chrome/Chromium para os modos de login. |
PJE_CHROMEDP_DEBUG |
(sem padrao) | Opcional; qualquer valor nao-vazio liga o trace do protocolo CDP em stderr. |
PJE_SIGNER_PRIORITY=pfx \
PJE_PFX_PATH=/run/secrets/cert.pfx \
PJE_PFX_PASS=<senha-do-pfx> \
./bin/pjeheadlessPJE_SIGNER_PRIORITY=pkcs11,pfx \
PJE_PKCS11_MODULE=/usr/lib/libaetpkss.so \
PJE_PKCS11_PIN=<pin> \
PJE_PFX_PATH=/run/secrets/cert.pfx \
PJE_PFX_PASS=<senha-do-pfx> \
./bin/pjeheadlessRoda no host que detem o token A3 e serve a assinatura por gRPC. O bind padrao
(:9090) escuta em todas as interfaces para atender o assinante remoto pela LAN.
PJE_MODE=signer-only \
PJE_SIGNER_PRIORITY=pkcs11 \
PJE_PKCS11_MODULE=/usr/lib/libaetpkss.so \
PJE_PKCS11_PIN=<pin> \
PJE_GRPC_ADDR=:9090 \
./bin/pjeheadlessExecuta um unico login headless e imprime LOGIN_OK bearer_len=N. Com
PJE_SIGNER_PRIORITY=remote, delega o handshake do certificado ao signer-only
do host do token; sem ele, usa o Signer local.
PJE_MODE=login \
PJE_SIGNER_PRIORITY=remote \
PJE_SIGNER_REMOTE_ADDR=host-do-token:9090 \
PJE_CHROME_PATH=/usr/bin/chromium \
PJE_2FA_TOTP_SECRET=<segredo-base32> \
./bin/pjeheadlessExpoe o LoginService gRPC com reuso de sessao. O bind padrao
(127.0.0.1:9091) escuta apenas em loopback; para servir um consumidor em outro
host, defina PJE_LOGIN_GRPC_ADDR para uma interface de LAN confiavel.
PJE_MODE=login-service \
PJE_SIGNER_PRIORITY=remote \
PJE_SIGNER_REMOTE_ADDR=host-do-token:9090 \
PJE_CHROME_PATH=/usr/bin/chromium \
PJE_2FA_TOTP_SECRET=<segredo-base32> \
PJE_LOGIN_GRPC_ADDR=127.0.0.1:9091 \
./bin/pjeheadlessOs modos de login (login e login-service) tratam o segundo fator
automaticamente quando a conta exige TOTP:
- Login recorrente. O codigo de 6 digitos e calculado a cada login a partir
de
PJE_2FA_TOTP_SECRET(segredo base32, RFC 6238, janela de 30s). Defina essa variavel com o segredo da conta; sem ela, um login que exija 2FA falha com erro explicito (nao submete codigo em branco). - Primeiro cadastro (enrollment). Se a conta ainda nao tem autenticador e o
SSO exige o cadastro (required-action
CONFIGURE_TOTP), o servico cadastra um novo dispositivo TOTP sozinho e registra UMA unica vez, em logINFO, o segredo cunhado (PJE_2FA_TOTP_SECRET=<segredo>). Esse segredo e o entregavel: persista-o com seguranca (cofre/secret de runtime) e informe-o emPJE_2FA_TOTP_SECRETnos logins seguintes. So este ponto registra o segredo; o codigo de 6 digitos nunca e logado pelo fluxo.
O segredo TOTP e uma credencial: trate-o como .env/secret de runtime e nunca o
versione (ver "Seguranca"). Para um login MANUAL no navegador (fora do fluxo
headless), o codigo atual pode ser obtido pelo modo totp deste binario
(PJE_MODE=totp, lendo via docker logs) ou por qualquer gerador TOTP RFC 6238
sobre o mesmo segredo base32.
Health-check. Retorna GIF 1x1 com status 200.
curl -I http://127.0.0.1:8800/pjeOffice/Modo "image ping" (legado).
Modo principal. Corpo JSON:
{
"servidor": "https://exemplo.jus.br",
"versao": "2.5.16",
"sessao": "AWSALB=...; JSESSIONID=...",
"tarefa": "{\"mensagem\":\"<DESAFIO>\",\"enviarPara\":\"/callback\",\"token\":\"<UUID>\",\"algoritmoAssinatura\":\"MD5withRSA\"}"
}Campos:
servidor: base do host de destino (concatenado comenviarPara).versao: cabecalhoversaoenviado ao tribunal (default interno2.5.16).sessao: cookies de sessao (opcional).tarefa(string JSON aninhada):mensagem: desafio a assinar.enviarPara: path do endpoint remoto.token: identificador UUID.algoritmoAssinatura:MD5withRSA(padrao historico),SHA1withRSAouSHA256withRSA.
Exemplo:
curl -sS -X POST http://127.0.0.1:8800/pjeOffice/requisicao/ \
-H "Content-Type: application/json" \
-d '{
"servidor": "https://exemplo.jus.br",
"tarefa": "{\"mensagem\":\"abc123\",\"enviarPara\":\"/api/assinatura\",\"token\":\"uuid-123\",\"algoritmoAssinatura\":\"SHA256withRSA\"}"
}'Contratos em proto/. Transporte TCP puro por padrao (mTLS e item de backlog).
SignerService (proto/signer.proto, modo signer-only, padrao :9090):
Sign— assina uma frase com o algoritmo dado; retorna a assinatura PKCS#1 v1.5 em base64.CertChainPKIPath— retorna o PKIPath ASN.1 da cadeia (folha primeiro) em base64.Identity— metadados do certificado folha (subject, issuer, serial, validade).Health— liveness/readiness; nao muta estado.
LoginService (proto/login.proto, modo login-service, padrao 127.0.0.1:9091):
GetBearer— retorna um bearer valido. Reusa o token em cache quando ainda fresco; dispara um login headless somente quando nao ha token fresco ou quandoforce=true. Chamadas concorrentes coalescem num unico login. A resposta incluiexpires_at_unix(exp do JWT;0quando desconhecido) efrom_cache.Health— reporta se ha um bearer nao-expirado em cache e quando ele expira; nao dispara login.
- Bind loopback por padrao.
PJE_BIND_ADDRpadrao e127.0.0.1; o servico nao aceita conexoes externas sem configuracao explicita. - SSRF scheme-guard. Somente
httpehttpssao aceitos comoservidor; outros schemes sao rejeitados. - Segredos nunca versionados. PIN, senha do PFX e qualquer credencial devem
ser fornecidos exclusivamente via variavel de ambiente ou secret de runtime
(Docker secret, Kubernetes secret). Nunca commitar
.pfx,.pem,.key,.envoucj.txt. - Token A3: o modulo PKCS#11 (
.so) e o daemonpcscddo host precisam estar acessiveis ao processo. Em containers, montar o socket do pcscd e o modulo como volumes (ver Dockerfile e a secao Docker). - Bearer e credencial;
LoginServiceem loopback por padrao. OLoginService(GetBearer) devolve um bearer (uma credencial reutilizavel) em TCP puro, sem autenticacao por chamada. Por issoPJE_LOGIN_GRPC_ADDRfaz bind em127.0.0.1:9091por padrao (fail-safe: nao alcancavel fora do host sem opt-in explicito). Para servir um consumidor em outro host, defina o bind para uma interface de LAN confiavel; confine a um segmento de rede confiavel. O bearer nunca e logado por inteiro. SignerServiceem todas as interfaces por necessidade. OSignerService(signer-only, padrao:9090) faz bind em todas as interfaces porque o host do token precisa servir o assinante remoto pela LAN. Mantenha-o num segmento confiavel. Para ambos os servicos gRPC, mTLS e item de backlog.- Container nao-root. O Dockerfile cria e usa usuario
appsem privilegios. - Mixed content. Paginas
https://podem bloquear chamadas ahttp://localhostpor politica do navegador. Nesse caso, coloque um reverse proxy local (Nginx/Caddy) com TLS apontando parahttp://127.0.0.1:8800.
A imagem runtime atual (debian-slim) nao inclui Chrome, entao atende os
modos full e signer-only. Os modos de login (login / login-service)
exigem Chrome: rode o binario direto no host com Chrome instalado, ou estenda a
imagem para incluir chromium (e, para token A3, montar o modulo PKCS#11 e o
socket do pcscd).
docker build -t pjeoffice-headless:2.0 .docker run --rm \
-e PJE_SIGNER_PRIORITY=pfx \
-e PJE_PFX_PATH=/run/secrets/cert.pfx \
-e PJE_PFX_PASS=<senha-do-pfx> \
-v /caminho/real/cert.pfx:/run/secrets/cert.pfx:ro \
-p 127.0.0.1:8800:8800 \
pjeoffice-headless:2.0docker compose up -dJa implementado: login headless browser (chromedp, SSO + 2FA TOTP + captura do
bearer); reuso de sessao loginsvc (cache com expiracao e coalescencia); API gRPC
(SignerService e LoginService); e o RemoteSigner que delega a assinatura ao
host do token.
Pendente:
- Deploy do container no host do token, com Chrome para os modos de login e os
mounts de PKCS#11 /
pcscd. - Migracao do consumidor para obter o bearer via
GetBearergRPC. - Hardening com mTLS nos servicos gRPC (
SignerServiceeLoginService).
GNU General Public License v3.0. Projeto nao afiliado ao CNJ.