dev_codec_httpsig_proxy.erl - HTTP Signature HMAC Proxy Functions
Overview
Purpose: Proxy functions for HMAC commitments with custom device names
Module: dev_codec_httpsig_proxy
Pattern: Derive Secret → Sign with httpsig@1.0 → Relabel Device
Used By: cookie@1.0, http-auth@1.0
This utility module provides proxy functions for calling the httpsig@1.0 codec's HMAC commitment functions with secret keys while changing the commitment-device label. It enables other devices to leverage HTTPSig's HMAC signing infrastructure while maintaining their own device identities.
Standard Pattern
- Device verifies user request / derives secret key
- Device commits message with user's secret using
secret:[hash]scheme - Commitment modified to reference different
commitment-device - Verification uses
httpsig@1.0codec under-the-hood
Dependencies
- HyperBEAM:
hb_message,hb_maps,hb_util - Codecs:
dev_codec_httpsig(indirect via hb_message) - Testing:
eunit - Includes:
include/hb.hrl
Public Functions Overview
%% Proxy Commitment Functions
-spec commit(Device, Secret, Base, Req, Opts) -> {ok, SignedMsg}.
-spec verify(Secret, Base, Req, Opts) -> {ok, boolean()}.Public Functions
1. commit/5
-spec commit(Device, Secret, Base, Req, Opts) -> {ok, SignedMsg}
when
Device :: binary(),
Secret :: binary(),
Base :: map(),
Req :: map(),
Opts :: map(),
SignedMsg :: map().Description: Commit to a message with a given secret key, using HMAC-SHA256 via httpsig@1.0, then relabel the commitment-device to the specified device.
- Extract existing commitments from base message
- If commitments exist, use only committed portions
- Sign with
httpsig@1.0using HMAC-SHA256 and secret scheme - Extract the new commitment
- Change
commitment-deviceto specified device - Return message with relabeled commitment
-module(dev_codec_httpsig_proxy_commit_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
commit_basic_test() ->
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"data">> => <<"test">>},
Device = <<"custom-device@1.0">>,
{ok, Signed} = dev_codec_httpsig_proxy:commit(
Device,
Secret,
Base,
#{},
#{}
),
?assert(maps:is_key(<<"commitments">>, Signed)),
Commitments = maps:get(<<"commitments">>, Signed),
[CommID] = maps:keys(Commitments),
Commitment = maps:get(CommID, Commitments),
?assertEqual(Device, maps:get(<<"commitment-device">>, Commitment)).
commit_preserves_data_test() ->
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"key">> => <<"value">>, <<"num">> => <<"123">>},
{ok, Signed} = dev_codec_httpsig_proxy:commit(
<<"test@1.0">>,
Secret,
Base,
#{},
#{}
),
?assertEqual(<<"value">>, maps:get(<<"key">>, Signed)),
?assertEqual(<<"123">>, maps:get(<<"num">>, Signed)).
commit_with_existing_commitments_test() ->
% Test that commit produces proper commitment structure
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"data">> => <<"test">>},
{ok, Signed} = dev_codec_httpsig_proxy:commit(
<<"device@1.0">>,
Secret,
Base,
#{},
#{}
),
Commitments = maps:get(<<"commitments">>, Signed),
% Verify single commitment exists with correct device
?assertEqual(1, maps:size(Commitments)),
[CommID] = maps:keys(Commitments),
Commitment = maps:get(CommID, Commitments),
?assertEqual(<<"device@1.0">>, maps:get(<<"commitment-device">>, Commitment)).
commit_uses_secret_scheme_test() ->
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"data">> => <<"test">>},
{ok, Signed} = dev_codec_httpsig_proxy:commit(
<<"device@1.0">>,
Secret,
Base,
#{},
#{}
),
[CommID] = maps:keys(maps:get(<<"commitments">>, Signed)),
Commitment = maps:get(CommID, maps:get(<<"commitments">>, Signed)),
?assertEqual(<<"hmac-sha256">>, maps:get(<<"type">>, Commitment)),
KeyID = maps:get(<<"keyid">>, Commitment),
?assert(binary:match(KeyID, <<"secret:">>) =/= nomatch).2. verify/4
-spec verify(Secret, Base, Req, Opts) -> {ok, boolean()}
when
Secret :: binary(),
Base :: map(),
Req :: map(),
Opts :: map().Description: Verify a message commitment by using the httpsig@1.0 HMAC commitment scheme with the provided secret.
- Create proxy request with
httpsig@1.0as commitment device - Add secret to request
- Use
hb_message:verify/3to validate HMAC
-module(dev_codec_httpsig_proxy_verify_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
verify_valid_signature_test() ->
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"data">> => <<"test">>},
Device = <<"custom@1.0">>,
% Commit
{ok, Signed} = dev_codec_httpsig_proxy:commit(
Device,
Secret,
Base,
#{},
#{}
),
% Verify
[CommID] = maps:keys(maps:get(<<"commitments">>, Signed)),
Commitment = maps:get(CommID, maps:get(<<"commitments">>, Signed)),
{ok, IsValid} = dev_codec_httpsig_proxy:verify(
Secret,
Base,
Commitment,
#{}
),
?assert(IsValid).
verify_wrong_secret_test() ->
Secret1 = hb_util:encode(crypto:strong_rand_bytes(64)),
Secret2 = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"data">> => <<"test">>},
% Commit with Secret1
{ok, Signed1} = dev_codec_httpsig_proxy:commit(
<<"device@1.0">>,
Secret1,
Base,
#{},
#{}
),
% Commit with Secret2 (different)
{ok, Signed2} = dev_codec_httpsig_proxy:commit(
<<"device@1.0">>,
Secret2,
Base,
#{},
#{}
),
% Different secrets should produce different signatures
[CommID1] = maps:keys(maps:get(<<"commitments">>, Signed1)),
[CommID2] = maps:keys(maps:get(<<"commitments">>, Signed2)),
Sig1 = maps:get(<<"signature">>, maps:get(CommID1, maps:get(<<"commitments">>, Signed1))),
Sig2 = maps:get(<<"signature">>, maps:get(CommID2, maps:get(<<"commitments">>, Signed2))),
?assertNotEqual(Sig1, Sig2).
verify_tampered_message_test() ->
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base1 = #{<<"data">> => <<"original">>},
Base2 = #{<<"data">> => <<"modified">>},
% Commit both messages
{ok, Signed1} = dev_codec_httpsig_proxy:commit(
<<"device@1.0">>,
Secret,
Base1,
#{},
#{}
),
{ok, Signed2} = dev_codec_httpsig_proxy:commit(
<<"device@1.0">>,
Secret,
Base2,
#{},
#{}
),
% Different messages with same secret should produce different signatures
[CommID1] = maps:keys(maps:get(<<"commitments">>, Signed1)),
[CommID2] = maps:keys(maps:get(<<"commitments">>, Signed2)),
Sig1 = maps:get(<<"signature">>, maps:get(CommID1, maps:get(<<"commitments">>, Signed1))),
Sig2 = maps:get(<<"signature">>, maps:get(CommID2, maps:get(<<"commitments">>, Signed2))),
?assertNotEqual(Sig1, Sig2).
verify_roundtrip_test() ->
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{
<<"key1">> => <<"value1">>,
<<"key2">> => <<"value2">>
},
{ok, Signed} = dev_codec_httpsig_proxy:commit(
<<"test@1.0">>,
Secret,
Base,
#{},
#{}
),
[CommID] = maps:keys(maps:get(<<"commitments">>, Signed)),
Commitment = maps:get(CommID, maps:get(<<"commitments">>, Signed)),
{ok, true} = dev_codec_httpsig_proxy:verify(
Secret,
Base,
Commitment,
#{}
).Internal Processing
Commitment Isolation
When existing commitments are present:
ExistingComms = maps:get(<<"commitments">>, Base, #{}),
OnlyCommittedBase =
case map_size(ExistingComms) of
0 -> Base;
_ ->
hb_message:uncommitted(
hb_message:with_only_committed(Base, Opts),
Opts
)
endThis ensures that only committed portions are signed when adding new commitments.
Device Relabeling
After HTTPSig commitment:
ModCommittedMsg =
CommittedMsg#{
<<"commitments">> =>
ExistingComms#{
CommitmentID =>
Commitment#{
<<"commitment-device">> => Device
}
}
}The commitment-device is changed from httpsig@1.0 to the custom device.
Common Patterns
%% Cookie-based authentication commit
Secret = hb_util:encode(crypto:strong_rand_bytes(64)),
Base = #{<<"user-data">> => <<"sensitive">>},
{ok, Signed} = dev_codec_httpsig_proxy:commit(
<<"cookie@1.0">>,
Secret,
Base,
#{},
#{}
).
% Commitment device will be "cookie@1.0", not "httpsig@1.0"
%% HTTP Basic auth commit
DerivedSecret = derive_from_password(Username, Password),
Base = #{<<"action">> => <<"transfer">>},
{ok, Signed} = dev_codec_httpsig_proxy:commit(
<<"http-auth@1.0">>,
DerivedSecret,
Base,
#{},
#{}
).
%% Verify with same secret
[CommID] = maps:keys(maps:get(<<"commitments">>, Signed)),
Commitment = maps:get(CommID, maps:get(<<"commitments">>, Signed)),
{ok, true} = dev_codec_httpsig_proxy:verify(
Secret,
Base,
Commitment,
#{}
).
%% Stack multiple device commitments
{ok, WithCookie} = dev_codec_httpsig_proxy:commit(
<<"cookie@1.0">>,
CookieSecret,
Base,
#{},
#{}
),
{ok, WithAuth} = dev_codec_httpsig_proxy:commit(
<<"http-auth@1.0">>,
AuthSecret,
WithCookie,
#{},
#{}
).
% Result has two commitments with different devicesUse Cases
1. Cookie Authentication (cookie@1.0)
% Generate or retrieve cookie secret
{ok, Secret} = generate_cookie_secret(Request),
% Commit with cookie device
{ok, SignedMsg} = dev_codec_httpsig_proxy:commit(
<<"cookie@1.0">>,
Secret,
BaseMsg,
Request,
Opts
),
% Store secret in cookie
{ok, WithCookie} = store_secret_in_cookie(Secret, SignedMsg).2. HTTP Basic Auth (http-auth@1.0)
% Derive secret from credentials
{ok, DerivedKey} = pbkdf2(Username, Password, Salt, Iterations),
% Commit with HTTP auth device
{ok, SignedMsg} = dev_codec_httpsig_proxy:commit(
<<"http-auth@1.0">>,
DerivedKey,
BaseMsg,
Request,
Opts
).3. Custom Authentication Device
% Custom device implementation
-module(my_custom_auth).
commit(Base, Req, Opts) ->
% Derive or retrieve secret
{ok, Secret} = my_secret_derivation(Req),
% Use proxy to commit
dev_codec_httpsig_proxy:commit(
<<"my-auth@1.0">>,
Secret,
Base,
Req,
Opts
).
verify(Base, Req, Opts) ->
% Retrieve secret
{ok, Secret} = my_secret_retrieval(Req),
% Use proxy to verify
dev_codec_httpsig_proxy:verify(Secret, Base, Req, Opts).Commitment Structure
Before Relabeling
#{
<<"commitment-device">> => <<"httpsig@1.0">>,
<<"type">> => <<"hmac-sha256">>,
<<"scheme">> => <<"secret">>,
<<"keyid">> => <<"secret:abc123...">>,
<<"signature">> => <<"hmac_value">>,
<<"committed">> => [<<"key1">>, <<"key2">>]
}After Relabeling
#{
<<"commitment-device">> => <<"cookie@1.0">>, % Changed!
<<"type">> => <<"hmac-sha256">>,
<<"scheme">> => <<"secret">>,
<<"keyid">> => <<"secret:abc123...">>,
<<"signature">> => <<"hmac_value">>,
<<"committed">> => [<<"key1">>, <<"key2">>]
}Only the commitment-device field changes.
Proxy Request Structure
Commit Request
Req#{
<<"commitment-device">> => <<"httpsig@1.0">>,
<<"type">> => <<"hmac-sha256">>,
<<"scheme">> => <<"secret">>,
<<"secret">> => Secret
}Verify Request
RawReq#{
<<"commitment-device">> => <<"httpsig@1.0">>,
<<"path">> => <<"verify">>,
<<"secret">> => Secret
}Integration with hb_message
The module uses hb_message functions for the actual cryptographic operations:
% Commit
CommittedMsg = hb_message:commit(OnlyCommittedBase, Opts, ProxyReq),
% Get commitment
{ok, CommitmentID, Commitment} =
hb_message:commitment(Filter, CommittedMsg, Opts),
% Verify
{ok, IsValid} = hb_message:verify(Base, ProxyRequest, Opts).Error Handling
The module doesn't add additional error handling beyond what hb_message provides. Errors from the underlying HTTPSig commitment will propagate up.
- Invalid secret format
- Commitment verification failure
- Missing required fields in request
Security Considerations
- Secret Handling: Secrets should be derived securely (PBKDF2, etc.)
- Transport: Secrets transmitted in request should be over secure channel
- Storage: Never log or persist raw secrets
- Validation: Always verify commitments before trusting message content
- Scope: Each device should use unique secrets (no secret reuse)
References
- HTTPSig Module -
dev_codec_httpsig.erl - Cookie Auth -
dev_codec_cookie_auth.erl - HTTP Auth -
dev_codec_http_auth.erl - Message System -
hb_message.erl - KeyID Module -
dev_codec_httpsig_keyid.erl
Notes
- Proxy Pattern: Enables code reuse while maintaining device identity
- HMAC Only: Only works with HMAC-SHA256, not RSA-PSS
- Secret Scheme: Always uses
secret:keyid scheme - Device Relabeling: Core function is changing commitment-device label
- Commitment Preservation: Maintains existing commitments when adding new ones
- Isolated Signing: Only committed portions signed when commitments exist
- Verification Path: Sets
path = verifyin proxy request - No Additional Crypto: All cryptography delegated to httpsig@1.0
- Lightweight: Minimal wrapper around hb_message functions
- Extensible: Easy pattern for new authentication devices
- Stack-Friendly: Supports multiple commitments on same message
- Used By: Currently used by cookie and HTTP auth devices
- Future-Proof: Pattern works for any secret-based auth scheme