Coverage Report

Created: 2026-01-09 06:49

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