Coverage Report

Created: 2026-03-04 06:17

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 = WINPR_C_ARRAY_INIT;
62
  va_start(ap, fmt);
63
  const int rc = vsnprintf(nullptr, 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 nullptr", 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 = nullptr;
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 = nullptr;
131
132
  WINPR_JSON* prop = nullptr;
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 nullptr", key);
146
    *result = str;
147
    rc = str != nullptr;
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 = nullptr;
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 nullptr", key);
163
  return *result != nullptr;
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 != nullptr;
196
}
197
198
static BOOL aad_get_nonce(rdpAad* aad)
199
{
200
  BOOL ret = FALSE;
201
  BYTE* response = nullptr;
202
  long resp_code = 0;
203
  size_t response_length = 0;
204
  WINPR_JSON* json = nullptr;
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=nullptr' 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 == nullptr");
281
    return -1;
282
  }
283
284
  aad->hostname = _strdup(hostname);
285
  if (!aad->hostname)
286
  {
287
    WLog_Print(aad->log, WLOG_ERROR, "_strdup(hostname) == nullptr");
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 == nullptr");
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 = nullptr;
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 nullptr;
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(nullptr);
353
354
  WINPR_ASSERT(aad);
355
356
  char* e = nullptr;
357
  char* n = nullptr;
358
  if (!get_encoded_rsa_params(aad->log, aad->key, &e, &n))
359
    return nullptr;
360
361
  /* Construct the base64url encoded JWS payload */
362
  char* buffer = nullptr;
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 nullptr;
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 = nullptr;
404
405
  WINPR_ASSERT(aad);
406
  WINPR_ASSERT(ctx);
407
408
  size_t siglen = 0;
409
  const int dsf = winpr_DigestSign_Final(ctx, nullptr, &siglen);
410
  if (dsf <= 0)
411
  {
412
    WLog_Print(aad->log, WLOG_ERROR, "winpr_DigestSign_Final failed with %d", dsf);
413
    return nullptr;
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 = nullptr;
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 = nullptr;
478
  char* jws_payload = nullptr;
479
  char* jws_signature = nullptr;
480
481
  WINPR_ASSERT(aad);
482
  WINPR_ASSERT(ts_nonce);
483
484
  wStream* s = Stream_New(nullptr, 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 = nullptr;
541
  int ret = -1;
542
  WINPR_JSON* json = nullptr;
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 = nullptr;
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 = nullptr;
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] = WINPR_C_ARRAY_INIT;
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 = nullptr;
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
  return aad->kid != nullptr;
680
}
681
682
BOOL generate_pop_key(rdpAad* aad)
683
{
684
  BOOL ret = FALSE;
685
  char* buffer = nullptr;
686
  char* b64_hash = nullptr;
687
  char* e = nullptr;
688
  char* n = nullptr;
689
690
  WINPR_ASSERT(aad);
691
692
  /* Generate a 2048-bit RSA key pair */
693
  if (!generate_rsa_2048(aad))
694
    goto fail;
695
696
  /* Encode the public key as a JWK */
697
  if (!get_encoded_rsa_params(aad->log, aad->key, &e, &n))
698
    goto fail;
699
700
  {
701
    size_t blen = 0;
702
    const int alen =
703
        winpr_asprintf(&buffer, &blen, "{\"e\":\"%s\",\"kty\":\"RSA\",\"n\":\"%s\"}", e, n);
704
    if (alen < 0)
705
      goto fail;
706
707
    /* Hash the encoded public key */
708
    b64_hash = generate_rsa_digest_base64_str(aad, buffer, blen);
709
    if (!b64_hash)
710
      goto fail;
711
  }
712
713
  /* Encode a JSON object with a single property "kid" whose value is the encoded hash */
714
  ret = generate_json_base64_str(aad, b64_hash);
715
716
fail:
717
  free(b64_hash);
718
  free(buffer);
719
  free(e);
720
  free(n);
721
  return ret;
722
}
723
724
static char* bn_to_base64_url(wLog* wlog, rdpPrivateKey* key, enum FREERDP_KEY_PARAM param)
725
{
726
  WINPR_ASSERT(wlog);
727
  WINPR_ASSERT(key);
728
729
  size_t len = 0;
730
  BYTE* bn = freerdp_key_get_param(key, param, &len);
731
  if (!bn)
732
    return nullptr;
733
734
  char* b64 = crypto_base64url_encode(bn, len);
735
  free(bn);
736
737
  if (!b64)
738
    WLog_Print(wlog, WLOG_ERROR, "failed  base64 url encode BIGNUM");
739
740
  return b64;
741
}
742
743
BOOL get_encoded_rsa_params(wLog* wlog, rdpPrivateKey* key, char** pe, char** pn)
744
{
745
  BOOL rc = FALSE;
746
  char* e = nullptr;
747
  char* n = nullptr;
748
749
  WINPR_ASSERT(wlog);
750
  WINPR_ASSERT(key);
751
  WINPR_ASSERT(pe);
752
  WINPR_ASSERT(pn);
753
754
  *pe = nullptr;
755
  *pn = nullptr;
756
757
  e = bn_to_base64_url(wlog, key, FREERDP_KEY_PARAM_RSA_E);
758
  if (!e)
759
  {
760
    WLog_Print(wlog, WLOG_ERROR, "failed  base64 url encode RSA E");
761
    goto fail;
762
  }
763
764
  n = bn_to_base64_url(wlog, key, FREERDP_KEY_PARAM_RSA_N);
765
  if (!n)
766
  {
767
    WLog_Print(wlog, WLOG_ERROR, "failed  base64 url encode RSA N");
768
    goto fail;
769
  }
770
771
  rc = TRUE;
772
fail:
773
  if (!rc)
774
  {
775
    free(e);
776
    free(n);
777
  }
778
  else
779
  {
780
    *pe = e;
781
    *pn = n;
782
  }
783
  return rc;
784
}
785
#else
786
int aad_client_begin(rdpAad* aad)
787
0
{
788
0
  WINPR_ASSERT(aad);
789
0
  WLog_Print(aad->log, WLOG_ERROR, "AAD security not compiled in, aborting!");
790
0
  return -1;
791
0
}
792
793
int aad_recv(rdpAad* aad, wStream* s)
794
0
{
795
0
  WINPR_ASSERT(aad);
796
0
  WLog_Print(aad->log, WLOG_ERROR, "AAD security not compiled in, aborting!");
797
0
  return -1;
798
0
}
799
800
static BOOL ensure_wellknown(WINPR_ATTR_UNUSED rdpContext* context)
801
0
{
802
0
  return FALSE;
803
0
}
804
805
#endif
806
807
rdpAad* aad_new(rdpContext* context)
808
15.4k
{
809
15.4k
  WINPR_ASSERT(context);
810
811
15.4k
  rdpAad* aad = (rdpAad*)calloc(1, sizeof(rdpAad));
812
813
15.4k
  if (!aad)
814
0
    return nullptr;
815
816
15.4k
  aad->log = WLog_Get(FREERDP_TAG("aad"));
817
15.4k
  aad->key = freerdp_key_new();
818
15.4k
  if (!aad->key)
819
0
    goto fail;
820
15.4k
  aad->rdpcontext = context;
821
822
15.4k
  return aad;
823
0
fail:
824
0
  WINPR_PRAGMA_DIAG_PUSH
825
0
  WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC
826
0
  aad_free(aad);
827
0
  WINPR_PRAGMA_DIAG_POP
828
0
  return nullptr;
829
15.4k
}
830
831
void aad_free(rdpAad* aad)
832
30.8k
{
833
30.8k
  if (!aad)
834
15.4k
    return;
835
836
15.4k
  free(aad->hostname);
837
15.4k
  free(aad->scope);
838
15.4k
  free(aad->nonce);
839
15.4k
  free(aad->access_token);
840
15.4k
  free(aad->kid);
841
15.4k
  freerdp_key_free(aad->key);
842
843
15.4k
  free(aad);
844
15.4k
}
845
846
AAD_STATE aad_get_state(rdpAad* aad)
847
0
{
848
0
  WINPR_ASSERT(aad);
849
0
  return aad->state;
850
0
}
851
852
BOOL aad_is_supported(void)
853
0
{
854
#ifdef WITH_AAD
855
  return TRUE;
856
#else
857
0
  return FALSE;
858
0
#endif
859
0
}
860
861
char* freerdp_utils_aad_get_access_token(wLog* log, const char* data, size_t length)
862
0
{
863
0
  char* token = nullptr;
864
0
  WINPR_JSON* access_token_prop = nullptr;
865
0
  const char* access_token_str = nullptr;
866
867
0
  WINPR_JSON* json = WINPR_JSON_ParseWithLength(data, length);
868
0
  if (!json)
869
0
  {
870
0
    WLog_Print(log, WLOG_ERROR,
871
0
               "Failed to parse access token response [got %" PRIuz " bytes: %s", length,
872
0
               WINPR_JSON_GetErrorPtr());
873
0
    goto cleanup;
874
0
  }
875
876
0
  access_token_prop = WINPR_JSON_GetObjectItemCaseSensitive(json, "access_token");
877
0
  if (!access_token_prop)
878
0
  {
879
0
    WLog_Print(log, WLOG_ERROR, "Response has no \"access_token\" property");
880
0
    goto cleanup;
881
0
  }
882
883
0
  access_token_str = WINPR_JSON_GetStringValue(access_token_prop);
884
0
  if (!access_token_str)
885
0
  {
886
0
    WLog_Print(log, WLOG_ERROR, "Invalid value for \"access_token\"");
887
0
    goto cleanup;
888
0
  }
889
890
0
  token = _strdup(access_token_str);
891
892
0
cleanup:
893
0
  WINPR_JSON_Delete(json);
894
0
  return token;
895
0
}
896
897
BOOL aad_fetch_wellknown(wLog* log, rdpContext* context)
898
0
{
899
0
  WINPR_ASSERT(context);
900
901
0
  rdpRdp* rdp = context->rdp;
902
0
  WINPR_ASSERT(rdp);
903
904
0
  if (rdp->wellknown)
905
0
    return TRUE;
906
907
0
  const char* base =
908
0
      freerdp_settings_get_string(context->settings, FreeRDP_GatewayAzureActiveDirectory);
909
0
  const BOOL useTenant =
910
0
      freerdp_settings_get_bool(context->settings, FreeRDP_GatewayAvdUseTenantid);
911
0
  const char* tenantid = "common";
912
0
  if (useTenant)
913
0
    tenantid = freerdp_settings_get_string(context->settings, FreeRDP_GatewayAvdAadtenantid);
914
0
  rdp->wellknown = freerdp_utils_aad_get_wellknown(log, base, tenantid);
915
0
  return rdp->wellknown != nullptr;
916
0
}
917
918
const char* freerdp_utils_aad_get_wellknown_string(rdpContext* context, AAD_WELLKNOWN_VALUES which)
919
0
{
920
0
  return freerdp_utils_aad_get_wellknown_custom_string(
921
0
      context, freerdp_utils_aad_wellknwon_value_name(which));
922
0
}
923
924
const char* freerdp_utils_aad_get_wellknown_custom_string(rdpContext* context, const char* which)
925
0
{
926
0
  WINPR_ASSERT(context);
927
0
  WINPR_ASSERT(context->rdp);
928
929
0
  if (!ensure_wellknown(context))
930
0
    return nullptr;
931
932
0
  WINPR_JSON* obj = WINPR_JSON_GetObjectItemCaseSensitive(context->rdp->wellknown, which);
933
0
  if (!obj)
934
0
    return nullptr;
935
936
0
  return WINPR_JSON_GetStringValue(obj);
937
0
}
938
939
const char* freerdp_utils_aad_wellknwon_value_name(AAD_WELLKNOWN_VALUES which)
940
0
{
941
0
  switch (which)
942
0
  {
943
0
    case AAD_WELLKNOWN_token_endpoint:
944
0
      return "token_endpoint";
945
0
    case AAD_WELLKNOWN_token_endpoint_auth_methods_supported:
946
0
      return "token_endpoint_auth_methods_supported";
947
0
    case AAD_WELLKNOWN_jwks_uri:
948
0
      return "jwks_uri";
949
0
    case AAD_WELLKNOWN_response_modes_supported:
950
0
      return "response_modes_supported";
951
0
    case AAD_WELLKNOWN_subject_types_supported:
952
0
      return "subject_types_supported";
953
0
    case AAD_WELLKNOWN_id_token_signing_alg_values_supported:
954
0
      return "id_token_signing_alg_values_supported";
955
0
    case AAD_WELLKNOWN_response_types_supported:
956
0
      return "response_types_supported";
957
0
    case AAD_WELLKNOWN_scopes_supported:
958
0
      return "scopes_supported";
959
0
    case AAD_WELLKNOWN_issuer:
960
0
      return "issuer";
961
0
    case AAD_WELLKNOWN_request_uri_parameter_supported:
962
0
      return "request_uri_parameter_supported";
963
0
    case AAD_WELLKNOWN_userinfo_endpoint:
964
0
      return "userinfo_endpoint";
965
0
    case AAD_WELLKNOWN_authorization_endpoint:
966
0
      return "authorization_endpoint";
967
0
    case AAD_WELLKNOWN_device_authorization_endpoint:
968
0
      return "device_authorization_endpoint";
969
0
    case AAD_WELLKNOWN_http_logout_supported:
970
0
      return "http_logout_supported";
971
0
    case AAD_WELLKNOWN_frontchannel_logout_supported:
972
0
      return "frontchannel_logout_supported";
973
0
    case AAD_WELLKNOWN_end_session_endpoint:
974
0
      return "end_session_endpoint";
975
0
    case AAD_WELLKNOWN_claims_supported:
976
0
      return "claims_supported";
977
0
    case AAD_WELLKNOWN_kerberos_endpoint:
978
0
      return "kerberos_endpoint";
979
0
    case AAD_WELLKNOWN_tenant_region_scope:
980
0
      return "tenant_region_scope";
981
0
    case AAD_WELLKNOWN_cloud_instance_name:
982
0
      return "cloud_instance_name";
983
0
    case AAD_WELLKNOWN_cloud_graph_host_name:
984
0
      return "cloud_graph_host_name";
985
0
    case AAD_WELLKNOWN_msgraph_host:
986
0
      return "msgraph_host";
987
0
    case AAD_WELLKNOWN_rbac_url:
988
0
      return "rbac_url";
989
0
    default:
990
0
      return "UNKNOWN";
991
0
  }
992
0
}
993
994
WINPR_JSON* freerdp_utils_aad_get_wellknown_object(rdpContext* context, AAD_WELLKNOWN_VALUES which)
995
0
{
996
0
  return freerdp_utils_aad_get_wellknown_custom_object(
997
0
      context, freerdp_utils_aad_wellknwon_value_name(which));
998
0
}
999
1000
WINPR_JSON* freerdp_utils_aad_get_wellknown_custom_object(rdpContext* context, const char* which)
1001
0
{
1002
0
  WINPR_ASSERT(context);
1003
0
  WINPR_ASSERT(context->rdp);
1004
1005
0
  if (!ensure_wellknown(context))
1006
0
    return nullptr;
1007
1008
0
  return WINPR_JSON_GetObjectItemCaseSensitive(context->rdp->wellknown, which);
1009
0
}
1010
1011
WINPR_ATTR_MALLOC(WINPR_JSON_Delete, 1)
1012
WINPR_ATTR_NODISCARD
1013
WINPR_JSON* freerdp_utils_aad_get_wellknown(wLog* log, const char* base, const char* tenantid)
1014
0
{
1015
0
  WINPR_ASSERT(base);
1016
0
  WINPR_ASSERT(tenantid);
1017
1018
0
  char* str = nullptr;
1019
0
  size_t len = 0;
1020
0
  winpr_asprintf(&str, &len, "https://%s/%s/v2.0/.well-known/openid-configuration", base,
1021
0
                 tenantid);
1022
1023
0
  if (!str)
1024
0
  {
1025
0
    WLog_Print(log, WLOG_ERROR, "failed to create request URL for tenantid='%s'", tenantid);
1026
0
    return nullptr;
1027
0
  }
1028
1029
0
  BYTE* response = nullptr;
1030
0
  long resp_code = 0;
1031
0
  size_t response_length = 0;
1032
0
  const BOOL rc = freerdp_http_request(str, nullptr, &resp_code, &response, &response_length);
1033
0
  if (!rc || (resp_code != HTTP_STATUS_OK))
1034
0
  {
1035
0
    WLog_Print(log, WLOG_ERROR, "request for '%s' failed with: %s", str,
1036
0
               freerdp_http_status_string(resp_code));
1037
0
    free(str);
1038
0
    free(response);
1039
0
    return nullptr;
1040
0
  }
1041
0
  free(str);
1042
1043
0
  WINPR_JSON* json = WINPR_JSON_ParseWithLength((const char*)response, response_length);
1044
0
  free(response);
1045
1046
0
  if (!json)
1047
0
    WLog_Print(log, WLOG_ERROR, "failed to parse response as JSON: %s",
1048
0
               WINPR_JSON_GetErrorPtr());
1049
1050
0
  return json;
1051
0
}