Created
November 17, 2011 18:54
-
-
Save snej/1374080 to your computer and use it in GitHub Desktop.
JSON signature verification, in Erlang
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
%%%' 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