Coverage Report

Created: 2025-12-14 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/FreeRDP/libfreerdp/core/aad.c
Line
Count
Source
1
/**
2
 * FreeRDP: A Remote Desktop Protocol Implementation
3
 * Network Level Authentication (NLA)
4
 *
5
 * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
20
#include <freerdp/config.h>
21
22
#include <stdio.h>
23
#include <string.h>
24
25
#include <freerdp/crypto/crypto.h>
26
#include <freerdp/crypto/privatekey.h>
27
#include "../crypto/privatekey.h"
28
#include <freerdp/utils/http.h>
29
#include <freerdp/utils/aad.h>
30
31
#include <winpr/crypto.h>
32
#include <winpr/json.h>
33
34
#include "transport.h"
35
#include "rdp.h"
36
37
#include "aad.h"
38
39
struct rdp_aad
40
{
41
  AAD_STATE state;
42
  rdpContext* rdpcontext;
43
  rdpTransport* transport;
44
  char* access_token;
45
  rdpPrivateKey* key;
46
  char* kid;
47
  char* nonce;
48
  char* hostname;
49
  char* scope;
50
  wLog* log;
51
};
52
53
#ifdef WITH_AAD
54
55
static BOOL aad_fetch_wellknown(wLog* log, rdpContext* context);
56
static BOOL get_encoded_rsa_params(wLog* wlog, rdpPrivateKey* key, char** e, char** n);
57
static BOOL generate_pop_key(rdpAad* aad);
58
59
WINPR_ATTR_FORMAT_ARG(2, 3)
60
static SSIZE_T stream_sprintf(wStream* s, WINPR_FORMAT_ARG const char* fmt, ...)
61
{
62
  va_list ap = { 0 };
63
  va_start(ap, fmt);
64
  const int rc = vsnprintf(NULL, 0, fmt, ap);
65
  va_end(ap);
66
67
  if (rc < 0)
68
    return rc;
69
70
  if (!Stream_EnsureRemainingCapacity(s, (size_t)rc + 1))
71
    return -1;
72
73
  char* ptr = Stream_PointerAs(s, char);
74
  va_start(ap, fmt);
75
  const int rc2 = vsnprintf(ptr, WINPR_ASSERTING_INT_CAST(size_t, rc) + 1, fmt, ap);
76
  va_end(ap);
77
  if (rc != rc2)
78
    return -23;
79
  if (!Stream_SafeSeek(s, (size_t)rc2))
80
    return -3;
81
  return rc2;
82
}
83
84
static BOOL json_get_object(wLog* wlog, WINPR_JSON* json, const char* key, WINPR_JSON** obj)
85
{
86
  WINPR_ASSERT(json);
87
  WINPR_ASSERT(key);
88
89
  if (!WINPR_JSON_HasObjectItem(json, key))
90
  {
91
    WLog_Print(wlog, WLOG_ERROR, "[json] does not contain a key '%s'", key);
92
    return FALSE;
93
  }
94
95
  WINPR_JSON* prop = WINPR_JSON_GetObjectItemCaseSensitive(json, key);
96
  if (!prop)
97
  {
98
    WLog_Print(wlog, WLOG_ERROR, "[json] object for key '%s' is NULL", key);
99
    return FALSE;
100
  }
101
  *obj = prop;
102
  return TRUE;
103
}
104
105
static BOOL json_get_number(wLog* wlog, WINPR_JSON* json, const char* key, double* result)
106
{
107
  BOOL rc = FALSE;
108
  WINPR_JSON* prop = NULL;
109
  if (!json_get_object(wlog, json, key, &prop))
110
    return FALSE;
111
112
  if (!WINPR_JSON_IsNumber(prop))
113
  {
114
    WLog_Print(wlog, WLOG_ERROR, "[json] object for key '%s' is NOT a NUMBER", key);
115
    goto fail;
116
  }
117
118
  *result = WINPR_JSON_GetNumberValue(prop);
119
120
  rc = TRUE;
121
fail:
122
  return rc;
123
}
124
125
static BOOL json_get_const_string(wLog* wlog, WINPR_JSON* json, const char* key,
126
                                  const char** result)
127
{
128
  BOOL rc = FALSE;
129
  WINPR_ASSERT(result);
130
131
  *result = NULL;
132
133
  WINPR_JSON* prop = NULL;
134
  if (!json_get_object(wlog, json, key, &prop))
135
    return FALSE;
136
137
  if (!WINPR_JSON_IsString(prop))
138
  {
139
    WLog_Print(wlog, WLOG_ERROR, "[json] object for key '%s' is NOT a STRING", key);
140
    goto fail;
141
  }
142
143
  const char* str = WINPR_JSON_GetStringValue(prop);
144
  if (!str)
145
    WLog_Print(wlog, WLOG_ERROR, "[json] object for key '%s' is NULL", key);
146
  *result = str;
147
  rc = str != NULL;
148
149
fail:
150
  return rc;
151
}
152
153
static BOOL json_get_string_alloc(wLog* wlog, WINPR_JSON* json, const char* key, char** result)
154
{
155
  const char* str = NULL;
156
  if (!json_get_const_string(wlog, json, key, &str))
157
    return FALSE;
158
  free(*result);
159
  *result = _strdup(str);
160
  if (!*result)
161
    WLog_Print(wlog, WLOG_ERROR, "[json] object for key '%s' strdup is NULL", key);
162
  return *result != NULL;
163
}
164
165
static inline const char* aad_auth_result_to_string(DWORD code)
166
{
167
#define ERROR_CASE(cd, x)   \
168
  if ((cd) == (DWORD)(x)) \
169
    return #x;
170
171
  ERROR_CASE(code, S_OK)
172
  ERROR_CASE(code, SEC_E_INVALID_TOKEN)
173
  ERROR_CASE(code, E_ACCESSDENIED)
174
  ERROR_CASE(code, STATUS_LOGON_FAILURE)
175
  ERROR_CASE(code, STATUS_NO_LOGON_SERVERS)
176
  ERROR_CASE(code, STATUS_INVALID_LOGON_HOURS)
177
  ERROR_CASE(code, STATUS_INVALID_WORKSTATION)
178
  ERROR_CASE(code, STATUS_PASSWORD_EXPIRED)
179
  ERROR_CASE(code, STATUS_ACCOUNT_DISABLED)
180
  return "Unknown error";
181
}
182
183
static BOOL ensure_wellknown(rdpContext* context)
184
{
185
  if (context->rdp->wellknown)
186
    return TRUE;
187
188
  rdpAad* aad = context->rdp->aad;
189
  if (!aad)
190
    return FALSE;
191
192
  if (!aad_fetch_wellknown(aad->log, context))
193
    return FALSE;
194
  return context->rdp->wellknown != NULL;
195
}
196
197
static BOOL aad_get_nonce(rdpAad* aad)
198
{
199
  BOOL ret = FALSE;
200
  BYTE* response = NULL;
201
  long resp_code = 0;
202
  size_t response_length = 0;
203
  WINPR_JSON* json = NULL;
204
205
  WINPR_ASSERT(aad);
206
  WINPR_ASSERT(aad->rdpcontext);
207
208
  rdpRdp* rdp = aad->rdpcontext->rdp;
209
  WINPR_ASSERT(rdp);
210
211
  if (!ensure_wellknown(aad->rdpcontext))
212
    return FALSE;
213
214
  WINPR_JSON* obj = WINPR_JSON_GetObjectItemCaseSensitive(rdp->wellknown, "token_endpoint");
215
  if (!obj)
216
  {
217
    WLog_Print(aad->log, WLOG_ERROR, "wellknown does not have 'token_endpoint', aborting");
218
    return FALSE;
219
  }
220
  const char* url = WINPR_JSON_GetStringValue(obj);
221
  if (!url)
222
  {
223
    WLog_Print(aad->log, WLOG_ERROR,
224
               "wellknown does have 'token_endpoint=NULL' value, aborting");
225
    return FALSE;
226
  }
227
228
  if (!freerdp_http_request(url, "grant_type=srv_challenge", &resp_code, &response,
229
                            &response_length))
230
  {
231
    WLog_Print(aad->log, WLOG_ERROR, "nonce request failed");
232
    goto fail;
233
  }
234
235
  if (resp_code != HTTP_STATUS_OK)
236
  {
237
    WLog_Print(aad->log, WLOG_ERROR,
238
               "Server unwilling to provide nonce; returned status code %li", resp_code);
239
    if (response_length > 0)
240
      WLog_Print(aad->log, WLOG_ERROR, "[status message] %s", response);
241
    goto fail;
242
  }
243
244
  json = WINPR_JSON_ParseWithLength((const char*)response, response_length);
245
  if (!json)
246
  {
247
    WLog_Print(aad->log, WLOG_ERROR, "Failed to parse nonce response: %s",
248
               WINPR_JSON_GetErrorPtr());
249
    goto fail;
250
  }
251
252
  if (!json_get_string_alloc(aad->log, json, "Nonce", &aad->nonce))
253
    goto fail;
254
255
  ret = TRUE;
256
257
fail:
258
  free(response);
259
  WINPR_JSON_Delete(json);
260
  return ret;
261
}
262
263
int aad_client_begin(rdpAad* aad)
264
{
265
  size_t size = 0;
266
267
  WINPR_ASSERT(aad);
268
  WINPR_ASSERT(aad->rdpcontext);
269
270
  rdpSettings* settings = aad->rdpcontext->settings;
271
  WINPR_ASSERT(settings);
272
273
  /* Get the host part of the hostname */
