Blockchain-based Passwordless Authentication

The Problem with Passwords

Phương pháp chứng thực (authentication) đơn giản yêu cầu username và mật khẩu vốn thường (dễ) bị các vấn đề như: lost-forgot password, brute force, credential stuffing, phishing, key logging, man-in-the-middle attacks etc.

Simple to use authentication experience

Passwordless authentication (PA)

Xác thực không cần mật khẩu (PA) là phương thức để login vào hệ thống mà không cần gõ hay phải nhớ mật khẩu hay tokens bí mật.

PA có thể dưới dạng các hình thức như: Biometrics (Sinh trắc học): Dựa vào đặc điểm sinh học như dấu vân tay, võng mạc, … Hay Possession factors (tạm: Yếu tố tài sản riêng): Xác thực dựa vào đặc tính từ những vật bất ly thân của người dùng, ví dụ điện thoại dùng để nhận mã OTP từ SMS, hay sinh mã từ Authenticator App, tokens vật lý, magic links, vv.

Blockchain 101

Blockchain is a shared, immutable ledger that facilitates the process of recording transactions and tracking assets in a business network. In other word, a blockchain is a distributed database that is shared among the nodes of a computer network. As a database, a blockchain stores information electronically in digital format. A blockchain collects information together in groups, known as blocks, that hold sets of information.

Background – Signatures

Blockchain sử dụng Public-key cryptography (Xem thêm: Kĩ thuật mã hoá trong mạng máy tính) là hệ thống mật mã/chữ kí số dùng mã khoá công khai hay còn gọi là mã hoá bất đối xứng – gần như là trái tim của hầu hết các hệ blockchain cũng như các hệ thống phân tán khác.

Cụ thể hơn Bitcoin lựa chọn thuật toán ECDSA (Elliptic Curve Digital Signature Algorithm – based từ ECC) để ghi mật mã (cryptography). Ngoài các ưu điểm về tốc độ xử lý và chuỗi khoá ngắn gọn, ECDSA còn chính là kẻ-được-lựa-chọn (bởi Bitcoin) thế nên Ethereum và các hệ blockchain sau cũng gần như auto sử dụng ECDSA làm thuật toán chính cho mình.

Dĩ nhiên việc triển khai không phải tuỳ tiện mà tuân thủ theo secp256k1 ECDSA. Ở mức high-level (không ngồi lấy giấy nháp ra giải toán) thì ECDSA tạo ra một hàm một chiều (có thể hiểu như hàm băm), nghĩa là từ private-key có thể tính ra public-key nhưng từ public-key không thể suy ra được private-key (dễ dàng). Nhờ đó, từ một chữ kí số (ví dụ của một transaction), chúng ta có thể biết được: (1)

  • Public-key hay là address của user (người gửi) – proof of ownership
  • Kiểm tra tính đúng đắn, nguyên vẹn của transaction – validity, integrity, etc..
src: coincover

Metamask – A Crypto Wallet

Metamask vừa là ví tiền điện tử (crypto wallet) vừa là công cụ giúp người dùng tương tác với Web3, hoạt động dưới dạng một extension của trình duyệt (FF, Chrome…) và là ứng dụng di động (iOS, Android) độc lập với trình duyệt tích hợp riêng.

dApp (A Decentralized Application) gọi là ứng dụng phi-tập-trung nhưng thực tế hosted ở server tập trung (off-chain) và tương tác với smartcontracts ở on-chain. Có thể so sánh tương tự một website tương tác với server thông qua API truyền thống.

Signing

Ta sẽ sử dụng Metamask để ký vào thông điệp mà không cần tương tác với mạng on-chain (Ethereum). Hay nói một cách khác, Metamask đóng vai trò như một gateway xác thực dựa vào khoá bí mật đã có sẵn của người dùng – do Metamask là công cụ quản lý khoá bí mật – giống như một hệ thống web truyền thống (2.0) thường thấy.

metamask.js

function personalSign(from: string, msg: string, provider: any) {
  return new Promise((resolve, reject) => {
    provider.sendAsync(
      {
        method: 'personal_sign',
        params: [msg, from],
        from,
      },
      (err: any, result: any) => {
        const e = err || (result && result.error)
        if (e) {
          reject(e)
        } else {
          resolve(result)
        }
      },
    )
  })
}

const signature: any = await personalSign(
    from,
    msg,
    window.ethereum,
)

Trong ví dụ trên ta sử dụng personal_sign (proposed bởi Geth), là một trong những phương thức để signing phổ biến vì tính đơn giản (kế thừa từ eth_sign), hay thậm chí là method hoàn chỉnh nhất vì các phương thức còn lại (mới nhất đang là signTypedData_v4), vẫn chưa đạt tới trạng thái “final”, thậm chí còn chưa được “accepted” – EIP-712 vẫn chờ các todos như:

  • Finalize specification of structure hashing
  • Domain separators
  • Add test vectors
  • Review specification
  • Implement eth_signTypedData in major RPC providers.
  • Implement web3.eth.signTypedData in Web3 providers.
  • Implement keccak256 struct hashing in Solidity.

