1/*
2 * Copyright (c) 2011-2014 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24//
25// spctl - command-line access to system policy control (SecAssessment)
26//
27#include "spctl.h"
28#include "cs_utils.h"
29#include <security_utilities/unix++.h>
30#include <getopt.h>
31
32using namespace UnixPlusPlus;
33
34
35//
36// Operational mode
37//
38enum Operation {
39	doNothing,						// none given (print usage)
40	doAssess,						// assessment operation
41	doStatus,						// master query status
42	doMasterEnable,					// master honor assessment rejects
43	doMasterDisable,				// master bypass assessment rejects
44	doDevIDStatus,					// query devid status
45	doDevIDEnable,					// devid honor assessment rejects
46	doDevIDDisable,					// devid bypass assessment rejects
47	doAdd,							// add authority rule
48	doRemove,						// remove rule(s)
49	doRuleEnable,					// (re)enable rule(s)
50	doRuleDisable,					// disable rule(s)
51	doList,							// list authority rules
52	doPurge,						// purge object cache
53};
54Operation operation = doNothing;
55
56
57//
58// Specification type
59//
60enum Specification {
61	specPath,						// path to file(s)
62	specRequirement,				// code requirement(s)
63	specAnchor,						// path to anchor certificate(s)
64	specHash,						// CodeDirectory hash(es)
65	specRule,						// (removal by) rule number
66};
67Specification specification = specPath;
68
69
70//
71// Command-line arguments and options
72//
73const char *assessmentType;
74SecAssessmentFlags assessmentFlags;
75SecAssessmentFlags outcomeFlags;
76const char *featureCheck;
77const char *label;
78const char *priority;
79const char *remarks;
80bool rawOutput;
81CFMutableDictionaryRef context = makeCFMutableDictionary();
82// additional variables declared in cs_utils
83
84
85//
86// Feature set
87//
88static const char *features[] = {
89	NULL							// sentinel
90};
91
92
93//
94// Local functions
95//
96static void usage();
97static void checkFeatures(const char *arg);
98
99static void assess(const char *target);
100static void addAuthority(const char *target);
101static void removeAuthority(const char *target);
102static void enableAuthority(const char *target);
103static void disableAuthority(const char *target);
104static void listAuthority(const char *target);
105static void status(Operation op);
106
107static CFTypeRef typeKey(const char *type);
108static string hashArgument(const char *s);
109static string fileHash(const char *path);
110
111
112//
113// Command-line options
114//
115enum {
116	optNone = 0,	// null (no, absent) option
117	optAdd,
118	optAnchor,
119	optContext,
120	optContinue,
121	optDirect,
122	optEnforce,
123	optRuleEnable,
124	optRuleDisable,
125	optMasterEnable,
126	optMasterDisable,
127	optDevIDStatus,
128	optDevIDEnable,
129	optDevIDDisable,
130	optFeatures,
131	optHash,
132	optIgnoreCache,
133	optLabel,
134	optNoCache,
135	optPath,
136	optPriority,
137	optPurge,
138	optRawOutput,
139	optRemarks,
140	optRemove,
141	optRequirement,
142	optRule,
143	optStatus,
144};
145
146const struct option options[] = {
147	{ "add",		no_argument,			NULL, optAdd },
148	{ "anchor",		no_argument,			NULL, optAnchor },
149	{ "assess",		no_argument,			NULL, 'a' },
150	{ "context",	required_argument,		NULL, optContext },
151	{ "continue",	no_argument,			NULL, optContinue },
152	{ "direct",		no_argument,			NULL, 'D' },
153	{ "status",		optional_argument,		NULL, optStatus },
154	{ "enable",		no_argument,			NULL, optRuleEnable },
155	{ "enforce-assessment",	no_argument,	NULL, optEnforce },
156	{ "disable",	no_argument,			NULL, optRuleDisable },
157	{ "master-enable",	no_argument,		NULL, optMasterEnable },
158	{ "master-disable", no_argument,		NULL, optMasterDisable },
159	{ "test-devid-status",	no_argument,	NULL, optDevIDStatus },
160	{ "test-devid-enable",	no_argument,	NULL, optDevIDEnable },
161	{ "test-devid-disable", no_argument,	NULL, optDevIDDisable },
162	{ "features",	optional_argument,		NULL, optFeatures },
163	{ "hash",		no_argument,			NULL, optHash },
164	{ "ignore-cache", no_argument,			NULL, optIgnoreCache },
165	{ "label",		required_argument,		NULL, optLabel },
166	{ "list",		no_argument,			NULL, 'l' },
167	{ "no-cache",	no_argument,			NULL, optNoCache },
168	{ "path",		no_argument,			NULL, optPath },
169	{ "priority",	required_argument,		NULL, optPriority },
170	{ "purge",		no_argument,			NULL, optPurge },
171	{ "raw",		no_argument,			NULL, optRawOutput },
172	{ "remarks",	required_argument,		NULL, optRemarks },
173	{ "remove",		no_argument,			NULL, optRemove },
174	{ "requirement", no_argument,			NULL, optRequirement },
175	{ "rule",		no_argument,			NULL, optRule },
176	{ "type",		required_argument,		NULL, 't' },
177	{ "verbose",	optional_argument,		NULL, 'v' },
178	{ }
179};
180
181
182//
183// main command-line driver
184//
185int main(int argc, char *argv[])
186{
187	try {
188		int arg, argslot;
189		while (argslot = -1,
190				(arg = getopt_long(argc, argv, "aDlt:v", options, &argslot)) != -1)
191			switch (arg) {
192			case 'a':
193				operation = doAssess;
194				break;
195			case 'D':
196				assessmentFlags |= kSecAssessmentFlagDirect;
197				outcomeFlags |= kSecAssessmentFlagDirect;
198				break;
199			case 'l':
200				operation = doList;
201				break;
202			case 't':
203				assessmentType = optarg;
204				break;
205			case 'v':
206				verbose++;
207				break;
208
209			case optAdd:
210				operation = doAdd;
211				break;
212			case optAnchor:
213				specification = specAnchor;
214				break;
215			case optContext:
216				if (const char *eq = strchr(optarg, '=')) {	// key=value
217					CFDictionaryAddValue(context, CFTempString(string(optarg, eq - optarg)), CFTempString(eq+1));
218				} else {	// key, assume =true
219					CFDictionaryAddValue(context, CFTempString(optarg), kCFBooleanTrue);
220				}
221				break;
222			case optContinue:
223				continueOnError = true;
224				break;
225			case optEnforce:
226				assessmentFlags |= kSecAssessmentFlagEnforce;
227				outcomeFlags |= kSecAssessmentFlagEnforce;
228				break;
229			case optRuleDisable:
230				operation = doRuleDisable;
231				break;
232			case optRuleEnable:
233				operation = doRuleEnable;
234				break;
235			case optMasterDisable:
236				operation = doMasterDisable;
237				break;
238			case optMasterEnable:
239				operation = doMasterEnable;
240				break;
241			case optDevIDStatus:
242				operation = doDevIDStatus;
243				break;
244			case optDevIDDisable:
245				operation = doDevIDDisable;
246				break;
247			case optDevIDEnable:
248				operation = doDevIDEnable;
249				break;
250			case optFeatures:
251				featureCheck = optarg;
252				break;
253			case optHash:
254				specification = specHash;
255				break;
256			case optIgnoreCache:
257				assessmentFlags |= kSecAssessmentFlagIgnoreCache;
258				break;
259			case optLabel:
260				label = optarg;
261				break;
262			case optNoCache:
263				assessmentFlags |= kSecAssessmentFlagNoCache;
264				break;
265			case optPath:
266				specification = specPath;
267				break;
268			case optPriority:
269				priority = optarg;
270				break;
271			case optPurge:
272				operation = doPurge;
273				break;
274			case optRawOutput:
275				rawOutput = true;
276				break;
277			case optRemarks:
278				remarks = optarg;
279				break;
280			case optRemove:
281				operation = doRemove;
282				break;
283			case optRequirement:
284				specification = specRequirement;
285				break;
286			case optRule:
287				specification = specRule;
288				break;
289			case optStatus:
290				operation = doStatus;
291				break;
292
293			case '?':
294				usage();
295			}
296
297		if (featureCheck) {
298			checkFeatures(featureCheck);
299			if (operation == doNothing)
300				exit(0);
301		}
302
303		// dispatch operations with no arguments
304		switch (operation) {
305		case doNothing:
306			usage();
307		case doStatus:
308		case doMasterEnable:
309		case doMasterDisable:
310		case doDevIDStatus:
311		case doDevIDEnable:
312		case doDevIDDisable:
313			if (optind != argc)
314				usage();
315			status(operation);
316			exit(0);
317		case doRemove:		// optional arguments
318			if (optind == argc) {
319				removeAuthority(NULL);
320				exit(0);
321			}
322			break;
323		case doRuleEnable:
324			if (optind == argc) {
325				enableAuthority(NULL);
326				exit(0);
327			}
328			break;
329		case doRuleDisable:
330			if (optind == argc) {
331				disableAuthority(NULL);
332				exit(0);
333			}
334			break;
335		case doList:
336			if (optind == argc) {
337				listAuthority(NULL);
338				exit(0);
339			}
340			break;
341		default:
342			if (optind == argc)
343				usage();
344			break;
345		}
346
347		// operate on paths given after options
348		for ( ; optind < argc; optind++) {
349			const char *target = argv[optind];
350			try {
351				switch (operation) {
352				case doAssess:
353					assess(target);
354					break;
355				case doAdd:
356					addAuthority(target);
357					break;
358				case doRemove:
359					removeAuthority(target);
360					break;
361				case doRuleEnable:
362					enableAuthority(target);
363					break;
364				case doRuleDisable:
365					disableAuthority(target);
366					break;
367				case doList:
368					listAuthority(target);
369					break;
370				default:
371					assert(false);
372				}
373			} catch (...) {
374				diagnose(target);
375				if (!exitcode)
376					exitcode = exitFailure;
377				if (!continueOnError)
378					exit(exitFailure);
379			}
380		}
381
382	} catch (...) {
383		diagnose(NULL, exitFailure);
384	}
385
386	exit(exitcode);
387}
388
389void usage()
390{
391	fprintf(stderr, "Usage: spctl --assess [--type type] [-v] path ... # assessment\n"
392		"       spctl --add [--type type] [--path|--requirement|--anchor|--hash] spec ... # add rule(s)\n"
393		"       spctl [--enable|--disable|--remove] [--type type] [--path|--requirement|--anchor|--hash|--rule] spec # change rule(s)\n"
394		"       spctl --status | --master-enable | --master-disable # system master switch\n"
395	);
396	exit(exitUsage);
397}
398
399
400//
401// Perform an assessment operation.
402// This does not change anything (except possibly, indirectly, the object cache).
403//
404void assess(const char *target)
405{
406	SecAssessmentFlags flags = assessmentFlags;
407	if (verbose > 1)
408		flags |= kSecAssessmentFlagRequestOrigin;
409	if (assessmentType)
410		CFDictionaryAddValue(context, kSecAssessmentContextKeyOperation, typeKey(assessmentType));
411
412	CheckedRef<SecAssessmentRef> ass;
413	ass.check(SecAssessmentCreate(CFTempURL(target), flags, context, ass));
414	CheckedRef<CFDictionaryRef> outcome;
415	outcome.check(SecAssessmentCopyResult(ass, outcomeFlags, outcome));
416
417	CFDictionary result(outcome.get(), 0);
418	bool success = result.get<CFBooleanRef>(kSecAssessmentAssessmentVerdict) == kCFBooleanTrue;
419	CFDictionary authority(result.get<CFDictionaryRef>(kSecAssessmentAssessmentAuthority), 0);
420	CFStringRef source = NULL;
421	if (authority)
422		source = authority.get<CFStringRef>(kSecAssessmentAssessmentSource);
423
424	// if the result is a whitelisted weak signature, bend the polite (but not raw) output to our will
425	if (!rawOutput && authority) {
426		if (CFBooleanRef weak = authority.get<CFBooleanRef>(kSecAssessmentAssessmentWeakSignature))
427			if (CFEqual(weak, kCFBooleanTrue)) {
428					// succeeded only because of weak-signature whitelist. Report it as failed
429					success = false;
430					if (source && CFEqual(source, CFSTR("allowed cdhash")))
431						source = CFSTR("matched cdhash");
432				}
433	}
434
435	if (success) {
436		note(1, "%s: accepted", target);
437	} else {
438		note(0, "%s: rejected", target);
439		if (!exitcode)
440			exitcode = exitNoverify;
441	}
442
443	if (rawOutput) {
444		if (CFRef<CFDataRef> xml = makeCFData(outcome.get()))
445			fwrite(CFDataGetBytePtr(xml), CFDataGetLength(xml), 1, stdout);
446	} else if (verbose) {
447		if (authority) {
448			if (source)
449				note(1, "source=%s", cfString(source).c_str());
450			if (CFBooleanRef cached = authority.get<CFBooleanRef>(kSecAssessmentAssessmentFromCache)) {
451				if (cached == kCFBooleanFalse)
452					note(2, "cache=no");
453				else if (CFNumberRef row = authority.get<CFNumberRef>(kSecAssessmentAssessmentAuthorityRow))
454					note(2, "cache=yes,row %d", cfNumber<int>(row));
455				else
456					note(2, "cache=yes");
457			}
458			if (CFStringRef override = authority.get<CFStringRef>(kSecAssessmentAssessmentAuthorityOverride))
459				note(0, "override=%s", cfString(override).c_str());
460		} else
461			note(1, "authority=none");
462	}
463	if (CFStringRef originator = result.get<CFStringRef>(kSecAssessmentAssessmentOriginator))
464		note(2, "origin=%s", cfString(originator).c_str());
465}
466
467
468//
469// Apply a change to the system-wide authority configuration.
470// These are all privileged operations, of course.
471//
472static CFDictionaryRef updateOperation(const char *target, CFMutableDictionaryRef context,
473	CFStringRef operation)
474{
475	SecCSFlags flags = assessmentFlags;
476	CFDictionaryAddValue(context, kSecAssessmentContextKeyUpdate, operation);
477	if (assessmentType)
478		CFDictionaryAddValue(context, kSecAssessmentContextKeyOperation, typeKey(assessmentType));
479
480	CFRef<CFTypeRef> subject;
481	if (target)
482		switch (specification) {
483		case specPath:
484			{
485				subject = makeCFURL(target);
486				// For add operations, add a bookmark so we get icons
487				if (operation == kSecAssessmentUpdateOperationAdd) {
488					CFRef<CFDataRef> bookmark = CFURLCreateBookmarkData(NULL, subject.as<CFURLRef>(), 0, NULL, NULL, NULL);
489					if (bookmark)
490						CFDictionaryAddValue(context, kSecAssessmentRuleKeyBookmark, bookmark);
491				}
492				break;
493			}
494		case specRequirement:
495			MacOSError::check(SecRequirementCreateWithString(CFTempString(target),
496				kSecCSDefaultFlags, (SecRequirementRef *)&subject.aref()));
497			break;
498		case specAnchor:
499			{
500				string reqString;
501				if (target[0] == '/') {	// assume path to anchor cert on disk
502					reqString = "anchor " + fileHash(target);
503				} else {
504					reqString = "anchor " + hashArgument(target);
505				}
506				MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString),
507					kSecCSDefaultFlags, (SecRequirementRef *)&subject.aref()));
508				break;
509			}
510		case specHash:
511			{
512				string reqString = "cdhash " + hashArgument(target);
513				MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString),
514					kSecCSDefaultFlags, (SecRequirementRef *)&subject.aref()));
515				break;
516			}
517		case specRule:
518			{
519				if (operation == kSecAssessmentUpdateOperationAdd)
520					fail("cannot insert by rule number");
521				char *end;
522				uint64_t rule = strtol(target, &end, 0);
523				if (*end)
524					fail("%s: invalid rule number", target);
525				subject.take(CFTempNumber(rule));
526				break;
527			}
528		}
529
530	if (label)
531		CFDictionaryAddValue(context, kSecAssessmentUpdateKeyLabel, CFTempString(label));
532	if (priority) {
533		char *end;
534		double pri = strtod(priority, &end);
535		if (*end)	// empty or bad conversion
536			fail("%s: invalid rule priority", priority);
537		CFDictionaryAddValue(context, kSecAssessmentUpdateKeyPriority, CFTempNumber(pri));
538	}
539	if (remarks)
540		CFDictionaryAddValue(context, kSecAssessmentUpdateKeyRemarks, CFTempString(remarks));
541
542	ErrorCheck check;
543	CFRef<CFDictionaryRef> outcome = SecAssessmentCopyUpdate(subject.get(), flags, context, check);
544	check(outcome);
545
546	if (rawOutput)
547		if (CFRef<CFDataRef> xml = makeCFData(outcome.get()))
548			fwrite(CFDataGetBytePtr(xml), CFDataGetLength(xml), 1, stdout);
549
550	return outcome.yield();
551}
552
553void addAuthority(const char *target)
554{
555	CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationAdd), noErr);
556	if (verbose && !rawOutput)
557		printf("Created rule %lld\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyRow)));
558}
559
560
561void removeAuthority(const char *target)
562{
563	CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationRemove), noErr);
564	if (verbose && !rawOutput)
565		printf("Removed %lld rule(s)\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyCount)));
566}
567
568void enableAuthority(const char *target)
569{
570	CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationEnable), noErr);
571	if (verbose && !rawOutput)
572		printf("Enabled %lld rule(s)\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyCount)));
573}
574
575void disableAuthority(const char *target)
576{
577	CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationDisable), noErr);
578	if (verbose && !rawOutput)
579		printf("Disabled %lld rule(s)\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyCount)));
580}
581
582void listAuthority(const char *target)
583{
584	CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationFind), noErr);
585	if (rawOutput)
586		return;
587	CFArrayRef rules = result.get<CFArrayRef>(kSecAssessmentUpdateKeyFound);
588	CFIndex count = CFArrayGetCount(rules);
589	for (CFIndex n = 0; n < count; n++) {
590		CFDictionary rule(CFArrayGetValueAtIndex(rules, n), noErr);
591		string typeString = "?";
592		if (CFStringRef type = rule.get<CFStringRef>(kSecAssessmentRuleKeyType)) {
593			typeString = cfString(type);
594			string::size_type colon = typeString.find(':');
595			if (colon != string::npos)
596				typeString = typeString.substr(colon+1);
597		}
598		string label = "UNLABELED";
599		if (CFStringRef lab = rule.get<CFStringRef>(kSecAssessmentRuleKeyLabel))
600			label = cfString(lab);
601		printf("%lld[%s] P%g %s %s",
602			cfNumber<long long>(rule.get<CFNumberRef>(kSecAssessmentRuleKeyID)),
603			label.c_str(),
604			cfNumber<double>(rule.get<CFNumberRef>(kSecAssessmentRuleKeyPriority)),
605			(rule.get<CFBooleanRef>(kSecAssessmentRuleKeyAllow) == kCFBooleanTrue) ? "allow" : "deny",
606			typeString.c_str()
607		);
608		if (CFStringRef remarks = rule.get<CFStringRef>(kSecAssessmentRuleKeyRemarks))
609			printf(" [%s]", cfString(remarks).c_str());
610		printf("\n");
611		printf("\t%s\n",
612			cfString(rule.get<CFStringRef>(kSecAssessmentRuleKeyRequirement)).c_str()
613		);
614	}
615}
616
617
618//
619// Manipulate the master status.
620// This reports on, or changes, the master enable status.
621// It does not actually affect the authority database, though
622// it may tell the system to bypass it altogether.
623//
624void status(Operation op)
625{
626	ErrorCheck check;
627	CFBooleanRef state;
628	switch (op) {
629	case doStatus:
630		check(SecAssessmentControl(CFSTR("ui-status"), &state, check));
631		if (state == kCFBooleanTrue) {
632			printf("assessments enabled\n");
633			if (verbose > 0) {
634				check(SecAssessmentControl(CFSTR("ui-get-devid"), &state, check));
635				if (state == kCFBooleanTrue)
636					printf("developer id enabled\n");
637				else
638					printf("developer id disabled\n");
639			}
640			exit(0);
641		} else {
642			printf("assessments disabled\n");
643			exit(1);
644		}
645	case doDevIDStatus:
646		check(SecAssessmentControl(CFSTR("ui-get-devid"), &state, check));
647		if (state == kCFBooleanTrue) {
648			printf("devid enabled\n");
649			exit(0);
650		} else {
651			printf("devid disabled\n");
652			exit(1);
653		}
654	case doMasterEnable:
655		check(SecAssessmentControl(CFSTR("ui-enable"), NULL, check));
656		exit(0);
657	case doMasterDisable:
658		check(SecAssessmentControl(CFSTR("ui-disable"), NULL, check));
659		exit(0);
660	case doDevIDEnable:
661		check(SecAssessmentControl(CFSTR("ui-enable-devid"), NULL, check));
662		exit(0);
663	case doDevIDDisable:
664		check(SecAssessmentControl(CFSTR("ui-disable-devid"), NULL, check));
665		exit(0);
666	default:
667		assert(false);
668	}
669}
670
671
672//
673// Support helper functions
674//
675static CFTypeRef typeKey(const char *type)
676{
677	if (!strncmp(type, "execute", strlen(type)))
678		return kSecAssessmentOperationTypeExecute;
679	else if (!strncmp(type, "install", strlen(type)))
680		return kSecAssessmentOperationTypeInstall;
681	else if (!strncmp(type, "open", strlen(type)))
682		return kSecAssessmentOperationTypeOpenDocument;
683	else
684		fail("%s: unrecognized assessment type", type);
685}
686
687static string hashArgument(const char *s)
688{
689	for (const char *p = s; *p; p++)
690		if (!isxdigit(*p))
691			fail("%s: invalid hash specification", s);
692	return string("H\"") + s + "\"";
693}
694
695static string fileHash(const char *path)
696{
697	CFRef<CFDataRef> certData = cfLoadFile(path);
698	SHA1 hash;
699	hash.update(CFDataGetBytePtr(certData), CFDataGetLength(certData));
700	SHA1::Digest digest;
701	hash.finish(digest);
702	string s;
703	for (const SHA1::Byte *p = digest; p < digest + sizeof(digest); p++) {
704		char buf[3];
705		snprintf(buf, sizeof(buf), "%02.2x", *p);
706		s += buf;
707	}
708	return hashArgument(s.c_str());
709}
710
711
712//
713// Exit unless each of the comma-separated feature names is supported
714// by this version of spctl(8).
715//
716void checkFeatures(const char *arg)
717{
718	while (true) {
719		const char *comma = strchr(arg, ',');
720		string feature = comma ? string(arg, comma-arg) : arg;
721		if (feature.empty())
722			fail("Invalid feature name");
723		const char **p;
724		for (p = features; *p && feature != *p; p++) ;
725		if (!*p)
726			fail("%s: not supported in this version", feature.c_str());
727		if (comma) {
728			arg = comma + 1;
729			if (!*arg)	// tolerate trailing comma
730				break;
731		} else {
732			break;
733		}
734	}
735}
736