Quarta Entrega: EP de Criptografia em Python (PMR3412-2022)
Esta entrega é baseada nos conhecimentos básicos de criptografia e segurança fornecidos em aula. O objetivo é a criação de um sistema de autenticação Web usando a linguagem Python. Para este fim, serão utilizados o framework Flask de desenvolvimento de backend e os algoritmos criptográficos da biblioteca cryptography.
1. Sistema de cadastro e autenticação de usuários
A primeira parte desta entrega consiste em desenvolver um sistema de cadastro de usuários com senhas e funcionalidade de login usando o framework Flask. Os usuários serão cadastrados em um banco de dados SQLite3 e serão implementadas sessões para gerenciar os usuários logados.
1.1 Cadastro de usuários
Para o cadastro de usuários, é proposto o uso do Flask com a extensão Flask-SQLAlchemy, que implementa o ORM (Object–relational mapping). A seguir seguem instruções básicas de uso do Flask e desta extensão; caso prefira, pule para a seção que apresenta o enunciado desta parte.
Instalação e aplicação mínima do Flask
O Flask é um microframework voltado para o desenvolvimento de aplicações Web. Nas nossas aplicações, utilizaremos o Flask para simular o servidor de gerenciamento de usuário e senhas. Para instalá-lo, utilize o seu método favorito de instalação de bibliotecas em Python, ou confira em https://flask.palletsprojects.com/en/2.0.x/installation/.
Para testar a instalação do Flask, vamos gerar uma aplicação inicial.
Crie um arquivo server.py
com o conteúdo:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
Para executá-lo, digite em um terminal Powershell:
$ $Env:FLASK_APP = "server"
$ python -m flask run
No Linux ou macOS, substitua o primeiro comando por
export FLASK_APP=server
. No prompt do Windows, utilizeset FLASK_APP=server
.
Agora, abra uma aba do seu browser e digite o endereço http://localhost:5000/, que deve exibir uma página com o escrito "Hello, World!".
Instalação e aplicação mínima do Flask-SQLAlchemy
Podemos instalar o Flask-SQLAlchemy com o pip com o comando pip install -U Flask-SQLAlchemy
.
Para testar o seu funcionamento, vamos criar o nosso banco de dados baseado na aplicação mínima descrita em https://flask-sqlalchemy.palletsprojects.com/en/2.x/quickstart/#a-minimal-application/, que define um único modelo User
:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(
basedir, 'db.sqlite3')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
Para criar o arquivo do banco de dados, podemos executar os seguintes comandos no shell do Python:
>>> from database_test import db
>>> db.create_all()
onde database_test
deve ser substituído pelo nome do arquivo .py
.
Na mesma pasta, deve ter sido criado o arquivo db.sqlite3
, que armazena as nossas tabelas.
Note que estes comandos também precisam ser executados depois de mudanças/criação de modelos.
Adicionando dados e fazendo queries
Para inserir dados no nosso banco, podemos utilizar o método db.session.add(user)
, onde user
é um objeto da classe User
, como mostrado nas instruções seguintes:
from database_test import User, db
admin = User(username='admin', email='admin@example.com')
guest = User(username='guest', email='guest@example.com')
db.session.add(admin)
db.session.add(guest)
db.session.commit()
Para a leitura de dados, temos as seguintes funções principais:
User.query.all() # obtem todos as entradas de user
User.query.get(2) # obtem usuario com id igual a 2
user = User.query.filter_by(email='fulano@usp.br').first() # obtem o usuario com o email fulano@usp.br ou None se nao encontrar
user = User.query.filter_by(email='fulano@usp.br').first_or_404(description=f'There is no user with email fulano@usp.br') # o mesmo que o anterior mas invoca 404 se nao encontrar
Para informações mais detalhadas, consulte https://flask-sqlalchemy.palletsprojects.com/en/2.x/queries/?highlight=filter_by.
Enunciado da parte 1.1
Esta atividade consiste em criar uma aplicação Web com o Flask/Flask-SQLAlchemy para cadastro de usuários.
Seguem os requisitos do programa:
- implementar todo código em um único arquivo com o nome de
server.py
; - deve apresentar um modelo
User
(derivado dedb.Model
do Flask-SQLAlchemy) para armazenar os dados de usuário, que deve ter pelo menos os campos email, nome e senha, além da chave primária; - deve apresentar uma rota para cadastro de usuário, que aceita os métodos GET e POST do HTTP, onde
- o método GET deve responder com o código 200 (OK), incluindo no corpo uma página HTML contendo um formulário, que ao submetido, envia todos os dados de usuários em uma requisição POST para esta mesma rota; e
- o método POST deve criar uma nova entrada de usuário no banco de dados com os dados recebidos na requisição através do formulário HTML, e responder com o código 201 (Created), com um conteúdo qualquer.
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor.
- Screenshot do browser exibindo a página de cadastro.
- Para o método GET, screenshot mostrando os cabeçalhos da requisição e resposta na versão original (source ou raw), que podem ser visualizados com as ferramentas de desenvolvimento do browser.
- Para o método POST, screenshot mostrando os cabeçalhos da requisição e resposta na versão original (source ou raw).
- Para a requisição POST, liste os dados do corpo da requisição e o valor do cabeçalho
Content-TYpe
.
1.2 Autenticação básica com cookies não seguros
A próxima etapa consiste em implementar um sistema de autenticação simples, que apenas confere se o email e senha fornecidos correspondem com os dados cadastrados no banco. Além disso, para manter o usuário logado, será utilizado um cookie, que armazenará os dados de sessão no cliente.
A seguir seguem instruções básicas de uso de cookies no Flask; caso prefira, pule para a seção que apresenta o enunciado desta parte.
Armazenando e lendo cookies com o Flask
No Flask, podemos armazenar um cookie ao configurar a resposta de uma requisição. Para tal, vamos basear no exemplo da documentação do Flask em https://flask.palletsprojects.com/en/2.0.x/quickstart/#cookies. O código abaixo cria duas rotas, uma delas armazena um cookie com o nome do usuário e a outra faz a leitura do cookie para saber se o usuário já está definido:
from flask import make_response
@app.route('/login', methods=['GET', 'POST'])
def fake_login():
if request.method == 'POST':
resp = make_response(f'Fazendo o login como {request.json["user"]}')
resp.set_cookie('user', request.json["user"])
return resp
else:
user = request.cookies.get('user')
if user:
return f'Ja logado como {user}'
else:
return 'Nao logado ainda'
Para apagar o cookie, é necessário utilizar um pequeno "truque": sobrescrever o cookie antigo e definir a expiração imediata como no código:
@app.route('/delete-cookie')
def delete_cookie():
res = make_response("Cookie Removed")
res.set_cookie('user', 'qualquer_coisa', max_age=0)
return res
Enunciado da parte 1.2
Esta atividade consiste em extender a aplicação Web com um sistema de login com cookies. Nesta etapa, é pedido a criação de pelo menos 3 rotas.
Seguem os requisitos do programa:
- deve apresentar uma rota para fazer o login de usuário, que aceita os métodos GET e POST do HTTP, onde:
- o método GET deve responder com o código 200 (OK), incluindo no corpo uma página HTML contendo um formulário, que envia os dados de email e senha em uma requisição POST para esta mesma rota; e
- o método POST deve validar os dados recebidos na requisição através do formulário e:
- caso bem sucedido, responder com o código 200 (OK) ou 302 (REDIRECT) e definir novo cookie
user_id
contendo o id do usuário; - caso contrário, responder com 401 (Unauthorized);
- em ambos os casos, pode responder com um conteúdo qualquer no corpo;
- caso bem sucedido, responder com o código 200 (OK) ou 302 (REDIRECT) e definir novo cookie
- deve apresentar uma rota para logout de usuário, onde:
- o método GET deve responder com uma página HTML (código 200) contendo um formulário que faz uma requisição POST para esta mesma rota, sem dados;
- o método POST deve apaga o cookie
user_id
e responder com um conteúdo qualquer (código 200 ou 302);
- deve apresentar uma rota GET para a página principal que:
- deve apresentar o nome do usuário logado, caso logado;
- deve retornar 401 com a função
abort
, caso contrário.
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor.
- Screenshot do browser exibindo a página de login.
- Para o método POST na rota de login, screenshot mostrando os cabeçalhos da requisição e resposta na versão original (source ou raw).
- Para a requisição POST na rota de login, liste os dados do corpo da requisição e o valor do cabeçalho
Content-Type
. - Screenshot do browser exibindo erro de autorização na página inicial, junto com as informações de cookies nas ferramentas de desenvolvedor.
- Screenshot do browser exibindo página inicial após login bem sucedido, junto com as informações de cookies nas ferramentas de desenvolvedor.
- Para o método POST na rota de logout, screenshot mostrando os cabeçalhos da requisição e resposta na versão original (source ou raw) e um screenshot mostrando as informações de cookies nas ferramentas de desenvolvedor.
1.3 Sessões e autenticação com hash de senhas
A aplicação desenvolvida até aqui apresenta duas falhas graves de segurança: o armazenamento de senhas no banco e dados de sessão desprotegidos diretamente no cookie. Assim, nesta última parte do primeiro exercício o sistema será adaptado para salvar o hash (digest) da senha no banco e os dados de sessão no servidor.
Descrição do framework de sessões a ser implementado
Para as sessões, será criada uma nova tabela no banco de dados, cuja chave primária deve ser sempre gerada a partir do hash de um número aleatório. Além disso, deve conter uma coluna com o id do usuário logado na sessão. Deste modo, ao realizar o login, uma nova entrada é inserida na tabela sessões com o id do usuário e um novo cookie é criado com o id aleatório da sessão. Por fim, para verificar se o usuário está autenticado, basta checar se o id da sessão do cookie existe na banco e obter o usuário a partir da tabela de sessões.
Derivando o digest da senha
Para utilizar as funções listadas a seguir, é necessário instalar a biblioteca cryptography
com pip install cryptography
.
Para gerar o digest (aka hash ou fingerprint), vamos utilizar o método derive
da classe Scrypt
.
A sua utilização é bastante direta, o único cuidado que deve ser tomado é adotar os parâmetros corretos do algoritmo.
O livro texto "Practical Cryptography in Python: Learning Correct Cryptography by Example de Seth James Nielson e Christopher K. Monson." possui um exemplo com os parâmetros recomendados na listagem 2-7:
import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.backends import default_backend
salt = os.urandom(16)
kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1, backend=default_backend())
digest = kdf.derive(b"my great password")
Note que a senha é passada como um objeto bytes; caso fosse usar uma string, ela deveria ser convertida para bytes com o método encode('utf-8')
.
Este procedimento deve ser bastante utilizado nesta tarefa, pois os argumentos e o valor retornado das funções da biblioteca cryptography geralmente são do tipo bytes.
Verificação da senha
Para verificar se a senha está correta existe o método verify
da classe Scrypt
.
O código a seguir é adaptado da listagem 2-8 do livro texto:
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidKey
kdf = Scrypt(salt =salt, length =32, n=2**14, r=8, p=1, backend=default_backend())
try:
kdf.verify(b"my great password", key)
print("Success!")
except InvalidKey:
print("Fail!")
Codificação Base64
Trabalhar com hashes e criptografia traz uma complicação adicional, pois o resultado dessas operações gera objetos bytes que não possui uma representação em caracteres UTF-8 ou ASCII. O problema é que, tanto para armazenamento em banco quanto para os dados em cookies, é necessário representar os valores no formato texto.
Para tal, é possível utilizar a codificação Base64, que é capaz de representar qualquer sequência de bytes em caracteres. No Python, é possível codificar e descodificar com Base64, como mostrado no exemplo:
from base64 import b64encode, b64decode
hash_base64_em_str = b64encode(hash_em_bytes).decode('ascii')
hash_em_bytes_denovo = b64decode(hash_base64_em_str.encode('ascii'))
Note que a função b64encode
retorna um objeto bytes e, portanto, deve ser decodificado para converter em string.
A codificação ASCII, que é um subconjunto do UTF-8, é suficiente para os caracteres utilizados no Base64.
O mesmo pode ser dito para a função b64decode
, só que no sentido inverso.
ℹ️ Para fins de depuração (debug), os objetos tipo bytes podem ser impressos na tela em formato hexadecimal com o métodohex()
. Isto é, poderíamos visualizar um hash com o comandoprint(hash_em_bytes.hex())
. Naturalmente, este valor poderia ser armazenado no banco ou em cookies, mas a codificação Base64 é mais eficiente (menos caracteres por bytes).
Enunciado da parte 1.3
Esta atividade consiste em usar hashes para armazenar senhas e transferir as informações de sessões para o banco de dados do servidor.
Seguem os requisitos do programa:
- quando um usuário for criado, ao invés de armazenar a senha diretamente, concatene o sal e o digest e armazene sua representação Base64;
- o funcionamento da rota de login deve adotar o novo esquema de armazenamento de senhas;
- deve apresentar um modelo
Session
(derivado dedb.Model
do Flask-SQLAlchemy) para armazenar os dados de sessão, que deve ter pelo menos os campouser_id
, além da chave primária, que deve ser do tipodb.String
; - deve substituir o cookie
user_id
pelo o framework de sessões descrito no início da seção 1.3, onde:- para gerar o id da sessão, deve ser utilizado a biblioteca
hashlib
para gerar o hash MD5 de um número aleatório de 16 bytes que, por sua vez, pode ser gerado com a funçãoos.urandom
; - o cookie deve definido deve ter a chave
session_id
e conter a chave primária da sessão criada; e - a página principal deve usar o framework de sessões para apresentar o nome do usuário logado.
- para gerar o id da sessão, deve ser utilizado a biblioteca
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor.
- Usando temporariamente a função print do Python, gere um screenshot da saída do servidor durante o cadastro de um usuário, mostrando i) a senha original, ii) o sal e o digest em hexadecimal e iii) o valor final (em base64) do campo salvo no banco de dados.
- Usando temporariamente a função print do Python, gere um screenshot da saída do servidor durante o login de um usuário, mostrando o id da sessão salva no banco de dados.
- Screenshot do browser exibindo página inicial após login bem sucedido, junto com as informações de cookies nas ferramentas de desenvolvedor.
2. Implementando criptografia nos dados de aplicação
Dando prosseguimento ao desenvolvimento da nossa aplicação, deverá ser implementada a criptografia dos dados de aplicação. Mais especificamente, o usuário e a senha no login serão enviados ao servidor de forma segura.
2.1 Implementar a encriptação simétrica dos dados de formulário no login
Nesta primeira parte, os dados do login serão encriptados com criptografia simétrica antes de enviados. Também deve ser incluído o HMAC, a fim de garantir integridade dos dados. Note que, por enquanto, as chaves simétricas são enviadas em aberto, o que não é admissível. Corrigiremos isso nas próximas etapas.
Fazendo requisições no Python com a biblioteca requests
A fim de possibilitar o desenvolvimento do cliente com Python, será utilizada a biblioteca requests a fim de realizar as requisições HTTP, substituindo o uso do browser.
Como das outras vezes, para instalá-la, basta executar pip install requests
.
A utilização desta biblioteca é bastante simples (veja em https://requests.readthedocs.io/en/master/). Com o servidor em execução, é possível entrar no Python modo interativo e digitar os seguintes comandos para cadastrar um novo usuário, fazer o login e recuperar os cookies:
>>> import requests
>>> r = requests.post('http://localhost:5000/login', data={'email': 'beltrano@email.com','password': '123456'})
>>> r.status_code
200
>>> r.cookies['session_id']
'fc5db617b8734172f8834f9ea11eecbe'
O cookie pode então ser transmitido com:
>>> r = requests.get('http://localhost:5000/', cookies={'session_id': r.cookies['session_id']})
>>> r.status_code
200
Encriptação e decodificação simétrica com o AES-CTR
O uso do AES-CTR é bastante simples, só é necessário fornecer dois parâmetros: a chave e o IV. Ambos têm tamanho fixo e podem ser gerados aleatoriamente, sem nenhuma restrição, utilizando algoritmos randômicos criptograficamente seguros (para o nosso contexto pelo menos). Para verificar sua utilização, vamos verificar a listagem 3-9 do livro texto:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
class EncryptionManager:
def __init__(self):
key = os.urandom(32)
nonce = os.urandom(16)
aes_context = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=default_backend())
self.encryptor = aes_context.encryptor()
self.decryptor = aes_context.decryptor()
def updateEncryptor(self, plaintext):
return self.encryptor.update(plaintext)
def finalizeEncryptor(self):
return self.encryptor.finalize()
def updateDecryptor(self, ciphertext):
return self.decryptor.update(ciphertext)
def finalizeDecryptor(self):
return self.decryptor.finalize()
# Auto generate key/IV for encryption
manager = EncryptionManager()
plaintexts = [
b"SHORT",
b"MEDIUM MEDIUM MEDIUM",
b"LONG LONG LONG LONG LONG LONG"
]
ciphertexts = []
for m in plaintexts:
ciphertexts.append(manager.updateEncryptor(m))
ciphertexts.append(manager.finalizeEncryptor())
for c in ciphertexts:
print("Recovered", manager.updateDecryptor(c))
print("Recovered", manager.finalizeDecryptor())
Enunciado da parte 2.1
Esta atividade consiste em usar criptografia simétrica para enviar dados de login para o servidor.
Seguem os requisitos do programa cliente:
- implementar o código do cliente em um único arquivo com o nome de
client.py
; - deve receber os dados de login (email e senha) a partir da linha de comando;
- as chaves AES, MAC e o IV devem ser gerados aleatoriamente no início do programa;
- deve realizar a requisição POST na rota de login do servidor com os seguintes dados:
- session_keys: as chaves AES, MAC e o IV concatenados, convertidos para base64;
- cyphertext: os dados originais (email e senha), encriptados com o AES-CTR, usando a chave AES e o IV, convertidos para base64;
- hmac: o HMAC do texto cifrado convertido para base64;
- ao finalizar a requisição, deve ser impresso na tela o código da resposta e, caso bem sucedido, o conteúdo do cookie
session_id
.
Seguem os requisitos do programa servidor:
- deve ser adaptado para aceitar os dados encriptados, que devem ser decifrados e o HMAC validado antes de prosseguir com a autenticação original.
No relatório, apresente:
- Listagem do código em Python do arquivo do cliente e do servidor.
-
Screenshot único mostrando a execução tanto do cliente e do servidor para um login bem sucedido. No cliente, deve aparecer o código de resposta e o conteúdo do cookie
session_id
. Além disso, usando temporariamente a função print do Python, imprimir o conteúdo dos dados da requisição (session_keys, cyphertext e hmac), que devem estar em base64.
2.2 Troca de chaves com RSA
Para evitar o envio de chaves simétricas em aberto na nossa aplicação, será utilizada a criptografia assimétrica. Iremos adotar uma forma bastante simplificada de comunicação com troca de chaves RSA. Basicamente, para cada comunicação, as chaves simétricas serão encriptadas e enviadas juntas com a mensagem e o HMAC. Com isso, quando a transmissão completa é recebida pelo servidor, ele será capaz de decodificá-la apenas utilizando a sua chave privada.
Considere o envio de uma mensagem do cliente para o servidor. No nosso protocolo, a transmissão é realizada com um stream de bytes resultante da concatenação das seguintes informações: - A chave AES de encriptação, o IV e o HMAC (encriptado com a chave pública do servidor). - Mensagem encriptada com a chave AES+IV. - HMAC sobre toda a transmissão (utilizando a chave MAC).
O livro texto apresenta a implementação de um algoritmo parecido na listagem 6-1:
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes, hmac
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding, rsa
# WARNING: This code is NOT secure. DO NOT USE!
class TransmissionManager:
def __init__(self, send_private_key, recv_public_key):
self.send_private_key = send_private_key
self.recv_public_key = recv_public_key
self.ekey = os.urandom(32)
self.mkey = os.urandom(32)
self.iv = os.urandom(16)
self.encryptor = Cipher( algorithms.AES(self.ekey),
modes.CTR(self.iv), backend=default_backend()).encryptor()
self.mac = hmac.HMAC( self.mkey, hashes.SHA256(), backend=default_backend())
def initialize(self):
data = self.ekey + self.iv + self.mkey
h = hashes.Hash(hashes.SHA256(), backend=default_backend())
h.update(data)
data_digest = h.finalize()
signature = self.send_private_key.sign(data_digest,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())
ciphertext = self.recv_public_key.encrypt(data,
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), label=None))
ciphertext += signature
self.mac.update(ciphertext)
return ciphertext
def update(self, plaintext):
ciphertext = self.encryptor.update(plaintext)
self.mac.update(ciphertext)
return ciphertext
def finalize(self):
return self.mac.finalize()
Note que neste código é incluso a assinatura do cliente, que não utilizaremos pois vamos considerar chaves assimétricas apanas para o servidor, uma vez que a autenticação do cliente é feito com senha. Gaste um tempo analisando e tentando compreender o código, este contém os elementos principais para a parte três.
Ademais, está apenas implementada a transmissão de dados, você deverá desenvolver o recebimento também.
Para maiores esclarecimentos dos métodos usados, consulte a documentação das funcionalidades do RSA fornecidos pela biblioteca cryptography
em https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#module-cryptography.hazmat.primitives.asymmetric.rsa
Carregando as chaves RSA no formato PEM
No Python, para carregar a chave privada RSA e gerar a chave pública, podemos escrever (trecho retirado da listagem 4-4 do livro texto):
with open("my_key.pem", "rb") as private_key_file_object:
private_key = serialization.load_pem_private_key( private_key_file_object.read(),
backend = default_backend(), password = None)
public_key = private_key.public_key()
Por fim, para carregar apenas a chave pública, podemos usar
with open("my_key_pub.pem", "rb") as public_key_file_object:
public_key = serialization.load_pem_public_key(public_key_file_object.read(),
backend=default_backend())
ℹ️ Para gerar chaves RSA e salvá-las em disco, podemos usar o OpenSSL, como mostrado em aula, ou utilizar a bibliotecacryptography
. A listagem 4-4 do livro texto pode ser utilizada sem alterações para esta última opção.
Enunciado da parte 2.2
Esta atividade consiste em usar criptografia assimétrica (RSA) para a troca de chaves simétricas.
Antes de modificar o programa, faça o seguinte:
- crie a chave privada RSA do servidor salve em um arquivo com o formato PEM;
- a partir da chave privada, gere o arquivo PEM da chave pública com o comando
openssl rsa -in my_key.pem -out my_key_pub.pem -pubout
.
Seguem os requisitos do programa cliente:
- o nome do arquivo da chave pública deve ser recebido a partir da linha de comando;
- encriptar os dados enviados na chave
session_keys
com a chave pública do servidor, lida a partir do arquivo especificado; e - o HMAC deve ser calculado a partir dos dados completos da mensagem (chaves encriptadas + mensagem cifrada).
Seguem os requisitos do programa servidor:
- deve ser adaptado para aceitar os as chaves encriptadas e o novo HMAC.
No relatório, apresente:
- Listagem do código em Python do arquivo do cliente e do servidor.
- Listagem do arquivo de chave pública do servidor.
- Os comandos/código utilizado para gerar as chaves assimétricas.
-
Screenshot único mostrando a execução tanto do cliente e do servidor para um login bem sucedido. No cliente, deve aparecer o código de resposta e o conteúdo do cookie
session_id
. Além disso, usando temporariamente a função print do Python, imprimir o conteúdo dos dados da requisição (session_keys, cyphertext e hmac), que devem estar em base64.
2.3 Usando certificados X.509 para provar propriedade de chave privada
O cliente não deve aceitar qualquer chave pública que lhe é fornecido. Por este motivo, nesta última etapa, vamos utilizar certificados X.509 ao invés da chave pública apenas. Para tal, o pacote x509 da biblioteca cryptography será utilizada. A seguir seguem instruções básicas de uso deste pacote; caso prefira, pule para a seção que apresenta o enunciado desta parte.
Carregando certificados X.509 e verificando assinatura
Após criar os certificados, é possível interagir com eles através da biblioteca cryptography, no módulo X.509, cuja documentação pode ser encontrada em https://cryptography.io/en/latest/x509/index.html. Para carregar um certificado no formato PEM, é possível utilizar:
from cryptography import x509
cert = x509.load_pem_x509_certificate(cert_bytes, default_backend())
onde cert_bytes
é o conteúdo do certificado em bytes.
Depois disso, é possível ler os dados do certificado; por exemplo, para ler o CN do requerente e do emissor, faça:
from cryptography import x509
from cryptography.x509.oid import NameOID
cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
Para verificar a assinatura, é necessário calcular o hash em cima dos dados em cert.tbs_certificate_bytes
.
A assinatura e o algoritmo de hash devem ser lidos do certificado, e o padding padrão do OpenSSL para assinatura com chave RSA é o PKCS1v15.
Enunciado da parte 2.3
Para esta etapa, o servidor deve enviar o certificado para o cliente, que deve checar a validade antes de extrair e usar a chave pública.
Antes de modificar o programa, faça o seguinte:
- a partir da chave do servidor, crie um CSR usando o OpenSSL ou a biblioteca cryptography com os seguintes dados no campo do requerente (subject):
- C = BR
- ST = SAO PAULO
- L = SAO PAULO
- O = EPUSP
- OU = PMR
- CN = <seu número USP>
- emailAddress = <seu email cadastrado no Moodle>
- a partir do CSR, gere dois certificados seguindo as instruções:
- use o OpenSSL/cryptography para auto-assinar o certificado; e
- envie o CSR por email para pmr3412RedesIndustriais@gmail.com; que deve responder com o certificado assinado, caso os dados estejam corretos.
Seguem os requisitos do programa servidor:
- deve apresentar uma rota GET para transmissão do certificado, que deve carregar o arquivo do certificado e enviar o seu conteúdo na resposta.
Seguem os requisitos do programa cliente:
- definir uma variável global com o conteúdo do certificado raiz, que pode ser obtido da página da disciplina no Moodle, da seguinte forma:
ca_root_cert_pem = b'''-----BEGIN CERTIFICATE----- MIIDqTCCApECFD+Ve9BQTl0k1yEeHYwK... -----END CERTIFICATE----- '''
- antes da requisição de login, fazer uma requisição na nova rota do servidor para obtenção do certificado;
- uma vez carregado o certificado do servidor, checar:
- se o CN do requerente é o seu número USP;
- se o CN do emissor é igual ao CN do requerente do certificado raiz; e
- se a assinatura do certificado do servidor é válida;
- caso não passe por algum dos testes, o programa deve terminar; caso contrário, use a chave pública do certificado nas operações seguintes.
No relatório, apresente:
- Listagem do código em Python do arquivo do cliente e do servidor.
- Listagem do arquivo do certificado do servidor.
- Os comandos/código utilizado para gerar o CSR.
- Screenshot único mostrando a execução tanto do cliente e do servidor para a transmissão do certificado do servidor (qualquer versão), incluindo o conteúdo do certificado. Para tal, use temporariamente a função print do Python a fim de imprimir o conteúdo da resposta do servidor.
- Screenshot único mostrando a execução tanto do cliente e do servidor para o certificado auto-assinado. No cliente, use temporariamente a função print do Python a fim de imprimir o conteúdo do certificado recebido e emitir uma mensagem de erro.
-
Screenshot único mostrando a execução tanto do cliente e do servidor para um login bem sucedido com o certificado correto. No cliente, deve aparecer o conteúdo do certificado recebido, o código de resposta e o conteúdo do cookie
session_id
.