]> git.saurik.com Git - apple/securityd.git/blob - src/agentquery.cpp
cb1d4aa6610dd6c1b75d9b0cc14b5391cb8a3284
[apple/securityd.git] / src / agentquery.cpp
1 /*
2 * Copyright (c) 2000-2004 Apple Computer, Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * Copyright (c) 1999-2003 Apple Computer, Inc. All Rights Reserved.
7 *
8 * This file contains Original Code and/or Modifications of Original Code
9 * as defined in and that are subject to the Apple Public Source License
10 * Version 2.0 (the 'License'). You may not use this file except in
11 * compliance with the License. Please obtain a copy of the License at
12 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * file.
14 *
15 * The Original Code and all software distributed under the License are
16 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
17 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
18 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
20 * Please see the License for the specific language governing rights and
21 * limitations under the License.
22 *
23 * @APPLE_LICENSE_HEADER_END@
24 */
25
26 //
27 // passphrases - canonical code to obtain passphrases
28 //
29 #include "agentquery.h"
30 #include "authority.h"
31 #include "server.h"
32 #include "session.h"
33
34 #include <Security/AuthorizationTags.h>
35 #include <Security/AuthorizationTagsPriv.h>
36
37 //
38 // NOSA support functions. This is a test mode where the SecurityAgent
39 // is simulated via stdio in the client. Good for running automated tests
40 // of client programs. Only available if -DNOSA when compiling.
41 //
42 #if defined(NOSA)
43
44 #include <cstdarg>
45
46 static void getNoSA(char *buffer, size_t bufferSize, const char *fmt, ...)
47 {
48 // write prompt
49 va_list args;
50 va_start(args, fmt);
51 vfprintf(stdout, fmt, args);
52 va_end(args);
53
54 // read reply
55 memset(buffer, 0, bufferSize);
56 const char *nosa = getenv("NOSA");
57 if (!strcmp(nosa, "-")) {
58 if (fgets(buffer, bufferSize-1, stdin) == NULL)
59 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION);
60 buffer[strlen(buffer)-1] = '\0'; // remove trailing newline
61 if (!isatty(fileno(stdin)))
62 printf("%s\n", buffer); // echo to output if input not terminal
63 } else {
64 strncpy(buffer, nosa, bufferSize-1);
65 printf("%s\n", buffer);
66 }
67 if (buffer[0] == '\0') // empty input -> cancellation
68 CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED);
69 }
70
71 #endif //NOSA
72
73
74 //
75 // The default Mach service name for SecurityAgent
76 //
77 const char SecurityAgentQuery::defaultName[] = "com.apple.SecurityAgent";
78
79 using SecurityAgent::Reason;
80 using namespace Authorization;
81
82 //
83 // Construct a query object
84 //
85
86 SecurityAgentQuery::SecurityAgentQuery() :
87 SecurityAgent::Client(Server::process().uid(),
88 Server::session().bootstrapPort(),
89 defaultName),
90 mClientSession(Server::session())
91 {
92 secdebug("SecurityAgentQuery", "new query");
93
94 // XXX/cs set up the general settings for the client as hints
95 // any invoke will merge these and whatever the client passes on invoke
96
97 }
98
99 SecurityAgentQuery::SecurityAgentQuery(uid_t clientUID,
100 const Session &clientSession,
101 const char *agentName) :
102 SecurityAgent::Client(clientUID, clientSession.bootstrapPort(), agentName ? agentName : defaultName),
103 mClientSession(clientSession)
104 {
105 secdebug("SecurityAgentQuery", "new query");
106 }
107
108 SecurityAgentQuery::~SecurityAgentQuery()
109 {
110 secdebug("SecurityAgentQuery", "query dying");
111 Server::connection().useAgent(NULL);
112
113 #if defined(NOSA)
114 if (getenv("NOSA")) {
115 printf(" [query done]\n");
116 return;
117 }
118 #endif
119
120 if (state() != dead)
121 destroy();
122 }
123
124 void
125 SecurityAgentQuery::activate()
126 {
127 if (isActive())
128 return;
129
130 // Before popping up an agent: is UI session allowed?
131 if (!(mClientSession.attributes() & sessionHasGraphicAccess))
132 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION);
133
134 // this may take a while
135 Server::active().longTermActivity();
136 Server::connection().useAgent(this);
137
138 try {
139 SecurityAgent::Client::activate();
140 } catch (...) {
141 Server::connection().useAgent(NULL); // guess not
142 throw;
143 }
144
145
146 }
147
148
149 void
150 SecurityAgentQuery::inferHints(Process &thisProcess)
151 {
152 AuthItemSet processHints = clientHints(thisProcess.clientCode(), thisProcess.pid(), thisProcess.uid());
153 mClientHints.insert(processHints.begin(), processHints.end());
154 }
155
156 void
157 SecurityAgentQuery::readChoice()
158 {
159 allow = false;
160 remember = false;
161
162 AuthItem *allowAction = mContext.find(AGENT_CONTEXT_ALLOW);
163 if (allowAction)
164 {
165 string allowString;
166 if (allowAction->getString(allowString)
167 && (allowString == "YES"))
168 allow = true;
169 }
170
171 AuthItem *rememberAction = mContext.find(AGENT_CONTEXT_REMEMBER_ACTION);
172 if (rememberAction)
173 {
174 string rememberString;
175 if (rememberAction->getString(rememberString)
176 && (rememberString == "YES"))
177 remember = true;
178 }
179 }
180
181 void
182 SecurityAgentQuery::terminate()
183 {
184 if (!isActive())
185 activate();
186
187 // @@@ This happens already in the destructor; presumably we do this to tear things down orderly
188 Server::connection(true).useAgent(NULL);
189
190 SecurityAgent::Client::terminate();
191 }
192
193
194 //
195 // Perform the "rogue app" access query dialog
196 //
197 QueryKeychainUse::QueryKeychainUse(bool needPass, const Database *db)
198 : mPassphraseCheck(NULL)
199 {
200 // if passphrase checking requested, save KeychainDatabase reference
201 // (will quietly disable check if db isn't a keychain)
202 if (needPass)
203 mPassphraseCheck = dynamic_cast<const KeychainDatabase *>(db);
204 }
205
206 Reason QueryKeychainUse::queryUser (const char *database, const char *description, AclAuthorization action)
207 {
208 Reason reason = SecurityAgent::noReason;
209 int retryCount = 0;
210 OSStatus status;
211 AuthValueVector arguments;
212 AuthItemSet hints, context;
213
214 #if defined(NOSA)
215 if (getenv("NOSA")) {
216 char answer[maxPassphraseLength+10];
217
218 string applicationPath;
219 AuthItem *applicationPathItem = mClientHints.find(AGENT_HINT_APPLICATION_PATH);
220 if (applicationPathItem)
221 applicationPathItem->getString(applicationPath);
222
223 getNoSA(answer, sizeof(answer), "Allow %s to do %d on %s in %s? [yn][g]%s ",
224 applicationPath.c_str(), int(action), (description ? description : "[NULL item]"),
225 (database ? database : "[NULL database]"),
226 mPassphraseCheck ? ":passphrase" : "");
227 // turn passphrase (no ':') into y:passphrase
228 if (mPassphraseCheck && !strchr(answer, ':')) {
229 memmove(answer+2, answer, strlen(answer)+1);
230 memcpy(answer, "y:", 2);
231 }
232
233 allow = answer[0] == 'y';
234 remember = answer[1] == 'g';
235 return SecurityAgent::noReason;
236 }
237 #endif
238
239 activate();
240
241 // prepopulate with client hints
242 hints.insert(mClientHints.begin(), mClientHints.end());
243
244 // put action/operation (sint32) into hints
245 hints.insert(AuthItemRef(AGENT_HINT_ACL_TAG, AuthValueOverlay(sizeof(action), static_cast<SInt32*>(&action))));
246
247 // item name into hints
248
249 hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_ITEM_NAME, AuthValueOverlay(description ? strlen(description) : 0, const_cast<char*>(description))));
250
251 // keychain name into hints
252 hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH, AuthValueOverlay(database ? strlen(database) : 0, const_cast<char*>(database))));
253
254
255 if (mPassphraseCheck)
256 {
257 status = create("builtin", "confirm-access-password", NULL);
258
259 CssmAutoData data(Allocator::standard(Allocator::sensitive));
260
261 do
262 {
263
264 AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
265 hints.erase(triesHint); hints.insert(triesHint); // replace
266
267 if (retryCount++ > kMaximumAuthorizationTries)
268 {
269 reason = SecurityAgent::tooManyTries;
270 }
271
272 AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
273 hints.erase(retryHint); hints.insert(retryHint); // replace
274
275 status = invoke(arguments, hints, context);
276
277 if (retryCount > kMaximumAuthorizationTries)
278 {
279 return reason;
280 }
281
282 checkResult();
283
284 AuthItem *passwordItem = mContext.find(kAuthorizationEnvironmentPassword);
285 if (!passwordItem)
286 continue;
287
288 passwordItem->getCssmData(data);
289 }
290 while (reason = (const_cast<KeychainDatabase*>(mPassphraseCheck)->decode(data) ? SecurityAgent::noReason : SecurityAgent::invalidPassphrase));
291 }
292 else
293 {
294 create("builtin", "confirm-access", NULL);
295 invoke(arguments, hints, context);
296 }
297
298 readChoice();
299
300 return reason;
301 }
302
303 //
304 // Perform code signature ACL access adjustment dialogs
305 //
306 bool QueryCodeCheck::operator () (const char *aclPath)
307 {
308 OSStatus status;
309 AuthValueVector arguments;
310 AuthItemSet hints, context;
311
312 #if defined(NOSA)
313 if (getenv("NOSA")) {
314 char answer[10];
315
316 string applicationPath;
317 AuthItem *applicationPathItem = mClientHints.find(AGENT_HINT_APPLICATION_PATH);
318 if (applicationPathItem)
319 applicationPathItem->getString(applicationPath);
320
321 getNoSA(answer, sizeof(answer),
322 "Allow %s to match an ACL for %s [yn][g]? ",
323 applicationPath.c_str(), aclPath ? aclPath : "(unknown)");
324 allow = answer[0] == 'y';
325 remember = answer[1] == 'g';
326 return;
327 }
328 #endif
329
330 // prepopulate with client hints
331 hints.insert(mClientHints.begin(), mClientHints.end());
332
333 hints.insert(AuthItemRef(AGENT_HINT_APPLICATION_PATH, AuthValueOverlay(strlen(aclPath), const_cast<char*>(aclPath))));
334
335 MacOSError::check(create("builtin", "code-identity", NULL));
336
337 status = invoke(arguments, hints, context);
338
339 checkResult();
340
341 // MacOSError::check(status);
342
343 return kAuthorizationResultAllow == result();
344 }
345
346
347 //
348 // Obtain passphrases and submit them to the accept() method until it is accepted
349 // or we can't get another passphrase. Accept() should consume the passphrase
350 // if it is accepted. If no passphrase is acceptable, throw out of here.
351 //
352 Reason QueryUnlock::query()
353 {
354 Reason reason = SecurityAgent::noReason;
355 OSStatus status;
356 AuthValueVector arguments;
357 AuthItemSet hints, context;
358 CssmAutoData passphrase(Allocator::standard(Allocator::sensitive));
359 int retryCount = 0;
360
361 #if defined(NOSA)
362 // return the passphrase
363 if (getenv("NOSA")) {
364 char passphrase_[maxPassphraseLength];
365 getNoSA(passphrase, maxPassphraseLength, "Unlock %s [<CR> to cancel]: ", database.dbName());
366 passphrase.copy(passphrase_, strlen(passphrase_));
367 return database.decode(passphrase) ? SecurityAgent::noReason : SecurityAgent::invalidPassphrase;
368 }
369 #endif
370 activate();
371
372
373 // prepopulate with client hints
374
375 const char *keychainPath = database.dbName();
376 hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH, AuthValueOverlay(strlen(keychainPath), const_cast<char*>(keychainPath))));
377
378 hints.insert(mClientHints.begin(), mClientHints.end());
379
380 MacOSError::check(create("builtin", "unlock-keychain", NULL));
381
382 do
383 {
384 AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
385 hints.erase(triesHint); hints.insert(triesHint); // replace
386
387 ++retryCount;
388
389 if (retryCount > maxTries)
390 {
391 reason = SecurityAgent::tooManyTries;
392 }
393
394 AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
395 hints.erase(retryHint); hints.insert(retryHint); // replace
396
397 status = invoke(arguments, hints, context);
398
399 if (retryCount > maxTries)
400 {
401 return reason;
402 }
403
404 checkResult();
405
406 AuthItem *passwordItem = mContext.find(kAuthorizationEnvironmentPassword);
407 if (!passwordItem)
408 continue;
409
410 passwordItem->getCssmData(passphrase);
411 }
412 while (reason = database.decode(passphrase) ? SecurityAgent::noReason : SecurityAgent::invalidPassphrase);
413
414 return SecurityAgent::noReason;
415 }
416
417
418 //
419 // Get existing passphrase (unlock) Query
420 //
421 Reason QueryUnlock::operator () ()
422 {
423 return query();
424 }
425
426
427 //
428 // Obtain passphrases and submit them to the accept() method until it is accepted
429 // or we can't get another passphrase. Accept() should consume the passphrase
430 // if it is accepted. If no passphrase is acceptable, throw out of here.
431 //
432 Reason QueryNewPassphrase::query()
433 {
434 Reason reason = initialReason;
435 CssmAutoData passphrase(Allocator::standard(Allocator::sensitive));
436 CssmAutoData oldPassphrase(Allocator::standard(Allocator::sensitive));
437
438 OSStatus status;
439 AuthValueVector arguments;
440 AuthItemSet hints, context;
441
442 int retryCount = 0;
443
444 #if defined(NOSA)
445 if (getenv("NOSA")) {
446 char passphrase_[maxPassphraseLength];
447 getNoSA(passphrase_, maxPassphraseLength,
448 "New passphrase for %s (reason %d) [<CR> to cancel]: ",
449 database.dbName(), reason);
450 return SecurityAgent::noReason;
451 }
452 #endif
453
454 activate();
455
456 // prepopulate with client hints
457 hints.insert(mClientHints.begin(), mClientHints.end());
458
459 // keychain name into hints
460 hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH, AuthValueOverlay(database.dbName())));
461
462 switch (initialReason)
463 {
464 case SecurityAgent::newDatabase:
465 MacOSError::check(create("builtin", "new-passphrase", NULL));
466 break;
467 case SecurityAgent::changePassphrase:
468 MacOSError::check(create("builtin", "change-passphrase", NULL));
469 break;
470 default:
471 assert(false);
472 }
473
474 do
475 {
476 AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
477 hints.erase(triesHint); hints.insert(triesHint); // replace
478
479 if (++retryCount > maxTries)
480 {
481 reason = SecurityAgent::tooManyTries;
482 }
483
484 AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
485 hints.erase(retryHint); hints.insert(retryHint); // replace
486
487 status = invoke(arguments, hints, context);
488
489 if (retryCount > maxTries)
490 {
491 return reason;
492 }
493
494 checkResult();
495
496 if (SecurityAgent::changePassphrase == initialReason)
497 {
498 AuthItem *oldPasswordItem = mContext.find(AGENT_PASSWORD);
499 if (!oldPasswordItem)
500 continue;
501
502 oldPasswordItem->getCssmData(oldPassphrase);
503 }
504
505 AuthItem *passwordItem = mContext.find(AGENT_CONTEXT_NEW_PASSWORD);
506 if (!passwordItem)
507 continue;
508
509 passwordItem->getCssmData(passphrase);
510
511 }
512 while (reason = accept(passphrase, (initialReason == SecurityAgent::changePassphrase) ? &oldPassphrase.get() : NULL));
513
514 return SecurityAgent::noReason;
515 }
516
517
518 //
519 // Get new passphrase Query
520 //
521 Reason QueryNewPassphrase::operator () (CssmOwnedData &passphrase)
522 {
523 if (Reason result = query())
524 return result; // failed
525 passphrase = mPassphrase;
526 return SecurityAgent::noReason; // success
527 }
528
529 Reason QueryNewPassphrase::accept(CssmManagedData &passphrase, CssmData *oldPassphrase)
530 {
531 //@@@ acceptance criteria are currently hardwired here
532 //@@@ This validation presumes ASCII - UTF8 might be more lenient
533
534 // if we have an old passphrase, check it
535 if (oldPassphrase && !database.validatePassphrase(*oldPassphrase))
536 return SecurityAgent::oldPassphraseWrong;
537
538 // sanity check the new passphrase (but allow user override)
539 if (!(mPassphraseValid && passphrase.get() == mPassphrase)) {
540 mPassphrase = passphrase;
541 mPassphraseValid = true;
542 if (mPassphrase.length() == 0)
543 return SecurityAgent::passphraseIsNull;
544 if (mPassphrase.length() < 6)
545 return SecurityAgent::passphraseTooSimple;
546 }
547
548 // accept this
549 return SecurityAgent::noReason;
550 }
551
552 //
553 // Get a passphrase for unspecified use
554 //
555 Reason QueryGenericPassphrase::operator () (const char *prompt, bool verify,
556 string &passphrase)
557 {
558 return query(prompt, verify, passphrase);
559 }
560
561 Reason QueryGenericPassphrase::query(const char *prompt, bool verify,
562 string &passphrase)
563 {
564 Reason reason = SecurityAgent::noReason;
565 OSStatus status; // not really used; remove?
566 AuthValueVector arguments;
567 AuthItemSet hints, context;
568
569 #if defined(NOSA)
570 if (getenv("NOSA")) {
571 // FIXME 3690984
572 return SecurityAgent::noReason;
573 }
574 #endif
575
576 activate();
577
578 hints.insert(mClientHints.begin(), mClientHints.end());
579 hints.insert(AuthItemRef(AGENT_HINT_CUSTOM_PROMPT, AuthValueOverlay(prompt ? strlen(prompt) : 0, const_cast<char*>(prompt))));
580 // XXX/gh defined by dmitch but no analogous hint in
581 // AuthorizationTagsPriv.h:
582 // CSSM_ATTRIBUTE_ALERT_TITLE (optional alert panel title)
583
584 if (false == verify) { // import
585 MacOSError::check(create("builtin", "generic-unlock", NULL));
586 } else { // verify passphrase (export)
587 // new-passphrase-generic works with the pre-4 June 2004 agent;
588 // generic-new-passphrase is required for the new agent
589 MacOSError::check(create("builtin", "generic-new-passphrase", NULL));
590 }
591
592 AuthItem *passwordItem;
593
594 do {
595
596 status = invoke(arguments, hints, context);
597 checkResult();
598 passwordItem = mContext.find(AGENT_PASSWORD);
599
600 } while (!passwordItem);
601
602 passwordItem->getString(passphrase);
603
604 return reason;
605 }
606
607
608 QueryInvokeMechanism::QueryInvokeMechanism() :
609 SecurityAgentQuery() { }
610
611 QueryInvokeMechanism::QueryInvokeMechanism(uid_t clientUID, const Session &session, const char *agentName) :
612 SecurityAgentQuery(clientUID, session, agentName)
613 {
614 }
615
616 void QueryInvokeMechanism::initialize(const string &inPluginId, const string &inMechanismId, const SessionId inSessionId)
617 {
618 activate();
619
620 if (init == state())
621 MacOSError::check(create(inPluginId.c_str(), inMechanismId.c_str(), inSessionId));
622 }
623
624 // XXX/cs should return AuthorizationResult
625 void QueryInvokeMechanism::run(const AuthValueVector &inArguments, AuthItemSet &inHints, AuthItemSet &inContext, AuthorizationResult *outResult)
626 {
627 // prepopulate with client hints
628 inHints.insert(mClientHints.begin(), mClientHints.end());
629
630 MacOSError::check(invoke(inArguments, inHints, inContext));
631
632 if (outResult) *outResult = result();
633
634 inHints = hints();
635 inContext = context();
636 }
637
638 void QueryInvokeMechanism::terminateAgent()
639 {
640 terminate();
641 }
642