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/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)); + } + } + }); });