Cách thức generate ra thông điệp của personal_sign có thể tóm gọn như sau:

keccak256("\x19Ethereum Signed Message:\n32" + keccak256(message)")
// Là hàm quy ước trong Ethereum
// Trong đó, "Ethereum Signed Message" là fixed string theo quy ước.
// "32", 32 luôn là fixed length của keccak256(message)
// keccak256 là thuật toán hash từ bytestrings 𝔹⁸ⁿ, tới 256-bit strings 𝔹²⁵⁶.

Validations

#1 Solidity:

ECDSA signatures được cấu thành gồm 3 tham số r, s, v – v: là giá trị Ethereum thêm vào – cũng là 3 giá trị đầu vào của hàm ecrecover mà Solidity đã cung cấp sẵn để extract ra address của người kí. Nếu kết quả address nhận về trùng với address của người đang gửi thì đó là thông điệp chính xác và toàn vẹn (không bị thay đổi).

Ta sẽ cần viết mã để validate ECDSA signature phù hợp với quy ước (1) ở trên.

Bởi vì là pure function nên user sẽ không tốn phí gas khi call lên on-chain.

validate.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

contract SignatureValidator {

    function validate(address signer, bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (bool) {
        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 prefixedHashMessage = keccak256(abi.encodePacked(prefix, hash));
        // Return true if the signer's address equals the recovered address from hashes.
        return signer == ecrecover(prefixedHashMessage, v, r, s);
    }
}

Để ý hàm validate() require params v, r, s nên ta cần đưa hàm tách v, r, s từ client

// Using ethereumjs-util
const signatureParams = ethereumjs-util.fromRpcSig(signature)
console.log(
    signatureParams.v,
    signatureParams.r,
    signatureParams.s)
// --------
// Or, split signature
const r = signature.slice(0, 66)
const s = "0x" + signature.slice(66, 130)
const v = parseInt(signature.slice(130, 132), 16)
console.log(r, s, v)

#2 Javascript/nodejs:

validate.ts

import {
    toBuffer,
    fromUtf8,
    hashPersonalMessage,
    fromRpcSig,
    ecrecover,
    publicToAddress,
    bufferToHex,
} from 'ethereumjs-util'

export function validate(
    address,
    signedMessage,
    msg
) {
    // We now are in possession of msg, publicAddress and signature. We
    // can perform an elliptic curve signature verification with ecrecover
    const msgBuffer = toBuffer(fromUtf8(msg))
    const msgHash = hashPersonalMessage(msgBuffer)
    const signatureParams = fromRpcSig(signedMessage)
    const publicKey = ecrecover(
        msgHash,
        signatureParams.v,
        signatureParams.r,
        signatureParams.s
    )
    const addressBuffer = publicToAddress(publicKey)
    const _address = bufferToHex(addressBuffer)

    // The signature verification is successful if the address found with
    // ecrecover matches the initial publicAddress
    if (_address.toLowerCase() === address.toLowerCase()) {
    }
        // Validated
    else {
        // Signature verification failed
    }
}

Metamask + firebase anonymous login == Passwordless Authentication & Authorization Flows

Authentication & Authorization (Xác thực & Phân quyền)

Xác thực là để định danh ai-là-ai, còn phân quyền là quy trình thẩm tra một user (đã được định danh) có quyền hạn thực thi tác vụ nào đó hay không.

Firebase hỗ trợ 1 cách thức login không danh tính (anonymous-auth), ta sẽ kết hợp dịch vụ này để xây dựng bộ authentication (metamask-firebase) + authorization (firebase). Nghĩa là metamask sẽ đảm bảo tính xác thực của người dùng hiện tại còn firebase sẽ quyết định việc có nên mở quyền access tới protected resources cho người dùng hiện hữu hay không.

Vì firebase là nền web-base Backend-as-a-Service cho nên ta sẽ chỉ inject vào validate.ts (off-chain application). Sau khi user đã được chứng thực, app sẽ cần firebase khởi tạo một JSON Web Tokens, dựa vào đó để authorize user với các quyền truy cập nhất định vào các dịch vụ của firebase, chẳng hạn như truy vấn data.

const userId = address // This is a user address that has been recovered during "validate.ts" process
const additionalClaims = {
  premiumAccount: true, // Authorize user to use all the services.
}

getAuth()
  .createCustomToken(userId, additionalClaims)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error)
  })

P/s: Phút 93 MUN – LEI vẫn đang 1-1.

Leave a comment