gblinux 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gblinux-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.3
2
+ Name: gblinux
3
+ Version: 0.1.0
4
+ Summary: Assinador de PDF com token A3 ICP-Brasil para Linux
5
+ Author: renatormc
6
+ Author-email: renatormc <renatomartinsrmc@gmail.com>
7
+ Requires-Dist: asn1crypto>=1.5.1
8
+ Requires-Dist: cryptography>=50.0.2
9
+ Requires-Dist: pillow>=13.0.0
10
+ Requires-Dist: pyhanko>=0.38.1
11
+ Requires-Dist: pykcs11>=1.5.21
12
+ Requires-Dist: pyside6-essentials>=6.12.0
13
+ Requires-Dist: requests>=2.34.2
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Assinador de PDF com token A3
18
+
19
+ Programa para Linux que assina documentos PDF com certificado digital ICP-Brasil A3 em token USB.
20
+ Ele fica aberto no computador e atende os pedidos de assinatura feitos pela página web do sistema.
21
+
22
+ Estas instruções são para Ubuntu e derivados, como Linux Mint e Pop!_OS. É preciso ter o
23
+ Python 3.13 ou mais novo; para conferir a versão, use `python3 --version`.
24
+
25
+ ## 1. Instale o driver do token
26
+
27
+ ```bash
28
+ sudo apt update
29
+ sudo apt install -y pcscd libpcsclite1 libccid pcsc-tools opensc libnss3-tools
30
+
31
+
32
+ cd ~/Downloads
33
+
34
+ # 1. Baixar e extrair o pacote oficial do ePass 2003
35
+ curl -O "https://site.solutinet.com.br/drivers/Token/ePass2003/Linux/ePass2003_Castle_20141128.tar.gz"
36
+ tar -zxvf ePass2003_Castle_20141128.tar.gz
37
+
38
+ # 2. Copiar para /usr/lib no formato esperado pelo Assinador Serpro
39
+ sudo rm -rf /usr/lib/ePass2003-Linux-x64
40
+ sudo cp -rf ~/Downloads/ePass2003-Castle-20141128 /usr/lib/ePass2003-Linux-x64
41
+
42
+ # 3. Criar atalhos da biblioteca PKCS#11 (libcastle.so.1.0.0) em /usr/lib
43
+ sudo cp /usr/lib/ePass2003-Linux-x64/x86_64/redist/libcastle.so.1.0.0 /usr/lib/
44
+ sudo ln -sf /usr/lib/libcastle.so.1.0.0 /usr/lib/libcastle.so
45
+
46
+ # 4. Configurar regras USB (udev) e reiniciar o daemon pcscd
47
+ sudo sh /usr/lib/ePass2003-Linux-x64/x86_64/config/config.sh
48
+ sudo systemctl enable --now pcscd.socket
49
+ sudo systemctl restart pcscd
50
+ ```
51
+
52
+ Depois disso, retire o token e conecte de novo.
53
+
54
+ ## 2. Instale o pipx
55
+
56
+ ```bash
57
+ sudo apt update
58
+ sudo apt install pipx libxcb-cursor0
59
+ pipx ensurepath
60
+ ```
61
+
62
+ Depois disso, **feche o terminal e abra outro**.
63
+
64
+ ## 3. Instale o assinador
65
+
66
+ ```bash
67
+ pipx install gblinux
68
+ ```
69
+
70
+ ## 4. Crie a entrada no menu de aplicativos
71
+
72
+ ```bash
73
+ gblinux --instalar-atalho
74
+ ```
75
+
76
+ O **Assinador de PDF** vai aparecer no menu de aplicativos. Se não aparecer logo, encerre a sessão e
77
+ entre de novo.
78
+
79
+ ## Como usar
80
+
81
+ 1. Conecte o token.
82
+ 2. Abra o **Assinador de PDF** pelo menu.
83
+ 3. Deixe a janela aberta e peça a assinatura na página web.
84
+ 4. Se a janela de senha aparecer, digite a senha do token.
85
+
86
+ Para encerrar, clique em **Fechar**.
87
+
88
+ **Cuidado com a senha:** cada senha errada gasta uma tentativa, e o token é bloqueado quando as
89
+ tentativas acabam.
90
+
91
+ ## Como testar
92
+
93
+ Com o assinador aberto, rode em um terminal:
94
+
95
+ ```bash
96
+ testar-assinatura arquivo.pdf
97
+ ```
98
+
99
+ O arquivo assinado é salvo na mesma pasta, como `arquivo-assinado.pdf`.
100
+
101
+ ## Problemas comuns
102
+
103
+ | O que acontece | O que fazer |
104
+ |---|---|
105
+ | `comando não encontrado` depois de instalar | Feche o terminal e abra outro |
106
+ | `Token não encontrado` | Confira se o token está conectado e se o driver foi instalado |
107
+ | A janela de senha não aparece, ou `Assinador não respondeu` | Feche o assinador, retire o token, conecte de novo e abra o assinador |
108
+ | `O assinador já está aberto` | Procure a janela que já está aberta |
109
+ | Aviso de porta ocupada | Feche o assinador antigo ou o outro programa que usa as portas 3399 e 4499 |
110
+
111
+ ## Atualizar
112
+
113
+ ```bash
114
+ pipx upgrade gblinux
115
+ ```
116
+
117
+ ## Desinstalar
118
+
119
+ ```bash
120
+ rm ~/.local/share/applications/gblinux.desktop
121
+ rm ~/.local/share/icons/hicolor/scalable/apps/gblinux.svg
122
+ pipx uninstall gblinux
123
+ ```
@@ -0,0 +1,107 @@
1
+ # Assinador de PDF com token A3
2
+
3
+ Programa para Linux que assina documentos PDF com certificado digital ICP-Brasil A3 em token USB.
4
+ Ele fica aberto no computador e atende os pedidos de assinatura feitos pela página web do sistema.
5
+
6
+ Estas instruções são para Ubuntu e derivados, como Linux Mint e Pop!_OS. É preciso ter o
7
+ Python 3.13 ou mais novo; para conferir a versão, use `python3 --version`.
8
+
9
+ ## 1. Instale o driver do token
10
+
11
+ ```bash
12
+ sudo apt update
13
+ sudo apt install -y pcscd libpcsclite1 libccid pcsc-tools opensc libnss3-tools
14
+
15
+
16
+ cd ~/Downloads
17
+
18
+ # 1. Baixar e extrair o pacote oficial do ePass 2003
19
+ curl -O "https://site.solutinet.com.br/drivers/Token/ePass2003/Linux/ePass2003_Castle_20141128.tar.gz"
20
+ tar -zxvf ePass2003_Castle_20141128.tar.gz
21
+
22
+ # 2. Copiar para /usr/lib no formato esperado pelo Assinador Serpro
23
+ sudo rm -rf /usr/lib/ePass2003-Linux-x64
24
+ sudo cp -rf ~/Downloads/ePass2003-Castle-20141128 /usr/lib/ePass2003-Linux-x64
25
+
26
+ # 3. Criar atalhos da biblioteca PKCS#11 (libcastle.so.1.0.0) em /usr/lib
27
+ sudo cp /usr/lib/ePass2003-Linux-x64/x86_64/redist/libcastle.so.1.0.0 /usr/lib/
28
+ sudo ln -sf /usr/lib/libcastle.so.1.0.0 /usr/lib/libcastle.so
29
+
30
+ # 4. Configurar regras USB (udev) e reiniciar o daemon pcscd
31
+ sudo sh /usr/lib/ePass2003-Linux-x64/x86_64/config/config.sh
32
+ sudo systemctl enable --now pcscd.socket
33
+ sudo systemctl restart pcscd
34
+ ```
35
+
36
+ Depois disso, retire o token e conecte de novo.
37
+
38
+ ## 2. Instale o pipx
39
+
40
+ ```bash
41
+ sudo apt update
42
+ sudo apt install pipx libxcb-cursor0
43
+ pipx ensurepath
44
+ ```
45
+
46
+ Depois disso, **feche o terminal e abra outro**.
47
+
48
+ ## 3. Instale o assinador
49
+
50
+ ```bash
51
+ pipx install gblinux
52
+ ```
53
+
54
+ ## 4. Crie a entrada no menu de aplicativos
55
+
56
+ ```bash
57
+ gblinux --instalar-atalho
58
+ ```
59
+
60
+ O **Assinador de PDF** vai aparecer no menu de aplicativos. Se não aparecer logo, encerre a sessão e
61
+ entre de novo.
62
+
63
+ ## Como usar
64
+
65
+ 1. Conecte o token.
66
+ 2. Abra o **Assinador de PDF** pelo menu.
67
+ 3. Deixe a janela aberta e peça a assinatura na página web.
68
+ 4. Se a janela de senha aparecer, digite a senha do token.
69
+
70
+ Para encerrar, clique em **Fechar**.
71
+
72
+ **Cuidado com a senha:** cada senha errada gasta uma tentativa, e o token é bloqueado quando as
73
+ tentativas acabam.
74
+
75
+ ## Como testar
76
+
77
+ Com o assinador aberto, rode em um terminal:
78
+
79
+ ```bash
80
+ testar-assinatura arquivo.pdf
81
+ ```
82
+
83
+ O arquivo assinado é salvo na mesma pasta, como `arquivo-assinado.pdf`.
84
+
85
+ ## Problemas comuns
86
+
87
+ | O que acontece | O que fazer |
88
+ |---|---|
89
+ | `comando não encontrado` depois de instalar | Feche o terminal e abra outro |
90
+ | `Token não encontrado` | Confira se o token está conectado e se o driver foi instalado |
91
+ | A janela de senha não aparece, ou `Assinador não respondeu` | Feche o assinador, retire o token, conecte de novo e abra o assinador |
92
+ | `O assinador já está aberto` | Procure a janela que já está aberta |
93
+ | Aviso de porta ocupada | Feche o assinador antigo ou o outro programa que usa as portas 3399 e 4499 |
94
+
95
+ ## Atualizar
96
+
97
+ ```bash
98
+ pipx upgrade gblinux
99
+ ```
100
+
101
+ ## Desinstalar
102
+
103
+ ```bash
104
+ rm ~/.local/share/applications/gblinux.desktop
105
+ rm ~/.local/share/icons/hicolor/scalable/apps/gblinux.svg
106
+ pipx uninstall gblinux
107
+ ```
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "gblinux"
3
+ version = "0.1.0"
4
+ description = "Assinador de PDF com token A3 ICP-Brasil para Linux"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "asn1crypto>=1.5.1",
9
+ "cryptography>=50.0.2",
10
+ "pillow>=13.0.0",
11
+ "pyhanko>=0.38.1",
12
+ "pykcs11>=1.5.21",
13
+ "pyside6-essentials>=6.12.0",
14
+ "requests>=2.34.2",
15
+ ]
16
+
17
+ [[project.authors]]
18
+ name = "renatormc"
19
+ email = "renatomartinsrmc@gmail.com"
20
+
21
+ [project.scripts]
22
+ gblinux = "gblinux:main"
23
+ testar-assinatura = "gblinux.testar:main"
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.13.7,<0.14.0"]
27
+ build-backend = "uv_build"
28
+
29
+ [dependency-groups]
30
+ dev = ["pytest>=9.2.0"]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ pythonpath = ["tests"]
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "gblinux"
3
+ version = "0.1.0"
4
+ description = "Assinador de PDF com token A3 ICP-Brasil para Linux"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "renatormc", email = "renatomartinsrmc@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "asn1crypto>=1.5.1",
12
+ "cryptography>=50.0.2",
13
+ "pillow>=13.0.0",
14
+ "pyhanko>=0.38.1",
15
+ "pykcs11>=1.5.21",
16
+ "pyside6-essentials>=6.12.0",
17
+ "requests>=2.34.2",
18
+ ]
19
+
20
+ [project.scripts]
21
+ gblinux = "gblinux:main"
22
+ testar-assinatura = "gblinux.testar:main"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.13.7,<0.14.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=9.2.0",
31
+ ]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ pythonpath = ["tests"]
@@ -0,0 +1,8 @@
1
+ """Assinador de PDF com token A3 para Linux."""
2
+
3
+
4
+ def main() -> int:
5
+ # Importação tardia: o processo filho usa este pacote e não deve carregar o Qt.
6
+ from .aplicacao import main as principal
7
+
8
+ return principal()
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from . import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,86 @@
1
+ """Ponto de entrada do programa com janela (seção 9)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import signal
8
+ import sys
9
+
10
+ from PySide6.QtCore import QDir, QLockFile, QStandardPaths, QTimer
11
+ from PySide6.QtWidgets import QApplication, QMessageBox
12
+
13
+ from . import config as configuracao
14
+ from .atalho import instalar_atalho
15
+ from .interface import JanelaPrincipal, PerguntasQt
16
+ from .servico import ServicoAssinatura
17
+ from .servidor import Contexto, PortaOcupada, Servidores
18
+
19
+
20
+ def _avisar(mensagem: str) -> None:
21
+ print(mensagem, file=sys.stderr)
22
+ QMessageBox.critical(None, "Assinador de PDF", mensagem)
23
+
24
+
25
+ def main() -> int:
26
+ argumentos = argparse.ArgumentParser(description="Assinador de PDF com token A3")
27
+ argumentos.add_argument(
28
+ "--instalar-atalho", action="store_true", help="cria o atalho no menu de aplicativos e sai"
29
+ )
30
+ if argumentos.parse_args().instalar_atalho:
31
+ print(f"Atalho criado em {instalar_atalho()}")
32
+ return 0
33
+
34
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
35
+ aplicacao = QApplication(sys.argv[:1])
36
+ aplicacao.setApplicationName("gblinux")
37
+ aplicacao.setQuitOnLastWindowClosed(False)
38
+
39
+ pasta = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.RuntimeLocation)
40
+ trava = QLockFile(QDir(pasta or QDir.tempPath()).filePath("gblinux.lock"))
41
+ trava.setStaleLockTime(0)
42
+ if not trava.tryLock(0):
43
+ _avisar("O assinador já está aberto.")
44
+ return 1
45
+
46
+ config = configuracao.carregar()
47
+ perguntas = PerguntasQt()
48
+ janela: JanelaPrincipal | None = None
49
+
50
+ def ao_mudar_ocupado(ocupado: bool) -> None:
51
+ if janela is not None:
52
+ janela.ocupado_mudou.emit(ocupado)
53
+
54
+ def ao_resultado(mensagem: str) -> None:
55
+ if janela is not None:
56
+ janela.resultado_novo.emit(mensagem)
57
+
58
+ servico = ServicoAssinatura(config, perguntas, ao_mudar_ocupado)
59
+ servidores = Servidores(Contexto(config, servico, ao_resultado))
60
+ try:
61
+ servidores.iniciar()
62
+ except PortaOcupada as erro:
63
+ _avisar(
64
+ f"A porta {erro.porta} está ocupada: o assinador já está aberto ou outro "
65
+ "programa usa a porta."
66
+ )
67
+ return 1
68
+
69
+ def encerrar() -> None:
70
+ servidores.parar()
71
+ servico.encerrar()
72
+
73
+ janela = JanelaPrincipal(encerrar)
74
+ janela.show()
75
+
76
+ # O Python só trata sinais quando recupera o controle do laço de eventos do Qt.
77
+ def tratar_sinal(_sinal, _quadro) -> None:
78
+ QTimer.singleShot(0, janela.encerrar_sem_perguntar)
79
+
80
+ signal.signal(signal.SIGTERM, tratar_sinal)
81
+ signal.signal(signal.SIGINT, tratar_sinal)
82
+ relogio = QTimer()
83
+ relogio.timeout.connect(lambda: None)
84
+ relogio.start(200)
85
+
86
+ return aplicacao.exec()
@@ -0,0 +1,39 @@
1
+ """Atalho no menu de aplicativos, instalado na pasta do usuário, sem root (seção 9)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shlex
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ _ICONE = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
11
+ <path fill="#f5f5f5" stroke="#444" stroke-width="3" d="M14 5h26l12 12v42H14z"/>
12
+ <path fill="none" stroke="#444" stroke-width="3" d="M40 5v12h12"/>
13
+ <path fill="none" stroke="#1565c0" stroke-width="4" stroke-linecap="round"
14
+ d="M20 46c5-14 8-14 9-6s4 7 7 1 4-5 8 0"/>
15
+ </svg>
16
+ """
17
+
18
+
19
+ def instalar_atalho() -> Path:
20
+ dados = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share")
21
+ icone = dados / "icons/hicolor/scalable/apps/gblinux.svg"
22
+ icone.parent.mkdir(parents=True, exist_ok=True)
23
+ icone.write_text(_ICONE, encoding="utf-8")
24
+
25
+ atalho = dados / "applications/gblinux.desktop"
26
+ atalho.parent.mkdir(parents=True, exist_ok=True)
27
+ comando = shlex.join([sys.executable, "-m", "gblinux"])
28
+ atalho.write_text(
29
+ "[Desktop Entry]\n"
30
+ "Type=Application\n"
31
+ "Name=Assinador de PDF\n"
32
+ "Comment=Assina PDFs com o token A3\n"
33
+ f"Exec={comando}\n"
34
+ "Icon=gblinux\n"
35
+ "Terminal=false\n"
36
+ "Categories=Office;\n",
37
+ encoding="utf-8",
38
+ )
39
+ return atalho
@@ -0,0 +1,35 @@
1
+ """Configuração lida do ambiente. Os valores padrão vêm da especificação."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+ PORTA_PAGINA = 3399
9
+ PORTA_DIRETA = 4499
10
+
11
+
12
+ def _int(nome: str, padrao: int) -> int:
13
+ valor = os.environ.get(nome, "").strip()
14
+ return int(valor) if valor else padrao
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Config:
19
+ biblioteca_pkcs11: str = "/usr/lib/libcastle.so"
20
+ arquivo_cas: str | None = None
21
+ limite_bytes: int = 100 * 1024 * 1024
22
+ tempo_limite_s: float = 600.0
23
+ tempo_conectar_s: float = 30.0
24
+ tempo_ler_s: float = 120.0
25
+ # Tempo que o driver recebe para terminar depois de um SIGTERM.
26
+ tempo_encerrar_s: float = 15.0
27
+
28
+
29
+ def carregar() -> Config:
30
+ return Config(
31
+ biblioteca_pkcs11=os.environ.get("ASSINADOR_PKCS11_LIB") or Config.biblioteca_pkcs11,
32
+ arquivo_cas=os.environ.get("ASSINADOR_CA_BUNDLE") or None,
33
+ limite_bytes=_int("ASSINADOR_LIMITE_BYTES", Config.limite_bytes),
34
+ tempo_limite_s=_int("ASSINADOR_TEMPO_LIMITE_S", int(Config.tempo_limite_s)),
35
+ )
@@ -0,0 +1,35 @@
1
+ """Download dos PDFs (seção 4.5)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import urlsplit
6
+
7
+ import requests
8
+
9
+ from .config import Config
10
+
11
+
12
+ class FalhaDownload(Exception):
13
+ pass
14
+
15
+
16
+ def baixar(url: str, config: Config) -> bytes:
17
+ if urlsplit(url).scheme.lower() not in ("http", "https"):
18
+ raise FalhaDownload("somente URLs http e https são aceitas")
19
+ dados = bytearray()
20
+ try:
21
+ # Em redirecionamentos, o requests só aceita http e https, e recusa file:// ou ftp://.
22
+ with requests.get(
23
+ url,
24
+ stream=True,
25
+ timeout=(config.tempo_conectar_s, config.tempo_ler_s),
26
+ verify=config.arquivo_cas or True,
27
+ ) as resposta:
28
+ resposta.raise_for_status()
29
+ for bloco in resposta.iter_content(1024 * 1024):
30
+ dados += bloco
31
+ if len(dados) > config.limite_bytes:
32
+ raise FalhaDownload("o arquivo excede o tamanho máximo permitido")
33
+ except requests.RequestException as erro:
34
+ raise FalhaDownload(str(erro)) from erro
35
+ return bytes(dados)
@@ -0,0 +1,36 @@
1
+ """Mensagens de erro de assinatura (seção 7), mantidas iguais às do original."""
2
+
3
+ from __future__ import annotations
4
+
5
+ CANCELADO = "ASSINATURA DE DOCUMENTO CANCELADO PELO USUÁRIO"
6
+ TOKEN_NAO_ENCONTRADO = "Token não encontrado"
7
+ SENHA_INCORRETA = "Senha incorreta para o token"
8
+ PDF_INVALIDO = (
9
+ "Documento não pode ser aberto e editado. Verifique se o documento esta protegido de "
10
+ "edição ou contem senha e se os dados do TOKEN foram informados corretamente!"
11
+ )
12
+ NENHUM_CERTIFICADO = "Nenhum certificado valido. Procure o gestor da sua instituição!"
13
+ FALHA_LISTAR = (
14
+ "Houve um erro ao listar os dados do Token, verifique se o token ta inserido "
15
+ "corretamente e o dados informados"
16
+ )
17
+ ERRO_DRIVER = "Erro desconhecido, Verifique se foi instalado corretamente os drivers do token"
18
+ SENHA_BLOQUEADA = "Token bloqueado por excesso de tentativas de senha."
19
+ SEM_RESPOSTA = "Assinador não respondeu. Reinsira o token e tente novamente."
20
+ GOVBR_INDISPONIVEL = "Assinatura via Gov.br não está disponível nesta versão"
21
+ SEM_DOCUMENTO = "Nenhum arquivo para fazer a assinatura!"
22
+
23
+
24
+ def chave_nao_encontrada(cpf: str) -> str:
25
+ return (
26
+ f"Chave privada do token não encontrada para o CPF [{cpf}]. Verifique se o token está "
27
+ "inserido, se a chave privada está correta e se o certicado está valido!"
28
+ )
29
+
30
+
31
+ class ErroAssinatura(Exception):
32
+ """Erro que vira a resposta JSON da seção 7."""
33
+
34
+ def __init__(self, mensagem: str) -> None:
35
+ super().__init__(mensagem)
36
+ self.mensagem = mensagem