Skip to content

Commit

Permalink
sapphire-contracts: Add SIWE parser and tooling
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Jul 12, 2024
1 parent 52141ea commit 9411e94
Show file tree
Hide file tree
Showing 12 changed files with 847 additions and 4 deletions.
82 changes: 82 additions & 0 deletions contracts/contracts/DateTime.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* @title Utilities for managing the timestamps
* @notice Considers leap year, but not leap second.
* @custom:attribution https://github.com/pipermerriam/ethereum-datetime/blob/master/contracts/DateTime.sol
*/
library DateTime {
uint16 constant ORIGIN_YEAR = 1970;

function isLeapYear(uint16 year) internal pure returns (bool) {
if (year % 4 != 0) {
return false;
}
if (year % 100 != 0) {
return true;
}
if (year % 400 != 0) {
return false;
}
return true;
}

/**
* @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;

// Year
// TODO: Rewrite to O(1) time implementation.
for (i = ORIGIN_YEAR; i < year; i++) {
if (isLeapYear(i)) {
timestamp += 366 days;
}
else {
timestamp += 365 days;
}
}

// Month
// TODO: Use constants for monthDayCounts, rewrite to O(1) time implementation.
uint32[12] memory monthDayCounts;
monthDayCounts[0] = 31;
if (isLeapYear(year)) {
monthDayCounts[1] = 29;
}
else {
monthDayCounts[1] = 28;
}
monthDayCounts[2] = 31;
monthDayCounts[3] = 30;
monthDayCounts[4] = 31;
monthDayCounts[5] = 30;
monthDayCounts[6] = 31;
monthDayCounts[7] = 31;
monthDayCounts[8] = 30;
monthDayCounts[9] = 31;
monthDayCounts[10] = 30;
monthDayCounts[11] = 31;

for (i = 1; i < month; i++) {
timestamp += monthDayCounts[i - 1] * 1 days;
}

// Day
timestamp += uint32(day - 1) * 1 days;

// Hour
timestamp += uint32(hour) * 1 hours;

// Minute
timestamp += uint16(minute) * 1 minutes;

// Second
timestamp += second;

return timestamp;
}
}
213 changes: 213 additions & 0 deletions contracts/contracts/SiweParser.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {DateTime} from "./DateTime.sol";

struct ParsedSiweMessage {
bytes schemeDomain;
address addr;
bytes statement;
bytes uri;
bytes version;
uint chainId;
bytes nonce;
bytes issuedAt;
bytes expirationTime;
bytes notBefore;
bytes requestId;
bytes[] resources;
}

/**
* @title On-chain parser for EIP-4361 SIWE message
* @notice Call parseSiweMsg() and provide the EIP-4361 SIWE message. The parser
* will generate the ParsedSiweMessage struct which you can then use to
* extract the authentication information in your on-chain contract.
*/
library SiweParser {
/**
* @notice Convert string containing hex address without 0x prefix to solidity address object.
*/
function _hexStringToAddress(bytes memory s) internal pure returns (address) {
require(s.length == 40, "Invalid address length");
bytes memory r = new bytes(s.length/2);
for (uint i=0; i<s.length/2; ++i) {
r[i] = bytes1(_fromHexChar(uint8(s[2*i])) * 16 + _fromHexChar(uint8(s[2*i+1])));
}
return address(bytes20(r));
}

function _fromHexChar(uint8 c) internal pure returns (uint8) {
if (bytes1(c) >= bytes1('0') && bytes1(c) <= bytes1('9')) {
return c - uint8(bytes1('0'));
}
if (bytes1(c) >= bytes1('a') && bytes1(c) <= bytes1('f')) {
return 10 + c - uint8(bytes1('a'));
}
if (bytes1(c) >= bytes1('A') && bytes1(c) <= bytes1('F')) {
return 10 + c - uint8(bytes1('A'));
}
return 0;
}

/**
* @notice Substring.
*/
function _substr(bytes memory str, uint startIndex, uint endIndex) internal pure returns (bytes memory) {
bytes memory result = new bytes(endIndex-startIndex);
for(uint i = startIndex; i < endIndex && i<str.length; i++) {
result[i-startIndex] = str[i];
}
return result;
}

/**
* @notice String to Uint using decimal format. No error handling.
*/
function _parseUint(bytes memory b) internal pure returns (uint) {
uint result = 0;
for (uint i = 0; i < b.length; i++) {
result = result * 10 + (uint(uint8(b[i])) - 0x30);
}
return (result);
}


/**
* @notice Parse "NAME: VALUE" in str starting at index i and ending at \n or end of bytes.
* @return VALUE and new i, if NAME matched; otherwise empty value and old i.
*/
function _parseField(bytes calldata str, string memory name, uint i) internal pure returns (bytes memory value, uint) {
uint j=i;
for (; j<str.length; j++) {
if (str[j]==':') {
// Delimiter found, check the name.
if (keccak256(_substr(str, i, j))!=keccak256(bytes(name))) {
return ("", i);
}

// Skip :
j++;
if (j<str.length && str[j]==' ') {
// Skip blank
j++;
}

i=j;
break;
}
}

for (; j<str.length; j++) {
if (str[j]==0x0a) {
return (_substr(str, i, j), j+1);
}
}
return (_substr(str, i, j), j);
}

/**
* @notice Parse bullets, one per line in str starting at i.
* @return Array of parsed values and a new i.
*/
function _parseArray(bytes calldata str, uint i) internal pure returns (bytes[] memory, uint) {
// First count the number of resources.
uint j=i;
uint count=0;
for (; j<str.length-1; j++) {
if (str[j]=='-' && str[j+1]==' ') {
j+=2;
count++;
} else {
break;
}
for (; j<str.length && str[j]!=0x0a; j++) {}
}

// Then build an array.
bytes[] memory values = new bytes[](count);
j=i;
for (uint c=0; j<str.length-1 && c!=count; j++) {
if (str[j]=='-' && str[j+1]==' ') {
i=j+2;
}
for (; j<str.length && str[j]!=0x0a; j++) {}
values[c] = _substr(str, i, j);
c++;
if (j==str.length) {
j--; // Subtract 1 because of the outer loop.
}
}
return (values, j);
}

/**
* @notice Parse SIWE message.
* @return ParsedSiweMessage struct with populated fields from the message.
*/
function parseSiweMsg(bytes calldata siweMsg) internal pure returns (ParsedSiweMessage memory) {
ParsedSiweMessage memory p;
uint i=0;

// dApp Domain.
for (; i<siweMsg.length; i++) {
if (siweMsg[i]==' ') {
p.schemeDomain = _substr(siweMsg, 0, i);
break;
}
}

i += 50; // " wants you to sign in with your Ethereum account:\n"

// Address.
p.addr = _hexStringToAddress(_substr(siweMsg, i+=2, i+=40));
i+=2; // End of address new line + New line.

// (Optional) statement.
if (i<siweMsg.length && siweMsg[i] != "\n") {
for (uint j=i; j<siweMsg.length; j++) {
if (siweMsg[j]==0x0a) {
p.statement = _substr(siweMsg, i, j);
i=j+1; // End of statement new line.
break;
}
}
}

i++; // New line.

(p.uri, i) = _parseField(siweMsg, "URI", i);
(p.version, i) = _parseField(siweMsg, "Version", i);
bytes memory chainId;
(chainId, i) = _parseField(siweMsg, "Chain ID", i);
p.chainId = _parseUint(chainId);
(p.nonce, i) = _parseField(siweMsg, "Nonce", i);
(p.issuedAt, i) = _parseField(siweMsg, "Issued At", i);
(p.expirationTime, i) = _parseField(siweMsg, "Expiration Time", i);
(p.notBefore, i) = _parseField(siweMsg, "Not Before", i);
(p.requestId, i) = _parseField(siweMsg, "Request ID", i);

// Parse resources, if they exist.
uint newI;
(, newI) = _parseField(siweMsg, "Resources", i);
if (newI!=i) {
(p.resources, i) = _parseArray(siweMsg, newI);
}

return p;
}

/**
* @notice Parse RFC 3339 (ISO 8601) string to timestamp.
*/
function timestampFromIso(bytes memory str) internal pure returns (uint256) {
return DateTime.toTimestamp(
uint16(_parseUint(_substr(str, 0, 4))),
uint8(_parseUint(_substr(str, 5, 7))),
uint8(_parseUint(_substr(str, 8, 10))),
uint8(_parseUint(_substr(str, 11, 13))),
uint8(_parseUint(_substr(str, 14, 16))),
uint8(_parseUint(_substr(str, 17, 19)))
);
}
}
24 changes: 24 additions & 0 deletions contracts/contracts/auth/A13e.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../EthereumUtils.sol";

/**
* @title Interface for authenticatable contracts
* @notice The interface for universal authentication mechanism (e.g. SIWE and
* others). First, the user-facing app should call login() to generate on-chain
* bearer token. Then, the smart contract methods that require authentication
* accept this token and pass it to authMsgSender() to verify it and obtain the
* authenticated user address that can be used for the authorization.
*/
abstract contract A13e {
/**
* @notice Verify the login message and its signature and generate the bearer token.
*/
function login(string calldata message, SignatureRSV calldata sig) external virtual view returns (bytes memory);

/**
* @notice Validate the bearer token and return authenticated msg.sender.
*/
function authMsgSender(bytes calldata bearer) internal virtual view returns (address);
}
Loading

0 comments on commit 9411e94

Please sign in to comment.