HBSig

HyperBEAM / AO-Core requires complex encoding and signing involving multiple codecs and HTTP Message Signatures. However, it is not 100% compatible with the standard http-message-signatures libraries, and aoconnect only provides basic encoding for AOS messages.
hbsig handles encoding of arbitrarily complex objects, which works on HyperBEAM. It is built by emulating the HyperBEAM codec devices as well as creating workaround encoding strategies using a custom device for extensive tests with LLMs.
Installation
hbsig is a standalone package providing utility methods for HyperBEAM codecs, signatures, encoding and hash algorithms.
yarn add hbsigSign Message
createSigner
import { createSigner } from "hbsig"
const hyperbeam_url = "http://localhost:10001"
const sign = createSigner(jwk, hyperbeam_url)Sign and Send Message
hbsig can encode and sign almost any complex objects combining layers of HyperBEAM codecs and http-message-signatures. While there are a few edge cases where HyperBEAM's decoder has limitations, hbsig intelligently provides workarounds for many of these scenarios. The encoding strategies were refined through extensive battle-testing with LLMs.
import { send } from "hbsig"
const msg = { str: "abc", num: 123, bin: Buffer.from([1,2,3]) }
const signed = await sign({ path: "/~hbsig@1.0/msg2", ...msg })
const { out } = await send(signed)Exclude @path
@path is automatically included in the signed components. To exclude @path, set path=false in the 2nd argument. There are some cases you want to exclude @path due to HyperBEAM's non-standard handing of the path field. HyperBEAM strips off the leading / from path, and also removes @ from the field name in the signed body, which might not be compatible with the Http Message Signatures standard, and invalidates signatures in some scenarios.
const msg = await sign(
{ path: "/~hbsig@1.0/msg2", key: "value" },
{ path: false }
)Verify Message
You can verify signed messages while decoding signature-input.
import { verify } from "hbsig"
const {
valid, // should be true
verified,
signatureName,
keyId,
algorithm,
decodedSignatureInput : { components, params: { alg, keyid, tag }, raw }
} = await verify(signed)Commitments
You can sign a message and create commitments with the signature.
import { commit } from "hbsig"
const committed = await commit({path, ...msg}, { signer: sign })Commit IDs
A commitment needs to include two IDs, which are
- sha256 hash of the signature
- hmac-sha256 hash of the signed components
You can explicitly get these IDs from a signed message.
import { rsaid, hmacid } from "hbsig"
const hmac_id = hmacid(signed.headers)
const rsa_id = rsaid(signed.headers)Message ID
You can calculate the ID from committed message.
import { id } from "hbsig"
const msg_id = id(committed)Hashpath
You can calculate the next hashpath from the current hashpath and a new message.
import { hashpath } from "hbsig"
const next_hashpath = hashpath(current_hashpath, committed)base
A hashpath consists of the hash of the current hashpath and the new message ID joined by /. You can independently calculate the base hash with base.
import { base, id } from "hbsig"
const next_hashpath = `${base(current_hashpath)}/${id(committed)}`Utilities
toAddr
Synchronously calculate Arweave address from a public key. arweave.js provides only asynchronous method for this. You can extract a public key from jwk.n as well as verify(signed), which gives you keyId from signature-input.
import { toAddr } from "hbsig"
const address = toAddr(jwk.n) // toAddr(jwk) works tooCodecs
hbsig internally handles many different representations of the same object using multiple codecs from HyperBEAM (flat structured httpsig) and 2 added codecs to achieve seamless data exchange between JS and Erlang (erljson erlstr).
hbsig@1.0 Device
hbsig comes with an accompanying device (hbsig@1.0) on HyperBEAM to validate various encoding strategies.
git clone https://github.com/weavedb/wao.git && cd wao
git submodule update --init --recursivejson_to_erl/3: convert stringified JSON to an Erlang objectto_erl/1: convert stringified JSON inbodyto an Erlang objectto_str/1: convert an Erlang object to ErlStrstructured_from/3: exposestructured@1.0:fromstructured_to/3: exposestructured@1.0:tohttpsig_from/3: exposehttpsig@1.0:fromhttpsig_to/3: exposehttpsig@1.0:toflat_from/3: exposeflat@1.0:fromflat_to/3: exposeflat@1.0:tomsg2/3: returnMsg2as ErlStr so we can check the decoded message
ErlJSON
JSON and erlang objects have different types such as null, boolean, and atom. ErlJSON normalized JSON objects to Erlang compatible structures.
import { normalize, erl_json_from, erl_json_to } from "hbsig"ErlStr
Erlang doesn't differentiate between strings and buffers, and the built-in format method loses precision when converting binary data containing non-standard characters. To address this, ErlStr maintains both binary and stringified formats for accurate Erlang-to-JSON conversion.
import { erl_str_from, erl_str_to } from "hbsig"ErlJSON and ErlStr enable seamless conversions between JSON and Erlang objects, allowing precise encoding tests across both environments.
flat
import { flat_from, flat_to } from "hbsig"structured
import { structured_from, structured_to } from "hbsig"httpsig
import { httpsig_from, httpsig_to } from "hbsig"Example Test
You can find comprehensive encoding tests here.
import { structured_from, structured_to } from "../src/structured.js"
import { normalize, erl_json_to } from "../src/erl_json.js"
import { httpsig_from, httpsig_to } from "../src/httpsig.js"
describe(desc, function () {
let hbeam, sign
before(async () => {
hbeam = await new HyperBEAM({ reset: true }).ready()
sign = createSigner(hbeam.jwk, hbeam.url)
})
after(async () => hbeam.kill())
it("should validate", async ()=>{
const msg = { str: "abc", num: 123, bin: Buffer.from([1, 2, 3]) }
const structured = structured_from(normalize(msg))
const json = erl_json_to(structured)
const signed = await sign({
path: "/~hbsig@1.0/httpsig_to",
body: JSON.stringify(json)
})
const { out } = await send(signed)
const input = httpsig_to(normalize(structured))
const output = erl_str_from(out)
const expected = normalize(input, true)
const output_b = erl_str_from(out, true) // true for binary format
assert.deepEqual(expected, output_b) // compare in binary format
})
})To run all tests, you need to clone wao branch of HyperBEAM, compile it, add .wallet.json, and set CWD in .env.hyperbeam.
yarn test-all