From 84af64921ac0cb33637820fa990f423dc72e14c4 Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:55:54 +0300 Subject: [PATCH 1/3] contracts: implement HMAC SHA512-256 as it's used throughout the Oasis ecosystem --- contracts/contracts/HMAC_sha512_256.sol | 70 +++++++++++++++++++++++++ contracts/contracts/tests/HashTests.sol | 9 ++++ contracts/package.json | 3 +- contracts/test/hashes.ts | 28 ++++++---- 4 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 contracts/contracts/HMAC_sha512_256.sol diff --git a/contracts/contracts/HMAC_sha512_256.sol b/contracts/contracts/HMAC_sha512_256.sol new file mode 100644 index 00000000..69c7b99b --- /dev/null +++ b/contracts/contracts/HMAC_sha512_256.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import {sha512_256} from "./Sapphire.sol"; + +// Note that the SHA512_256 block size is 128 bytes, while the output is 32 bytes +uint256 constant SHA512_256_BLOCK_SIZE = 128; + +// We don't (yet) have the MCOPY opcode, so use the IDENTITY precompile +uint256 constant PRECOMPILE_IDENTITY_ADDRESS = 0x4; + +// HMAC block-sized inner padding +bytes32 constant HMAC_IPAD = 0x3636363636363636363636363636363636363636363636363636363636363636; + +// OPAD ^ IPAD, (OPAD = 0x5c) +bytes32 constant HMAC_OPAD_XOR_IPAD = 0x6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a; + +/** + * @notice Implements HMAC using SHA512-256. + * @dev https://en.wikipedia.org/wiki/HMAC + * @param key the secret key. + * @param message the message to be authenticated. + * + * #### Example + * + * ```solidity + * bytes memory key = "arbitrary length key"; + * bytes memory message = "arbitrary length message"; + * bytes32 hmac = HMAC_sha512_256(key, message) + * ``` + */ +function HMAC_sha512_256(bytes memory key, bytes memory message) + view + returns (bytes32) +{ + bytes32[4] memory buf; + + if (key.length > SHA512_256_BLOCK_SIZE) { + buf[0] = sha512_256(key); + } else { + bool success; + + assembly { + let size := mload(key) + success := staticcall( + gas(), + PRECOMPILE_IDENTITY_ADDRESS, + add(32, key), // Skip uint256 length prefix of key bytes + size, + buf, + size + ) + } + + require(success, "memcpy"); + } + + for (uint256 i = 0; i < buf.length; i++) { + buf[i] ^= HMAC_IPAD; + } + + bytes32 ihash = sha512_256(abi.encodePacked(buf, message)); + + for (uint256 i = 0; i < buf.length; i++) { + buf[i] ^= HMAC_OPAD_XOR_IPAD; + } + + return sha512_256(abi.encodePacked(buf, ihash)); +} diff --git a/contracts/contracts/tests/HashTests.sol b/contracts/contracts/tests/HashTests.sol index a20e8e01..6bf092d9 100644 --- a/contracts/contracts/tests/HashTests.sol +++ b/contracts/contracts/tests/HashTests.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import {sha512, sha512_256, sha384} from "../Sapphire.sol"; +import {HMAC_sha512_256} from "../HMAC_sha512_256.sol"; contract HashTests { function testSHA512(bytes memory data) @@ -24,4 +25,12 @@ contract HashTests { function testSHA512_256(bytes memory data) external view returns (bytes32) { return sha512_256(data); } + + function testHMAC_SHA512_256(bytes memory key, bytes memory data) + external + view + returns (bytes32) + { + return HMAC_sha512_256(key, data); + } } diff --git a/contracts/package.json b/contracts/package.json index 8eed07e7..15239410 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -53,7 +53,8 @@ "solidity-coverage": "^0.8.2", "ts-node": "^10.9.1", "typechain": "^8.3.2", - "typescript": "^4.8.3" + "typescript": "^4.8.3", + "@noble/hashes": "1.3.2" }, "dependencies": { "@openzeppelin/contracts": "^5.0.2" diff --git a/contracts/test/hashes.ts b/contracts/test/hashes.ts index 9ba0da21..17409265 100644 --- a/contracts/test/hashes.ts +++ b/contracts/test/hashes.ts @@ -2,21 +2,17 @@ import { expect } from 'chai'; import { randomBytes, createHash } from 'crypto'; import { ethers } from 'hardhat'; import { HashTests } from '../typechain-types/contracts/tests/HashTests'; -import { HashTests__factory } from '../typechain-types/factories/contracts/tests'; -import { BytesLike, Overrides } from 'ethers'; +import { BytesLike, hexlify, Overrides } from 'ethers'; +import { sha512_256 } from '@noble/hashes/sha512'; +import { hmac } from '@noble/hashes/hmac'; -type HasherTestT = ( - data: BytesLike, - overrides?: Overrides | undefined, -) => Promise; +type HasherTestT = (data: BytesLike, overrides?: Overrides) => Promise; describe('Hashes', () => { let contract: HashTests; before(async () => { - const factory = (await ethers.getContractFactory( - 'HashTests', - )) as HashTests__factory; + const factory = await ethers.getContractFactory('HashTests'); contract = await factory.deploy(); await contract.waitForDeployment(); }); @@ -41,4 +37,18 @@ describe('Hashes', () => { it('SHA384', async () => { await testHashes('SHA384', contract.testSHA384.bind(contract)); }); + + it('HMAC SHA512-256', async () => { + for (let i = 0; i < 1024; i = i + (1 + i / 5)) { + const key = randomBytes(i); + for (let j = 0; j < 1024; j = j + (1 + j / 5)) { + const msg = randomBytes(j); + const expected = new Uint8Array( + hmac.create(sha512_256, key).update(msg).digest().buffer, + ); + const actual = await contract.testHMAC_SHA512_256(key, msg); + expect(hexlify(actual)).eq(hexlify(expected)); + } + } + }); }); From 82dc83e1f9850f542feed25f613367a128911add Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:19:38 +0300 Subject: [PATCH 2/3] contracts: improve documentation comments in HMAC_sha512_256.sol function --- contracts/contracts/HMAC_sha512_256.sol | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/HMAC_sha512_256.sol b/contracts/contracts/HMAC_sha512_256.sol index 69c7b99b..119f9070 100644 --- a/contracts/contracts/HMAC_sha512_256.sol +++ b/contracts/contracts/HMAC_sha512_256.sol @@ -34,25 +34,33 @@ function HMAC_sha512_256(bytes memory key, bytes memory message) view returns (bytes32) { + // Declare a memory array of 4 elements, each element is a bytes32 bytes32[4] memory buf; + // If the key length is greater than the SHA512_256_BLOCK_SIZE constant if (key.length > SHA512_256_BLOCK_SIZE) { + // Hash the key using SHA512-256 and store the result in the first element of buf buf[0] = sha512_256(key); } else { + // If the key is not longer than the block size, we'll copy it directly bool success; + // Use inline assembly for low-level operations assembly { + // Get the length of the key let size := mload(key) + // Call the identity precompile to copy memory success := staticcall( - gas(), - PRECOMPILE_IDENTITY_ADDRESS, - add(32, key), // Skip uint256 length prefix of key bytes - size, - buf, - size + gas(), // Forward all available gas + PRECOMPILE_IDENTITY_ADDRESS, // Address of the identity precompile + add(32, key), // Start of the key data (skip the length prefix) + size, // Length of data to copy + buf, // Destination to copy to + size // Amount of memory to copy ) } + // Ensure the memory copy was successful require(success, "memcpy"); } From 60ec07dc339549ede8a34d93e8b8c22a07a01938 Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:26:28 +0300 Subject: [PATCH 3/3] contracts: incorporate feedback from matevz & bernhard for HMAC SHA512-256 --- ...MAC_sha512_256.sol => hmac_sha512_256.sol} | 52 +++++++++---------- contracts/contracts/tests/HashTests.sol | 4 +- 2 files changed, 28 insertions(+), 28 deletions(-) rename contracts/contracts/{HMAC_sha512_256.sol => hmac_sha512_256.sol} (50%) diff --git a/contracts/contracts/HMAC_sha512_256.sol b/contracts/contracts/hmac_sha512_256.sol similarity index 50% rename from contracts/contracts/HMAC_sha512_256.sol rename to contracts/contracts/hmac_sha512_256.sol index 119f9070..0e290fc2 100644 --- a/contracts/contracts/HMAC_sha512_256.sol +++ b/contracts/contracts/hmac_sha512_256.sol @@ -8,7 +8,7 @@ import {sha512_256} from "./Sapphire.sol"; uint256 constant SHA512_256_BLOCK_SIZE = 128; // We don't (yet) have the MCOPY opcode, so use the IDENTITY precompile -uint256 constant PRECOMPILE_IDENTITY_ADDRESS = 0x4; +uint256 constant PRECOMPILE_IDENTITY_ADDRESS = 4; // HMAC block-sized inner padding bytes32 constant HMAC_IPAD = 0x3636363636363636363636363636363636363636363636363636363636363636; @@ -16,6 +16,9 @@ bytes32 constant HMAC_IPAD = 0x3636363636363636363636363636363636363636363636363 // OPAD ^ IPAD, (OPAD = 0x5c) bytes32 constant HMAC_OPAD_XOR_IPAD = 0x6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a; +/// Copying key buffer failed (identity precompile error?) +error hmac_sha512_256_memcpy(); + /** * @notice Implements HMAC using SHA512-256. * @dev https://en.wikipedia.org/wiki/HMAC @@ -27,52 +30,49 @@ bytes32 constant HMAC_OPAD_XOR_IPAD = 0x6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a * ```solidity * bytes memory key = "arbitrary length key"; * bytes memory message = "arbitrary length message"; - * bytes32 hmac = HMAC_sha512_256(key, message) + * bytes32 hmac = hmac_sha512_256(key, message) * ``` */ -function HMAC_sha512_256(bytes memory key, bytes memory message) +function hmac_sha512_256(bytes memory key, bytes memory message) view returns (bytes32) { - // Declare a memory array of 4 elements, each element is a bytes32 + // Buffer is SHA512_256_BLOCK_SIZE bytes bytes32[4] memory buf; - // If the key length is greater than the SHA512_256_BLOCK_SIZE constant + // Key is hashed if longer than SHA512_256_BLOCK_SIZE + // Otherwise, copy into block buffer using the identity precompile if (key.length > SHA512_256_BLOCK_SIZE) { - // Hash the key using SHA512-256 and store the result in the first element of buf buf[0] = sha512_256(key); } else { - // If the key is not longer than the block size, we'll copy it directly bool success; - - // Use inline assembly for low-level operations assembly { - // Get the length of the key let size := mload(key) - // Call the identity precompile to copy memory success := staticcall( - gas(), // Forward all available gas - PRECOMPILE_IDENTITY_ADDRESS, // Address of the identity precompile - add(32, key), // Start of the key data (skip the length prefix) - size, // Length of data to copy - buf, // Destination to copy to - size // Amount of memory to copy + gas(), + PRECOMPILE_IDENTITY_ADDRESS, + add(32, key), // Skip 32 bytes for the key length + size, + buf, + size ) } - - // Ensure the memory copy was successful - require(success, "memcpy"); + if (!success) { + revert hmac_sha512_256_memcpy(); + } } - for (uint256 i = 0; i < buf.length; i++) { - buf[i] ^= HMAC_IPAD; - } + buf[0] ^= HMAC_IPAD; + buf[1] ^= HMAC_IPAD; + buf[2] ^= HMAC_IPAD; + buf[3] ^= HMAC_IPAD; bytes32 ihash = sha512_256(abi.encodePacked(buf, message)); - for (uint256 i = 0; i < buf.length; i++) { - buf[i] ^= HMAC_OPAD_XOR_IPAD; - } + buf[0] ^= HMAC_OPAD_XOR_IPAD; + buf[1] ^= HMAC_OPAD_XOR_IPAD; + buf[2] ^= HMAC_OPAD_XOR_IPAD; + buf[3] ^= HMAC_OPAD_XOR_IPAD; return sha512_256(abi.encodePacked(buf, ihash)); } diff --git a/contracts/contracts/tests/HashTests.sol b/contracts/contracts/tests/HashTests.sol index 6bf092d9..c98ab770 100644 --- a/contracts/contracts/tests/HashTests.sol +++ b/contracts/contracts/tests/HashTests.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {sha512, sha512_256, sha384} from "../Sapphire.sol"; -import {HMAC_sha512_256} from "../HMAC_sha512_256.sol"; +import {hmac_sha512_256} from "../hmac_sha512_256.sol"; contract HashTests { function testSHA512(bytes memory data) @@ -31,6 +31,6 @@ contract HashTests { view returns (bytes32) { - return HMAC_sha512_256(key, data); + return hmac_sha512_256(key, data); } }