-
-
Notifications
You must be signed in to change notification settings - Fork 180
Expand file tree
/
Copy pathmormot.lib.gssapi.pas
More file actions
1698 lines (1529 loc) · 61.1 KB
/
Copy pathmormot.lib.gssapi.pas
File metadata and controls
1698 lines (1529 loc) · 61.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// low-level access to the GssApi on Linux/POSIX
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.lib.gssapi;
{
*****************************************************************************
Generic Security Service API on POSIX/Linux
- Low-Level libgssapi_krb5/libgssapi.so Library Access
- Middle-Level GSSAPI Wrappers
- High-Level Client and Server Authentication using GSSAPI
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
{$ifdef OSWINDOWS}
// do-nothing-unit on non POSIX system
implementation
{$else}
uses
sysutils,
classes,
mormot.core.base,
mormot.core.os,
mormot.core.os.security, // for TKerberosKeyTab support
mormot.core.unicode, // e.g. for Split/SplitRight/IdemPChar
mormot.core.buffers; // for base-64 encoding
{ ****************** Low-Level libgssapi_krb5/libgssapi.so Library Access }
type
gss_name_t = pointer;
gss_name_t_ptr = ^gss_name_t;
gss_cred_id_t = pointer;
gss_ctx_id_t = pointer;
// we need to circumvent non-standard definitions of MacOS
{$ifdef OSDARWIN}
gss_length_t = cardinal; // no OM_STRING/xom.h on MacOS - OM_uint32 in gssapi.hin
{$ifdef CPUINTEL}
// #if defined(__APPLE__) && (defined(__ppc__) || defined(__ppc64__) || defined(__i386__) || defined(__x86_64__))
{$A2} // #pragma pack(push,2)
{$endif CPUINTEL}
{$else}
gss_length_t = PtrUInt;
{$endif OSDARWIN}
gss_OID_desc = record
length: gss_length_t;
elements: pointer;
end;
gss_OID = ^gss_OID_desc;
gss_OID_ptr = ^gss_OID;
gss_OID_array = array[word] of gss_OID_desc;
gss_OID_descs = ^gss_OID_array;
gss_OID_set_desc = record
count: PtrUInt; // size_t in all platforms
elements: gss_OID_descs;
end;
gss_OID_set = ^gss_OID_set_desc;
gss_OID_set_ptr = ^gss_OID_set;
gss_buffer_desc = record
length: PtrUInt; // size_t in all platforms
value: pointer;
end;
gss_buffer_t = ^gss_buffer_desc;
gss_channel_bindings_struct = record
initiator_addrtype: cardinal;
initiator_address: gss_buffer_desc;
acceptor_addrtype: cardinal;
acceptor_address: gss_buffer_desc;
application_data: gss_buffer_desc;
end;
gss_channel_bindings_t = ^gss_channel_bindings_struct;
gss_key_value_set_desc = record
count: PtrUInt; // should be OM_uint32 per RFC 2744 but GPF on MacOS
elements: pointer;
end;
gss_const_key_value_set_t = ^gss_key_value_set_desc;
{$A+} // back to usual class/record alignment
const
GSS_C_NO_NAME = nil;
GSS_C_NO_OID = nil;
GSS_C_NO_CHANNEL_BINDINGS = nil;
GSS_C_GSS_CODE = 1;
GSS_C_MECH_CODE = 2;
// Expiration time of 2^32-1 seconds means infinite lifetime
GSS_C_INDEFINITE = $ffffffff;
GSS_C_BOTH = 0;
GSS_C_INITIATE = 1;
GSS_C_ACCEPT = 2;
// Request that remote peer authenticate itself
GSS_C_MUTUAL_FLAG = 2;
// Enable replay detection for messages protected with gss_wrap or gss_get_mic
GSS_C_REPLAY_FLAG = 4;
// Enable detection of out-of-sequence protected messages
GSS_C_SEQUENCE_FLAG = 8;
// Request that confidentiality service be made available (via gss_wrap).
GSS_C_CONF_FLAG = 16;
// Request that integrity service be made available (via gss_wrap or gss_get_mic)
GSS_C_INTEG_FLAG = 32;
// Do not reveal the initiator's identity to the acceptor
GSS_C_ANON_FLAG = 64;
GSS_S_COMPLETE = 0;
GSS_C_CALLING_ERROR_OFFSET = 24;
GSS_C_ROUTINE_ERROR_OFFSET = 16;
GSS_C_SUPPLEMENTARY_OFFSET = 0;
GSS_C_CALLING_ERROR_MASK = $ff;
GSS_C_ROUTINE_ERROR_MASK = $ff;
GSS_C_SUPPLEMENTARY_MASK = $ffff;
GSS_S_CONTINUE_NEEDED = 1 shl (GSS_C_SUPPLEMENTARY_OFFSET + 0);
GSS_S_DUPLICATE_TOKEN = 1 shl (GSS_C_SUPPLEMENTARY_OFFSET + 1);
GSS_S_OLD_TOKEN = 1 shl (GSS_C_SUPPLEMENTARY_OFFSET + 2);
GSS_S_UNSEQ_TOKEN = 1 shl (GSS_C_SUPPLEMENTARY_OFFSET + 3);
GSS_S_GAP_TOKEN = 1 shl (GSS_C_SUPPLEMENTARY_OFFSET + 4);
// https://github.com/krb5/krb5/blob/master/src/lib/gssapi/generic/gssapi_generic.c#L35
// raw 1.2.840.113554.1.2.1.1 OID
// {iso(1) member-body(2) us(840) mit(113554) infosys(1) gssapi(2)
// generic(1) user-name(1)}
gss_nt_user_name: array[0..9] of byte = (
42, 134, 72, 134, 247, 18, 1, 2, 1, 1);
gss_nt_user_name_desc: gss_OID_desc = (
length: SizeOf(gss_nt_user_name);
elements: @gss_nt_user_name);
GSS_C_NT_USER_NAME: gss_OID = @gss_nt_user_name_desc;
// raw 1.2.840.113554.1.2.1.2 OID
// {iso(1) member-body(2) us(840) mit(113554) infosys(1) gssapi(2)
// generic(1) machine_uid_name(2)}
gss_nt_machine_name: array[0..9] of byte = (
42, 134, 72, 134, 247, 18, 1, 2, 1, 2);
gss_nt_machine_name_desc: gss_OID_desc = (
length: SizeOf(gss_nt_machine_name);
elements: @gss_nt_machine_name);
GSS_C_NT_MACHINE_UID_NAME: gss_OID = @gss_nt_machine_name_desc;
// raw 1.2.840.113554.1.2.1.3 OID
// {iso(1) member-body(2) us(840) mit(113554) infosys(1) gssapi(2)
// generic(1) string_uid_name(3)}
gss_nt_stringuidname_name: array[0..9] of byte = (
42, 134, 72, 134, 247, 18, 1, 2, 1, 3);
gss_nt_stringuidname_name_desc: gss_OID_desc = (
length: SizeOf(gss_nt_stringuidname_name);
elements: @gss_nt_stringuidname_name);
GSS_C_NT_STRING_UID_NAME: gss_OID = @gss_nt_stringuidname_name_desc;
// raw 1.2.840.113554.1.2.1.2 OID
// {iso(1) member-body(2) us(840) mit(113554) infosys(1) gssapi(2)
// generic(1) service_name(4)}
gss_nt_hostbased_name: array[0..9] of byte = (
42, 134, 72, 134, 247, 18, 1, 2, 1, 4);
gss_nt_hostbased_name_desc: gss_OID_desc = (
length: SizeOf(gss_nt_hostbased_name);
elements: @gss_nt_hostbased_name);
GSS_C_NT_HOSTBASED_SERVICE: gss_OID = @gss_nt_hostbased_name_desc;
// raw 1.2.840.113554.1.2.2.1 OID
// {iso(1) member-body(2) us(840) mit(113554) infosys(1) gssapi(2)
// krb5(2) krb5-name(1)}
gss_nt_krb5_name: array[0..9] of byte = (
42, 134, 72, 134, 247, 18, 1, 2, 2, 1);
gss_nt_krb5_name_desc: gss_OID_desc = (
length: SizeOf(gss_nt_krb5_name);
elements: @gss_nt_krb5_name);
GSS_KRB5_NT_PRINCIPAL_NAME: gss_OID = @gss_nt_krb5_name_desc;
// raw 1.2.840.113554.1.2.2 OID
// {iso(1) member-body(2) us(840) mit(113554) infosys(1) gssapi(2) krb5(2)}
gss_mech_krb5: array[0..8] of byte = (
42, 134, 72, 134, 247, 18, 1, 2, 2);
gss_mech_krb5_desc: gss_OID_desc = (
length: SizeOf(gss_mech_krb5);
elements: @gss_mech_krb5);
GSS_C_MECH_KRB5: gss_OID = @gss_mech_krb5_desc;
// raw 1.3.6.1.5.5.2 OID for SPNEGO Simple and Protected Negotiation Mechanism
// {iso(1) org(3) dod(6) internet(1) security(5) mechanisms(5) snego(2)}
gss_mech_spnego: array[0..5] of byte = (
43, 6, 1, 5, 5, 2);
gss_mech_spnego_desc: gss_OID_desc = (
length: SizeOf(gss_mech_spnego);
elements: @gss_mech_spnego);
GSS_C_MECH_SPNEGO: gss_OID = @gss_mech_spnego_desc;
// raw 1.3.6.1.4.1.311.2.2.10 OID for GS2-NTLM (NTLM) Mechanism
// {iso(1) identified-organization(3) dod(6) internet(1) private(4)
// enterprise(1) 311 2 2 10}
gss_mech_ntlm: array[0..9] of byte = (
$2b, $06, $01, $04, $01, $82, $37, $02, $02, $0a);
gss_mech_ntlm_desc: gss_OID_desc = (
length: SizeOf(gss_mech_ntlm);
elements: @gss_mech_ntlm);
GSS_C_MECH_NTLM: gss_OID = @gss_mech_ntlm_desc;
type
/// direct access to the libgssapi functions
TGssApi = class(TSynLibrary)
public
/// convert a contiguous string name to internal form
// - returned output_name must be freed by the application after use
// with a call to gss_release_name()
gss_import_name: function (
out minor_status: cardinal;
input_name_buffer: gss_buffer_t;
input_name_type: gss_OID;
out output_name: gss_name_t): cardinal; cdecl;
/// convert an internal form name into its text string
// - output_name_buffer must be freed by the application after use
// with a call to gss_release_buffer()
gss_display_name: function (
out minor_status: cardinal;
input_name: gss_name_t;
output_name_buffer: gss_buffer_t;
output_name_type: gss_OID_ptr): cardinal; cdecl;
/// free an an internal form name storage allocated by the API
gss_release_name: function (
out minor_status: cardinal;
var name: gss_name_t): cardinal; cdecl;
/// obtain a credential handle for pre-existing credentials
gss_acquire_cred: function (
out minor_status: cardinal;
desired_name: gss_name_t;
time_req: cardinal;
desired_mechs: gss_OID_set;
cred_usage: integer;
out output_cred_handle: gss_cred_id_t;
actual_mechs: gss_OID_set_ptr;
time_rec: PCardinal): cardinal; cdecl;
/// obtain a credential handle for a given username and password pair
gss_acquire_cred_with_password: function (
out minor_status: cardinal;
desired_name: gss_name_t;
password: gss_buffer_t;
time_req: cardinal;
desired_mechs: gss_OID_set;
cred_usage: integer;
out output_cred_handle: gss_cred_id_t;
actual_mechs: gss_OID_set_ptr;
time_rec: PCardinal): cardinal; cdecl;
/// free a credential handle
gss_release_cred: function (
out minor_status: cardinal;
var cred_handle: gss_cred_id_t): cardinal; cdecl;
/// initiate a client security context with a peer application
gss_init_sec_context: function (
out minor_status: cardinal;
initiator_cred_handle: gss_cred_id_t;
var context_handle: gss_ctx_id_t;
target_name: gss_name_t;
mech_type: gss_OID;
req_flags: cardinal;
time_req: cardinal;
input_chan_bindings: gss_channel_bindings_t;
input_token: gss_buffer_t;
actual_mech_type: gss_OID_ptr;
output_token: gss_buffer_t;
ret_flags: PCardinal;
time_rec: PCardinal): cardinal; cdecl;
/// accept a server security context initiated by a peer application
gss_accept_sec_context: function (
out minor_status: cardinal;
var context_handle: pointer;
acceptor_cred_handle: pointer;
input_token_buffer: gss_buffer_t;
input_chan_bindings: gss_channel_bindings_t;
src_name: gss_name_t;
mech_type: gss_OID_ptr;
output_token: gss_buffer_t;
ret_flags: PCardinal;
time_rec: PCardinal;
delegated_cred_handle: PPointer): cardinal; cdecl;
/// obtain information about a security context
gss_inquire_context: function (
out minor_status: cardinal;
context_handle: gss_ctx_id_t;
src_name: gss_name_t_ptr;
targ_name: gss_name_t_ptr;
lifetime_rec: PCardinal;
mech_type: gss_OID_ptr;
ctx_flags: PCardinal;
locally_initiated: PInteger;
open: PInteger): cardinal; cdecl;
/// free a security context
gss_delete_sec_context: function (
out minor_status: cardinal;
var gss_context: gss_ctx_id_t;
buffer: gss_buffer_t): cardinal; cdecl;
/// return the SASL name types supported by the specified mechanism
gss_inquire_saslname_for_mech: function (
out minor_status: cardinal;
desired_mech: gss_OID;
sasl_mech_name: gss_buffer_t;
mech_name: gss_buffer_t;
mech_description: gss_buffer_t): cardinal; cdecl;
/// free a libgssapi-allocated buffer
gss_release_buffer: function (
out minor_status: cardinal;
var buffer: gss_buffer_desc): cardinal; cdecl;
/// identify and encrypt a message
gss_wrap: function (
out minor_status: cardinal;
context_handle: gss_ctx_id_t;
conf_req_flag: integer;
qop_req: cardinal;
input_message_buffer: gss_buffer_t;
conf_state: PInteger;
output_message_buffer: gss_buffer_t): cardinal; cdecl;
/// verify and decrypt a message
gss_unwrap: function (
out minor_status: cardinal;
context_handle: gss_ctx_id_t;
input_message_buffer: gss_buffer_t;
output_message_buffer: gss_buffer_t;
conf_state: PInteger;
qop_state: PCardinal): cardinal; cdecl;
/// return available underlying authentication mechanisms
// - returned mech_set should be freed after use with gss_release_oid_set()
gss_indicate_mechs: function (
out minor_status: cardinal;
out mech_set: gss_OID_set): cardinal; cdecl;
/// free a set of object identifiers
gss_release_oid_set: function (
out minor_status: cardinal;
out mech_set: gss_OID_set): cardinal; cdecl;
/// convert a libgssapi integer status code to text
gss_display_status: function (
out minor_status: cardinal;
status: cardinal;
status_type: integer;
mech_type: gss_OID;
out message_context: cardinal;
out status_string: gss_buffer_desc): cardinal; cdecl;
/// obtain a credential handle for pre-existing credentials - MIT 1.11+ only
gss_acquire_cred_from: function (
out minor_status: cardinal;
desired_name: gss_name_t;
time_req: cardinal;
desired_mechs: gss_OID_set;
cred_usage: integer;
cred_store: gss_const_key_value_set_t;
out output_cred_handle: gss_cred_id_t;
actual_mechs: gss_OID_set_ptr;
time_rec: PCardinal): cardinal; cdecl;
/// set the default credentials cache name for use by Kerberos
// - returned old_name must not be freed, but passed back upon a next call
// to this function
gss_krb5_ccache_name: function(
out minor_status: cardinal;
new_name: PUtf8Char;
old_name: PPUtf8Char): cardinal; cdecl;
/// thread-specific change of the Kerberos keytab file name to use
// - gss_krb5_import_cred() could be preferred but it is more complex, and
// the usual spnego-http-auth-nginx-module
krb5_gss_register_acceptor_identity: function (
path: PAnsiChar): cardinal; cdecl;
/// a simple way to identify that the GSS-API library is MIT (at least 1.11)
IsMit: boolean;
/// filled with either GSSAPI_ENV_CLIENT_KT_MIT ('KRB5_CLIENT_KTNAME') or
// GSSAPI_ENV_CLIENT_KT_HEIMDAL ('KRB5_KTNAME') at startup
EnvClientKtName: RawUtf8;
/// the value of EnvClientKtName at process startup
EnvClientKtValue: RawUtf8;
end;
/// Exception raised during libgssapi process
EGssApi = class(ExceptionWithProps)
private
fMajorStatus: cardinal;
fMinorStatus: cardinal;
public
/// initialize a libgssapi exception with the proper error message
constructor Create(aMajor, aMinor: cardinal; const aPrefix: RawUtf8);
published
/// associated GSS_C_GSS_CODE state value
property MajorStatus: cardinal
read fMajorStatus;
/// associated GSS_C_MECH_CODE state value
property MinorStatus: cardinal
read fMinorStatus;
end;
const
{$ifdef OSDARWIN}
GssMitDef = 'libgssapi_krb5.dylib';
GssHeimdalDef = 'libgssapi.dylib';
GssOSDef = '/System/Library/Frameworks/GSS.framework/GSS';
{$else}
GssMitDef = 'libgssapi_krb5.so.2';
GssHeimdalDef = 'libgssapi.so.3';
GssOSDef = '';
{$endif OSDARWIN}
var
/// direct access to the low-level libgssapi functions
GssApi: TGssApi;
/// custom library name for GSSAPI
// - tried before OS/MIT/Heimdal standard alternatives
// - you can overwrite with a custom value, make FreeAndNil(GssApi) and call
// LoadGssApi again
// - may be used on MacOS e.g. with '/full/path/to/libgssapi_krb5.dylib' for
// proper user/password credential without any previous kinit or logged user
GssLib_Custom: TFileName = '';
/// library name of the MIT implementation of GSSAPI
// - you can overwrite with a custom value, make FreeAndNil(GssApi) and call
// LoadGssApi again
GssLib_MIT: TFileName = GssMitDef;
/// library name of the Heimdal implementation of GSSAPI
// - you can overwrite with a custom value, make FreeAndNil(GssApi) and call
// LoadGssApi again
GssLib_Heimdal: TFileName = GssHeimdalDef;
/// library name of the system implementation of GSSAPI
// - only used on MacOS by default (GSS is available since 10.7 Lion in 2011)
// - you can overwrite with a custom value, make FreeAndNil(GssApi) and call
// LoadGssApi() again
GssLib_OS: TFileName = GssOSDef;
/// force a single library name for GSSAPI - for no system wide search
GssLib_ForceUnique: TFileName = '';
/// global information filled by LoadGssApi() on failure
GssApi_LastLoadError: string;
/// dynamically load GSSAPI library
// - do nothing if the library is already loaded
// - will try LibraryName, GssLib_Custom, GssLib_MIT, GssLib_Heimdal and
// GssLib_OS in this specific order, and maybe from the executable folder
procedure LoadGssApi(const LibraryName: TFileName = '');
/// check whether GSSAPI library is loaded or not
function GssApiLoaded: boolean;
{$ifdef HASINLINE} inline; {$endif}
/// check whether GSSAPI library was loaded and raise exception if not
procedure RequireGssApi;
// some macros for libgssapi functions process
function GSS_CALLING_ERROR(x: cardinal): boolean; inline;
function GSS_ROUTINE_ERROR(x: cardinal): boolean; inline;
function GSS_SUPPLEMENTARY_INFO(x: cardinal): boolean; inline;
function GSS_ERROR(x: cardinal): boolean; inline;
function gss_compare_oid(oid1, oid2: gss_OID): boolean;
{ ****************** Middle-Level GSSAPI Wrappers }
type
/// GSSAPI high-level Auth context
TSecContext = record
CredHandle: pointer;
CtxHandle: pointer;
ClientTargetName: gss_name_t;
ChannelBindingsHash: pointer;
ChannelBindingsHashLen: cardinal;
ResetEnv: boolean;
end;
PSecContext = ^TSecContext;
/// set aSecHandle fields to empty state for a new handshake
procedure InvalidateSecContext(var aSecContext: TSecContext);
/// Free aSecContext on client or server side
procedure FreeSecContext(var aSecContext: TSecContext);
/// Encrypts a message using 'sign and seal' (i.e. integrity and encryption)
// - aSecContext must be set e.g. from previous success call to ServerSspiAuth
// or ClientSspiAuth
// - aPlain contains data that must be encrypted
// - returns encrypted message
function SecEncrypt(var aSecContext: TSecContext;
const aPlain: RawByteString): RawByteString;
/// Decrypts a message
// - aSecContext must be set e.g. from previous success call to ServerSspiAuth
// or ClientSspiAuth
// - aEncrypted contains data that must be decrypted
// - returns decrypted message
function SecDecrypt(var aSecContext: TSecContext;
const aEncrypted: RawByteString): RawByteString;
/// Checks the return value of GSSAPI call and raises ESynGSSAPI exception
// when it indicates failure
procedure GssCheck(aMajorStatus, aMinorStatus: cardinal;
const aPrefix: RawUtf8 = '');
/// Lists supported security mechanisms in form
// sasl:name:description
// - not all mechanisms provide human readable name and description
// - optionally return the corresponding raw OID values, encoded as gss_OID
function GssEnlistMechsSupported(oid: PBytesDynArray = nil): TRawUtf8DynArray;
{ ****************** High-Level Client and Server Authentication using GSSAPI }
/// Client-side authentication procedure
// - aSecContext holds information between function calls
// - aInData contains data received from server
// - aSecKerberosSpn is the Service Principal Name,
// registered in domain, e.g.
// 'mymormotservice/myserver.mydomain.tld@MYDOMAIN.TLD'
// - aOutData contains data that must be sent to server
// - you can specify an optional Mechanism OID - default is SPNEGO
// - if function returns True, client must send aOutData to server
// and call function again with data, returned from server
function ClientSspiAuth(var aSecContext: TSecContext;
const aInData: RawByteString; const aSecKerberosSpn: RawUtf8;
out aOutData: RawByteString; aMech: gss_OID = nil): boolean;
/// Client-side authentication procedure with clear text password
// - This function must be used when application need to use different
// user credentials (not credentials of logged in user)
// - aSecContext holds information between function calls
// - aInData contains data received from server
// - aUserName is the domain and user name, in form of 'username' or
// 'username@MYDOMAIN.TLD' if aSecKerberosSpn is not set or if
// ClientForceSpn() has not been called ahead
// - aPassword is the user clear text password - you may set '' if you did a
// previous kinit for aUserName on the system and want to recover this token
// - use aLocalFile to force a local keytab/ccache file e.g. '/path/to/my.keytab'
// and if aUserName is '' it will try TKerberosKeyTab.MachineAccountPrincipal
// - aOutData contains data that must be sent back to the server
// - you can specify an optional Mechanism OID - default is SPNEGO / Kerberos
// - if the function returns True, client must send aOutData to server
// and re-call this function again with the data returned from server
// - see also ClientSspiAuthWithPasswordNoMemCcache global to disable the
// default transient memory ccache used during the authentication
function ClientSspiAuthWithPassword(var aSecContext: TSecContext;
const aInData: RawByteString; const aUserName: RawUtf8;
const aPassword: SpiUtf8; const aSecKerberosSpn: RawUtf8;
out aOutData: RawByteString; aMech: gss_OID = nil;
const aLocalFile: TFileName = ''): boolean;
/// check if a binary request packet from a client is using NTLM
function ServerSspiDataNtlm(const aInData: RawByteString): boolean;
/// Server-side authentication procedure
// - aSecContext holds information between function calls
// - aInData contains data received from client
// - aOutData contains data that must be sent to client
// - will raise an EGssApi if authentication failed (e.g. invalid credentials)
// - server must send aOutData to the client (if any), and if True was returned,
// call this function again with any new data receive from the client
function ServerSspiAuth(var aSecContext: TSecContext;
const aInData: RawByteString; out aOutData: RawByteString): boolean;
/// Server-side function that returns authenticated user name
// - aSecContext must be received from previous successful call to ServerSspiAuth
// - aUserName contains authenticated user name, as 'NETBIOSNAME\username' pattern,
// following ServerDomainMapRegister() mapping, or 'REALM.TLD\username' if
// global ServerDomainMapUseRealm was forced to true
procedure ServerSspiAuthUser(var aSecContext: TSecContext;
out aUserName: RawUtf8);
/// Returns name of the security package that has been used with the
// negotiation process
// - aSecContext must be received from previous success call to ServerSspiAuth
// or ClientSspiAuth
function SecPackageName(var aSecContext: TSecContext): RawUtf8;
/// force using a Kerberos SPN for server identification
// - aSecKerberosSpn is the Service Principal Name, as registered in domain,
// e.g. 'mymormotservice/myserver.mydomain.tld@MYDOMAIN.TLD'
procedure ClientForceSpn(const aSecKerberosSpn: RawUtf8);
/// return the value set by ClientForceSpn()
function ClientForcedSpn: RawUtf8;
type
/// allow to track keytab files and their changes at runtime
// - calling ServerForceKeytab() on each thread, only when needed
TServerSspiKeyTab = class(TObjectLightLock)
protected
fKeyTab: TFileName;
fKeyTabSize: Int64;
fKeyTabTime: TUnixMSTime;
fKeyTabSequence: integer; // stored in a threadvar
fLastRefresh: cardinal;
procedure _SetKeyTab(const aKeyTab: TFileName);
public
/// each thread should call this method before ServerSspiAuth()
// - will do nothing if the thread is already prepared for the keytab
procedure PrepareKeyTab;
/// propagate a keytab file to all server threads
// - returns true if the keytab was identified as changed
function SetKeyTab(const aKeyTab: TFileName): boolean;
/// can be called at Idle every few seconds to check if a keytab file changed
// - it will allow hot reload of the keytab, only if needed
// - returns true if the keytab was identified as changed
function TryRefresh(Tix32: cardinal): boolean;
/// parse HTTP input headers and perform Negotiate/Kerberos authentication
// - will identify 'Authorization: Negotiate <base64 encoding>' HTTP header
// - returns '' on error, or the 'WWW-Authenticate:' header on success
// - can optionally return the authenticated user name
// - will automatically call TryRefresh to check the file every 2 seconds
// - is a cut-down version of THttpServerSocketGeneric.Authorization(),
// assuming a simple two-way Negotiate/Kerberos handshake
function ComputeServerHeader(const InputHeaders: RawUtf8;
AuthUser: PRawUtf8 = nil): RawUtf8;
published
/// the keytab file name propagated to all server threads
property KeyTab: TFileName
read fKeyTab write _SetKeyTab;
/// the current number of assigned KeyTab since the start of this instance
property KeyTabSequence: integer
read fKeyTabSequence;
end;
/// force loading server credentials from specified keytab file
// - by default, clients may authenticate to any service principal
// in the default keytab (/etc/krb5.keytab or the value of the global
// KRB5_KTNAME environment variable)
// - this function is thread-specific and should be done on all threads, e.g.
// via TServerSspiKeyTab.PrepareKeyTab
function ServerForceKeytab(const aKeytab: TFileName): boolean;
const
/// the API available on this system to implement Kerberos
SECPKGNAMEAPI = 'GSSAPI';
/// HTTP Challenge name
// - GSS API only supports Negotiate/Kerberos - NTLM is unsafe and deprecated
SECPKGNAMEHTTP = 'Negotiate';
/// HTTP Challenge name, converted into uppercase for IdemPChar() pattern
SECPKGNAMEHTTP_UPPER = 'NEGOTIATE';
/// HTTP header to be set for authentication
// - GSS API only supports Negotiate/Kerberos - NTLM is unsafe and deprecated
SECPKGNAMEHTTPWWWAUTHENTICATE = 'WWW-Authenticate: Negotiate ';
/// HTTP header pattern received for authentication
SECPKGNAMEHTTPAUTHORIZATION = 'AUTHORIZATION: NEGOTIATE ';
/// character used as marker in user name to indicates the associated domain
SSPI_USER_CHAR = '@';
/// traditional/generic name for the environment variable of a keytab file
GSSAPI_ENV_CLIENT_KT_HEIMDAL = 'KRB5_KTNAME';
// MIT-specific extension for the environment variable of a keytab file
GSSAPI_ENV_CLIENT_KT_MIT = 'KRB5_CLIENT_KTNAME';
var
/// ServerSspiAuthUser() won't return NT4-style NetBIOS name but the realm
// - the GSS API only returns the realm (mydomain.tld) whereas Windows SSPI
// returns the NetBIOS name (e.g. MYDOMAIN)
// - default false will try to guess the NetBIOS name, or use
// ServerDomainRegister()
// - forcing this flag to true will let ServerSspiAuthUser() return the realm,
// i.e. 'MYDOMAIN.TLD\username'
ServerDomainMapUseRealm: boolean = false;
/// ClientSspiAuthWithPassword() won't try gss_krb5_ccache_name('MEMORY:...')
// - by default, a transient memory ccache will be used to not mess with the
// current ccache environment when acquiring a token from the server: on Mac,
// we have seen the transient token been added to the main klist :(
// - you may set this global to true to disable this feature (as with the
// initial behavior of this unit) if it seems to trigger some problems
// - on any GSS_ERROR on this memory ccache, this unit will force this global
// flag to true to avoid any further issue
ClientSspiAuthWithPasswordNoMemCcache: boolean = false;
/// disable MIT 1.11+ gss_acquire_cred_from() from ClientSspiAuthWithPassword()
// - fallback to default Heimdal/old-MIT compatible SetSystemEnv/ResetSystemEnv
ClientSspiAuthWithPasswordKerberosNoCredFrom: boolean = false;
/// help converting fully qualified domain names to NT4-style NetBIOS names
// - to use the same value for TAuthUser.LogonName on all platforms, user name
// should be changed from 'username@MYDOMAIN.TLD' to 'MYDOMAIN\username'
// - when converting a fully qualified domain name to NT4-style NetBIOS name,
// ServerDomainMapRegister() list is first checked. If domain name is not found,
// then it's truncated on first dot, e.g. 'user1@CORP.ABC.COM' into 'CORP\user1'
// - you can change domain name conversion by registering names at server startup,
// e.g. ServerDomainMapRegister('CORP.ABC.COM', 'ABCCORP') change conversion for
// previous example to 'ABCCORP\user1'
// - used only if automatic conversion (truncate on first dot) does it wrong,
// and if ServerDomainMapUseRealm flag has not been forced to true
// - this method is thread-safe
procedure ServerDomainMapRegister(const aOld, aNew: RawUtf8);
/// help converting fully qualified domain names to NT4-style NetBIOS names
procedure ServerDomainMapUnRegister(const aOld, aNew: RawUtf8);
/// help converting fully qualified domain names to NT4-style NetBIOS names
procedure ServerDomainMapUnRegisterAll;
/// high-level cross-platform initialization function
// - as called e.g. by mormot.rest.client/server.pas
// - in this unit, will just call LoadGssApi('')
// - you can set GssLib_Custom global variable to load a specific .so library
function InitializeDomainAuth: boolean;
{$ifdef HASINLINE} inline; {$endif}
implementation
{ ****************** Low-Level libgssapi_krb5/libgssapi.so Library Access }
function gss_compare_oid(oid1, oid2: gss_OID): boolean;
begin
result := (oid1 <> nil) and
(oid2 <> nil) and
(oid1^.length = oid2^.length) and
CompareMemSmall(oid1^.elements, oid2^.elements, oid1^.length);
end;
// see https://www.gnu.org/software/gss/manual/html_node/Error-Handling.html
function GSS_CALLING_ERROR(x: cardinal): boolean;
begin
result := (x and
(GSS_C_CALLING_ERROR_MASK shl GSS_C_CALLING_ERROR_OFFSET)) <> 0;
end;
function GSS_ROUTINE_ERROR(x: cardinal): boolean;
begin
result := (x and
(GSS_C_ROUTINE_ERROR_MASK shl GSS_C_ROUTINE_ERROR_OFFSET)) <> 0;
end;
function GSS_SUPPLEMENTARY_INFO(x: cardinal): boolean;
begin
result := (x and
(GSS_C_SUPPLEMENTARY_MASK shl GSS_C_SUPPLEMENTARY_OFFSET)) <> 0;
end;
function GSS_ERROR(x: cardinal): boolean;
begin
result := GSS_CALLING_ERROR(x) or
GSS_ROUTINE_ERROR(x);
end;
procedure GssCheck(AMajorStatus, AMinorStatus: cardinal; const APrefix: RawUtf8);
begin
if GSS_ERROR(AMajorStatus) then
raise EGssApi.Create(AMajorStatus, AMinorStatus, APrefix);
end;
const
GSS_ENTRIES: array[0 .. 20] of PAnsiChar = (
// GSSAPI entries
'gss_import_name',
'gss_display_name',
'gss_release_name',
'gss_acquire_cred',
'gss_acquire_cred_with_password',
'gss_release_cred',
'gss_init_sec_context',
'gss_accept_sec_context',
'gss_inquire_context',
'gss_delete_sec_context',
'gss_inquire_saslname_for_mech',
'gss_release_buffer',
'gss_wrap',
'gss_unwrap',
'gss_indicate_mechs',
'gss_release_oid_set',
'gss_display_status',
// MIT-only definitions
'?gss_acquire_cred_from',
// Kerberos specific entries - potentially with Heimdal alternative name
'?gss_krb5_ccache_name',
'?krb5_gss_register_acceptor_identity gsskrb5_register_acceptor_identity',
nil);
var
GssApiTried: TFileName;
procedure LoadGssApi(const LibraryName: TFileName);
var
api: TGssApi; // local instance for thread-safe load attempt
tried: TFileName;
begin
if GssApi <> nil then
// already loaded
exit;
tried := GssLib_ForceUnique;
if tried = '' then
tried := LibraryName + GssLib_Custom + GssLib_MIT + GssLib_Heimdal + GssLib_OS;
if GssApiTried = tried then
// retry LoadLibrary() only if any of the .so names changed
exit;
GssApiTried := tried;
api := TGssApi.Create;
api.TryFromExecutableFolder := GssLib_ForceUnique = ''; // check local first
if ((GssLib_ForceUnique = '') and
api.TryLoadResolve(
[LibraryName, GssLib_Custom, GssLib_MIT, GssLib_Heimdal, GssLib_OS], '',
@GSS_ENTRIES, @@api.gss_import_name, nil, @GssApi_LastLoadError)) or
((GssLib_ForceUnique <> '') and
api.TryLoadResolve([GssLib_ForceUnique], '',
@GSS_ENTRIES, @@api.gss_import_name, nil, @GssApi_LastLoadError)) then
begin
if Assigned(api.gss_acquire_cred) and
Assigned(api.gss_accept_sec_context) and
Assigned(api.gss_release_buffer) and
Assigned(api.gss_inquire_context) and
Assigned(api.gss_display_name) and
Assigned(api.gss_release_name) then
begin
// minimal API to work on server side -> thread safe setup into GSSAPI
api.IsMit := Assigned(api.gss_acquire_cred_from);
if api.IsMit then
api.EnvClientKtName := GSSAPI_ENV_CLIENT_KT_MIT // 'KRB5_CLIENT_KTNAME'
else
api.EnvClientKtName := GSSAPI_ENV_CLIENT_KT_HEIMDAL; // 'KRB5_KTNAME'
GetSystemEnv(api.EnvClientKtName, api.EnvClientKtValue); // retrieve once
GlobalLock;
try
if GssApi = nil then
begin
GssApi := api;
exit;
end;
finally
GlobalUnlock;
end;
end;
end;
// always release on setup failure
api.Free;
end;
function GssApiLoaded: boolean;
begin
result := GssApi <> nil
end;
procedure RequireGssApi;
begin
if GssApi = nil then
raise ENotSupportedException.Create('No GSSAPI library found - please ' +
'install either MIT or Heimdal GSSAPI implementation and do not ' +
'forget to call InitializeDomainAuth once');
end;
{ EGssApi }
procedure GetDisplayStatus(var Msg: RawUtf8; aErrorStatus: cardinal;
StatusType: integer);
var
Str: RawUtf8;
MsgCtx: cardinal;
MsgBuf: gss_buffer_desc;
MajSt, MinSt: cardinal;
begin
if (GssApi = nil) or
not Assigned(GssApi.gss_display_status) then
exit;
MsgCtx := 0;
repeat
MajSt := GssApi.gss_display_status(
MinSt, aErrorStatus, StatusType, nil, MsgCtx, MsgBuf);
FastSetString(Str, MsgBuf.value, MsgBuf.length);
GssApi.gss_release_buffer(MinSt, MsgBuf);
if Msg <> '' then
Msg := Join([Msg, ' - ', Str])
else
Msg := Str;
until GSS_ERROR(MajSt) or
(MsgCtx = 0);
end;
constructor EGssApi.Create(aMajor, aMinor: cardinal; const aPrefix: RawUtf8);
var
Msg: RawUtf8;
begin
Msg := aPrefix;
GetDisplayStatus(Msg, aMajor, GSS_C_GSS_CODE);
if (aMinor <> 0) and
(aMinor <> 100001) then
GetDisplayStatus(Msg, aMinor, GSS_C_MECH_CODE);
inherited Create(Utf8ToString(Msg));
fMajorStatus := aMajor;
fMinorStatus := aMinor;
end;
{ ****************** Middle-Level GSSAPI Wrappers }
procedure InvalidateSecContext(var aSecContext: TSecContext);
begin
FillCharFast(aSecContext, SizeOf(aSecContext), 0);
end;
procedure FreeSecContext(var aSecContext: TSecContext);
var
minstatus: cardinal;
begin
if aSecContext.CtxHandle <> nil then
GssApi.gss_delete_sec_context(minstatus, aSecContext.CtxHandle, nil);
if aSecContext.CredHandle <> nil then
GssApi.gss_release_cred(minstatus, aSecContext.CredHandle);
if aSecContext.ClientTargetName <> nil then
GssApi.gss_release_name(minstatus, aSecContext.ClientTargetName);
if aSecContext.ResetEnv and
Assigned(GssApi) then
ResetSystemEnv(GssApi.EnvClientKtName); // env should remain until the end
InvalidateSecContext(aSecContext);
end;
// see https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi
function SecEncrypt(var aSecContext: TSecContext;
const aPlain: RawByteString): RawByteString;
var
MajStatus, MinStatus: cardinal;
InBuf: gss_buffer_desc;
OutBuf: gss_buffer_desc;
begin
InBuf.length := Length(aPlain);
InBuf.value := pointer(aPlain);
MajStatus := GssApi.gss_wrap(
MinStatus, aSecContext.CtxHandle, 1, 0, @InBuf, nil, @OutBuf);
GssCheck(MajStatus, MinStatus, 'Failed to encrypt message');
FastSetRawByteString(result, OutBuf.value, OutBuf.length);
GssApi.gss_release_buffer(MinStatus, OutBuf);
end;
function SecDecrypt(var aSecContext: TSecContext;
const aEncrypted: RawByteString): RawByteString;
var
MajStatus, MinStatus: cardinal;
InBuf: gss_buffer_desc;
OutBuf: gss_buffer_desc;
begin
InBuf.length := Length(aEncrypted);
InBuf.value := pointer(aEncrypted);
MajStatus := GssApi.gss_unwrap(
MinStatus, aSecContext.CtxHandle, @InBuf, @OutBuf, nil, nil);
GssCheck(MajStatus, MinStatus, 'Failed to decrypt message');
FastSetRawByteString(result, OutBuf.value, OutBuf.length);
GssApi.gss_release_buffer(MinStatus, OutBuf);
end;
function GssEnlistMechsSupported(oid: PBytesDynArray): TRawUtf8DynArray;
var
i: PtrInt;
o: gss_OID;
MinSt: cardinal;
Mechs: gss_OID_set;
Buf_sasl, Buf_name, Buf_desc: gss_buffer_desc;
Sasl, Name, Desc: RawUtf8;
begin
result := nil;
RequireGssApi;
if not Assigned(GssApi.gss_indicate_mechs) or
not Assigned(GssApi.gss_inquire_saslname_for_mech) then
exit;
GssApi.gss_indicate_mechs(MinSt, Mechs);
SetLength(result, Mechs^.count);
if oid <> nil then
SetLength(oid^, Mechs^.count);
for i := 0 to Mechs^.count - 1 do
begin
if oid <> nil then
with Mechs^.elements^[i] do
begin // store as gss_OID() binary buffer