Skip to content

Instantly share code, notes, and snippets.

@snej
Created November 17, 2011 18:54
Show Gist options
  • Save snej/1374080 to your computer and use it in GitHub Desktop.
Save snej/1374080 to your computer and use it in GitHub Desktop.
JSON signature verification, in Erlang
%%%' HEADER
%% @author Jens Alfke <[email protected]>
%% @copyright Apache 2.0 license
%% @doc Verification of digital signatures in parsed JSON objects. (Assumes JSON is parsed the way it is by CouchDB, not as by the original mochijson library.)
%% @end
-module(signed_json).
-export([is_signed/1, verify/1]).
-export([test/0]). % TESTING
%%%.
%%%' PUBLIC API
% If T is a signed JSON object, returns the signature sub-object; else returns false.
is_signed(T) ->
case T of
{PList} when is_list(PList) ->
case proplists:lookup(<<"(signed)">>, PList) of
{<<"(signed)">>, SigTerm} -> SigTerm;
_ -> false
end;
_ -> false
end.
% Verifies a signed JSON object, returning the public key of the signer (an RSAPublicKey record, as
% defined by the public_key library) or throwing an error.
verify (Object) ->
{KeyBlob, Signature, Digest, UnsignedSigTerm, Payload} = components(Object),
io:format("canon_json:sha = ~p~n", [canon_json:sha(Payload)]),
io:format("Digest = ~p~n", [Digest]),
case canon_json:sha(Payload) of
Digest -> true;
_ -> error(digestMismatch) % Digest in sig block does not match payload
end,
Key = public_key:der_decode('RSAPublicKey', KeyBlob),
case public_key:verify(canon_json:encode_as_binary(UnsignedSigTerm), 'sha', Signature, Key) of
true -> Key;
_ -> error(invalidSignature) % Signature does not match sig block
end.
%%%.
%%%' PRIVATE FUNCTIONS
%% @private
% Extracts the components of a JSON signature from the signed object.
% Returns {Key, Signature, Digest, UnsignedSigTerm, Payload} or throws an exception.
components ({PList}) when is_list(PList) ->
try
{<<"(signed)">>, {SigTerm}} = proplists:lookup(<<"(signed)">>, PList),
{<<"digest">>, DigestTerm} = proplists:lookup(<<"digest">>, SigTerm),
Digest = decode_digest(DigestTerm),
{<<"key">>, KeyTerm} = proplists:lookup(<<"key">>, SigTerm),
Key = decode_key(KeyTerm),
{<<"sig">>, Base64Sig} = proplists:lookup(<<"sig">>, SigTerm),
Signature = decode_base64(Base64Sig),
UnsignedSigTerm = {proplists:delete(<<"sig">>, SigTerm)},
Payload = {proplists:delete(<<"(signed)">>, PList)},
io:format("Payload JSON = '~s'~n", [canon_json:encode(Payload)]),
{Key, Signature, Digest, UnsignedSigTerm, Payload}
catch
error:_ -> error(invalidSignatureFormat);
throw:Reason -> error(Reason)
end.
decode_digest ([<<"SHA1">>, Base64Digest]) ->
decode_base64(Base64Digest);
% ...add more clauses here to support other digest types...
decode_digest ([DigestType | _]) ->
throw({unsupportedDigestType, DigestType}).
decode_key ([<<"RSA">>, Base64Key]) ->
decode_base64(Base64Key);
% ...add more clauses here to support other public key types...
decode_key ([KeyType | _]) ->
throw({unsupportedKeyType, KeyType}).
% Decode base64, throwing a more intelligible exception if it fails.
decode_base64(Base64) ->
try base64:decode(Base64)
catch
error:_ -> throw({invalidBase64, Base64})
end.
%%%.
%%%' UNIT TESTS
testSynthetic() ->
Key = <<"this is the key">>,
Sig = <<"I am a signature">>,
Digest = <<"CAFEBABE">>,
UnsignedSigTerm = [{<<"key">>, [<<"RSA">>, base64:encode(Key)]},
{<<"digest">>, [<<"SHA1">>, base64:encode(Digest)]}],
SigTerm = {UnsignedSigTerm ++ [{<<"sig">>, base64:encode(Sig)}]},
Payload = [{foo, 1}, {bar, 2}],
Doc = {[{<<"(signed)">>, SigTerm} | Payload]},
io:format("Doc = ~p~n", [Doc]),
SigTerm = is_signed(Doc),
{Key, Sig, Digest, {UnsignedSigTerm}, {Payload}} = components(Doc),
io:format("*** PASSED testSynthetic ***~n").
testRealInput() ->
%Doc = json:decode( <<"{\"foo\":\"bar\",\"zero\":0,\"(signed)\":{\"digest\":[\"SHA1\",\"TcIKdNQxyB1Lc9TZe68cnBK8118=\"],\"key\":[\"RSA\",\"MIGJAoGBAMmnxc9PHkHN104XQUVQs11bLR4bIHPn271YYzTFQRos/JC8gc2a2Gwq4PWJcEPK8OPnraKcAlyfmu7T26ijzaWyez4LDPZViM2Rx/OmoCzLWjT6MmJLY3ddlvVIZI9vXgOtTRb9V3CiJeclDQyri+ovGkTWhGpwt2/PllZbnhdFAgMBAAE=\"],\"sig\":\"CJm8ZrekN98YPtnLQW8cZcHrP6+E3Sq7hZzq/LNz7yeV4GCp+4O4d/Ofwc3JQn/pIfKz4/IQ1WiZF/Xc8Zvn8PbvZw6oThTwjp9Xmju1BVNfPdsGwwXNR24Lpm2nWm9X0deLMqNvZoETFOVsf/xbutJWhtiKysPYiSzJJL7hY+w=\"},\"emptyArray\":[]}">>),
Doc = {[{<<"foo">>,<<"bar">>},
{<<"zero">>,0},
{<<"(signed)">>,
{[{<<"digest">>,[<<"SHA1">>,<<"TcIKdNQxyB1Lc9TZe68cnBK8118=">>]},
{<<"key">>,
[<<"RSA">>, <<"MIGJAoGBAMmnxc9PHkHN104XQUVQs11bLR4bIHPn271YYzTFQRos/JC8gc2a2Gwq4PWJcEPK8OPnraKcAlyfmu7T26ijzaWyez4LDPZViM2Rx/OmoCzLWjT6MmJLY3ddlvVIZI9vXgOtTRb9V3CiJeclDQyri+ovGkTWhGpwt2/PllZbnhdFAgMBAAE=">>]},
{<<"sig">>, <<"CJm8ZrekN98YPtnLQW8cZcHrP6+E3Sq7hZzq/LNz7yeV4GCp+4O4d/Ofwc3JQn/pIfKz4/IQ1WiZF/Xc8Zvn8PbvZw6oThTwjp9Xmju1BVNfPdsGwwXNR24Lpm2nWm9X0deLMqNvZoETFOVsf/xbutJWhtiKysPYiSzJJL7hY+w=">>}]}},
{<<"emptyArray">>,[]}]},
io:format("Signed doc =~n~p~n", [Doc]),
io:format("Parsed signed doc = ~p~n", [components(Doc)]),
Key = verify(Doc),
{'RSAPublicKey', _N, _E} = Key, % verify that this is a key
io:format("Signer = ~p~n", [Key]),
io:format("*** PASSED testRealInput ***~n").
test() ->
testSynthetic(),
testRealInput().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment