2 * Copyright (c) 2000-2001 Apple Computer, Inc. All Rights Reserved.
4 * The contents of this file constitute Original Code as defined in and are
5 * subject to the Apple Public Source License Version 1.2 (the 'License').
6 * You may not use this file except in compliance with the License. Please obtain
7 * a copy of the License at http://www.apple.com/publicsource and read it before
10 * This Original Code and all software distributed under the License are
11 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
12 * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
13 * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14 * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
15 * specific language governing rights and limitations under the License.
20 // SecurityAgentClient - client interface to SecurityAgent
22 // This file changes behavior depending on two environment variables.
23 // AGENTNAME/AGENTPATH: if defined, is the name and path to
24 // the SecurityAgent binary to autolaunch. If undefined, SecurityAgent must be running.
25 // NOSA: If set, check for NOSA environment variable and if set, simulate the Agent
26 // using stdio in the client.
28 // A note on message flow: the combined send/receive operation at the heart of each
29 // secagent_client_* call can receive three types of message:
30 // (o) SecurityAgent reply -- ok, process
31 // (o) Dead port notification -- agent died, translated to NO_USER_INTERACTION error thrown
32 // (o) Cancel message -- will come out as INVALID_ID error and throw
34 // @@@ SA keepalive option.
36 #include "SecurityAgentClient.h"
38 #include <Security/utilities.h>
39 #include <Security/Authorization.h>
42 #include <mach/mach_error.h>
44 #include <Security/debugging.h>
46 #include <sys/syslimits.h>
49 // @@@ Should be in <time.h> but it isn't as of Puma5F22
50 extern "C" int nanosleep(const struct timespec
*rqtp
, struct timespec
*rmtp
);
53 namespace SecurityAgent
{
56 using namespace Security
;
57 using namespace MachPlusPlus
;
65 Requestor(const OSXCode
*code
) { if (code
) extForm
= code
->encode(); }
66 operator const char * () const { return extForm
.c_str(); }
74 // Check a return from a MIG client call
76 void Client::check(kern_return_t error
)
78 // first check the Mach IPC return code
80 case KERN_SUCCESS
: // peachy
82 case MIG_SERVER_DIED
: // explicit can't-send-it's-dead
84 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
85 default: // some random Mach error
87 MachPlusPlus::Error::throwMe(error
);
90 // now check the OSStatus return from the server side
93 case errAuthorizationDenied
:
97 CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED
);
100 MacOSError::throwMe(status
);
104 void Client::unstage()
106 if (stage
!= mainStage
) {
107 mStagePort
.deallocate();
114 // NOSA support functions. This is a test mode where the SecurityAgent
115 // is simulated via stdio in the client. Good for running automated tests
116 // of client programs. Only available if -DNOSA when compiling.
122 static void getNoSA(char *buffer
, size_t bufferSize
, const char *fmt
, ...)
127 vfprintf(stdout
, fmt
, args
);
131 memset(buffer
, 0, bufferSize
);
132 const char *nosa
= getenv("NOSA");
133 if (!strcmp(nosa
, "-")) {
134 if (fgets(buffer
, bufferSize
-1, stdin
) == NULL
)
135 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
136 buffer
[strlen(buffer
)-1] = '\0'; // remove trailing newline
137 if (!isatty(fileno(stdin
)))
138 printf("%s\n", buffer
); // echo to output if input not terminal
140 strncpy(buffer
, nosa
, bufferSize
-1);
141 printf("%s\n", buffer
);
143 if (buffer
[0] == '\0') // empty input -> cancellation
144 CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED
);
151 // Initialize our CSSM interface
153 Client::Client() : mActive(false), mKeepAlive(false), stage(mainStage
)
164 // Activate a session
166 void Client::activate(const char *name
)
169 establishServer(name
? name
: "SecurityAgent");
172 mClientPort
.allocate(MACH_PORT_RIGHT_RECEIVE
);
173 mClientPort
.insertRight(MACH_MSG_TYPE_MAKE_SEND
);
175 // get notified if the server dies (shouldn't happen, but...)
176 mServerPort
.requestNotify(mClientPort
, MACH_NOTIFY_DEAD_NAME
, true);
183 void Client::terminate()
186 mServerPort
.deallocate();
187 mClientPort
.destroy();
194 // Cancel a client call.
195 // This actually sends a reply message to the thread waiting for a reply,
196 // thereby unblocking it.
197 // @@@ Theoretically we should thread-lock this so only one cancel message
198 // ever gets sent. But right now, this is only used to completely tear down
199 // a client session, so duplicate replies don't bother us.
201 void Client::cancel()
203 // this is the common prefix of SecurityAgent client call replies
205 mach_msg_header_t Head
;
207 kern_return_t result
;
211 request
.Head
.msgh_bits
= MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND
, 0);
212 request
.Head
.msgh_remote_port
= mClientPort
;
213 request
.Head
.msgh_local_port
= MACH_PORT_NULL
;
214 request
.Head
.msgh_id
= cancelMessagePseudoID
;
215 request
.NDR
= NDR_record
;
217 // set call succeeded, no error status
218 request
.result
= KERN_SUCCESS
;
219 request
.status
= noErr
;
221 // send it (do not receive a reply). Use zero timeout to avoid hangs
222 MachPlusPlus::check(mach_msg_overwrite(&request
.Head
, MACH_SEND_MSG
|MACH_SEND_TIMEOUT
,
223 sizeof(request
), 0, MACH_PORT_NULL
, MACH_MSG_TIMEOUT_NONE
,
224 MACH_PORT_NULL
, (mach_msg_header_t
*) NULL
, 0));
229 // Get the port for the SecurityAgent.
230 // Start it if necessary (and possible). Throw an exception if we can't get to it.
231 // Sets mServerPort on success.
233 void Client::establishServer(const char *name
)
237 // If the userids don't match, that means you can't do user interaction
238 // @@@ Expose this to caller so it can implement its own idea of getuid()!
239 if (desktopUid
!= getuid() && getuid() != 0)
240 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
242 // if the server is already running, we're done
243 Bootstrap
bootstrap(pbsBootstrap
);
244 if (mServerPort
= bootstrap
.lookupOptional(name
))
247 #if defined(AGENTNAME) && defined(AGENTPATH)
248 // switch the bootstrap port to that of the logged-in user
249 StBootstrap
bootSaver(pbsBootstrap
);
251 // try to start the agent
252 switch (pid_t pid
= fork()) {
255 // Setup the environment for the SecurityAgent
260 debug("SAclnt", "setgid(%d)", desktopGid
);
261 setgid(desktopGid
); // switch to login-user gid
262 debug("SAclnt", "setuid(%d)", desktopUid
);
263 // Must be setuid and not seteuid since we do not want the agent to be able
264 // to call seteuid(0) successfully.
265 setuid(desktopUid
); // switch to login-user uid
267 // construct path to SecurityAgent
268 char agentExecutable
[PATH_MAX
+ 1];
269 const char *path
= getenv("SECURITYAGENT");
272 snprintf(agentExecutable
, sizeof(agentExecutable
), "%s/Contents/MacOS/" AGENTNAME
, path
);
273 debug("SAclnt", "execl(%s)", agentExecutable
);
274 execl(agentExecutable
, agentExecutable
, NULL
);
275 debug("SAclnt", "execl of SecurityAgent failed, errno=%d", errno
);
277 // Unconditional suicide follows.
278 // See comments below on why we can't use abort()
282 // NOTE: OS X abort() is implemented as kill(getuid()), which fails
283 // for a setuid-root process that has setuid'd. Go back to root to die...
288 case -1: // error (in parent)
289 UnixError::throwMe();
292 static const int timeout
= 300;
294 debug("SAclnt", "Starting security agent (%d seconds timeout)", timeout
);
295 struct timespec rqtp
;
296 memset(&rqtp
, 0, sizeof(rqtp
));
297 rqtp
.tv_nsec
= 100000000; /* 10^8 nanaseconds = 1/10th of a second */
298 for (int n
= timeout
; n
> 0; nanosleep(&rqtp
, NULL
), n
--) {
299 if (mServerPort
= bootstrap
.lookupOptional(name
))
302 switch (pid_t rc
= waitpid(pid
, &status
, WNOHANG
)) {
303 case 0: // child still running
308 case EAGAIN
: // transient
310 case ECHILD
: // no such child (dead; already reaped elsewhere)
311 debug("SAclnt", "child is dead (reaped elsewhere)");
312 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
314 debug("SAclnt", "waitpid failed: errno=%d", errno
);
315 UnixError::throwMe();
319 debug("SAclnt", "child died without claiming the SecurityAgent port");
320 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
324 if (mServerPort
== 0) { // couldn't contact Security Agent
325 debug("SAclnt", "Autolaunch failed");
326 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
328 debug("SAclnt", "SecurityAgent located");
334 // well, this didn't work. Too bad
335 debug("SAclnt", "Cannot contact SecurityAgent");
336 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
); //@@@ or INTERNAL_ERROR?
341 // Staged query maintainance
343 void Client::finishStagedQuery()
345 if (stage
== mainStage
)
346 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
348 if (getenv("NOSA")) {
349 printf(" [query done]\n");
354 check(secagent_client_finishStagedQuery(mStagePort
, mClientPort
, &status
));
359 void Client::cancelStagedQuery(Reason reason
)
361 if (stage
== mainStage
)
362 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
364 if (getenv("NOSA")) {
365 printf(" [query canceled; reason=%d]\n", reason
);
370 check(secagent_client_cancelStagedQuery(mStagePort
, mClientPort
, &status
, reason
));
377 // Query the user to unlock a keychain. This is a staged protocol with a private side-port.
379 void Client::queryUnlockDatabase(const OSXCode
*requestor
, pid_t requestPid
,
380 const char *database
, char passphrase
[maxPassphraseLength
])
382 Requestor
req(requestor
);
385 if (getenv("NOSA")) {
386 getNoSA(passphrase
, maxPassphraseLength
, "Unlock %s [<CR> to cancel]: ", database
);
392 check(secagent_client_unlockDatabase(mServerPort
, mClientPort
,
393 &status
, req
, requestPid
, database
, &mStagePort
.port(), passphrase
));
397 void Client::retryUnlockDatabase(Reason reason
, char passphrase
[maxPassphraseLength
])
399 if (stage
!= unlockStage
)
400 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
402 if (getenv("NOSA")) {
403 getNoSA(passphrase
, maxPassphraseLength
, "Retry unlock [<CR> to cancel]: ");
407 check(secagent_client_retryUnlockDatabase(mStagePort
, mClientPort
,
408 &status
, reason
, passphrase
));
413 // Ask for a (new) password for something.
415 void Client::queryNewPassphrase(const OSXCode
*requestor
, pid_t requestPid
,
416 const char *database
, Reason reason
, char passphrase
[maxPassphraseLength
])
418 Requestor
req(requestor
);
421 if (getenv("NOSA")) {
422 getNoSA(passphrase
, maxPassphraseLength
,
423 "New passphrase for %s (reason %d) [<CR> to cancel]: ",
424 (database
? database
: "[NULL database]"), reason
);
425 stage
= newPassphraseStage
;
430 check(secagent_client_queryNewPassphrase(mServerPort
, mClientPort
,
431 &status
, req
, requestPid
, database
, reason
,
432 &mStagePort
.port(), passphrase
));
433 stage
= newPassphraseStage
;
436 void Client::retryNewPassphrase(Reason reason
, char passphrase
[maxPassphraseLength
])
438 if (stage
!= newPassphraseStage
)
439 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
441 if (getenv("NOSA")) {
442 getNoSA(passphrase
, maxPassphraseLength
,
443 "retry new passphrase (reason %d) [<CR> to cancel]: ", reason
);
447 check(secagent_client_retryNewPassphrase(mStagePort
, mClientPort
,
448 &status
, reason
, passphrase
));
453 // Ask the user permission to use an item.
454 // This is used by the keychain-style ACL subject type (only).
456 void Client::queryKeychainAccess(const OSXCode
*requestor
, pid_t requestPid
,
457 const char *database
, const char *itemName
, AclAuthorization action
,
458 Client::KeychainChoice
&choice
)
460 Requestor
req(requestor
);
463 if (getenv("NOSA")) {
465 getNoSA(answer
, sizeof(answer
), "Allow [someone] to do %d on %s in %s? ",
466 int(action
), (itemName
? itemName
: "[NULL item]"),
467 (database
? database
: "[NULL database]"));
468 choice
.allowAccess
= answer
[0] == 'y';
469 choice
.continueGrantingToCaller
= answer
[1] == 'g';
474 check(secagent_client_queryKeychainAccess(mServerPort
, mClientPort
,
475 &status
, req
, requestPid
, (database
? database
: ""), itemName
, action
, &choice
));
481 // Query the user for a generic existing passphrase, with selectable prompt.
483 void Client::queryOldGenericPassphrase(const OSXCode
*requestor
, pid_t requestPid
,
485 KeychainBox
&addToKeychain
, char passphrase
[maxPassphraseLength
])
487 Requestor
req(requestor
);
490 if (getenv("NOSA")) {
491 getNoSA(passphrase
, maxPassphraseLength
,
492 "Old passphrase (\"%s\") [<CR> to cancel]: ", prompt
);
493 // @@@ addToKeychain not hooked up; stays unchanged
494 stage
= oldGenericPassphraseStage
;
499 MigBoolean addBox
= addToKeychain
.setting
;
500 check(secagent_client_queryOldGenericPassphrase(mServerPort
, mClientPort
,
501 &status
, req
, requestPid
, prompt
, &mStagePort
.port(),
502 addToKeychain
.show
, &addBox
, passphrase
));
503 addToKeychain
.setting
= addBox
;
504 stage
= oldGenericPassphraseStage
;
507 void Client::retryOldGenericPassphrase(Reason reason
,
508 bool &addToKeychain
, char passphrase
[maxPassphraseLength
])
510 if (stage
!= oldGenericPassphraseStage
)
511 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
513 if (getenv("NOSA")) {
514 getNoSA(passphrase
, maxPassphraseLength
,
515 "Retry old passphrase [<CR> to cancel]: ");
520 check(secagent_client_retryOldGenericPassphrase(mStagePort
, mClientPort
,
521 &status
, reason
, &addBox
, passphrase
));
522 addToKeychain
= addBox
;
527 // Ask for a new passphrase for something (with selectable prompt).
529 void Client::queryNewGenericPassphrase(const OSXCode
*requestor
, pid_t requestPid
,
530 const char *prompt
, Reason reason
,
531 KeychainBox
&addToKeychain
, char passphrase
[maxPassphraseLength
])
533 Requestor
req(requestor
);
536 if (getenv("NOSA")) {
537 getNoSA(passphrase
, maxPassphraseLength
,
538 "New passphrase (\"%s\") (reason %d) [<CR> to cancel]: ",
540 // @@@ addToKeychain not hooked up; stays unchanged
541 stage
= newGenericPassphraseStage
;
546 MigBoolean addBox
= addToKeychain
.setting
;
547 check(secagent_client_queryNewGenericPassphrase(mServerPort
, mClientPort
,
548 &status
, req
, requestPid
, prompt
, reason
,
549 &mStagePort
.port(), addToKeychain
.show
, &addBox
, passphrase
));
550 addToKeychain
.setting
= addBox
;
551 stage
= newGenericPassphraseStage
;
554 void Client::retryNewGenericPassphrase(Reason reason
,
555 bool &addToKeychain
, char passphrase
[maxPassphraseLength
])
557 if (stage
!= newGenericPassphraseStage
)
558 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
560 if (getenv("NOSA")) {
561 getNoSA(passphrase
, maxPassphraseLength
,
562 "retry new passphrase (reason %d) [<CR> to cancel]: ", reason
);
567 check(secagent_client_retryNewGenericPassphrase(mStagePort
, mClientPort
,
568 &status
, reason
, &addBox
, passphrase
));
569 addToKeychain
= addBox
;
574 // Authorization by authentication
576 bool Client::authorizationAuthenticate(const OSXCode
*requestor
, pid_t requestPid
,
577 const char *neededGroup
, const char *candidateUser
,
578 char user
[maxUsernameLength
], char passphrase
[maxPassphraseLength
])
580 Requestor
req(requestor
);
583 if (getenv("NOSA")) {
584 getNoSA(user
, maxUsernameLength
,
585 "User to authenticate for group %s (try \"%s\" [\"-\" to deny]): ",
586 neededGroup
, (candidateUser
? candidateUser
: "[NULL]"));
587 if (strcmp(user
, "-"))
588 getNoSA(passphrase
, maxPassphraseLength
,
589 "Passphrase for user %s: ", user
);
590 stage
= authorizeStage
;
591 return strcmp(user
, "-");
595 check(secagent_client_authorizationAuthenticate(mServerPort
, mClientPort
,
596 &status
, req
, requestPid
, neededGroup
, candidateUser
, &mStagePort
.port(), user
, passphrase
));
597 stage
= authorizeStage
;
598 return status
== noErr
;
601 bool Client::retryAuthorizationAuthenticate(Reason reason
, char user
[maxUsernameLength
],
602 char passphrase
[maxPassphraseLength
])
604 if (stage
!= authorizeStage
)
605 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
); //@@@ invent a "state mismatch error"?
607 if (getenv("NOSA")) {
608 getNoSA(user
, maxUsernameLength
,
609 "Retry authenticate (reason=%d) ([\"-\" to deny again]): ", reason
);
610 if (strcmp(user
, "-"))
611 getNoSA(passphrase
, maxPassphraseLength
,
612 "Passphrase for user %s: ", user
);
613 return strcmp(user
, "-");
616 check(secagent_client_retryAuthorizationAuthenticate(mStagePort
, mClientPort
,
617 &status
, reason
, user
, passphrase
));
618 return status
== noErr
;
623 // Locate and identify the current desktop.
624 // This is moderately atrocious code. There really ought to be a way to identify
625 // the logged-in (graphics console) user (and whether there is one). As it stands,
626 // we locate the "pbs" (pasteboard server) process and obtain its uid. No pbs, no
627 // user interaction. (By all accounts, a dead pbs is a death sentence anyway.)
629 #include <sys/sysctl.h>
630 #include <mach/mach_error.h>
632 void Client::locateDesktop()
634 int mib
[3] = { CTL_KERN
, KERN_PROC
, KERN_PROC_ALL
};
636 struct kinfo_proc
*procBuf
;
638 if (sysctl(mib
, 3, NULL
, &bufSize
, NULL
, 0) < 0) {
643 procBuf
= (struct kinfo_proc
*)malloc(bufSize
); //@@@ which allocator?
644 if (sysctl(mib
, 3, procBuf
, &bufSize
, NULL
, 0))
645 CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR
);
646 int count
= bufSize
/ sizeof(struct kinfo_proc
);
647 struct kinfo_proc
*pbsProc
= NULL
;
648 for (struct kinfo_proc
*proc
= procBuf
; proc
< procBuf
+ count
; proc
++) {
649 if (!strncmp(proc
->kp_proc
.p_comm
, "pbs", MAXCOMLEN
)) {
655 if (!pbsProc
) { // no pasteboard server -- user not logged in
656 debug("SAclnt", "No pasteboard server - no user logged in");
657 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
660 desktopUid
= pbsProc
->kp_eproc
.e_ucred
.cr_uid
;
661 desktopGid
= pbsProc
->kp_eproc
.e_ucred
.cr_gid
;
662 pid_t pbsPid
= pbsProc
->kp_proc
.p_pid
;
664 debug("SAclnt", "Desktop has uid %d", desktopUid
);
667 kern_return_t result
;
668 mach_port_t pbsTaskPort
;
669 result
= task_for_pid(mach_task_self(), pbsPid
, &pbsTaskPort
);
672 mach_error("task_for_pid(pbs)", result
);
673 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
676 result
= task_get_bootstrap_port(pbsTaskPort
, &pbsBootstrap
);
679 mach_error("task_get_bootstrap_port(pbs)", result
);
680 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
684 } // end namespace SecurityAgent
685 } // end namespace Security