WEB3DEV

Cover image for Autenticação social baseada em Solidity— Enviando Cryptos para qualquer conta do Google
Adriano P. Araujo
Adriano P. Araujo

Posted on

Autenticação social baseada em Solidity— Enviando Cryptos para qualquer conta do Google

Já tentou enviar um Bored Ape* para o seu avô? As chances são de que ele não é o cara mais experiente em crypto. Todo usuário da web3 está familiarizado com o pesadelo a bordo. Você nem tentaria explicar a configuração da carteira ao seu avô.

  • Nota do tradutor: Bored Ape é uma coleção de tokens não fungíveis construída na blockchain Ethereum.A coleção apresenta fotos de perfil de macacos de desenhos animados que são gerados processualmente por um algoritmo.

Precisamos encontrar maneiras de enviar cryptos com segurança para qualquer pessoa. Além disso, precisamos de uma solução que não exija um cadastro dos usuários. Qualquer pessoa na internet deve poder receber cryptos ou — pelo bem deste artigo —, digamos que pelo menos qualquer pessoa com uma conta do Google.

O conceito de usar o Login do Google em um contrato inteligente não é novo. Este projeto foi inspirado por um exemplo inicial de validar JWTs do Google pelo OpenZeppelin. Este artigo o orienta no processo de extensão da solução da OZ, eliminando a necessidade de integração e o conectando a um ChainLink oracle. O resultado é um cofre inteligente, sem custódia, baseado em um contrato, que permite aos usuários depositarem tokens ETH e ERC20 que só podem ser acessados pela conta Google do destinatário.

Não é necessário qualquer integração, qualquer pessoa pode receberDemo SocialLock

JW o quê?

A autenticação do Google é baseada em JWTs (JSON Web Tokens). Mais especificamente, o Google usa o OAuth 2.0, um padrão aberto para autorização. Se você não conhece os JWTs, recomendo https://jwt.io/.

Usaremos o JWT do Google para autenticar o destinatário dos fundos. Para acessar os fundos bloqueados, o usuário receptor deve entrar com sua conta do Google e fornecer um JWT válido ao contrato inteligente. Em outras palavras, a única maneira de retirar o ETH do cofre é fornecer um JWT válido (assinado pelo Google) e, ter acesso à conta do Google do destinatário.

Os JWTs são um padrão da indústria amplamente usado e provavelmente a maneira mais comum de lidar com a autenticação da web. A especificação é definida em RFC7519. Você não precisa entender completamente a RFC; o importante é saber que o JWT do Google usa o mecanismo de assinatura RSA256. Isso significa que o valor da assinatura do JWT é uma assinatura RSA do hash  SHA256 da carga útil. Precisamos entender isso quando se trata de validar a assinatura no contrato inteligente.

1. Depósito do Cofre

Vamos começar com a parte simples: bloquear fundos para um determinado endereço de email. Isso pode ser alcançado com uma simples função de bloqueio. O contrato usa um mapeamento para armazenar o saldo de cada usuário.

/**
/**

* Esse mapeamento armazena o endereço de email do Google e o mapeia para equilibrar

* @dev Os e-mails precisam ser hashs keccak256 antes de serem usados como chave

*/

mapping (bytes32 => uint) public balances;




/**

* Deposite ETH no contrato para um determinado endereço de email

* @param email O endereço de email do Google do usuário ( precisa ser heccak256 antes de ser usado como uma chave )

*/

function deposit(bytes32 email) public payable {

  balances[email] += msg.value;

}

Enter fullscreen mode Exit fullscreen mode

Como você pode ver, o mapeamento usa um hash kecccak256(abi.encode(email)) do endereço de email. Usar o email de texto sem formatação seria suficiente para o mapeamento funcionar, mas comprometeria a privacidade até certo ponto. Usar o hash  keccak256 do email garante que o comprimento da saída seja sempre de 32 bytes e que o email não seja enviado em texto sem formatação.

Cuidado: Esteja ciente de que o email ainda será exposto na função de retirada, pois a carga útil do JWT precisa ser enviada ao contrato. O endereço de email codificado em base64 faz parte do JWT, o que significa que alguém pode decodificar o campo de dados de uma transação de retirada para obter o email. No entanto, o uso do hash ainda é mais seguro que o texto sem formatação, pois não expõe o email sem uma retirada.

2. Retirada do Lockbox

O destinatário pode desbloquear os fundos fornecendo um JWT válido.A função  withdraw verifica o JWT e garante que o email na carga útil corresponda ao email no mapeamento do saldo do contrato inteligente.

