Queremos que nossos dados digitais fiquem seguros. Queremos visitar sites, enviar dados bancários, digitar senhas, assinar documentos on-line, fazer login em computadores remotos, criptografar dados antes de armazená-los em bancos de dados e garantir que ninguém possa adulterá-los. A criptografia pode fornecer um alto grau de segurança de dados, mas precisamos proteger as chaves criptográficas.
Ao mesmo tempo, não podemos ter nossa chave escrita em algum lugar seguro e acessá-la apenas ocasionalmente. Muito pelo contrário, ela está envolvida em todas as solicitações nas quais fazemos operações criptográficas. Se um site for compatível com TLS, a chave privada será usada para estabelecer cada conexão.
Infelizmente, as chaves criptográficas às vezes vazam e, quando isso acontece, é um grande problema. Muitos vazamentos acontecem devido a bugs de software e vulnerabilidades de segurança. Neste post, vamos aprender como o kernel do Linux pode ajudar a proteger as chaves criptográficas de toda uma classe de possíveis vulnerabilidades de segurança: as violações de acesso à memória.
Violações de acesso à memória
De acordo com a NSA, cerca de 70% das vulnerabilidades no código da Microsoft e do Google estavam relacionadas a problemas de segurança de memória. Uma das consequências dos acessos incorretos à memória é o vazamento de dados de segurança (incluindo chaves criptográficas). As chaves criptográficas são apenas alguns dados (na sua maior parte aleatórios) armazenados na memória, portanto, podem estar sujeitas a vazamentos de memória como qualquer outro dado na memória. O exemplo abaixo mostra como uma chave criptográfica pode vazar acidentalmente por meio da reutilização da memória da pilha:
broken.c
Compile e execute nosso programa:
#include <stdio.h>
#include <stdint.h>
static void encrypt(void)
{
uint8_t key[] = "hunter2";
printf("encrypting with super secret key: %s\n", key);
}
static void log_completion(void)
{
/* oh no, we forgot to init the msg */
char msg[8];
printf("not important, just fyi: %s\n", msg);
}
int main(void)
{
encrypt();
/* notify that we're done */
log_completion();
return 0;
}
Ops, imprimimos a chave secreta no logger “fyi” em vez da mensagem de log pretendida. Existem dois problemas com o código acima:
$ gcc -o broken broken.c
$ ./broken
encrypting with super secret key: hunter2
not important, just fyi: hunter2
não destruímos a chave com segurança em nossa função de pseudocriptografia (sobrescrevendo os dados da chave com zeros, por exemplo), quando terminamos de usá-la
nossa função buggy logging tem acesso a qualquer memória dentro do nosso processo
E, embora possamos corrigir facilmente o primeiro problema com algum código adicional, o segundo problema é o resultado inerente de como o software é executado dentro do sistema operacional.
Cada processo recebe um bloco de memória virtual contígua do sistema operacional. Ele permite que o kernel compartilhe recursos limitados do computador entre vários processos em execução simultaneamente. Essa abordagem é chamada de gerenciamento de memória virtual. Dentro da memória virtual, um processo tem seu próprio espaço de endereçamento e não tem acesso à memória de outros processos, mas pode acessar qualquer memória dentro de seu espaço de endereçamento. Em nosso exemplo, estamos interessados em uma parte da memória do processo chamado a pilha.
A pilha consiste em quadros de pilha. Um quadro de pilha é um espaço alocado dinamicamente para a função em execução no momento. Ele contém as variáveis locais, argumentos e endereço de retorno da função. Ao compilar uma função, o compilador calcula quanta memória precisa ser alocada e solicita um quadro de pilha deste tamanho. Quando uma função termina a execução, o quadro de pilha é marcado como livre e pode ser usado novamente. Um quadro de pilha é um bloco lógico, não fornece nenhuma verificação de limite, não é apagado, apenas marcado como livre. Além disso, a memória virtual é um bloco contíguo de endereços. Ambas as declarações oferecem a possibilidade de malware/código com bugs acessar dados de qualquer lugar dentro da memória virtual.
A pilha do nosso programa broken.c
ficará assim:
No início, temos um quadro de pilha da função principal. Além disso, a função main()
chama encrypt()
que será colocada na pilha imediatamente abaixo de main()
(a pilha de código cresce para baixo). Dentro de encrypt()
o compilador solicita oito bytes para a variável key
(sete bytes de dados + caractere C-nulo). Quando encrypt()
termina a execução, os mesmos endereços de memória são obtidos por log_completion()
. Dentro de log_completion()
o compilador aloca oito bytes para a variável msg
. Acidentalmente, ela foi colocada na pilha no mesmo local onde nossa chave privada estava armazenada antes. A memória para msg
foi apenas alocada, mas não inicializada, os dados da função anterior foram deixados como estão.
Além disso, para os bugs de código, as linguagens de programação fornecem funções inseguras conhecidas pelas vulnerabilidades de memória segura. Por exemplo, para C tais funções são printf()
, strcpy()
, gets()
. A função printf()
não verifica quantos argumentos devem ser passados para substituir todos os espaços reservados na string de formato. Os argumentos da função são colocados na pilha acima do frame da pilha da função, printf()
busca os argumentos de acordo com os números e tipos de espaços reservados, saindo facilmente de seus argumentos e acessando os dados do frame da pilha da função anterior.
A NSA nos aconselha a usar linguagens de memória de segurança como Python, Go, Rust. Mas isso vai nos proteger completamente?
O compilador Python definitivamente vai verificar os limites para você em muitos casos e notificar com um erro:
No entanto, esta é uma citação de uma das 36 (por enquanto) vulnerabilidades:
>>> print("x: {}, y: {}, {}".format(1, 2))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: Replacement index 2 out of range for positional args tuple
O Python 2.7.14 é vulnerável a Heap-Buffer-Overflow, bem como a Heap-Use-After-Free.
O Golang tem sua própria lista de vulnerabilidades de estouro e tem um pacote inseguro. O nome do pacote fala por si, regras e verificações usuais não funcionam dentro deste pacote.
Heartbleed
Em 2014, o bug Heartbleed foi descoberto. A (na época) biblioteca de criptografia mais usada OpenSSL vazou chaves privadas. Nós também sofremos com ele.
Mitigação
Os bugs de memória são um fato da vida e, na verdade não podemos nos proteger totalmente contra eles. Mas, já que as chaves criptográficas são muito mais valiosas do que os outros dados, podemos pelo menos protege-las melhor?
Como já dissemos, um espaço de endereçamento de memória normalmente está associado a um processo. E por padrão, dois processos diferentes não compartilham memória, portanto, são naturalmente isolados um do outro. Assim, um possível bug de memória em um dos processos não vaza acidentalmente uma chave criptográfica de outro processo. A segurança do ssh-agent baseia-se neste princípio. Há sempre dois processos envolvidos: um cliente/solicitante e o agente.
O agente nunca envia uma chave privada por seu canal de solicitação. Em vez disso, as operações que requerem uma chave privada são executadas pelo agente e o resultado retorna ao solicitante. Dessa forma, as chaves privadas não são expostas aos clientes que usam o agente.
Um solicitante geralmente é um processo voltado para a rede e/ou processamento de entrada não confiável. Portanto, é muito mais provável que o solicitante seja suscetível a vulnerabilidades relacionadas à memória, mas nesse esquema ele nunca teria acesso às chaves criptográficas (porque as chaves residem em um espaço de endereçamento de processo separado) e, assim, não é possível vazá-las.
Na Cloudflare, empregamos o mesmo princípio no Keyless SSL. As chaves privadas do cliente são armazenadas em um ambiente isolado e protegidas de conexões voltadas para a internet.
Linux Kernel Key Retention Service
A abordagem cliente/solicitante e agente oferece melhor proteção para segredos ou chaves criptográficas, mas traz algumas desvantagens:
precisamos desenvolver e manter dois programas diferentes em vez de um
também precisamos projetar uma interface bem definida para comunicação entre os dois processos
precisamos implementar o suporte de comunicação entre os dois processos (sockets Unix, memória compartilhada, etc.)
podemos precisar autenticar e dar suporte a ACLs entre os processos, pois não queremos que nenhum solicitante em nosso sistema possa usar nossas chaves criptográficas armazenadas no agente
precisamos garantir que o processo do agente esteja funcionando, ao trabalhar com o processo do cliente/solicitante
E se substituirmos o processo do agente pelo próprio kernel do Linux?
ele já está rodando em nosso sistema (caso contrário nosso software não funcionaria)
ele possui uma interface bem definida para comunicação (chamadas de sistema)
ele pode impor várias ACLs em objetos do kernel
e ele é executado em um espaço de endereçamento separado.
Felizmente, o Linux Kernel Key Retention Service pode executar todas as funções de um processo de agente típico e provavelmente ainda mais.
Inicialmente, ele foi projetado para serviços de kernel como dm-crypt/ecryptfs, mas depois foi aberto para uso por programas de espaço de usuário. Isso nos dá algumas vantagens:
as chaves são armazenadas fora do espaço de endereçamento do processo
a interface bem definida e a camada de comunicação são implementadas através de syscalls
as chaves são objetos do kernel e, portanto, têm permissões e ACLs associadas
o ciclo de vida das chaves pode ser vinculado implicitamente ao ciclo de vida do processo
O Linux Kernel Key Retention Service opera com dois tipos de entidades: chaves e chaveiros, onde um chaveiro é uma chave de um tipo especial. Se colocarmos em analogia com arquivos e diretórios, podemos dizer que uma chave é um arquivo e um chaveiro é um diretório. Além disso, eles representam uma hierarquia de chaves semelhante a uma hierarquia de árvore do sistema de arquivos: os chaveiros fazem referência a chaves e outros chaveiros, mas apenas as chaves podem conter o material criptográfico real semelhante aos arquivos que contêm os dados reais.
As chaves têm tipos. O tipo de chave determina quais operações podem ser executadas sobre as chaves. Por exemplo, as chaves dos tipos de usuário e logon podem conter blobs arbitrários de dados, mas as chaves de logon nunca podem ser lidas de volta no espaço do usuário, elas são usadas exclusivamente pelos serviços do kernel.
Para fins de uso do kernel em vez de um processo de agente, o tipo de chave mais interessante é o tipo assimétrico. Ele pode conter uma chave privada dentro do kernel e fornece a capacidade para que os aplicativos permitidos descriptografem ou assinem alguns dados com a chave. Atualmente, apenas chaves RSA são compatíveis, mas o trabalho está em andamento para adicionar compatibilidade com chaves ECDSA.
Enquanto as chaves são responsáveis por proteger o material criptográfico dentro do kernel, os chaveiros determinam o tempo de vida das chaves e o acesso compartilhado. De forma mais simples, quando um chaveiro específico é destruído, todas as chaves vinculadas apenas a esse chaveiro também são destruídas com segurança. Podemos criar chaveiros personalizados manualmente, mas provavelmente um dos recursos mais poderosos do serviço são os “special keyrings”
Esses chaveiros são criados implicitamente pelo kernel e seu tempo de vida é vinculado ao tempo de vida de um objeto de kernel diferente, como um processo ou um usuário. (Atualmente existem quatro categorias de chaveiros “implícitos”), mas para os propósitos deste post estamos interessados nos dois mais usados: process keyrings e user keyrings.
O tempo de vida do user keyring está vinculado à existência de um determinado usuário e esse chaveiro é compartilhado entre todos os processos do mesmo ID do usuário. Assim, um processo, por exemplo, pode armazenar uma chave em um user keyring e outro processo em execução com o mesmo usuário pode recuperar/usar a chave. Quando o ID do usuário é removido do sistema, todas as chaves (e outros chaveiros) sob o user keyring associado serão destruídas com segurança pelo kernel.
Os process keyrings estão vinculados a alguns processos e podem ser de três tipos diferentes em semântica: process, thread e session. Um process keyring é vinculado e privado a um processo específico. Assim, qualquer código dentro do processo pode armazenar/usar chaves no chaveiro, mas outros processos (mesmo com o mesmo ID de usuário ou processos filhos) não podem obter acesso. E quando o processo expira, o chaveiro e as chaves associadas são destruídos com segurança. Além da vantagem de armazenar nossos segredos/chaves em um espaço de endereçamento isolado, o process keyring nos dá a garantia de que as chaves serão destruídas independentemente do motivo do encerramento do processo: mesmo que nosso aplicativo trave completamente, sem ter a oportunidade de executar qualquer código de limpeza, nossas chaves ainda serão destruídas com segurança pelo kernel.
Um thread keyring é semelhante a um process keyring, mas é privado e vinculado a um thread específico. Por exemplo, podemos construir um servidor web multithread, que pode atender conexões TLS usando várias chaves privadas, e podemos ter certeza de que as conexões/código em um thread nunca podem usar uma chave privada, que está associada a outro thread (por exemplo, servindo um nome de domínio diferente).
Um session keyring disponibiliza suas chaves para o processo atual e todos os seus filhos. Ele é destruído quando o processo superior termina e os processos filhos podem armazenar/acessar chaves, enquanto o processo superior existe. É mais útil em ambientes shell e interativos, quando empregamos a keyctl tool para acessar o Linux Kernel Key Retention Service, em vez de usar a interface de chamada do sistema do kernel. No shell, geralmente não podemos usar o process keyring, pois cada comando executado cria um novo processo. Assim, se adicionarmos uma chave ao process keyring a partir da linha de comando, essa chave será imediatamente destruída, porque o processo “adicionar” termina quando o comando termina de ser executado. Vamos confirmar isso com [bpftrace](https://github.com/iovisor/bpftrace)
.
Em um terminal vamos rastrear a função [user_destroy](https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L146)
, que é responsável por excluir uma chave de usuário:
E em outro terminal vamos tentar adicionar uma chave ao process keyring:
$ sudo bpftrace -e 'kprobe:user_destroy { printf("destroying key %d\n", ((struct key *)arg0)->serial) }'
Att
Voltando ao primeiro terminal podemos ver imediatamente:
$ keyctl add user mykey hunter2 @p
742524855
E podemos confirmar que a chave não está disponível ao tentar acessá-la:
…
Attaching 1 probe...
destroying key 742524855
Portanto, no exemplo acima, a chave “mykey” foi adicionada ao process keyring do subshell executando keyctl add user mykey hunter2 @p
. Mas como o processo do subshell terminou no momento em que o comando foi executado, tanto o process keyring quanto a chave adicionada foram destruídos.
$ keyctl print 742524855
keyctl_read_alloc: Required key not available
Em vez disso, o session keyring permite que nossos comandos interativos adicionem chaves ao nosso ambiente de shell atual e os comandos subsequentes as consumam. As chaves ainda serão destruídas com segurança, quando nosso processo de shell principal terminar (provavelmente, quando sairmos do sistema).
Portanto, ao selecionar o tipo de chaveiro apropriado, podemos garantir que as chaves sejam destruídas com segurança, quando não forem necessárias. Mesmo se o aplicativo travar. Esta é uma introdução muito breve, mas permitirá que você use nossos exemplos. Para o contexto completo, acesse a documentação oficial.
Substituir o ssh-agent pelo Linux Kernel Key Retention Service
Fornecemos uma longa descrição de como podemos substituir dois processos isolados pelo Linux Kernel Retention Service. É hora de colocar nossas palavras em código. Também falamos sobre o ssh-agent, então será um bom exercício substituir nossa chave privada armazenada na memória do agente por uma do kernel. Escolhemos a implementação SSH mais popular, OpenSSH, como nosso alvo.
Algumas pequenas alterações precisam ser adicionadas ao código para adicionar funcionalidade para recuperar uma chave do kernel:
openssh.patch
Precisamos baixar e corrigir OpenSSH do git mais recente, pois a correção acima não funciona na versão mais recente (V_9_1_P1
no momento da redação deste artigo):
diff --git a/ssh-rsa.c b/ssh-rsa.c
index 6516ddc1..797739bb 100644
--- a/ssh-rsa.c
+++ b/ssh-rsa.c
@@ -26,6 +26,7 @@
#include <stdarg.h>
#include <string.h>
+#include <stdbool.h>
#include "sshbuf.h"
#include "compat.h"
@@ -63,6 +64,7 @@ ssh_rsa_cleanup(struct sshkey *k)
{
RSA_free(k->rsa);
k->rsa = NULL;
+ k->serial = 0;
}
static int
@@ -220,9 +222,14 @@ ssh_rsa_deserialize_private(const char *ktype, struct sshbuf *b,
int r;
BIGNUM *rsa_n = NULL, *rsa_e = NULL, *rsa_d = NULL;
BIGNUM *rsa_iqmp = NULL, *rsa_p = NULL, *rsa_q = NULL;
+ bool is_keyring = (strncmp(ktype, "ssh-rsa-keyring", strlen("ssh-rsa-keyring")) == 0);
+ if (is_keyring) {
+ if ((r = ssh_rsa_deserialize_public(ktype, b, key)) != 0)
+ goto out;
+ }
/* Note: can't reuse ssh_rsa_deserialize_public: e, n vs. n, e */
- if (!sshkey_is_cert(key)) {
+ else if (!sshkey_is_cert(key)) {
if ((r = sshbuf_get_bignum2(b, &rsa_n)) != 0 ||
(r = sshbuf_get_bignum2(b, &rsa_e)) != 0)
goto out;
@@ -232,28 +239,46 @@ ssh_rsa_deserialize_private(const char *ktype, struct sshbuf *b,
}
rsa_n = rsa_e = NULL; /* transferred */
}
- if ((r = sshbuf_get_bignum2(b, &rsa_d)) != 0 ||
- (r = sshbuf_get_bignum2(b, &rsa_iqmp)) != 0 ||
- (r = sshbuf_get_bignum2(b, &rsa_p)) != 0 ||
- (r = sshbuf_get_bignum2(b, &rsa_q)) != 0)
- goto out;
- if (!RSA_set0_key(key->rsa, NULL, NULL, rsa_d)) {
- r = SSH_ERR_LIBCRYPTO_ERROR;
- goto out;
- }
- rsa_d = NULL; /* transferred */
- if (!RSA_set0_factors(key->rsa, rsa_p, rsa_q)) {
- r = SSH_ERR_LIBCRYPTO_ERROR;
- goto out;
- }
- rsa_p = rsa_q = NULL; /* transferred */
if ((r = sshkey_check_rsa_length(key, 0)) != 0)
goto out;
- if ((r = ssh_rsa_complete_crt_parameters(key, rsa_iqmp)) != 0)
- goto out;
- if (RSA_blinding_on(key->rsa, NULL) != 1) {
- r = SSH_ERR_LIBCRYPTO_ERROR;
- goto out;
+
+ if (is_keyring) {
+ char *name;
+ size_t len;
+
+ if ((r = sshbuf_get_cstring(b, &name, &len)) != 0)
+ goto out;
+
+ key->serial = request_key("asymmetric", name, NULL, KEY_SPEC_PROCESS_KEYRING);
+ free(name);
+
+ if (key->serial == -1) {
+ key->serial = 0;
+ r = SSH_ERR_KEY_NOT_FOUND;
+ goto out;
+ }
+ } else {
+ if ((r = sshbuf_get_bignum2(b, &rsa_d)) != 0 ||
+ (r = sshbuf_get_bignum2(b, &rsa_iqmp)) != 0 ||
+ (r = sshbuf_get_bignum2(b, &rsa_p)) != 0 ||
+ (r = sshbuf_get_bignum2(b, &rsa_q)) != 0)
+ goto out;
+ if (!RSA_set0_key(key->rsa, NULL, NULL, rsa_d)) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto out;
+ }
+ rsa_d = NULL; /* transferred */
+ if (!RSA_set0_factors(key->rsa, rsa_p, rsa_q)) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto out;
+ }
+ rsa_p = rsa_q = NULL; /* transferred */
+ if ((r = ssh_rsa_complete_crt_parameters(key, rsa_iqmp)) != 0)
+ goto out;
+ if (RSA_blinding_on(key->rsa, NULL) != 1) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto out;
+ }
}
/* success */
r = 0;
@@ -333,6 +358,21 @@ rsa_hash_alg_nid(int type)
}
}
+static const char *
+rsa_hash_alg_keyctl_info(int type)
+{
+ switch (type) {
+ case SSH_DIGEST_SHA1:
+ return "enc=pkcs1 hash=sha1";
+ case SSH_DIGEST_SHA256:
+ return "enc=pkcs1 hash=sha256";
+ case SSH_DIGEST_SHA512:
+ return "enc=pkcs1 hash=sha512";
+ default:
+ return NULL;
+ }
+}
+
int
ssh_rsa_complete_crt_parameters(struct sshkey *key, const BIGNUM *iqmp)
{
@@ -433,7 +473,14 @@ ssh_rsa_sign(struct sshkey *key,
goto out;
}
- if (RSA_sign(nid, digest, hlen, sig, &len, key->rsa) != 1) {
+ if (key->serial > 0) {
+ len = keyctl_pkey_sign(key->serial, rsa_hash_alg_keyctl_info(hash_alg), digest, hlen, sig, slen);
+ if ((long)len == -1) {
+ ret = SSH_ERR_LIBCRYPTO_ERROR;
+ goto out;
+ }
+ }
+ else if (RSA_sign(nid, digest, hlen, sig, &len, key->rsa) != 1) {
ret = SSH_ERR_LIBCRYPTO_ERROR;
goto out;
}
@@ -705,6 +752,18 @@ const struct sshkey_impl sshkey_rsa_impl = {
/* .funcs = */ &sshkey_rsa_funcs,
};
+const struct sshkey_impl sshkey_rsa_keyring_impl = {
+ /* .name = */ "ssh-rsa-keyring",
+ /* .shortname = */ "RSA",
+ /* .sigalg = */ NULL,
+ /* .type = */ KEY_RSA,
+ /* .nid = */ 0,
+ /* .cert = */ 0,
+ /* .sigonly = */ 0,
+ /* .keybits = */ 0,
+ /* .funcs = */ &sshkey_rsa_funcs,
+};
+
const struct sshkey_impl sshkey_rsa_cert_impl = {
/* .name = */ "[email protected]",
/* .shortname = */ "RSA-CERT",
diff --git a/sshkey.c b/sshkey.c
index 43712253..3524ad37 100644
--- a/sshkey.c
+++ b/sshkey.c
@@ -115,6 +115,7 @@ extern const struct sshkey_impl sshkey_ecdsa_nistp521_cert_impl;
# endif /* OPENSSL_HAS_NISTP521 */
# endif /* OPENSSL_HAS_ECC */
extern const struct sshkey_impl sshkey_rsa_impl;
+extern const struct sshkey_impl sshkey_rsa_keyring_impl;
extern const struct sshkey_impl sshkey_rsa_cert_impl;
extern const struct sshkey_impl sshkey_rsa_sha256_impl;
extern const struct sshkey_impl sshkey_rsa_sha256_cert_impl;
@@ -154,6 +155,7 @@ const struct sshkey_impl * const keyimpls[] = {
&sshkey_dss_impl,
&sshkey_dsa_cert_impl,
&sshkey_rsa_impl,
+ &sshkey_rsa_keyring_impl,
&sshkey_rsa_cert_impl,
&sshkey_rsa_sha256_impl,
&sshkey_rsa_sha256_cert_impl,
diff --git a/sshkey.h b/sshkey.h
index 771c4bce..a7ae45f6 100644
--- a/sshkey.h
+++ b/sshkey.h
@@ -29,6 +29,7 @@
#include <sys/types.h>
#ifdef WITH_OPENSSL
+#include <keyutils.h>
#include <openssl/rsa.h>
#include <openssl/dsa.h>
# ifdef OPENSSL_HAS_ECC
@@ -153,6 +154,7 @@ struct sshkey {
size_t shielded_len;
u_char *shield_prekey;
size_t shield_prekey_len;
+ key_serial_t serial;
};
#define ED25519_SK_SZ crypto_sign_ed25519_SECRETKEYBYTES
Agora compile e crie o OpenSSH corrigido
$ git clone https://github.com/openssh/openssh-portable.git
…
$ cd openssl-portable
$ $ patch -p1 < ../openssh.patch
patching file ssh-rsa.c
patching file sshkey.c
patching file sshkey.h
Observe que instruímos o sistema de criação a vincular-se adicionalmente com [libkeyutils](https://man7.org/linux/man-pages/man3/keyctl.3.html)
, que fornece wrappers convenientes para acessar o Linux Kernel Key Retention Service. Além disso, tivemos que desabilitar o suporte a PKCS11, pois o código tem uma função com o mesmo nome, "libkeyutils", então há um conflito de nomenclatura. Pode haver uma correção melhor para isso, mas está fora do escopo deste post.
$ autoreconf
$ ./configure --with-libs=-lkeyutils --disable-pkcs11
…
$ make
…
Agora que temos o OpenSSH corrigido, vamos testá-lo. Primeiramente, precisamos gerar uma nova chave SSH RSA que vamos usar para acessar o sistema. Como o kernel do Linux é compatível apenas com chaves privadas no formato PKCS8, vamos usar desde o início (em vez do formato OpenSSH padrão):
Normalmente, usaríamos "ssh-add" para adicionar esta chave ao nosso agente ssh. Em nosso caso, precisamos usar um script de substituição, que adiciona a chave ao nosso session keyring atual:
$ ./ssh-keygen -b 4096 -m PKCS8
Generating public/private rsa key pair.
…
ssh-add-keyring.sh
Dependendo de como nosso kernel foi compilado, também podemos precisar carregar alguns módulos do kernel para compatibilidade com a chave privada assimétrica:
#/bin/bash -e
in=$1
key_desc=$2
keyring=$3
in_pub=$in.pub
key=$(mktemp)
out="${in}_keyring"
function finish {
rm -rf $key
}
trap finish EXIT
# https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
# null-terminanted openssh-key-v1
printf 'openssh-key-v1\0' > $key
# cipher: none
echo '00000004' | xxd -r -p >> $key
echo -n 'none' >> $key
# kdf: none
echo '00000004' | xxd -r -p >> $key
echo -n 'none' >> $key
# no kdf options
echo '00000000' | xxd -r -p >> $key
# one key in the blob
echo '00000001' | xxd -r -p >> $key
# grab the hex public key without the (00000007 || ssh-rsa) preamble
pub_key=$(awk '{ print $2 }' $in_pub | base64 -d | xxd -s 11 -p | tr -d '\n')
# size of the following public key with the (0000000f || ssh-rsa-keyring) preamble
printf '%08x' $(( ${#pub_key} / 2 + 19 )) | xxd -r -p >> $key
# preamble for the public key
# ssh-rsa-keyring in prepended with length of the string
echo '0000000f' | xxd -r -p >> $key
echo -n 'ssh-rsa-keyring' >> $key
# the public key itself
echo $pub_key | xxd -r -p >> $key
# the private key is just a key description in the Linux keyring
# ssh will use it to actually find the corresponding key serial
# grab the comment from the public key
comment=$(awk '{ print $3 }' $in_pub)
# so the total size of the private key is
# two times the same 4 byte int +
# (0000000f || ssh-rsa-keyring) preamble +
# a copy of the public key (without preamble) +
# (size || key_desc) +
# (size || comment )
priv_sz=$(( 8 + 19 + ${#pub_key} / 2 + 4 + ${#key_desc} + 4 + ${#comment} ))
# we need to pad the size to 8 bytes
pad=$(( 8 - $(( priv_sz % 8 )) ))
# so, total private key size
printf '%08x' $(( $priv_sz + $pad )) | xxd -r -p >> $key
# repeated 4-byte int
echo '0102030401020304' | xxd -r -p >> $key
# preamble for the private key
echo '0000000f' | xxd -r -p >> $key
echo -n 'ssh-rsa-keyring' >> $key
# public key
echo $pub_key | xxd -r -p >> $key
# private key description in the keyring
printf '%08x' ${#key_desc} | xxd -r -p >> $key
echo -n $key_desc >> $key
# comment
printf '%08x' ${#comment} | xxd -r -p >> $key
echo -n $comment >> $key
# padding
for (( i = 1; i <= $pad; i++ )); do
echo 0$i | xxd -r -p >> $key
done
echo '-----BEGIN OPENSSH PRIVATE KEY-----' > $out
base64 $key >> $out
echo '-----END OPENSSH PRIVATE KEY-----' >> $out
chmod 600 $out
# load the PKCS8 private key into the designated keyring
openssl pkcs8 -in $in -topk8 -outform DER -nocrypt | keyctl padd asymmetric $key_desc $keyring
Por fim, nossa chave ssh privada é adicionada ao chaveiro da sessão atual com o nome “myssh”. Além disso, ssh-add-keyring.sh
criará um arquivo de chave pseudoprivada em ~/.ssh/id_rsa_keyring
, que precisa ser passado para o processo ssh principal. É uma chave pseudoprivada, pois não possui nenhum material criptográfico sensível. Em vez disso, ele possui apenas o identificador “myssh” em um formato OpenSSH nativo. Se usarmos várias chaves SSH, teremos que informar ao processo ssh
principal, de alguma forma, qual nome de chave no kernel deve ser solicitado ao sistema.
$ sudo modprobe pkcs8_key_parser
$ ./ssh-add-keyring.sh ~/.ssh/id_rsa myssh @s
Enter pass phrase for ~/.ssh/id_rsa:
723263309
Antes de começarmos a testar, vamos nos certificar de que nosso servidor SSH (sendo executado localmente) aceitará a chave recém-gerada como uma autenticação válida:
Agora podemos experimentar o SSH no sistema:
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
Funcionou. Observe que estamos redefinindo a variável de ambiente "SSH_AUTH_SOCK" para garantir que não usemos nenhuma chave de um agente ssh em execução no sistema. Ainda assim, o fluxo de login não solicita nenhuma senha para nossa chave privada, a própria chave reside no espaço de endereçamento do kernel e nós fazemos referência a ela usando seu serial para operações de assinatura.
$ SSH_AUTH_SOCK="" ./ssh -i ~/.ssh/id_rsa_keyring localhost
The authenticity of host 'localhost (::1)' can't be established.
ED25519 key fingerprint is SHA256:3zk7Z3i9qZZrSdHvBp2aUYtxHACmZNeLLEqsXltynAY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
Linux dev 5.15.79-cloudflare-2022.11.6 #1 SMP Mon Sep 27 00:00:00 UTC 2010 x86_64
…
User keyring ou session keyring?
No exemplo acima, configuramos nossa chave privada SSH no session keyring. Podemos verificar se ela está lá:
Poderíamos ter usado o user keyring também. Qual é a diferença? Atualmente, o tempo de vida da chave “myssh” é limitado à sessão de login atual. Ou seja, se sairmos e fizermos login novamente, a chave desaparecerá e teremos que executar o script ssh-add-keyring.sh
novamente. Da mesma forma, se fizermos login em um segundo terminal, não veremos esta chave:
$ keyctl show
Session Keyring
577779279 --alswrv 1000 1000 keyring: _ses
846694921 --alswrv 1000 65534 \_ keyring: _uid.1000
723263309 --als--v 1000 1000 \_ asymmetric: myssh
Observe que o número de série do session keyring _ses
no segundo terminal é diferente. Um novo chaveiro foi criado e a chave “myssh” junto com o session keyring anterior não existe mais:
$ keyctl show
Session Keyring
333158329 --alswrv 1000 1000 keyring: _ses
846694921 --alswrv 1000 65534 \_ keyring: _uid.1000
Se, em vez disso, dissermos ao ssh-add-keyring.sh
para carregar a chave privada no user keyring (substitua @s
por @u
nos parâmetros da linha de comando), ela vai ficar disponível e acessível em ambas as sessões de login. Neste caso, durante a saída e o retorno, a mesma chave será apresentada. Contudo, isso tem uma desvantagem de segurança: qualquer processo em execução com nosso ID de usuário poderá acessar e usar a chave.
$ SSH_AUTH_SOCK="" ./ssh -i ~/.ssh/id_rsa_keyring localhost
Load key "/home/ignat/.ssh/id_rsa_keyring": key not found
…
Resumo
Neste post, aprendemos sobre uma das formas mais comuns de vazamento de dados, incluindo chaves criptográficas altamente valiosas. Conversamos sobre alguns exemplos reais, que impactaram muitos usuários ao redor do mundo, inclusive a Cloudflare. Por fim, aprendemos como o Linux Kernel Retention Service pode nos ajudar a proteger nossas chaves e segredos criptográficos.
Também apresentamos uma correção de trabalho para o OpenSSH para usar esse recurso interessante do kernel do Linux, assim você pode experimentá-lo facilmente. Ainda há muitos recursos do Linux Kernel Key Retention Service sobre os quais não falamos, que podem ser um tópico para outro post no blog. Fique atento.