Recentemente, conversei com um desenvolvedor Thunderbird sobre design de API. Durante esta conversa, compartilhei minhas idéias sobre RNP , uma nova implementação do OpenPGP que o Thunderbird recentemente começou a usar em vez do GnuPG .
O interlocutor duvidou da minha tese de que a API RNP precisa de melhorias e perguntou: "Não é subjetivo - qual API é melhor e qual é pior?" Concordo que não temos boas métricas para avaliar APIs. Mas discordo que nós, em princípio, não somos capazes de julgar a API.
Na verdade, suspeito que a maioria dos programadores experientes reconhecerá uma API ruim se a virem. Acho que mais adiante neste artigo acabarei desenvolvendo uma boa heurística, que tentarei construir em minha própria experiência com (e mais) GnuPG, Sequoiae RNP. Em seguida, examinarei a API RNP. Infelizmente, essa API não só pode ser facilmente mal utilizada, mas também enganosa, por isso ainda não deve ser usada em contextos críticos de segurança. Mas o público-alvo do Thunderbird são pessoas reconhecidamente vulneráveis, como jornalistas, ativistas, advogados e seus parceiros de comunicação; todas essas pessoas precisam de proteção. Na minha opinião, isso significa que o Thunderbird deve pensar mais uma vez sobre a possibilidade de usar RNP.
Observação: também sugiro a leitura deste e-mail: Vamos usar bibliotecas GPL no Thunderbird! que enviei para a postagem de planejamento de desenvolvimento do Thunderbird .
Quais são as características de uma API ruim?
Antes de começarmos o Sequoia com Justus e Kai , nós três trabalhamos no GnuPG . Nós não apenas nos aprofundamos no gpg, mas também conversamos e colaboramos com muitos usuários subsequentes do gpg. As pessoas foram capazes de dizer muitas coisas boas sobre o GnuPG .
No que diz respeito às críticas ao gpg, as mais significativas foram dois tipos de críticas à API. O primeiro se resume a isso: a API gpg é muito dogmática. Por exemplo, o gpg usa uma abordagem de chaveiro. Portanto, você só pode visualizar ou usar um certificado OpenPGP se ele tiver sido importado para o keybase pessoal. Mas alguns desenvolvedores desejam examinar o certificado primeiro e só depois importá-lo. Por exemplo, ao pesquisar um certificado em um servidor de chaves por sua impressão digital, você pode verificar e certificar-se de que o certificado retornado é realmente aquele de que você precisa.porque seu URL é autoautenticado. Isso pode ser feito usando gpg, mas apenas como alternativa, contornando os princípios do modelo de programação que está embutido nele. A ideia básica é esta: crie um diretório temporário, adicione um arquivo de configuração a ele, diga ao gpg para usar um diretório alternativo, importe o certificado lá, verifique o certificado e, em seguida, limpe o diretório temporário. Esta é uma recomendação oficial adicionada por Justus com base em nossas conversas com usuários gpg subsequentes. Sim, esse método funciona. Mas requer a escrita de um código específico para o sistema operacional, esse código é lento e com frequência são introduzidos bugs.
Outra classe de observações que vimos muitas vezes é que, para trabalhar com o gpg, você precisa saber muitas coisas não óbvias - para não abusar desse mecanismo. Ou, dito de outra forma, você precisa ter muito cuidado ao usar a API gpg para evitar introduzir inadvertidamente uma vulnerabilidade em seu código.
Para entender melhor a segunda preocupação, considere as vulnerabilidades EFAIL... O principal problema com a API de descriptografia gpg: ao descriptografar uma mensagem, o gpg fornecerá texto simples, mesmo se a entrada tiver sido corrompida. O gpg retorna um erro neste caso, mas alguns programas ainda geram texto simples em uma forma corrompida. Então por que não? É definitivamente melhor mostrar pelo menos parte da mensagem do que não mostrar nada, certo? Bem, as vulnerabilidades EFAIL demonstram como um invasor pode tirar vantagem disso para injetar um bug da web em uma mensagem criptografada. Quando um usuário visualiza esta postagem, um bug da web vaza da postagem. Ufa.
Então, de quem é a culpa desse bug? Os desenvolvedores do GnuPG insistiram que o problema está no nível do aplicativo, no sentido de que eles usam gpg incorretamente:
É recomendado que os agentes de usuário de e-mail respeitem o código de status DECRYPTION_FAILED e não exibam dados, ou pelo menos escolham uma maneira apropriada de exibir e-mails potencialmente corrompidos sem criar um oráculo e informar ao usuário que o e-mail não inspira confiança.
gpg sinalizou um erro; os aplicativos não respeitam o contrato de API. Tenho que concordar com os desenvolvedores do GnuPG e acrescentar: a interface do gpg era (e ainda é) uma bomba-relógio porque não diz ao usuário como proceder. Ao contrário, uma ação fácil e aparentemente benéfica está errada. E uma API desse tipoinfelizmente são comuns no GnuPG.
O que torna uma boa API?
Perceber essas duas coisas - que a API gpg é muito dogmática e difícil de usar adequadamente - moldou meus planos. Quando iniciamos o projeto Sequoia, concordamos que queríamos evitar esses erros. Com base em nossas observações, colocamos em prática dois testes que continuamos a usar como pontos de referência para o desenvolvimento da API Sequoia. Primeiro, além de qualquer API de alto nível, deve haver uma API de baixo nível que não seja dogmática - no sentido de que não impede o usuário de fazer nada que não seja proibido. Ao mesmo tempo, a API deve orientar o usuário para as coisas certas (embutidas em código), tornando as ações certas fáceis de executar e mais óbvias ao escolher uma ação .
Para cumprir esses dois objetivos ligeiramente conflitantes de tornar tudo possível, mas evitar erros, confiamos principalmente em duas ferramentas: tipos e exemplos. Os tipos dificultam o uso de um objeto de maneira não intencional porque o contrato de API é formalizado no momento da compilação e até mesmo impõe conversões específicas . Exemplos - trechos de código - serão copiados . Portanto, bons exemplos não apenas ensinarão os usuários como usar a função corretamente, mas também influenciarão muito como eles a usarão.
Tipos
Vou mostrar a você um exemplo de como usamos tipos no Sequoia e como eles nos ajudam a fazer uma boa API. Para tornar o exemplo mais claro, será útil relembrar alguns contextos relacionados ao OpenPGP.
OpenPGP
Existem vários tipos de dados fundamentais no OpenPGP, a saber, certificados, componentes (como chaves e IDs de usuário) e assinaturas de ligação. A raiz do certificado é a chave primária que identifica totalmente a impressão digital do certificado (impressão digital = Hash (chave primária)). Um certificado geralmente inclui componentes como subchaves e IDs de usuário. O OpenPGP liga um componente a um certificado usando a chamada assinatura de ligação. Quando usamos um hash de chave primária regular como uma impressão digital e assinaturas para vincular componentes à chave primária, são criadas condições para que componentes adicionais possam ser adicionados posteriormente. As assinaturas de ligação também incluem propriedades. Portanto, é possível alterar o componente, por exemplo, para estender o período de validade da subchave.Como consequência, várias assinaturas válidas podem ser associadas a um determinado componente. As assinaturas de âncora não são apenas fundamentais, mas também integrantes do mecanismo de segurança do OpenPGP.
Como pode haver muitas assinaturas de vinculação válidas, deve haver uma maneira de selecionar a que você deseja. Como uma primeira aproximação, suponha que a assinatura que desejamos seja a assinatura válida mais recente, não expirada, não revogada, que não foi adiada para o futuro. Mas o que é uma assinatura válida? Na Sequoia, a assinatura não deve apenas passar por uma verificação matemática, mas também ser consistente com a política. Por exemplo, devido à nossa capacidade de resistir a colisões comprometidas , permitimos o SHA-1 apenas em um número muito pequeno de situações . ( Paul Schaub , trabalhando em PGP Stainless , escreveu recentemente sobre essas complexidades..) Ao forçar o usuário da API a manter todas essas considerações em mente, criamos um terreno fértil para vulnerabilidades. No Sequoia, a maneira mais fácil de obter o tempo de expiração é a maneira segura. Considere o seguinte código, que funciona conforme o esperado:
let p = &StandardPolicy::new();
let cert = Cert::from_str(CERT)?;
for k in cert.with_policy(p, None)?.keys().subkeys() {
println!("Key {}: expiry: {}",
k.fingerprint(),
if let Some(t) = k.key_expiration_time() {
DateTime::<Utc>::from(t).to_rfc3339()
} else {
"never".into()
});
}
cert
É um certificado. Começamos aplicando a política a ele. (As políticas são definidas pelo usuário, mas como regra, StandardPolicy não é apenas suficiente, mas também a mais apropriada). Na verdade, é aqui que uma visão do certificado é criada, na qual apenas os componentes com uma assinatura de ligação válida são visíveis. É importante ressaltar que ele também modifica e introduz uma série de novos métodos. O método keys, por exemplo, foi alterado para retornar ValidKeyAmalgamation em vez de KeyAmalgamation . (Esta é uma fusão, pois o resultado inclui não apenas a chave, mas todas as assinaturas associadas a ela; alguns acreditam que este processo seria melhor denominado Katamari... ¯ \ _ (ツ) _ / ¯) ValidKeyAmalgamation tem uma assinatura de âncora válida de acordo com os critérios acima. Ele também fornece métodos como key_expiration_time, que só faz sentido com uma chave válida! Observe também que o tipo de retorno usado com key_expiration_time é ergonômico. Em vez de retornar um valor bruto, key_expiration_time retorna SystemTime , que é seguro e fácil de usar.
Em linha com nosso primeiro princípio de "permitir todos", o desenvolvedor ainda mantém o acesso a assinaturas únicas e explora subpacotespara descobrir a partir de um vínculo de assinatura diferente quando a chave expira. Mas, em comparação com a maneira como a API do Sequoia deve saber corretamente a expiração de uma chave, qualquer outra abordagem contradiria a API. Esta é uma boa API em nossa opinião.
Exemplos de
O lançamento 1.0 da biblioteca Sequoia ocorreu em dezembro de 2020. Nove meses antes, entramos em uma situação de feature complete e estávamos prontos para o lançamento. Mas eles esperaram . Levamos os próximos nove meses para adicionar documentação e exemplos à API pública. Dê uma olhada na documentação da estrutura de dados Cert para um exemplo, veja o que temos. Como apontado em nossa postagem, não foi possível fornecer exemplos para cada recurso em um, mas fizemos um pouco. Como um bônus por escrever os exemplos, também conseguimos encontrar algumas arestas, que polimos no processo.
Após o lançamento, pudemos conversar com muitos dos desenvolvedores que incluíram o Sequoia em seu código. Um ponto comum em seus comentários foi o reconhecimento da utilidade da documentação e dos exemplos. Podemos confirmar que, embora este seja o nosso código, examinamos a documentação quase que diariamente e copiamos nossos próprios exemplos. É mais fácil. Já que os exemplos mostram como usar uma função específica corretamente, por que refazê-la do zero?
API RNP
RNP é uma nova implementação do OpenPGP, desenvolvida principalmente pela Ribose . Há cerca de dois anos , o Thunderbird decidiu integrar o Enigmail ao Thunderbird e, ao mesmo tempo, substituir o GnuPG pelo RNP . O fato de o Thunderbird ter escolhido a RNP não é apenas lisonjeiro para a RNP; também significa que a RNP se tornou indiscutivelmente a implementação mais solicitada do OpenPGP para criptografar e-mails.
A crítica é fácil de perceber como negativa. Quero ser muito claro: acho o trabalho que a Ribose está fazendo bom e importante, agradeço a eles por investirem tempo e esforço em uma nova implementação do OpenPGP. O ecossistema OpenPGP precisa desesperadamente adicionar variedade. Mas isso não é uma desculpa para lançar um produto imaturo para uso em um contexto crítico de segurança.
Infraestrutura crítica de segurança
Infelizmente, a RNP ainda não atingiu um estado em que, na minha opinião, possa ser implantada com segurança. O Enigmail foi usado não apenas por pessoas preocupadas com a privacidade de seus dados, mas também por jornalistas, ativistas e advogados que se preocupam com sua própria segurança e a segurança de seus interlocutores. Em uma entrevista de 2017, Benjamin Ismail, chefe da seção Ásia-Pacífico da Repórteres Sem Fronteiras , disse:
Usamos principalmente o GPG para nos comunicarmos livremente com nossas fontes. As informações que eles nos fornecem sobre direitos humanos e violações desses direitos não são seguras para eles, portanto, é necessário proteger a integridade de nossas conversas.
Entrevista com Benjamin Ismail da organização Repórteres sem fronteiras
É fundamental que o Thunderbird continue a fornecer a esses usuários a experiência mais segura possível, mesmo durante o período de transição.
RNP e assinaturas de ligação de subchave
Ao falar sobre como usamos tipos no Sequoia para dificultar o uso indevido da API, mostrei como descobrir a data de validade de uma chave em apenas algumas linhas de código. Eu queria começar com um exemplo demonstrando para uma pessoa inexperiente em OpenPGP ou RNP como a mesma funcionalidade pode ser implementada usando RNP. O código a seguir itera sobre as subchaves do certificado (chave) e exibe a data de expiração de cada subchave. Como um lembrete, o tempo de expiração é armazenado na assinatura de vinculação da subchave e um valor 0 indica que a chave nunca irá expirar.
int i;
for (i = 0; i < sk_count; i ++) {
rnp_key_handle_t sk;
err = rnp_key_get_subkey_at(key, i, &sk);
if (err) {
printf("rnp_key_get_subkey_at(%d): %x\n", i, err);
return 1;
}
uint32_t expiration_time;
err = rnp_key_get_expiration(sk, &expiration_time);
if (err) {
printf("#%d (%s). rnp_key_get_expiration: %x\n",
i + 1, desc[i], err);
} else {
printf("#%d (%s) expires %"PRIu32" seconds after key's creation time.\n",
i + 1, desc[i],
expiration_time);
}
}
Testei esse código em um certificado com cinco subchaves. A primeira subchave tem uma assinatura de ligação válida e não expira; o segundo tem uma assinatura vinculativa válida e irá expirar no futuro; o terceiro tem uma assinatura de vinculação válida, mas já expirou; a quarta tem uma assinatura de ligação inválida, de acordo com a qual a subchave irá expirar no futuro; a quinta assinatura não tem âncora. Aqui está o resultado:
#1 (doesn't expire) expires 0 seconds after key's creation time.
#2 (expires) expires 94670781 seconds after key's creation time.
#3 (expired) expires 86400 seconds after key's creation time.
#4 (invalid sig) expires 0 seconds after key's creation time.
#5 (no sig) expires 0 seconds after key's creation time.
Primeiro, observe que a chamada rnp_key_get_expiration é bem-sucedida, independentemente de a subchave ter uma assinatura de vinculação válida, uma assinatura de vinculação inválida ou nenhuma assinatura de vinculação. Se você ler a documentação , esse comportamento parece um pouco surpreendente. Diz:
.
: 0 , .
Como o tempo de expiração da chave é armazenado na assinatura de ligação, como um especialista em OpenPGP eu entendo desta forma: uma chamada para rnp_key_get_expiration só terá sucesso se a subchave tiver uma assinatura de ligação válida. Na verdade, verifica-se que, se não houver uma assinatura de vinculação válida, a função simplesmente assume 0 por padrão, o que, dada a observação acima, o usuário da API esperaria interpretar como: esta chave é válida indefinidamente.
Para melhorar este código, primeiro você precisa verificar se a chave tem uma assinatura de ligação válida. Várias funções para fazer exatamente isso foram recentemente adicionadas ao RNP para endereçar CVE-2021-23991 . Em particular, os desenvolvedores RNP adicionou a função rnp_key_is_valid para retornar informações sobre se uma chave é válida. Este add-on melhora a situação, mas requer que o desenvolvedor escolha explicitamente se essas verificações críticas de segurança devem ser realizadas (ao invés de abandonar explicitamente as verificações já definidas - como seria o caso com o Sequoia). Como as verificações de segurança não são para fazer um trabalho útil, é fácil esquecê-las: o código funciona mesmo se nenhuma verificação de segurança tiver sido realizada. E como é necessário conhecimento especializado para fazer a escolha certa do que verificar, os cheques são esquecidos.
O código a seguir fornece verificações de segurança e ignora quaisquer chaves que rnp_key_is_valid considere inválidas:
int i;
for (i = 0; i < sk_count; i ++) {
rnp_key_handle_t sk;
err = rnp_key_get_subkey_at(key, i, &sk);
if (err) {
printf("rnp_key_get_subkey_at(%d): %x\n", i, err);
return 1;
}
bool is_valid = false;
err = rnp_key_is_valid(sk, &is_valid);
if (err) {
printf("rnp_key_is_valid: %x\n", err);
return 1;
}
if (! is_valid) {
printf("#%d (%s) is invalid, skipping.\n",
i + 1, desc[i]);
continue;
}
uint32_t expiration_time;
err = rnp_key_get_expiration(sk, &expiration_time);
if (err) {
printf("#%d (%s). rnp_key_get_expiration: %x\n",
i + 1, desc[i], err);
} else {
printf("#%d (%s) expires %"PRIu32" seconds after key's creation time.\n",
i + 1, desc[i],
expiration_time);
}
}
Resultado:
#1 (doesn't expire) expires 0 seconds after key's creation time.
#2 (expires) expires 94670781 seconds after key's creation time.
#3 (expired) is invalid, skipping.
#4 (invalid sig) is invalid, skipping.
#5 (no sig) is invalid, skipping.
Este código pula corretamente duas chaves que não têm uma assinatura de vinculação válida, mas também pula uma chave expirada - o que provavelmente não é o que queríamos, embora a documentação nos avise que esta função "verifica ... datas de expiração."
Embora também aconteça que não queremos usar uma chave ou certificado expirado, às vezes os recorremos. Por exemplo, se um usuário esquecer de renovar a chave, ele poderá ver que a chave expirou e, em seguida, verificar o certificado e também renovar a chave neste caso. Embora
gpg --list-keys
não mostre chaves expiradas, ao editar um certificado, as subchaves expiradas ainda estão visíveis, para que o usuário possa renovar sua validade:
$ gpg --edit-key 93D3A2B8DF67CE4B674999B807A5D8589F2492F9
Secret key is available.
sec ed25519/07A5D8589F2492F9
created: 2021-04-26 expires: 2024-04-26 usage: C
trust: unknown validity: unknown
ssb ed25519/1E2F512A0FE99515
created: 2021-04-27 expires: never usage: S
ssb cv25519/8CDDC2BC5EEB61A3
created: 2021-04-26 expires: 2024-04-26 usage: E
ssb ed25519/142D550E6E6DF02E
created: 2021-04-26 expired: 2021-04-27 usage: S
[ unknown] (1). Alice <alice@example.org>
Existem outras situações em que uma chave expirada não deve ser invalidada. Suponha, por exemplo, que Alice envie a Bob uma mensagem assinada: "Pagarei 100 euros por um ano" e a chave de assinatura expire em seis meses. Quando o ano acabar, Alice ficará em dívida com Bob com base nesta assinatura? Acho que sim. A assinatura era válida no momento da afixação. O fato de a chave já ter expirado é irrelevante. É claro que, quando a chave expirou, as assinaturas seladas por ela após o momento de sua expiração devem ser consideradas inválidas. Da mesma forma, uma mensagem não deve ser criptografada com uma chave expirada.
Resumindo, se uma chave deve ser considerada válida é altamente sensível ao contexto. rnp_key_is_valid é melhor do que nada, mas apesar do nome, esta função é bastante matizada para determinar se uma chave é válida.
Como parte desse commit, a segunda função foi adicionada
rnp_key_valid_till
. Esta função retorna "um carimbo de data / hora antes do qual a chave pode ser considerada válida ... Se a chave nunca foi válida, zero é retornado como o valor." Usando esta função, você pode determinar se a chave alguma vez foi válida, para isso você precisa verificar se esta função retorna um valor diferente de zero:
int i;
for (i = 0; i < sk_count; i ++) {
rnp_key_handle_t sk;
err = rnp_key_get_subkey_at(key, i, &sk);
if (err) {
printf("rnp_key_get_subkey_at(%d): %x\n", i, err);
return 1;
}
uint32_t valid_till;
err = rnp_key_valid_till(sk, &valid_till);
if (err) {
printf("rnp_key_valid_till: %x\n", err);
return 1;
}
printf("#%d (%s) valid till %"PRIu32" seconds after epoch; ",
i + 1, desc[i], valid_till);
if (valid_till == 0) {
printf("invalid, skipping.\n");
continue;
}
uint32_t expiration_time;
err = rnp_key_get_expiration(sk, &expiration_time);
if (err) {
printf("rnp_key_get_expiration: %x\n", err);
} else {
printf("expires %"PRIu32" seconds after key's creation time.\n",
expiration_time);
}
}
Resultados:
#1 (doesn't expire) valid till 1714111110 seconds after epoch; expires 0 seconds after key's creation time.
#2 (expires) valid till 1714111110 seconds after epoch; expires 94670781 seconds after key's creation time.
#3 (expired) valid till 1619527593 seconds after epoch; expires 86400 seconds after key's creation time.
#4 (invalid sig) valid till 0 seconds after epoch; invalid, skipping.
#5 (no sig) valid till 0 seconds after epoch; invalid, skipping.
Agora obtivemos os resultados que queríamos! Exibimos os tempos de expiração corretos para as três primeiras subchaves e também indicamos que as duas últimas subchaves são inválidas.
Mas vamos dar uma olhada mais de perto
rnp_key_valid_till
. Primeiro, no OpenPGP, o tempo de expiração da chave é armazenado como um recuo de 32 bits não assinado a partir do momento em que a chave foi criada, também no formato de 32 bits não assinado. Portanto, a função teria que usar um tipo mais amplo, ou pelo menos verificar o código para overflows. (Eu relatei esse problema e ele já foi corrigido.)
Mas, mesmo se ignorarmos esta ombreira, a função ainda é estranha. No OpenPGP, uma chave pode ser válida por vários períodos de tempo. Digamos que a chave expire em 1º de julho e o usuário a renove apenas a partir de 10 de julho. Durante o período de 1 de julho a 10 de julho, a chave era inválida e as assinaturas geradas durante esse período também devem ser consideradas inválidas. Então, o que a função considerada deve retornar para tal chave? Mais importante, como um usuário de tal API deve interpretar o resultado? É apropriado usar tal API? ( Sim, eu perguntei .)
Na Sequoia, nós fomos para o outro lado. Em vez de retornar informações de que a chave é válida, invertemos a situação; O usuário da API pode perguntar: esta chave é válida no tempo t . Em nossa experiência, isso é tudo o que realmente foi exigido em todos os casos que conhecemos.
Não pense que estou mexendo especificamente com esse problema específico com a API RNP. Esta é apenas uma complicação na qual eu estava pensando recentemente. Quando reimplementamos a API RNP para criar um back-end OpenPGP alternativo para Thunderbird, enfrentamos muitos problemas semelhantes .
Conclusão
Os erros cometidos pelos desenvolvedores da RNP são compreensíveis e desculpáveis. O OpenPGP é complexo, como muitos outros protocolos. Mas pode ser muito simplificada se esforçar para mantê-lo flexível e confiável PKI , e não apenas tem uma ferramenta de criptografia de arquivos.
No entanto, a API RNP é perigosa. O Thunderbird é usado em contextos críticos de segurança. Em uma entrevista de 2017 , Michal 'Rysiek' Wozniak do Centro de Crime Organizado e Pesquisa da Corrupção (OCCRP) deixou claro que a vida de alguém está em jogo:
Eu realmente acredito que se não estivéssemos usando GnuPG todo esse tempo, muitos de nossos informantes e jornalistas estariam em perigo ou atrás das grades ...
Entrevista com Michal 'Rysiek' Wozniak do Centro para o Estudo da Corrupção e Organização Crime
Como isso afetará o Thunderbird? Eu vejo três opções. Primeiro, o Thunderbird poderia voltar para o Enigmail. Você pode pensar que portar o Enigmail para o Thunderbird 78 seria difícil, mas ouvi de muitos desenvolvedores do Thunderbird que isso é tecnicamente viável com um elevador. Mas uma das razões pelas quais o Thunderbird escolheu deixar o Enigmail é a enorme quantidade de tempo que os desenvolvedores do Enigmail gastaram ajudando os usuários a instalar e configurar o GnuPG corretamente. Portanto, esse caminho não é o ideal.
Em segundo lugar, o Thunderbird poderia mudar para uma implementação OpenPGP diferente. Há um monte deles hoje em dia. escolher a partir de. Pessoalmente, acho que o Thunderbird deveria ter mudado para o Sequoia. Claro, sou um desenvolvedor Sequoia, então sou tendencioso. Mas não se trata de dinheiro: o fundo me paga, e no mercado livre me ofereceriam, talvez, o dobro do que ganho agora. Eu trabalho para proteger os usuários. Mas, mesmo além da API Sequoia e dos benefícios da implementação, o Thunderbird também vence neste caso em mais um aspecto: já fizemos essa implementação funcionar. Algumas semanas atrás, lançamos o Octopus , um backend OpenPGP alternativo para o Thunderbird. Ele não só tem paridade funcional com RNP, mas também tem uma série de recursos que faltavam anteriormente, por exemplo, integração com gpg, além de corrigir algumas falhas de segurança e atender a vários requisitos não funcionais.
Terceiro, o Thunderbird poderia ter parado de usar OpenPGP completamente. Esta decisão não me convém. Mas, em várias ocasiões, estive preocupado com a segurança dos usuários mais vulneráveis do Thunderbird, e acredito que não fornecer nenhum suporte a OpenPGP talvez seja ainda mais seguro do que o status quo.
Macleod VPS são ideais para desenvolvimento de API.
Cadastre-se pelo link acima ou clicando no banner e ganhe 10% de desconto no primeiro mês de aluguel de um servidor de qualquer configuração!