function withdraw(string memory headerJson, string memory payloadJson, bytes memory signature, uint amount) public {

// valida o JWT

  string memory email = validateJwt(headerJson, payloadJson, signature);

  bytes32 emailHash = keccak256(abi.encodePacked(email));

 // verificação do saldo

 // isso usa o endereço de email do Google JWT para verificar se há um //saldo para este email

  require(balances[emailHash] > 0, "No balance for this email");

  require(balances[emailHash] >= amount, "Not enough balance for this email");


// transferência

  balances[emailHash] -= amount;

  msg.sender.transfer(amount);

}

Enter fullscreen mode Exit fullscreen mode

Vamos dar uma olhada mais de perto na função validateJwt. Verifica as seguintes coisas:

  1. JWT signature (Assinatura JWT)

  2. Audience — este é o ID do cliente do Google OAuth

  3. Nonce — este é o endereço Ethereum do destinatário

function validateJwt(string memory headerJson, string memory payloadJson, bytes memory signature) internal returns (string memory) {

  string memory headerBase64 = headerJson.encode();

  string memory payloadBase64 = payloadJson.encode();

  StringUtils.slice[] memory slices = new StringUtils.slice[](2);

  slices[0] = headerBase64.toSlice();

  slices[1] = payloadBase64.toSlice();

  string memory message = ".".toSlice().join(slices);

  string memory kid = parseHeader(headerJson);

  bytes memory exponent = getRsaExponent(kid);

  bytes memory modulus = getRsaModulus(kid);



// verificação de assinatura

  require(message.pkcs1Sha256VerifyStr(signature, exponent, modulus) == 0, "RSA signature check failed");



  (string memory aud, string memory nonce, string memory sub, string memory email) = parseToken(payloadJson);

// verificação da audience, este é o cliente do Google OAuth2 Web App

  require(aud.strCompare(audience) == 0, "Audience does not match");



 // verificação nonce

 // o nonce é o endereço de destino que receberá os fundos e também o endereço que aciona esta transação ( msg.sender )

 // estamos validando se o Google JWT inclui o nonce ( msg.sender ) na carga útil

  string memory senderBase64 = string(abi.encodePacked(msg.sender)).encode();

  console.log('senderBase64', senderBase64);

  console.log('nonce', nonce);

  require(senderBase64.strCompare(nonce) == 0, "Sender does not match nonce");



  return email;

}

Enter fullscreen mode Exit fullscreen mode

A função verifica a assinatura JWT e verifica se o audience corresponde ao ID do cliente do Google usado no construtor do contrato SocialLock. Por fim, garante que o nonce corresponda ao msg.sender do Endereço Ethereum. Inspirada na implementação do OpenZeppelin, essa verificação não impede que alguém use o mesmo JWT para enviar algo para outro endereço ou acionar outra transação.

Em produção, mais fatores devem ser incorporados ao nonce, pois esse é o valor que o Google assinará. Idealmente, o provedor de autenticação deve assinar todos os detalhes da transação, incluindo os campos de dados e valor, que serão validados no contrato inteligente.

3. Hora do Oracle — obtendo o módulo RSA

A validação da assinatura RSA requer chaves públicas. Eles são expostos pelo Google como JSON Web Keys ( JWK ) em um JSON Web Key Set (JWKS). O JWKS é publicado aqui e as chaves são atualizadas a cada 48 horas: https://www.googleapis.com/oauth2/v3/certs

Podemos obter o JWKS usando um oracle baseado em ChainLink. O JWT incluirá o kid ( ID da chave ), bem como o módulo RSA n. Conforme definido em RFC7517, o kid é usado para escolher entre um conjunto válido de chaves e será incluído posteriormente no JWT para validação.


/**

* @notice Solicite as últimas chaves JWKS do Google

* @dev Após a implantação, chame esta função pelo menos uma vez para obter as chaves JWKS mais recentes

*/

function requestJwks() public returns (bytes32 requestId) {

    Chainlink.Request memory req = buildChainlinkRequest(

        '631ceadc6a534f9694ead93a9617706c', // Shout out to Mathias @ https://glink.solutions/ for creating and hosting this job

        address(this),

        this.fulfill.selector // function selector to point to the fulfill function

    );

    req.add("get", "https://www.googleapis.com/oauth2/v3/certs");

    req.add("path1", "keys,0,kid"); // kid1

    req.add("path2", "keys,0,n"); // modulus1

    req.add("path3", "keys,1,kid"); // kid2

    req.add("path4", "keys,1,n"); // modulus2

    return sendChainlinkRequest(req, fee);

}

Enter fullscreen mode Exit fullscreen mode

O trabalho do oracle usado para buscar essas informações não é  exatamente o padrão. Requerem um URL e quatro parâmetros de caminho que serão resolvidos e enviados de volta ao contrato. Um salve para o Mathias da https://glink.solutions/ por criar e hospedar o trabalho do oracle que chama a API para este contrato.

