-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
sapphire-contracts: Add SIWE parser and tooling
- Loading branch information
Showing
12 changed files
with
847 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.