274
  const char* hostname = freerdp_settings_get_string(settings, FreeRDP_AadServerHostname);
275
  if (!hostname)
276
    hostname = freerdp_settings_get_server_name(settings);
277
  if (!hostname)
278
  {
279
    WLog_Print(aad->log, WLOG_ERROR, "hostname == NULL");
280
    return -1;
281
  }
282
283
  aad->hostname = _strdup(hostname);
284
  if (!aad->hostname)
285
  {
286
    WLog_Print(aad->log, WLOG_ERROR, "_strdup(hostname) == NULL");
287
    return -1;
288
  }
289
290
  char* p = strchr(aad->hostname, '.');
291
  if (p)
292
    *p = '\0';
293
294
  if (winpr_asprintf(&aad->scope, &size,
295
                     "ms-device-service%%3A%%2F%%2Ftermsrv.wvd.microsoft.com%%2Fname%%2F%s%%"
296
                     "2Fuser_impersonation",
297
                     aad->hostname) <= 0)
298
    return -1;
299
300
  if (!generate_pop_key(aad))
301
    return -1;
302
303
  /* Obtain an oauth authorization code */
304
  pGetCommonAccessToken GetCommonAccessToken = freerdp_get_common_access_token(aad->rdpcontext);
305
  if (!GetCommonAccessToken)
