Skip to content

Commit

Permalink
Merge pull request #439 from oasisprotocol/CedarMist/1-of-many-PRs-fo…
Browse files Browse the repository at this point in the history
…r-matevz-HMAC-SHA512-256

contracts: implement HMAC SHA512-256, as it's used throughout the Oasis ecosystem
  • Loading branch information
CedarMist authored Oct 23, 2024
2 parents b787929 + 60ec07d commit 77c1abe
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 10 deletions.
78 changes: 78 additions & 0 deletions contracts/contracts/hmac_sha512_256.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 = 4;

// HMAC block-sized inner padding
bytes32 constant HMAC_IPAD = 0x3636363636363636363636363636363636363636363636363636363636363636;

// 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
* @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)
{
// Buffer is SHA512_256_BLOCK_SIZE bytes
bytes32[4] memory buf;

// 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) {
buf[0] = sha512_256(key);
} else {
bool success;
assembly {
let size := mload(key)
success := staticcall(
gas(),
PRECOMPILE_IDENTITY_ADDRESS,
add(32, key), // Skip 32 bytes for the key length
size,
buf,
size
)
}
if (!success) {
revert hmac_sha512_256_memcpy();
}
}

buf[0] ^= HMAC_IPAD;
buf[1] ^= HMAC_IPAD;
buf[2] ^= HMAC_IPAD;
buf[3] ^= HMAC_IPAD;

bytes32 ihash = sha512_256(abi.encodePacked(buf, message));

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));
}
9 changes: 9 additions & 0 deletions contracts/contracts/tests/HashTests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
}
3 changes: 2 additions & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
28 changes: 19 additions & 9 deletions contracts/test/hashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
type HasherTestT = (data: BytesLike, overrides?: Overrides) => Promise<string>;

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();
});
Expand All @@ -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));
}
}
});
});

0 comments on commit 77c1abe

Please sign in to comment.