O módulo n é enviado para o contrato como um valor de byte codificado por hexadecimal, mas o valor em si é codificado em base64url. Decodificar esse valor foi complicado, pois não consegui encontrar uma biblioteca Solidity que pudesse lidar com isso. No entanto, consegui editar uma biblioteca existente e criar uma função de decodificação. Você pode vê-lo em ação na função fulfill() que será chamada pelo oracle.


/**

* @notice Esta função é chamada pelo oráculo após o cumprimento da solicitação

* @dev O registro do modificadorChainlinkFulfillment impede que a função seja chamada por qualquer pessoa, exceto o oráculo

*/

function fulfill(

    bytes32 _requestId,

    string memory kid1,

    bytes memory modulus1,

    string memory kid2,

    bytes memory modulus2

) public recordChainlinkFulfillment(_requestId) {

    emit FulfilledJWKS(_requestId, kid1, modulus1, kid2, modulus2);

  // Decodifique o módulo codificado base64url

 // Os valores JWKS são codificados de acordo com rfc4648 ( [https://tools.ietf.org/html/rfc4648](https://tools.ietf.org/html/rfc4648))

(https://tools.ietf.org/html/rfc4648)

    jwks[kid1] = modulus1.decode();

    jwks[kid2] = modulus2.decode();

}

Enter fullscreen mode Exit fullscreen mode

Com a configuração do oracle concluída, você pode obter os valores financiando seu oracle com o LINK e chamando a função requestJwks() para acionar uma atualização. O trabalho da Oracle chamará seu contrato inteligente e atualizará os valores do  kid e n. Você pode chamar a função getModulus(kid) do seu contrato para obter os valores mais recentes. Sinta-se livre para usar esta implantação para testá-la. Você pode usar qualquer kid do Google JWKS.

Recuperando o valor do módulo do contrato oracle

Vale ressaltar que uma validação bem-sucedida da assinatura RSA também requer o expoente RSA. No entanto, um fato surpreendente sobre a RSA é que é prática comum sempre usar o mesmo número que o expoente, especificamente 65537 ou AQAB em hexadecimal. Você encontrará esse número no JWKS publicado pelo Google. Como é sempre o mesmo valor, não precisamos de um oracle para lê-lo. Nós codificamos esse valor no  contrato inteligente SocialLock.sol.

 

function getRsaExponent(string memory) internal pure returns (bytes memory) {

  // expoente rsa codificado = AQAB
  return hex"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001";

}

Enter fullscreen mode Exit fullscreen mode

4. Construindo a interface do usuário

Eu criei um interface demo do usuário totalmente funcional e escrita em React / Vite. Como apontado na parte do deposit, o contrato armazenará um mapeamento de hashes keccak256(email) para saldos. Portanto, antes de depositarmos, precisamos enviar o email e obter um valor de bytes32 para enviar ao contrato. Fazer isso em JavaScript é assim:

// corresponde a este código de solidity: bytes32 emailHash = keccak256 ( abi.encodePacked ( email ) );

const encodedSender = ethers.utils.hexlify(Buffer.from(ethers.utils.toUtf8Bytes(address)));

Enter fullscreen mode Exit fullscreen mode

Com o hash descoberto, chamar a função deposit deve ser fácil. A última peça desafiadora é a withdraw onde todos os aspectos do JWT serão verificados. Confundir o nonce é fácil, portanto, certifique-se de acertar este. A carga útil do JWT é codificada base64url. O valor nonce manterá o msg.sender , o endereço que está acionando a retirada. Lembra da função de contrato inteligente que usamos para validação nonce?


string memory senderBase64 = string(abi.encodePacked(msg.sender)).encode();

require(senderBase64.strCompare(_nonce_) == 0, "O remetente não corresponde a nenhum _nonce_");

Enter fullscreen mode Exit fullscreen mode

Para construir a parte do contador na interface do usuário, precisamos codificar a basear64url da msg.sender . Observe o replace() adicional chamado para tornar esse URL de codificação seguro para o JWT.


// O código base64url a seguir codifica o endereço a ser usado como nonce no JWT



const base64Address = ethers.utils.base64

.encode(address)

.replace('=', '')

.replace('+', '-')

.replaceAll('/', '_');

Enter fullscreen mode Exit fullscreen mode

Eventualmente, podemos incluir o nonce no componente Login do Google:


<GoogleLogin

  nonce={base64Address}

  onSuccess={onLogin}

  onError={ () => console.log('Login Falhou) } />

Enter fullscreen mode Exit fullscreen mode

Após configurar seu aplicativo Google OAuth e fazer login na interface do usuário, decodifique seu JWT usando https://jwt.io. A carga útil deve incluir um nonce que corresponda ao seu endereço:


{

  "aud": "<audience>", // verifique se isso corresponde ao seu aud

  "email": "[email protected]", // verifique se isso corresponde ao seu email

 // decodificar e verificar o nonce, verifique se ele corresponde ao seu remetente

  "nonce": "cJl5cMUYEtw6AQx9AbUODRfcecg",// = 0x70997970c51812dc3a010c7d01b50e0d17dc79c8

  "email_verified": true,

  "name": "Lucas Henning",

  "iat": 1677616519,

  "exp": 1677620119,

// alguns valores removidos

}

Enter fullscreen mode Exit fullscreen mode

Verifique duas vezes os valores email, aud, e nonce na sua carga útil JWT. Qualquer coisa que não corresponda às suas configurações fará com que a função de retirada falhe. Para decodificar seu valor de nonce, você pode usar este código:


const nonce = "cJl5cMUYEtw6AQx9AbUODRfcecg";

const address = '0x' + Buffer.from(nonce, 'base64').toString('hex');

// address = 0x70997970c51812dc3a010c7d01b50e0d17dc79c8

Enter fullscreen mode Exit fullscreen mode

Gás

Não surpreende que as operações criptográficas nesses contratos consumam uma quantidade considerável de gás. Em um ambiente de produção, pode-se mover algumas dessas operações para um processador fora da cadeia ou recuperar resultados de um oracle. No entanto, o objetivo deste artigo é realizar a validação na cadeia, e há certos benefícios relacionados à confiança para fazê-lo dessa maneira.

Vejamos o gás real necessário para uma retirada. Quando você clona o repo e roda npx hardhat test os testes imprimirão o gás consumido.


gas used: BigNumber { value: "1780178" }

Enter fullscreen mode Exit fullscreen mode

Para ter uma idéia do que isso significa, vamos assumir um preço médio de gás de 50 Gwei (, você pode verificar o valor mais recente em https://ethgasstation.info/).


50 gwei * 1.780.178 gases = 89.008.900 gwei

89.009.900 * 10 ^ -9 = 0,0890099 ETH

Enter fullscreen mode Exit fullscreen mode

Em outras palavras, uma recuperação custaria $163 na rede principal (em $1800 / ETH ) ou $0,09 em Polygon (em $1 / MaTIC). Embora isso possa parecer alto, é muito improvável que uma operação como essa seja executada na cadeia principal. Em vez disso, seria preferível movê-lo para L2 ou executá-lo como uma meta transação.

Próximos passos

Agora que o problema de integração está resolvido, é importante considerar os próximos passos. É claro que o mecanismo de autenticação mostrado não deve ser usado como uma carteira independente. Em vez disso, pode servir como o primeiro fator de uma carteira inteligente multisig que será estendida por várias outras assinaturas.

Também é importante notar que confiar no Google como provedor de identidade pode não ser aceitável para alguns na comunidade crypto. Além disso, devemos confiar no serviço da oracle para fornecer JWKS precisos para validação. Para melhorar a confiança nesse sistema, vários provedores de identidade e oracles podem ser usados. Dessa forma, a dependência de uma única entidade é reduzida e a precisão da validação pode ser verificada com mais profundidade.

Em produção, mecanismos como esse podem ser usados para implementar a autenticação social como parte da Abstração de Contas, recuperação social compatível com EIP-4337 e criação de carteira para usuários de mídia social. Ao fornecer integração fácil e aumentar gradualmente a segurança por meio de assinaturas adicionais, podemos tornar a criptomoeda mais acessível a todos.

Clique sem medo, confira suku.world para ver algumas das ferramentas que estamos construindo para atingir esse objetivo.

Conclusão

Demonstramos como criar um cofre sem custódia que permita aos usuários enviar com segurança criptomoedas para qualquer conta do Google. Embora ainda seja uma prova de conceito e ainda não esteja pronta para produção, precisamos tomar medidas como essa para tornar a criptomoeda mais acessível a todos, incluindo aqueles que podem não estar familiarizados com as complexidades das carteiras tradicionais e dos processos de integração. É essencial priorizar o embarque no receptor em vez do remetente, e precisamos desenvolver protocolos que permitam a qualquer pessoa receber criptomoedas sem conhecimento ou configuração prévia.

A abstração de contas é um conceito-chave nessa direção, e o EIP-4337 propõe um avanço significativo ao separar o gerenciamento de contas e o processamento de transações. Com essa melhoria, podemos integrar novos usuários sem exigir que eles configurem uma conta Ethereum, tornando as criptomoedas mais fáceis de usar.

A boa notícia é que, com esse experimento, estamos um passo mais perto de tornar as criptomoedas acessíveis a todos, incluindo seu avô.

Repositório no Github

https://github.com/lucashenning/solidity-google-auth

Créditos


Este artigo foi escrito por Lucas Henning e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Oldest comments (0)