Skip to content

Commit

Permalink
sapphire-contracts: SiweAuth tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Jul 12, 2024
1 parent 69cb603 commit c13b609
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 24 deletions.
6 changes: 3 additions & 3 deletions contracts/contracts/DateTime.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.0;

/**
* @title Utilities for managing the timestamps
* @notice Considers leap year, but not leap second. Original upstream
* @notice Considers leap year, but not leap second.
* @custom:attribution https://github.com/pipermerriam/ethereum-datetime/blob/master/contracts/DateTime.sol
*/
library DateTime {
Expand All @@ -23,8 +23,8 @@ library DateTime {
}

/**
* @notice Convert ISO-8601 date and time string to Unix timestamp.
* @dev No leap seconds are considered!
* @notice Convert year, month, day, hour, minute, second to Unix timestamp.
* @dev Leap second is not supported.
*/
function toTimestamp(uint16 year, uint8 month, uint8 day, uint8 hour, uint8 minute, uint8 second) internal pure returns (uint timestamp) {
uint16 i;
Expand Down
11 changes: 7 additions & 4 deletions contracts/contracts/auth/SiweAuth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ParsedSiweMessage, SiweParser } from "../SiweParser.sol";
import "../Sapphire.sol";

struct Bearer {
string domain;
string domain; // [ scheme "://" ] domain.
address userAddr;
uint256 validUntil; // in Unix timestamp.
}
Expand All @@ -17,7 +17,7 @@ contract SiweAuth is A13e {
string _domain;
bytes32 _bearerEncKey;

uint constant DEFAULT_VALIDITY=24*3600; // in seconds.
uint constant DEFAULT_VALIDITY=24 hours; // in seconds.

constructor(string memory in_domain) {
_bearerEncKey = bytes32(Sapphire.randomBytes(32, ""));
Expand All @@ -35,22 +35,25 @@ contract SiweAuth is A13e {
ParsedSiweMessage memory p = SiweParser.parseSiweMsg(bytes(siweMsg));

require(p.chainId==block.chainid, "chain ID mismatch");

require(keccak256(p.schemeDomain)==keccak256(bytes(_domain)), "domain mismatch");
b.domain = string(p.schemeDomain);

require(p.addr==addr, "SIWE address does not match signer's address");

uint expirationTime = SiweParser.timestampFromIso(p.expirationTime);
if (p.notBefore.length!=0) {
require(block.timestamp > SiweParser.timestampFromIso(p.notBefore), "not before not reached yet");
}
require(block.timestamp < expirationTime, "expired");

if (p.expirationTime.length!=0) {
// Compute expected block number at expiration time.
uint expirationTime = SiweParser.timestampFromIso(p.expirationTime);
b.validUntil = expirationTime;
} else {
// Otherwise, just take the default validity.
b.validUntil = block.timestamp + DEFAULT_VALIDITY;
}
require(block.timestamp < b.validUntil, "expired");

bytes memory encB = Sapphire.encrypt(_bearerEncKey, 0, abi.encode(b), "");
return encB;
Expand Down
55 changes: 38 additions & 17 deletions contracts/test/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

import { expect } from 'chai';
import { config, ethers } from 'hardhat';
import { Signer } from "ethers";
import { SiweMessage } from 'siwe';
import "@nomicfoundation/hardhat-chai-matchers";

import { SiweAuthTests__factory } from '../typechain-types/factories/contracts/tests';
import { SiweAuthTests } from '../typechain-types/contracts/tests/auth/SiweAuthTests';
import { Signer } from "ethers";

describe('Auth', function () {
async function deploy() {
async function deploy(domain: string) {
const SiweAuthTests_factory = await ethers.getContractFactory("SiweAuthTests");
const siweAuthTests = await SiweAuthTests_factory.deploy("localhost");
const siweAuthTests = await SiweAuthTests_factory.deploy(domain);
await siweAuthTests.waitForDeployment();
return { siweAuthTests };
return siweAuthTests;
}

async function siweMsg(domain: string, signerIdx: number, expiration?: Date): Promise<string> {
Expand All @@ -34,54 +35,74 @@ describe('Auth', function () {
}

it("Should login", async function () {
const {siweAuthTests} = await deploy();
// Skip this test on non-sapphire chains.
// It require on-chain encryption and/or signing.
if ((await ethers.provider.getNetwork()).chainId == 31337n) {
this.skip();
}

const siweAuthTests = await deploy("localhost");

// Correct login.
const accounts = config.networks.hardhat.accounts;
const account = ethers.HDNodeWallet.fromMnemonic(ethers.Mnemonic.fromPhrase(accounts.mnemonic), accounts.path+'/0');
const siweStr = await siweMsg("localhost", 0);
const bearer = await siweAuthTests.testLogin(siweStr, await erc191sign(siweStr, account));
await expect(await siweAuthTests.testLogin(siweStr, await erc191sign(siweStr, account))).lengthOf.to.be.greaterThan(2); // Test if not 0x or empty.

// Wrong domain.
const siweStrWrongDomain = await siweMsg("localhost2", 0);
await expect(await siweAuthTests.testLogin(siweStrWrongDomain, await erc191sign(siweStrWrongDomain, account))).to.be.reverted;
await expect(siweAuthTests.testLogin(siweStrWrongDomain, await erc191sign(siweStrWrongDomain, account))).to.be.reverted;

// Mismatch signature based on the SIWE message.
const siweStrWrongSig = await siweMsg("localhost", 1);
await expect(await siweAuthTests.testLogin(siweStrWrongSig, await erc191sign(siweStrWrongSig, account))).to.be.reverted;
await expect(siweAuthTests.testLogin(siweStrWrongSig, await erc191sign(siweStrWrongSig, account))).to.be.reverted;

// Expired login.
let now = new Date();
const siweStrExpired = await siweMsg("localhost", 1, new Date(now.getFullYear(), now.getMonth()-1, 1));
await expect(await siweAuthTests.testLogin(siweStrExpired, await erc191sign(siweStrExpired, account))).to.be.reverted;
await expect(siweAuthTests.testLogin(siweStrExpired, await erc191sign(siweStrExpired, account))).to.be.reverted;
});

it("Should authenticate", async function () {
const {siweAuthTests} = await deploy();
it("Should call authenticated method", async function () {
// Skip this test on non-sapphire chains.
// It require on-chain encryption and/or signing.
if ((await ethers.provider.getNetwork()).chainId == 31337n) {
this.skip();
}

const siweAuthTests = await deploy("localhost");

// Author should read a very secret message.
const accounts = config.networks.hardhat.accounts;
const account = ethers.HDNodeWallet.fromMnemonic(ethers.Mnemonic.fromPhrase(accounts.mnemonic), accounts.path+'/0');
const siweStr = await siweMsg("localhost", 0);
const bearer = await siweAuthTests.testLogin(siweStr, await erc191sign(siweStr, account));
console.log("abc2");
expect(await siweAuthTests.testVerySecretMessage(bearer)).to.be.equal("Very secret message");
await expect(await siweAuthTests.testVerySecretMessage(bearer)).to.be.equal("Very secret message");

// Anyone else trying to read the very secret message should fail.
const acc2 = ethers.HDNodeWallet.fromMnemonic(ethers.Mnemonic.fromPhrase(accounts.mnemonic), accounts.path+'/1');
const siweStr2 = await siweMsg("localhost", 1);
const bearer2 = await siweAuthTests.testLogin(siweStr2, await erc191sign(siweStr2, acc2));
await expect(siweAuthTests.testVerySecretMessage(bearer2)).to.be.reverted;

// Same user, another domain.
// Same user, hijacked bearer from another contract/domain.
const siweAuthTests2 = await deploy("localhost2");
const siweStr3 = await siweMsg("localhost2", 0);
const bearer3 = await siweAuthTests.testLogin(siweStr3, await erc191sign(siweStr3, account));
const bearer3 = await siweAuthTests2.testLogin(siweStr3, await erc191sign(siweStr3, account));
await expect(siweAuthTests.testVerySecretMessage(bearer3)).to.be.reverted;

// Expired bearer.
const siweStr4 = await siweMsg("localhost", 0, new Date(Date.now() + 1000)); // Expire after 1 second.
const siweStr4 = await siweMsg("localhost", 0, new Date(Date.now()+10)); // Expire after 0.01 seconds.
const bearer4 = await siweAuthTests.testLogin(siweStr4, await erc191sign(siweStr4, account));
await new Promise(r => setTimeout(r, 1000));
// Wait for 2 blocks. The first block may still have a timestamp earlier than the expiration time.
await new Promise(async r => {
const desiredBlockNumber = await ethers.provider.getBlockNumber()+2;
ethers.provider.on("block", (blockNumber) => {
if (blockNumber == desiredBlockNumber) {
r();
}
})
});
await expect(siweAuthTests.testVerySecretMessage(bearer4)).to.be.reverted;
});

Expand Down

0 comments on commit c13b609

Please sign in to comment.