-
Notifications
You must be signed in to change notification settings - Fork 13
https cliente ral
Home > Configurar HTTPS no RAL > Como funciona o HTTPS no cliente RAL
⚠ Versão 1.1 ou superior do PascalRAL
O TRALClient decide sozinho com qual certificado de servidor ele aceita
conversar. Essa decisão fica no cliente, nunca no engine, então ela se
escreve e se comporta igual, seja qual for o EngineType.
São quatro membros, e é raro precisar de mais de um:
| Membro | Para que serve |
|---|---|
SSL.Pins |
Quais certificados são aceitos, e de qual host |
SSL.Required |
Recusa http puro, diga o que disser a URL |
SSL.Verify |
O que o próprio engine faz com o certificado |
OnValidateServerCert |
Decidir em código, quando a lista não expressa a regra |
Nada disso muda coisa alguma enquanto você não mexer: um cliente que não define nenhum deles se comporta exatamente como sempre se comportou.
Certificado assinado por CA pública não precisa de configuração — o sistema operacional já confia nele, e o RAL deixa o engine validar como sempre.
O problema é todo o resto:
- um servidor interno com certificado autoassinado, acessado por IP;
- um servidor cujo IP muda, e então nenhum certificado consegue nomeá-lo de antemão;
- uma aplicação que fala com vários servidores ao mesmo tempo, uns com certificado de CA pública e outros autoassinados.
A saída de sempre — instalar uma CA própria em cada máquina — não funciona em todo lugar: desde o Android 7, um app não confia em CA que o usuário instalou. Ou seja, a confiança precisa ficar na aplicação, não no aparelho.
É isso que o SSL.Pins faz: você informa ao cliente o SHA-256 do certificado
que espera, e ele passa a aceitar aquele e recusar todos os outros, sem
instalar nada em lugar nenhum.
A partir do arquivo do certificado:
openssl x509 -in servidor.crt -noout -fingerprint -sha256
sha256 Fingerprint=65:42:DB:04:A4:77:8A:33:BD:BD:F9:EC:5C:95:37:93:D4:F7:35:72:EA:80:4C:14:5F:BB:55:64:17:35:29:0A
A partir de um servidor no ar, sem ter o arquivo:
openssl s_client -connect 192.168.0.11:9988 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256
Cole na notação que vier — com dois-pontos, com espaços, em minúsculas. O RAL normaliza. O que ele não aceita é o que não for um SHA-256: valor errado ou truncado levanta erro na hora em que a linha é adicionada, em vez de virar uma recusa misteriosa em produção.
RALClient1.BaseURL.Text := 'https://192.168.0.11:9988';
RALClient1.SSL.Pins.Add('6542DB04A4778A33BDBDF9EC5C953793D4F73572EA804C145FBB55641735290A');É toda a configuração. A partir daí:
- a loja de certificados da máquina deixa de importar — não precisa instalar nada, nem no Windows nem no Android;
- somente aquele certificado é aceito. Até um certificado em que a máquina confia é recusado, se não for este;
-
CNeSANdeixam de ser conferidos, então o IP do servidor não precisa estar no certificado, e trocar esse IP não quebra o cliente; -
httppuro passa a ser recusado para esse host — pin sobre texto claro não estaria conferindo nada.
É por isso que Pins é lista e não um valor só. Cada linha diz onde aquele
certificado é aceito:
RALClient1.SSL.Pins.Text :=
'192.168.0.11=CD34EF...'#13#10 + // autoassinado, so' nesse host
'10.0.0.7:8443=90FFEE...'#13#10 + // autoassinado, nesse host e porta
'[2001:db8::1]:9988=1234AB...'; // IPv6 vai entre colchetese a regra, aplicada por conexão:
| Situação | O que acontece |
|---|---|
| existe linha para o host chamado | só aqueles certificados são aceitos, mesmo contra a loja do sistema |
| não existe linha para aquele host | o certificado é validado normalmente |
Ou seja: os servidores com certificado de CA pública simplesmente não entram na lista. Continuam sendo validados pela cadeia, e nada quebra quando eles renovarem — que é o ponto, já que certificado público é trocado de poucos em poucos meses.
Uma linha sem = vale para qualquer host:
AB12CD... aceito de qualquer host com que este cliente falar
| Forma | Significado |
|---|---|
impressao_digital |
qualquer host |
host=impressao_digital |
aquele host, qualquer porta |
host:porta=impressao_digital |
aquele host naquela porta |
[ipv6]:porta=impressao_digital |
IPv6 com porta — colchetes obrigatórios |
ipv6=impressao_digital |
IPv6 sem porta, qualquer porta |
O lugar e o hash são separados por = e não por : porque os dois lados
são cheios de dois-pontos: AB:CD:EF:… na impressão digital, do jeito que o
openssl imprime, e [::1]:8443 num endereço IPv6. O RALSplitHostPort é a
única função que interpreta o lado esquerdo, e ela é pública — então o que
escreve essa configuração consegue lê-la de volta do mesmo jeito.
Várias linhas para o mesmo host valem todas:
RALClient1.SSL.Pins.Add('192.168.0.11=' + PIN_ATUAL);
RALClient1.SSL.Pins.Add('192.168.0.11=' + PIN_NOVO);Distribua o cliente com as duas, troque o certificado do servidor quando quiser e remova a linha antiga na versão seguinte. Em nenhum momento o cliente fica sem conseguir conectar.
Quando a regra é algo que a lista não expressa — validade, emissor, política por rota — atribua o evento. Ele é a última palavra: com um handler atribuído, nem o pin nem o veredito do engine decidem.
function TForm1.ValidaCert(ASender: TObject;
const ACert: TRALCertInfo): boolean;
begin
if ACert.Host = '192.168.0.11' then
Result := ACert.Fingerprint = PIN_INTERNO
else
Result := ACert.Trusted; // o resto: validacao normal
end;
RALClient1.OnValidateServerCert := ValidaCert;O que chega no handler:
| Campo | |
|---|---|
Fingerprint |
SHA-256, hexadecimal maiúsculo, sem separadores. Vazio quando o engine não consegue produzir |
Subject, Issuer, SerialNumber
|
como o engine reportou |
NotBefore, NotAfter
|
validade |
Trusted |
o que a validação do próprio engine concluiu (cadeia, host, datas) |
Error |
por que não, quando Trusted é False |
Host, Port
|
quem estava sendo chamado — vem da URL, não do certificado |
Host e Port são o que permitem um handler só atender vários servidores. Eles
vêm da entrada de BaseURL que aquela tentativa usou, então batem com o que
você escreveu lá.
Devolver False recusa a conexão antes de sair um único byte da requisição
— o token inclusive.
Os engines nunca concordaram nesse ponto, e até agora não havia como dizer o que você queria:
TRALSSLVerify = (svEngine, svAlways, svNever);| Valor | Significado |
|---|---|
svEngine (padrão)
|
o que cada engine sempre fez — não é a mesma coisa em todos, ver a tabela adiante |
svAlways |
liga a validação onde ela está desligada |
svNever |
aceita qualquer certificado, em todos os engines |
Ele só decide quando não há pin para aquele host nem evento; esses dois, quando definidos, são a decisão.
svNever é para desenvolvimento contra um certificado descartável. Não vá
para produção com ele: aceita qualquer certificado de qualquer um, que é
exatamente o que o TLS existe para evitar.
svAlways tem uma pegadinha no Windows: o OpenSSL que o Indy e o fpHTTP
carregam não tem loja de certificados lá, então ligar a validação faz os dois
recusarem tudo, inclusive CA pública, até você fornecer um pacote de CAs ao
OpenSSL. No Linux, onde os caminhos padrão encontram /etc/ssl/certs, ele se
comporta como se espera.
| Engine | Valida por padrão | SSL.Pins |
OnValidateServerCert |
|---|---|---|---|
| Indy | não | sim | sim |
| fpHTTP | não | sim | sim |
| mORMot2 com OpenSSL | sim | sim | sim |
| mORMot2 no SChannel | sim | não | não |
| netHTTP | sim | não | sim¹ |
¹ no Windows esse engine chama o handler apenas quando a validação dele passou — ou seja, é um direito de veto sobre um certificado bom, não uma chance de salvar um ruim.
Três coisas que vale saber:
-
Indy e fpHTTP não validam certificado nenhum a menos que um pin, um evento
ou
svAlwayspeçam. É o comportamento histórico dos dois e foi mantido de propósito: ligar para todo mundo quebraria HTTPS comum no Windows, onde o OpenSSL deles não tem loja. Se você usa um dos dois sobrehttpse não definiu nenhum dos três, você não está autenticando o servidor. -
O netHTTP nunca vai pinar, em plataforma nenhuma: o
TCertificateda RTL não carrega impressão digital (no Android, nem a chave pública). Comparar porSubjectouSerialNumberpareceria pin e não seria — são textos copiáveis. Por isso o RAL recusa a requisição em vez de fingir. -
O mORMot2 no Windows usa SChannel a menos que a aplicação carregue o
OpenSSL, e o SChannel nunca chama os callbacks de TLS. Carregue-o
(
OpenSslIsAvailable, demormot.lib.openssl11, com as DLLs do OpenSSL ao lado do executável) e o pin funciona normalmente.
Para pinar em Delphi e FPC, no Windows e no Android, com uma configuração só:
use o engine Indy. Ele precisa dos binários do OpenSSL ao lado da aplicação
(libeay32/ssleay32 para o Indy que vem com o Delphi).
Certificado recusado é reportado como um erro de transporte próprio, em todos os engines, então você nunca precisa comparar texto de mensagem:
procedure TForm1.Resposta(ASender: TObject; AResponse: TRALResponse;
AException: StringRAL);
begin
if AResponse.TransportError = rteCertificate then
Mostrar('O servidor apresentou um certificado que não é o esperado.')
else if AResponse.TransportError = rteConnect then
Mostrar('Servidor fora do ar.');
end;rteCertificate nunca dispara nova tentativa na próxima BaseURL — certificado
que não confere não é coisa para tentar de novo em outro lugar por acidente.
O
TransportErroré legível nas sobrecargas de callback (Get/Post(rota, handler, comportamento)), que entregam a resposta mesmo quando a chamada falha. As sobrecargasvar AResponselevantam exceção, e a mensagem carrega o mesmo motivo.
Mensagens que você pode ver:
| Mensagem | Significado |
|---|---|
| Certificado do servidor recusado | o pin ou o seu handler disse não |
| SSL.Pins não é suportado pelo engine … nesta plataforma | aquele engine não consegue ler impressão digital — ver a tabela |
| O engine … não conseguiu inspecionar o certificado do servidor nesta conexão | o mORMot2 caiu no SChannel, que não pergunta |
| Toda linha de SSL.Pins deve terminar no SHA-256 do certificado | linha malformada, avisada no momento em que foi adicionada |
| SSL.Required está definido, ou uma linha de SSL.Pins vale para este host, então http puro é recusado | você pediu TLS e a URL é http
|
O que o pin garante. O cliente aceita exatamente o certificado que você nomeou e nada mais — nem outro autoassinado, nem um de CA em que a máquina confia, nem um emitido para o mesmo host com outra chave. É mais forte que a validação por CA, não mais fraco.
Onde o pin deve morar. Na aplicação, não em arquivo de configuração editável. Um pin que alguém pode editar permite apontar o cliente para outro servidor mudando uma linha — que é justamente o que o pin existe para impedir. Deixe o hash em constante ou recurso que viaja com o binário; host e porta podem ficar na configuração, porque mudá-los não enfraquece nada: host sem linha simplesmente cai na validação normal.
Rotação. Um certificado pinado precisa ser trocado no cliente antes de ser trocado no servidor, ou durante uma janela em que os dois são aceitos — ver a Receita 3.
O que o pin não faz. Não cifra nada por si só, não protege uma conexão feita
por http puro (para isso existe o SSL.Required) e não diz nada sobre o que o
servidor faz com os dados depois que eles chegam.
- Instalação
- Atualização
-
Exemplos de Uso
↗️ - Documentação de Samples
-
Documentação dos fontes do projeto
↗️