306
  {
307
    WLog_Print(aad->log, WLOG_ERROR, "GetCommonAccessToken == NULL");
308
    return -1;
309
  }
310
311
  if (!aad_fetch_wellknown(aad->log, aad->rdpcontext))
312
    return -1;
313
314
  const BOOL arc = GetCommonAccessToken(aad->rdpcontext, ACCESS_TOKEN_TYPE_AAD,
315
                                        &aad->access_token, 2, aad->scope, aad->kid);
316
  if (!arc)
317
  {
318
    WLog_Print(aad->log, WLOG_ERROR, "Unable to obtain access token");
319
    return -1;
320
  }
321
322
  /* Send the nonce request message */
323
  if (!aad_get_nonce(aad))
324
  {
325
    WLog_Print(aad->log, WLOG_ERROR, "Unable to obtain nonce");
326
    return -1;
327
  }
328
329
  return 1;
330
}
331
332
static char* aad_create_jws_header(rdpAad* aad)
333
{
334
  WINPR_ASSERT(aad);
335
336
  /* Construct the base64url encoded JWS header */
337
  char* buffer = NULL;
338
  size_t bufferlen = 0;
339
  const int length =
340
      winpr_asprintf(&buffer, &bufferlen, "{\"alg\":\"RS256\",\"kid\":\"%s\"}", aad->kid);
341
  if (length < 0)
342
    return NULL;
343
344
  char* jws_header = crypto_base64url_encode((const BYTE*)buffer, bufferlen);
345
  free(buffer);
346
  return jws_header;
347
}
348
349
static char* aad_create_jws_payload(rdpAad* aad, const char* ts_nonce)
350
{
351
  const time_t ts = time(NULL);
352
353
  WINPR_ASSERT(aad);
354
355
  char* e = NULL;
356
  char* n = NULL;
357
  if (!get_encoded_rsa_params(aad->log, aad->key, &e, &n))
358
    return NULL;
359
360
  /* Construct the base64url encoded JWS payload */
361
  char* buffer = NULL;
362
  size_t bufferlen = 0;
363
  const int length =
364
      winpr_asprintf(&buffer, &bufferlen,
365
                     "{"
366
                     "\"ts\":\"%li\","
367
                     "\"at\":\"%s\","
368
                     "\"u\":\"ms-device-service://termsrv.wvd.microsoft.com/name/%s\","
369
                     "\"nonce\":\"%s\","
370
                     "\"cnf\":{\"jwk\":{\"kty\":\"RSA\",\"e\":\"%s\",\"n\":\"%s\"}},"
371
                     "\"client_claims\":\"{\\\"aad_nonce\\\":\\\"%s\\\"}\""
372
                     "}",
373
                     ts, aad->access_token, aad->hostname, ts_nonce, e, n, aad->nonce);
374
  free(e);
375
  free(n);
376
377
  if (length < 0)
378
    return NULL;
379
380
  char* jws_payload = crypto_base64url_encode((BYTE*)buffer, bufferlen);
381
  free(buffer);
382
  return jws_payload;
383
}
384
385
static BOOL aad_update_digest(rdpAad* aad, WINPR_DIGEST_CTX* ctx, const char* what)
386
{
387
  WINPR_ASSERT(aad);
388
  WINPR_ASSERT(ctx);
389
  WINPR_ASSERT(what);
390
391
  const BOOL dsu1 = winpr_DigestSign_Update(ctx, what, strlen(what));
392
  if (!dsu1)
393
  {
394
    WLog_Print(aad->log, WLOG_ERROR, "winpr_DigestSign_Update [%s] failed", what);
395
    return FALSE;
396
  }
397
  return TRUE;
398
}
399
400
static char* aad_final_digest(rdpAad* aad, WINPR_DIGEST_CTX* ctx)
401
{
402
  char* jws_signature = NULL;
403
404
  WINPR_ASSERT(aad);
405
  WINPR_ASSERT(ctx);
406
407
  size_t siglen = 0;
408
  const int dsf = winpr_DigestSign_Final(ctx, NULL, &siglen);
409
  if (dsf <= 0)
410
  {
411
    WLog_Print(aad->log, WLOG_ERROR, "winpr_DigestSign_Final failed with %d", dsf);
412
    return NULL;
413
  }
414
415
  char* buffer = calloc(siglen + 1, sizeof(char));
416
  if (!buffer)
417
  {
418
    WLog_Print(aad->log, WLOG_ERROR, "calloc %" PRIuz " bytes failed", siglen + 1);
419
    goto fail;
420
  }
421
422
  size_t fsiglen = siglen;
423
  const int dsf2 = winpr_DigestSign_Final(ctx, (BYTE*)buffer, &fsiglen);
424
  if (dsf2 <= 0)
425
  {
426
    WLog_Print(aad->log, WLOG_ERROR, "winpr_DigestSign_Final failed with %d", dsf2);
427
    goto fail;
428
  }
429
430
  if (siglen != fsiglen)
431
  {
432
    WLog_Print(aad->log, WLOG_ERROR,
433
               "winpr_DigestSignFinal returned different sizes, first %" PRIuz " then %" PRIuz,
434
               siglen, fsiglen);
435
    goto fail;
436
  }
437
  jws_signature = crypto_base64url_encode((const BYTE*)buffer, fsiglen);
438
fail:
439
  free(buffer);
440
  return jws_signature;
441
}
442
443
static char* aad_create_jws_signature(rdpAad* aad, const char* jws_header, const char* jws_payload)
444
{
445
  char* jws_signature = NULL;
446
447
  WINPR_ASSERT(aad);
448
449
  WINPR_DIGEST_CTX* md_ctx = freerdp_key_digest_sign(aad->key, WINPR_MD_SHA256);
450
  if (!md_ctx)
451
  {
452
    WLog_Print(aad->log, WLOG_ERROR, "winpr_Digest_New failed");
453
    goto fail;
454
  }
455
456
  if (!aad_update_digest(aad, md_ctx, jws_header))
457
    goto fail;
458
  if (!aad_update_digest(aad, md_ctx, "."))
459
    goto fail;
460
  if (!aad_update_digest(aad, md_ctx, jws_payload))
461
    goto fail;
462
463
  jws_signature = aad_final_digest(aad, md_ctx);
464
fail:
465
  winpr_Digest_Free(md_ctx);
466
  return jws_signature;
467
}
468
469
static int aad_send_auth_request(rdpAad* aad, const char* ts_nonce)
470
{
471
  int ret = -1;
472
  char* jws_header = NULL;
473
  char* jws_payload = NULL;
474
  char* jws_signature = NULL;
475
476
  WINPR_ASSERT(aad);
477
  WINPR_ASSERT(ts_nonce);
478
479
  wStream* s = Stream_New(NULL, 1024);
480
  if (!s)
481
    goto fail;
482
483
  /* Construct the base64url encoded JWS header */
484
  jws_header = aad_create_jws_header(aad);
485
  if (!jws_header)
486
    goto fail;
487
488
  /* Construct the base64url encoded JWS payload */
489
  jws_payload = aad_create_jws_payload(aad, ts_nonce);
490
  if (!jws_payload)
491
    goto fail;
492
493
  /* Sign the JWS with the pop key */
494
  jws_signature = aad_create_jws_signature(aad, jws_header, jws_payload);
495
  if (!jws_signature)
496
    goto fail;
497
498
  /* Construct the Authentication Request PDU with the JWS as the RDP Assertion */
499
  if (stream_sprintf(s, "{\"rdp_assertion\":\"%s.%s.%s\"}", jws_header, jws_payload,
500
                     jws_signature) < 0)
501
    goto fail;
502
503
  /* Include null terminator in PDU */
504
  Stream_Write_UINT8(s, 0);
505
506
  Stream_SealLength(s);
507
508
  if (transport_write(aad->transport, s) < 0)
509
  {
510
    WLog_Print(aad->log, WLOG_ERROR, "transport_write [%" PRIuz " bytes] failed",
511
               Stream_Length(s));
512
  }
513
  else
514
  {
515
    ret = 1;
516
    aad->state = AAD_STATE_AUTH;
517
  }
518
fail:
519
  Stream_Free(s, TRUE);
520
  free(jws_header);
521
  free(jws_payload);
522
  free(jws_signature);
523
524
  return ret;
525
}
526
527
static int aad_parse_state_initial(rdpAad* aad, wStream* s)
528
{
529
  const char* jstr = Stream_PointerAs(s, char);
530
  const size_t jlen = Stream_GetRemainingLength(s);
531
  const char* ts_nonce = NULL;
532
  int ret = -1;
533
  WINPR_JSON* json = NULL;
534
535
  if (!Stream_SafeSeek(s, jlen))
536
    goto fail;
537
538
  json = WINPR_JSON_ParseWithLength(jstr, jlen);
539
  if (!json)
540
  {
541
    WLog_Print(aad->log, WLOG_ERROR, "WINPR_JSON_ParseWithLength failed: %s",
542
               WINPR_JSON_GetErrorPtr());
543
    goto fail;
544
  }
545
546
  if (!json_get_const_string(aad->log, json, "ts_nonce", &ts_nonce))
547
    goto fail;
548
549
  ret = aad_send_auth_request(aad, ts_nonce);
550
fail:
551
  WINPR_JSON_Delete(json);
552
  return ret;
553
}
554
555
static int aad_parse_state_auth(rdpAad* aad, wStream* s)
556
{
557
  int rc = -1;
558
  double result = 0;
559
  DWORD error_code = 0;
560
  WINPR_JSON* json = NULL;
561
  const char* jstr = Stream_PointerAs(s, char);
562
  const size_t jlength = Stream_GetRemainingLength(s);
563
564
  if (!Stream_SafeSeek(s, jlength))
565
    goto fail;
566
567
  json = WINPR_JSON_ParseWithLength(jstr, jlength);
568
  if (!json)
569
  {
570
    WLog_Print(aad->log, WLOG_ERROR, "WINPR_JSON_ParseWithLength: %s",
571
               WINPR_JSON_GetErrorPtr());
572
    goto fail;
573
  }
574
575
  if (!json_get_number(aad->log, json, "authentication_result", &result))
576
    goto fail;
577
  error_code = (DWORD)result;
578
579
  if (error_code != S_OK)
580
  {
581
    WLog_Print(aad->log, WLOG_ERROR, "Authentication result: %s (0x%08" PRIx32 ")",
582
               aad_auth_result_to_string(error_code), error_code);
583
    goto fail;
584
  }
585
  aad->state = AAD_STATE_FINAL;
586
  rc = 1;
587
fail:
588
  WINPR_JSON_Delete(json);
589
  return rc;
590
}
591
592
int aad_recv(rdpAad* aad, wStream* s)
593
{
594
  WINPR_ASSERT(aad);
595
  WINPR_ASSERT(s);
596
597
  switch (aad->state)
598
  {
599
    case AAD_STATE_INITIAL:
600
      return aad_parse_state_initial(aad, s);
601
    case AAD_STATE_AUTH:
602
      return aad_parse_state_auth(aad, s);
603
    case AAD_STATE_FINAL:
604
    default:
605
      WLog_Print(aad->log, WLOG_ERROR, "Invalid AAD_STATE %u", aad->state);
606
      return -1;
607
  }
608
}
609
610
static BOOL generate_rsa_2048(rdpAad* aad)
611
{
612
  WINPR_ASSERT(aad);
613
  return freerdp_key_generate(aad->key, "RSA", 1, 2048);
614
}
615
616
static char* generate_rsa_digest_base64_str(rdpAad* aad, const char* input, size_t ilen)
617
{
618
  char* b64 = NULL;
619
  WINPR_DIGEST_CTX* digest = winpr_Digest_New();
620
  if (!digest)
621
  {
622
    WLog_Print(aad->log, WLOG_ERROR, "winpr_Digest_New failed");
623
    goto fail;
624
  }
625
626
  if (!winpr_Digest_Init(digest, WINPR_MD_SHA256))
627
  {
628
    WLog_Print(aad->log, WLOG_ERROR, "winpr_Digest_Init(WINPR_MD_SHA256) failed");
629
    goto fail;
630
  }
631
632
  if (!winpr_Digest_Update(digest, (const BYTE*)input, ilen))
633
  {
634
    WLog_Print(aad->log, WLOG_ERROR, "winpr_Digest_Update(%" PRIuz ") failed", ilen);
635
    goto fail;
636
  }
637
638
  BYTE hash[WINPR_SHA256_DIGEST_LENGTH] = { 0 };
639
  if (!winpr_Digest_Final(digest, hash, sizeof(hash)))
640
  {
641
    WLog_Print(aad->log, WLOG_ERROR, "winpr_Digest_Final(%" PRIuz ") failed", sizeof(hash));
642
    goto fail;
643
  }
644
645
  /* Base64url encode the hash */
646
  b64 = crypto_base64url_encode(hash, sizeof(hash));
647
648
fail:
649
  winpr_Digest_Free(digest);
650
  return b64;
651
}
652
653
static BOOL generate_json_base64_str(rdpAad* aad, const char* b64_hash)
654
{
655
  WINPR_ASSERT(aad);
656
657
  char* buffer = NULL;
658
  size_t blen = 0;
659
  const int length = winpr_asprintf(&buffer, &blen, "{\"kid\":\"%s\"}", b64_hash);
660
  if (length < 0)
661
    return FALSE;
662
663
  /* Finally, base64url encode the JSON text to form the kid */
664
  free(aad->kid);
665
  aad->kid = crypto_base64url_encode((const BYTE*)buffer, (size_t)length);
666
  free(buffer);
667
668
  if (!aad->kid)
669
  {
670
    return FALSE;
671
  }
672
  return TRUE;
673
}
674
675
BOOL generate_pop_key(rdpAad* aad)
676
{
677
  BOOL ret = FALSE;
678
  char* buffer = NULL;
679
  char* b64_hash = NULL;
680
  char* e = NULL;
681
  char* n = NULL;
682
683
  WINPR_ASSERT(aad);
684
685
  /* Generate a 2048-bit RSA key pair */
686
  if (!generate_rsa_2048(aad))
687
    goto fail;
688
689
  /* Encode the public key as a JWK */
690
  if (!get_encoded_rsa_params(aad->log, aad->key, &e, &n))
691
    goto fail;
692
693
  size_t blen = 0;
694
  const int alen =
695
      winpr_asprintf(&buffer, &blen, "{\"e\":\"%s\",\"kty\":\"RSA\",\"n\":\"%s\"}", e, n);
696
  if (alen < 0)
697
    goto fail;
698
699
  /* Hash the encoded public key */
700
  b64_hash = generate_rsa_digest_base64_str(aad, buffer, blen);
701
  if (!b64_hash)
702
    goto fail;
703
704
  /* Encode a JSON object with a single property "kid" whose value is the encoded hash */
705
  ret = generate_json_base64_str(aad, b64_hash);
706
707
fail:
708
  free(b64_hash);
709
  free(buffer);
710
  free(e);
711
  free(n);
712
  return ret;
713
}
714
715
static char* bn_to_base64_url(wLog* wlog, rdpPrivateKey* key, enum FREERDP_KEY_PARAM param)
716
{
717
  WINPR_ASSERT(wlog);
718
  WINPR_ASSERT(key);
719
720
  size_t len = 0;
721
  BYTE* bn = freerdp_key_get_param(key, param, &len);
722
  if (!bn)
723
    return NULL;
724
725
  char* b64 = crypto_base64url_encode(bn, len);
726
  free(bn);
727
728
  if (!b64)
729
    WLog_Print(wlog, WLOG_ERROR, "failed  base64 url encode BIGNUM");
730
731
  return b64;
732
}
733
734
BOOL get_encoded_rsa_params(wLog* wlog, rdpPrivateKey* key, char** pe, char** pn)
735
{
736
  BOOL rc = FALSE;
737
  char* e = NULL;
738
  char* n = NULL;
739
740
  WINPR_ASSERT(wlog);
741
  WINPR_ASSERT(key);
742
  WINPR_ASSERT(pe);
743
  WINPR_ASSERT(pn);
744
745
  *pe = NULL;
746
  *pn = NULL;
747
748
  e = bn_to_base64_url(wlog, key, FREERDP_KEY_PARAM_RSA_E);
749
  if (!e)
750
  {
751
    WLog_Print(wlog, WLOG_ERROR, "failed  base64 url encode RSA E");
752
    goto fail;
753
  }
754
755
  n = bn_to_base64_url(wlog, key, FREERDP_KEY_PARAM_RSA_N);
756
  if (!n)
757
  {
758
    WLog_Print(wlog, WLOG_ERROR, "failed  base64 url encode RSA N");
759
    goto fail;
760
  }
761
762
  rc = TRUE;
763
fail:
764
  if (!rc)
765
  {
766
    free(e);
767
    free(n);
768
  }
769
  else
770
  {
771
    *pe = e;
772
    *pn = n;
773
  }
774
  return rc;
775
}
776
#else
777
int aad_client_begin(rdpAad* aad)
778
0
{
779
0
  WINPR_ASSERT(aad);
780
0
  WLog_Print(aad->log, WLOG_ERROR, "AAD security not compiled in, aborting!");
781
0
  return -1;
782
0
}
783
784
int aad_recv(rdpAad* aad, wStream* s)
785
0
{
786
0
  WINPR_ASSERT(aad);
787
0
  WLog_Print(aad->log, WLOG_ERROR, "AAD security not compiled in, aborting!");
788
0
  return -1;
789
0
}
790
791
static BOOL ensure_wellknown(WINPR_ATTR_UNUSED rdpContext* context)
792
0
{
793
0
  return FALSE;
794
0
}
795
796
#endif
797
798
rdpAad* aad_new(rdpContext* context, rdpTransport* transport)
799
18.1k
{
800
18.1k
  WINPR_ASSERT(transport);
801
18.1k
  WINPR_ASSERT(context);
802
803
18.1k
  rdpAad* aad = (rdpAad*)calloc(1, sizeof(rdpAad));
804
805
18.1k
  if (!aad)
806
0
    return NULL;
807
808
18.1k
  aad->log = WLog_Get(FREERDP_TAG("aad"));
809
18.1k
  aad->key = freerdp_key_new();
810
18.1k
  if (!aad->key)
811
0
    goto fail;
812
18.1k
  aad->rdpcontext = context;
813
18.1k
  aad->transport = transport;
814
815
18.1k
  return aad;
816
0
fail:
817
0
  WINPR_PRAGMA_DIAG_PUSH
818
0
  WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC
819
0
  aad_free(aad);
820
0
  WINPR_PRAGMA_DIAG_POP
821
0
  return NULL;
822
18.1k
}
823
824
void aad_free(rdpAad* aad)
825
36.2k
{
826
36.2k
  if (!aad)
827
18.1k
    return;
828
829
18.1k
  free(aad->hostname);
830
18.1k
  free(aad->scope);
831
18.1k
  free(aad->nonce);
832
18.1k
  free(aad->access_token);
833
18.1k
  free(aad->kid);
834
18.1k
  freerdp_key_free(aad->key);
835
836
18.1k
  free(aad);
837
18.1k
}
838
839
AAD_STATE aad_get_state(rdpAad* aad)
840
0
{
841
0
  WINPR_ASSERT(aad);
842
0
  return aad->state;
843
0
}
844
845
BOOL aad_is_supported(void)
846
0
{
847
#ifdef WITH_AAD
848
  return TRUE;
849
#else
850
0
  return FALSE;
851
0
#endif
852
0
}
853
854
char* freerdp_utils_aad_get_access_token(wLog* log, const char* data, size_t length)
855
0
{
856
0
  char* token = NULL;
857
0
  WINPR_JSON* access_token_prop = NULL;
858
0
  const char* access_token_str = NULL;
859
860
0
  WINPR_JSON* json = WINPR_JSON_ParseWithLength(data, length);
861
0
  if (!json)
862
0
  {
863
0
    WLog_Print(log, WLOG_ERROR,
864
0
               "Failed to parse access token response [got %" PRIuz " bytes: %s", length,
865
0
               WINPR_JSON_GetErrorPtr());
866
0
    goto cleanup;
867
0
  }
868
869
0
  access_token_prop = WINPR_JSON_GetObjectItemCaseSensitive(json, "access_token");
870
0
  if (!access_token_prop)
871
0
  {
872
0
    WLog_Print(log, WLOG_ERROR, "Response has no \"access_token\" property");
873
0
    goto cleanup;
874
0
  }
875
876
0
  access_token_str = WINPR_JSON_GetStringValue(access_token_prop);
877
0
  if (!access_token_str)
878
0
  {
879
0
    WLog_Print(log, WLOG_ERROR, "Invalid value for \"access_token\"");
880
0
    goto cleanup;
881
0
  }
882
883
0
  token = _strdup(access_token_str);
884
885
0
cleanup:
886
0
  WINPR_JSON_Delete(json);
887
0
  return token;
888
0
}
889
890
BOOL aad_fetch_wellknown(wLog* log, rdpContext* context)
891
0
{
892
0
  WINPR_ASSERT(context);
893
894
0
  rdpRdp* rdp = context->rdp;
895
0
  WINPR_ASSERT(rdp);
896
897
0
  if (rdp->wellknown)
898
0
    return TRUE;
899
900
0
  const char* base =
901
0
      freerdp_settings_get_string(context->settings, FreeRDP_GatewayAzureActiveDirectory);
902
0
  const BOOL useTenant =
903
0
      freerdp_settings_get_bool(context->settings, FreeRDP_GatewayAvdUseTenantid);
904
0
  const char* tenantid = "common";
905
0
  if (useTenant)
906
0
    tenantid = freerdp_settings_get_string(context->settings, FreeRDP_GatewayAvdAadtenantid);
907
0
  rdp->wellknown = freerdp_utils_aad_get_wellknown(log, base, tenantid);
908
0
  return rdp->wellknown ? TRUE : FALSE;
909
0
}
910
911
const char* freerdp_utils_aad_get_wellknown_string(rdpContext* context, AAD_WELLKNOWN_VALUES which)
912
0
{
913
0
  return freerdp_utils_aad_get_wellknown_custom_string(
914
0
      context, freerdp_utils_aad_wellknwon_value_name(which));
915
0
}
916
917
const char* freerdp_utils_aad_get_wellknown_custom_string(rdpContext* context, const char* which)
918
0
{
919
0
  WINPR_ASSERT(context);
920
0
  WINPR_ASSERT(context->rdp);
921
922
0
  if (!ensure_wellknown(context))
923
0
    return NULL;
924
925
0
  WINPR_JSON* obj = WINPR_JSON_GetObjectItemCaseSensitive(context->rdp->wellknown, which);
926
0
  if (!obj)
927
0
    return NULL;
928
929
0
  return WINPR_JSON_GetStringValue(obj);
930
0
}
931
932
const char* freerdp_utils_aad_wellknwon_value_name(AAD_WELLKNOWN_VALUES which)
933
0
{
934
0
  switch (which)
935
0
  {
936
0
    case AAD_WELLKNOWN_token_endpoint:
937
0
      return "token_endpoint";
938
0
    case AAD_WELLKNOWN_token_endpoint_auth_methods_supported:
939
0
      return "token_endpoint_auth_methods_supported";
940
0
    case AAD_WELLKNOWN_jwks_uri:
941
0
      return "jwks_uri";
942
0
    case AAD_WELLKNOWN_response_modes_supported:
943
0
      return "response_modes_supported";
944
0
    case AAD_WELLKNOWN_subject_types_supported:
945
0
      return "subject_types_supported";
946
0
    case AAD_WELLKNOWN_id_token_signing_alg_values_supported:
947
0
      return "id_token_signing_alg_values_supported";
948
0
    case AAD_WELLKNOWN_response_types_supported:
949
0
      return "response_types_supported";
950
0
    case AAD_WELLKNOWN_scopes_supported:
951
0
      return "scopes_supported";
952
0
    case AAD_WELLKNOWN_issuer:
953
0
      return "issuer";
954
0
    case AAD_WELLKNOWN_request_uri_parameter_supported:
955
0
      return "request_uri_parameter_supported";
956
0
    case AAD_WELLKNOWN_userinfo_endpoint:
957
0
      return "userinfo_endpoint";
958
0
    case AAD_WELLKNOWN_authorization_endpoint:
959
0
      return "authorization_endpoint";
960
0
    case AAD_WELLKNOWN_device_authorization_endpoint:
961
0
      return "device_authorization_endpoint";
962
0
    case AAD_WELLKNOWN_http_logout_supported:
963
0
      return "http_logout_supported";
964
0
    case AAD_WELLKNOWN_frontchannel_logout_supported:
965
0
      return "frontchannel_logout_supported";
966
0
    case AAD_WELLKNOWN_end_session_endpoint:
967
0
      return "end_session_endpoint";
968
0
    case AAD_WELLKNOWN_claims_supported:
969
0
      return "claims_supported";
970
0
    case AAD_WELLKNOWN_kerberos_endpoint:
971
0
      return "kerberos_endpoint";
972
0
    case AAD_WELLKNOWN_tenant_region_scope:
973
0
      return "tenant_region_scope";
974
0
    case AAD_WELLKNOWN_cloud_instance_name:
975
0
      return "cloud_instance_name";
976
0
    case AAD_WELLKNOWN_cloud_graph_host_name:
977
0
      return "cloud_graph_host_name";
978
0
    case AAD_WELLKNOWN_msgraph_host:
979
0
      return "msgraph_host";
980
0
    case AAD_WELLKNOWN_rbac_url:
981
0
      return "rbac_url";
982
0
    default:
983
0
      return "UNKNOWN";
984
0
  }
985
0
}
986
987
WINPR_JSON* freerdp_utils_aad_get_wellknown_object(rdpContext* context, AAD_WELLKNOWN_VALUES which)
988
0
{
989
0
  return freerdp_utils_aad_get_wellknown_custom_object(
990
0
      context, freerdp_utils_aad_wellknwon_value_name(which));
991
0
}
992
993
WINPR_JSON* freerdp_utils_aad_get_wellknown_custom_object(rdpContext* context, const char* which)
994
0
{
995
0
  WINPR_ASSERT(context);
996
0
  WINPR_ASSERT(context->rdp);
997
998
0
  if (!ensure_wellknown(context))
999
0
    return NULL;
1000
1001
0
  return WINPR_JSON_GetObjectItemCaseSensitive(context->rdp->wellknown, which);
1002
0
}
1003
1004
WINPR_ATTR_MALLOC(WINPR_JSON_Delete, 1)
1005
WINPR_JSON* freerdp_utils_aad_get_wellknown(wLog* log, const char* base, const char* tenantid)
1006
0
{
1007
0
  WINPR_ASSERT(base);
1008
0
  WINPR_ASSERT(tenantid);
1009
1010
0
  char* str = NULL;
1011
0
  size_t len = 0;
1012
0
  winpr_asprintf(&str, &len, "https://%s/%s/v2.0/.well-known/openid-configuration", base,
1013
0
                 tenantid);
1014
1015
0
  if (!str)
1016
0
  {
1017
0
    WLog_Print(log, WLOG_ERROR, "failed to create request URL for tenantid='%s'", tenantid);
1018
0
    return NULL;
1019
0
  }
1020
1021
0
  BYTE* response = NULL;
1022
0
  long resp_code = 0;
1023
0
  size_t response_length = 0;
1024
0
  const BOOL rc = freerdp_http_request(str, NULL, &resp_code, &response, &response_length);
1025
0
  if (!rc || (resp_code != HTTP_STATUS_OK))
1026
0
  {
1027
0
    WLog_Print(log, WLOG_ERROR, "request for '%s' failed with: %s", str,
1028
0
               freerdp_http_status_string(resp_code));
1029
0
    free(str);
1030
0
    free(response);
1031
0
    return NULL;
1032
0
  }
1033
0
  free(str);
1034
1035
0
  WINPR_JSON* json = WINPR_JSON_ParseWithLength((const char*)response, response_length);
1036
0
  free(response);
1037
1038
0
  if (!json)
1039
0
    WLog_Print(log, WLOG_ERROR, "failed to parse response as JSON: %s",
1040
0
               WINPR_JSON_GetErrorPtr());
1041
1042
0
  return json;
1043
0
}