1%% -*- mode: erlang; tab-width: 4; indent-tabs-mode: 1; st-rulers: [70] -*-
2%% vim: ts=4 sw=4 ft=erlang noet
3%%%-------------------------------------------------------------------
4%%% @author Andrew Bennett <potatosaladx@gmail.com>
5%%% @copyright 2014-2015, Andrew Bennett
6%%% @doc JSON Web Signature (JWS)
7%%% See RFC 7515: https://tools.ietf.org/html/rfc7515
8%%% See RFC 7797: https://tools.ietf.org/html/rfc7797
9%%% @end
10%%% Created :  21 Jul 2015 by Andrew Bennett <potatosaladx@gmail.com>
11%%%-------------------------------------------------------------------
12-module(jose_jws).
13
14-include("jose_jwk.hrl").
15-include("jose_jws.hrl").
16
17-callback from_map(Fields) -> State
18	when
19		Fields :: map(),
20		State  :: any().
21-callback to_map(State, Fields) -> Map
22	when
23		State  :: any(),
24		Fields :: map(),
25		Map    :: map().
26
27%% Decode API
28-export([from/1]).
29-export([from_binary/1]).
30-export([from_file/1]).
31-export([from_map/1]).
32%% Encode API
33-export([to_binary/1]).
34-export([to_file/2]).
35-export([to_map/1]).
36%% API
37-export([compact/1]).
38-export([expand/1]).
39-export([generate_key/1]).
40-export([merge/2]).
41-export([peek/1]).
42-export([peek_payload/1]).
43-export([peek_protected/1]).
44-export([peek_signature/1]).
45-export([sign/3]).
46-export([sign/4]).
47-export([signing_input/2]).
48-export([signing_input/3]).
49-export([verify/2]).
50-export([verify_strict/3]).
51
52-define(ALG_ECDSA_MODULE,          jose_jws_alg_ecdsa).
53-define(ALG_EDDSA_MODULE,          jose_jws_alg_eddsa).
54-define(ALG_HMAC_MODULE,           jose_jws_alg_hmac).
55-define(ALG_NONE_MODULE,           jose_jws_alg_none).
56-define(ALG_POLY1305_MODULE,       jose_jws_alg_poly1305).
57-define(ALG_RSA_PKCS1_V1_5_MODULE, jose_jws_alg_rsa_pkcs1_v1_5).
58-define(ALG_RSA_PSS_MODULE,        jose_jws_alg_rsa_pss).
59
60%%====================================================================
61%% Decode API functions
62%%====================================================================
63
64from(List) when is_list(List) ->
65	[from(Element) || Element <- List];
66from({Modules, Map}) when is_map(Modules) andalso is_map(Map) ->
67	from_map({Modules, Map});
68from({Modules, Binary}) when is_map(Modules) andalso is_binary(Binary) ->
69	from_binary({Modules, Binary});
70from(JWS=#jose_jws{}) ->
71	JWS;
72from(Other) when is_map(Other) orelse is_binary(Other) ->
73	from({#{}, Other}).
74
75from_binary(List) when is_list(List) ->
76	[from_binary(Element) || Element <- List];
77from_binary({Modules, Binary}) when is_map(Modules) andalso is_binary(Binary) ->
78	from_map({Modules, jose:decode(Binary)});
79from_binary(Binary) when is_binary(Binary) ->
80	from_binary({#{}, Binary}).
81
82from_file({Modules, File}) when is_map(Modules) andalso (is_binary(File) orelse is_list(File)) ->
83	case file:read_file(File) of
84		{ok, Binary} ->
85			from_binary({Modules, Binary});
86		ReadError ->
87			ReadError
88	end;
89from_file(File) when is_binary(File) orelse is_list(File) ->
90	from_file({#{}, File}).
91
92from_map(List) when is_list(List) ->
93	[from_map(Element) || Element <- List];
94from_map(Map) when is_map(Map) ->
95	from_map({#{}, Map});
96from_map({Modules, Map}) when is_map(Modules) andalso is_map(Map) ->
97	from_map({#jose_jws{}, Modules, Map});
98from_map({JWS, Modules = #{ alg := Module }, Map=#{ <<"alg">> := _ }}) ->
99	{ALG, Fields} = Module:from_map(Map),
100	from_map({JWS#jose_jws{ alg = {Module, ALG} }, maps:remove(alg, Modules), Fields});
101from_map({JWS, Modules, Map=#{ <<"b64">> := B64 }}) ->
102	from_map({JWS#jose_jws{ b64 = B64 }, Modules, maps:remove(<<"b64">>, Map)});
103from_map({JWS, Modules, Map=#{ <<"alg">> := << "Ed25519", _/binary >> }}) ->
104	from_map({JWS, Modules#{ alg => ?ALG_EDDSA_MODULE }, Map});
105from_map({JWS, Modules, Map=#{ <<"alg">> := << "Ed448", _/binary >> }}) ->
106	from_map({JWS, Modules#{ alg => ?ALG_EDDSA_MODULE }, Map});
107from_map({JWS, Modules, Map=#{ <<"alg">> := << "EdDSA", _/binary >> }}) ->
108	from_map({JWS, Modules#{ alg => ?ALG_EDDSA_MODULE }, Map});
109from_map({JWS, Modules, Map=#{ <<"alg">> := << "ES", _/binary >> }}) ->
110	from_map({JWS, Modules#{ alg => ?ALG_ECDSA_MODULE }, Map});
111from_map({JWS, Modules, Map=#{ <<"alg">> := << "HS", _/binary >> }}) ->
112	from_map({JWS, Modules#{ alg => ?ALG_HMAC_MODULE }, Map});
113from_map({JWS, Modules, Map=#{ <<"alg">> := << "Poly1305" >> }}) ->
114	from_map({JWS, Modules#{ alg => ?ALG_POLY1305_MODULE }, Map});
115from_map({JWS, Modules, Map=#{ <<"alg">> := << "PS", _/binary >> }}) ->
116	from_map({JWS, Modules#{ alg => ?ALG_RSA_PSS_MODULE }, Map});
117from_map({JWS, Modules, Map=#{ <<"alg">> := << "RS", _/binary >> }}) ->
118	from_map({JWS, Modules#{ alg => ?ALG_RSA_PKCS1_V1_5_MODULE }, Map});
119from_map({JWS, Modules, Map=#{ <<"alg">> := << "none" >> }}) ->
120	from_map({JWS, Modules#{ alg => ?ALG_NONE_MODULE }, Map});
121from_map({#jose_jws{ alg = undefined }, _Modules, _Map}) ->
122	{error, {missing_required_keys, [<<"alg">>]}};
123from_map({JWS, _Modules, Fields}) ->
124	JWS#jose_jws{ fields = Fields }.
125
126%%====================================================================
127%% Encode API functions
128%%====================================================================
129
130to_binary(List) when is_list(List) ->
131	[to_binary(Element) || Element <- List];
132to_binary(JWS=#jose_jws{}) ->
133	{Modules, Map} = to_map(JWS),
134	{Modules, jose:encode(Map)};
135to_binary(Other) ->
136	to_binary(from(Other)).
137
138to_file(File, JWS=#jose_jws{}) when is_binary(File) orelse is_list(File) ->
139	{Modules, Binary} = to_binary(JWS),
140	case file:write_file(File, Binary) of
141		ok ->
142			{Modules, File};
143		WriteError ->
144			WriteError
145	end;
146to_file(File, Other) when is_binary(File) orelse is_list(File) ->
147	to_file(File, from(Other)).
148
149to_map(List) when is_list(List) ->
150	[to_map(Element) || Element <- List];
151to_map(JWS=#jose_jws{fields=Fields}) ->
152	record_to_map(JWS, #{}, Fields);
153to_map(Other) ->
154	to_map(from(Other)).
155
156%%====================================================================
157%% API functions
158%%====================================================================
159
160compact({Modules, #{
161			<<"payload">> := Payload,
162			<<"signatures">> := Signatures }}) when is_list(Signatures) ->
163	{Modules, [do_compact(Map#{ <<"payload">> => Payload }) || Map <- Signatures]};
164compact({Modules, Map}) when is_map(Map) ->
165	{Modules, do_compact(Map)};
166compact({Modules, List}) when is_list(List) ->
167	{Modules, [do_compact(Map) || Map <- List]};
168compact(Map) when is_map(Map) ->
169	compact({#{}, Map});
170compact(List) when is_list(List) ->
171	compact({#{}, List});
172compact(BadArg) ->
173	erlang:error({badarg, [BadArg]}).
174
175expand({Modules, Binary}) when is_binary(Binary) ->
176	{Modules, do_expand(Binary)};
177expand({Modules, List}) when is_list(List) ->
178	Expanded = [do_expand(Binary) || Binary <- List],
179	Eligible = lists:foldl(fun
180		(_, false) ->
181			false;
182		(#{ <<"payload">> := Payload }, undefined) when is_binary(Payload) ->
183			Payload;
184		(#{ <<"payload">> := Payload }, Payload) when is_binary(Payload) ->
185			Payload;
186		(_, _) ->
187			false
188	end, undefined, Expanded),
189	case Eligible of
190		_ when Eligible =:= false orelse Eligible =:= undefined ->
191			{Modules, Expanded};
192		Payload ->
193			Signatures = [maps:remove(<<"payload">>, Map) || Map <- Expanded],
194			{Modules, #{
195				<<"payload">> => Payload,
196				<<"signatures">> => Signatures
197			}}
198	end;
199expand(Binary) when is_binary(Binary) ->
200	expand({#{}, Binary});
201expand(List) when is_list(List) ->
202	expand({#{}, List}).
203
204generate_key(List) when is_list(List) ->
205	[generate_key(Element) || Element <- List];
206generate_key(#jose_jws{alg={Module, ALG}, fields=Fields}) ->
207	Module:generate_key(ALG, Fields);
208generate_key(Other) ->
209	generate_key(from(Other)).
210
211merge(LeftJWS=#jose_jws{}, RightMap) when is_map(RightMap) ->
212	{Modules, LeftMap} = to_map(LeftJWS),
213	from_map({Modules, maps:merge(LeftMap, RightMap)});
214merge(LeftOther, RightJWS=#jose_jws{}) ->
215	merge(LeftOther, element(2, to_map(RightJWS)));
216merge(LeftOther, RightMap) when is_map(RightMap) ->
217	merge(from(LeftOther), RightMap).
218
219peek(Signed) ->
220	peek_payload(Signed).
221
222peek_payload({_Modules, Signed}) when is_binary(Signed) or is_map(Signed) ->
223	peek_payload(Signed);
224peek_payload(SignedBinary) when is_binary(SignedBinary) ->
225	peek_payload(expand(SignedBinary));
226peek_payload(#{ <<"payload">> := Payload }) ->
227	jose_jwa_base64url:decode(Payload).
228
229peek_protected({_Modules, Signed}) when is_binary(Signed) or is_map(Signed) ->
230	peek_protected(Signed);
231peek_protected(SignedBinary) when is_binary(SignedBinary) ->
232	peek_protected(expand(SignedBinary));
233peek_protected(#{ <<"protected">> := Protected }) ->
234	jose_jwa_base64url:decode(Protected).
235
236peek_signature({_Modules, Signed}) when is_binary(Signed) or is_map(Signed) ->
237	peek_signature(Signed);
238peek_signature(SignedBinary) when is_binary(SignedBinary) ->
239	peek_signature(expand(SignedBinary));
240peek_signature(#{ <<"signature">> := Signature }) ->
241	jose_jwa_base64url:decode(Signature).
242
243sign(KeyList, PlainText, SignerList)
244		when is_list(KeyList)
245		andalso is_list(SignerList)
246		andalso length(KeyList) =:= length(SignerList) ->
247	HeaderList = [#{} || _ <- SignerList],
248	sign(KeyList, PlainText, HeaderList, SignerList);
249sign(KeyList, PlainText, SignerList)
250		when is_list(KeyList)
251		andalso is_list(SignerList)
252		andalso length(KeyList) =/= length(SignerList) ->
253	erlang:error({badarg, [KeyList, PlainText, SignerList]});
254sign(KeyOrKeyList, PlainText, JWS=#jose_jws{}) ->
255	sign(KeyOrKeyList, PlainText, #{}, JWS);
256sign(KeyOrKeyList, PlainText, Other) ->
257	sign(KeyOrKeyList, PlainText, from(Other)).
258
259sign(KeyList, PlainText, Header, Signer=#jose_jws{})
260		when is_list(KeyList)
261		andalso is_binary(PlainText)
262		andalso is_map(Header) ->
263	HeaderList = [Header || _ <- KeyList],
264	SignerList = [Signer || _ <- KeyList],
265	sign(KeyList, PlainText, HeaderList, SignerList);
266sign(KeyList, PlainText, Header, SignerList)
267		when is_list(KeyList)
268		andalso is_binary(PlainText)
269		andalso is_map(Header)
270		andalso is_list(SignerList)
271		andalso length(KeyList) =:= length(SignerList) ->
272	HeaderList = [Header || _ <- KeyList],
273	sign(KeyList, PlainText, HeaderList, SignerList);
274sign(KeyList, PlainText, HeaderList, Signer=#jose_jws{})
275		when is_list(KeyList)
276		andalso is_binary(PlainText)
277		andalso is_list(HeaderList)
278		andalso length(KeyList) =:= length(HeaderList) ->
279	SignerList = [Signer || _ <- KeyList],
280	sign(KeyList, PlainText, HeaderList, SignerList);
281sign(KeyList, PlainText, HeaderList, SignerList)
282		when is_list(KeyList)
283		andalso is_binary(PlainText)
284		andalso is_list(HeaderList)
285		andalso is_list(SignerList)
286		andalso length(KeyList) =:= length(SignerList)
287		andalso length(KeyList) =:= length(HeaderList) ->
288	Keys = jose_jwk:from(KeyList),
289	Signers = from(SignerList),
290	Payload = jose_jwa_base64url:encode(PlainText),
291	Signatures = map_signatures(Keys, PlainText, HeaderList, Signers, []),
292	{#{}, #{
293		<<"payload">> => Payload,
294		<<"signatures">> => Signatures
295	}};
296sign(Key=#jose_jwk{}, PlainText, Header, JWS=#jose_jws{alg={ALGModule, ALG}})
297		when is_binary(PlainText)
298		andalso is_map(Header) ->
299	_ = code:ensure_loaded(ALGModule),
300	NewALG = case erlang:function_exported(ALGModule, presign, 2) of
301		false ->
302			ALG;
303		true ->
304			ALGModule:presign(Key, ALG)
305	end,
306	NewJWS = JWS#jose_jws{alg={ALGModule, NewALG}},
307	{Modules, ProtectedBinary} = to_binary(NewJWS),
308	Protected = jose_jwa_base64url:encode(ProtectedBinary),
309	Payload = jose_jwa_base64url:encode(PlainText),
310	SigningInput = signing_input(PlainText, Protected, NewJWS),
311	Signature = jose_jwa_base64url:encode(ALGModule:sign(Key, SigningInput, NewALG)),
312	{Modules, maps:put(<<"payload">>, Payload, signature_to_map(Protected, Header, Key, Signature))};
313sign(Key=none, PlainText, Header, JWS=#jose_jws{alg={ALGModule, ALG}})
314		when is_binary(PlainText)
315		andalso is_map(Header) ->
316	_ = code:ensure_loaded(ALGModule),
317	NewALG = case erlang:function_exported(ALGModule, presign, 2) of
318		false ->
319			ALG;
320		true ->
321			ALGModule:presign(Key, ALG)
322	end,
323	NewJWS = JWS#jose_jws{alg={ALGModule, NewALG}},
324	{Modules, ProtectedBinary} = to_binary(NewJWS),
325	Protected = jose_jwa_base64url:encode(ProtectedBinary),
326	Payload = jose_jwa_base64url:encode(PlainText),
327	SigningInput = signing_input(PlainText, Protected, NewJWS),
328	Signature = jose_jwa_base64url:encode(ALGModule:sign(Key, SigningInput, NewALG)),
329	{Modules, maps:put(<<"payload">>, Payload, signature_to_map(Protected, Header, Key, Signature))};
330sign(KeyList, PlainText, HeaderList, SignerList)
331		when (is_list(KeyList)
332			andalso is_list(HeaderList)
333			andalso length(KeyList) =/= length(HeaderList))
334		orelse (is_list(KeyList)
335			andalso is_list(SignerList)
336			andalso length(KeyList) =/= length(SignerList))
337		orelse (is_list(HeaderList)
338			andalso is_list(SignerList)
339			andalso length(HeaderList) =/= length(SignerList))
340		orelse (is_list(HeaderList)
341			andalso not is_list(KeyList)
342			andalso not is_list(SignerList)) ->
343	erlang:error({badarg, [KeyList, PlainText, HeaderList, SignerList]});
344sign(KeyOrKeyList, PlainText, Header, Other)
345		when is_binary(PlainText)
346		andalso is_map(Header) ->
347	sign(jose_jwk:from(KeyOrKeyList), PlainText, Header, from(Other)).
348
349%% See https://tools.ietf.org/html/rfc7797
350signing_input(Payload, JWS=#jose_jws{}) ->
351	{_, ProtectedBinary} = to_binary(JWS),
352	Protected = jose_jwa_base64url:encode(ProtectedBinary),
353	signing_input(Payload, Protected, JWS);
354signing_input(Payload, Other) ->
355	signing_input(Payload, from(Other)).
356
357signing_input(PlainText, Protected, #jose_jws{b64=B64})
358		when (B64 =:= true
359			orelse B64 =:= undefined) ->
360	Payload = jose_jwa_base64url:encode(PlainText),
361	<< Protected/binary, $., Payload/binary >>;
362signing_input(Payload, Protected, #jose_jws{b64=false}) ->
363	<< Protected/binary, $., Payload/binary >>.
364
365verify(Key, SignedMap) when is_map(SignedMap) ->
366	verify(Key, {#{}, SignedMap});
367verify(Key, SignedBinary) when is_binary(SignedBinary) ->
368	verify(Key, expand(SignedBinary));
369verify(Key, {Modules, SignedBinary}) when is_binary(SignedBinary) ->
370	verify(Key, expand({Modules, SignedBinary}));
371verify(Key, {Modules, #{
372		<<"payload">> := Payload,
373		<<"protected">> := Protected,
374		<<"signature">> := EncodedSignature}}) ->
375	JWS = #jose_jws{alg={ALGModule, ALG}} = from_binary({Modules, jose_jwa_base64url:decode(Protected)}),
376	Signature = jose_jwa_base64url:decode(EncodedSignature),
377	PlainText = jose_jwa_base64url:decode(Payload),
378	SigningInput = signing_input(PlainText, Protected, JWS),
379	{ALGModule:verify(Key, SigningInput, Signature, ALG), PlainText, JWS};
380verify(Keys = [_ | _], {Modules, Signed=#{
381		<<"payload">> := _Payload,
382		<<"signatures">> := EncodedSignatures}})
383		when is_list(EncodedSignatures) ->
384	[begin
385		{Key, verify(Key, {Modules, Signed})}
386	end || Key <- Keys];
387verify(Key, {Modules, #{
388		<<"payload">> := Payload,
389		<<"signatures">> := EncodedSignatures}})
390		when is_list(EncodedSignatures) ->
391	[begin
392		verify(Key, {Modules, maps:put(<<"payload">>, Payload, EncodedSignature)})
393	end || EncodedSignature <- EncodedSignatures].
394
395verify_strict(Key, Allow, SignedMap) when is_map(SignedMap) ->
396	verify_strict(Key, Allow, {#{}, SignedMap});
397verify_strict(Key, Allow, SignedBinary) when is_binary(SignedBinary) ->
398	verify_strict(Key, Allow, expand(SignedBinary));
399verify_strict(Key, Allow, {Modules, SignedBinary}) when is_binary(SignedBinary) ->
400	verify_strict(Key, Allow, expand({Modules, SignedBinary}));
401verify_strict(Key, Allow, {Modules, #{
402		<<"payload">> := Payload,
403		<<"protected">> := Protected,
404		<<"signature">> := EncodedSignature}}) ->
405	ProtectedMap = jose:decode(jose_jwa_base64url:decode(Protected)),
406	Signature = jose_jwa_base64url:decode(EncodedSignature),
407	PlainText = jose_jwa_base64url:decode(Payload),
408	case ProtectedMap of
409		#{ <<"alg">> := Algorithm } ->
410			case lists:member(Algorithm, Allow) of
411				false ->
412					{false, PlainText, ProtectedMap};
413				true ->
414					JWS = #jose_jws{alg={ALGModule, ALG}} = from_map({Modules, ProtectedMap}),
415					SigningInput = signing_input(PlainText, Protected, JWS),
416					{ALGModule:verify(Key, SigningInput, Signature, ALG), PlainText, JWS}
417			end;
418		_ ->
419			{false, PlainText, ProtectedMap}
420	end;
421verify_strict(Keys = [_ | _], Allow, {Modules, Signed=#{
422		<<"payload">> := _Payload,
423		<<"signatures">> := EncodedSignatures}})
424		when is_list(EncodedSignatures) ->
425	[begin
426		{Key, verify_strict(Key, Allow, {Modules, Signed})}
427	end || Key <- Keys];
428verify_strict(Key, Allow, {Modules, #{
429		<<"payload">> := Payload,
430		<<"signatures">> := EncodedSignatures}})
431		when is_list(EncodedSignatures) ->
432	[begin
433		verify_strict(Key, Allow, {Modules, maps:put(<<"payload">>, Payload, EncodedSignature)})
434	end || EncodedSignature <- EncodedSignatures].
435
436%%%-------------------------------------------------------------------
437%%% Internal functions
438%%%-------------------------------------------------------------------
439
440%% @private
441do_compact(#{
442		<<"payload">> := Payload,
443		<<"protected">> := Protected,
444		<<"signature">> := Signature}) ->
445	<<
446		Protected/binary, $.,
447		Payload/binary, $.,
448		Signature/binary
449	>>;
450do_compact(BadArg) ->
451	erlang:error({badarg, [BadArg]}).
452
453%% @private
454do_expand(Binary) when is_binary(Binary) ->
455	case binary:split(Binary, <<".">>, [global]) of
456		[Protected, Payload, Signature] ->
457			#{
458				<<"payload">> => Payload,
459				<<"protected">> => Protected,
460				<<"signature">> => Signature
461			};
462		_ ->
463			erlang:error({badarg, [Binary]})
464	end;
465do_expand(BadArg) ->
466	erlang:error({badarg, [BadArg]}).
467
468%% @private
469map_signatures([Key | Keys], PlainText, [Header | Headers], [Signer=#jose_jws{alg={ALGModule, ALG}} | Signers], Acc) ->
470	_ = code:ensure_loaded(ALGModule),
471	NewALG = case erlang:function_exported(ALGModule, presign, 2) of
472		false ->
473			ALG;
474		true ->
475			ALGModule:presign(Key, ALG)
476	end,
477	NewSigner = Signer#jose_jws{alg={ALGModule, NewALG}},
478	{_Modules, ProtectedBinary} = to_binary(NewSigner),
479	Protected = jose_jwa_base64url:encode(ProtectedBinary),
480	SigningInput = signing_input(PlainText, Protected, NewSigner),
481	Signature = jose_jwa_base64url:encode(ALGModule:sign(Key, SigningInput, NewALG)),
482	map_signatures(Keys, PlainText, Headers, Signers, [signature_to_map(Protected, Header, Key, Signature) | Acc]);
483map_signatures([], _PlainText, [], [], Acc) ->
484	lists:reverse(Acc).
485
486%% @private
487record_to_map(JWS=#jose_jws{alg={Module, ALG}}, Modules, Fields0) ->
488	Fields1 = Module:to_map(ALG, Fields0),
489	record_to_map(JWS#jose_jws{alg=undefined}, Modules#{ alg => Module }, Fields1);
490record_to_map(JWS=#jose_jws{b64=B64}, Modules, Fields0) when is_boolean(B64) ->
491	Fields1 = Fields0#{ <<"b64">> => B64 },
492	record_to_map(JWS#jose_jws{b64=undefined}, Modules, Fields1);
493record_to_map(_JWS, Modules, Fields) ->
494	{Modules, Fields}.
495
496%% @private
497signature_to_map(Protected, Header, #jose_jwk{fields=Fields}, Signature) ->
498	signature_to_map(Protected, Header, Fields, Signature);
499signature_to_map(Protected, Header, #{ <<"kid">> := KID }, Signature) ->
500	#{
501		<<"protected">> => Protected,
502		<<"header">> => maps:put(<<"kid">>, KID, Header),
503		<<"signature">> => Signature
504	};
505signature_to_map(Protected, Header, _Fields, Signature) ->
506	case maps:size(Header) of
507		0 ->
508			#{
509				<<"protected">> => Protected,
510				<<"signature">> => Signature
511			};
512		_ ->
513			#{
514				<<"protected">> => Protected,
515				<<"header">> => Header,
516				<<"signature">> => Signature
517			}
518	end.
519