/ OSX / libsecurity_codesigning / lib / notarization.cpp
notarization.cpp
  1  /*
  2   * Copyright (c) 2018 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  #include "SecAssessment.h"
 24  #include "notarization.h"
 25  #include <security_utilities/unix++.h>
 26  
 27  typedef struct __attribute__((packed)) _package_trailer {
 28  	uint8_t magic[4];
 29  	uint16_t version;
 30  	uint16_t type;
 31  	uint32_t length;
 32  	uint8_t reserved[4];
 33  } package_trailer_t;
 34  
 35  enum TrailerType {
 36  	TrailerTypeInvalid = 0,
 37  	TrailerTypeTerminator,
 38  	TrailerTypeTicket,
 39  };
 40  
 41  static const char *TrailerMagic = "t8lr";
 42  
 43  namespace Security {
 44  namespace CodeSigning {
 45  
 46  static void
 47  registerStapledTicketWithSystem(CFDataRef data)
 48  {
 49  	secinfo("notarization", "Registering stapled ticket with system");
 50  
 51  #if TARGET_OS_OSX
 52  	CFRef<CFErrorRef> error;
 53  	if (!SecAssessmentTicketRegister(data, &error.aref())) {
 54  		secerror("Error registering stapled ticket: %@", error.get());
 55  	}
 56  #endif // TARGET_OS_OSX
 57  }
 58  
 59  bool
 60  checkNotarizationServiceForRevocation(CFDataRef hash, SecCSDigestAlgorithm hashType, double *date)
 61  {
 62  	bool is_revoked = false;
 63  
 64  	secinfo("notarization", "checking with online notarization service for hash: %@", hash);
 65  
 66  #if TARGET_OS_OSX
 67  	CFRef<CFErrorRef> error;
 68  	if (!SecAssessmentTicketLookup(hash, hashType, kSecAssessmentTicketFlagForceOnlineCheck, date, &error.aref())) {
 69  		CFIndex err = CFErrorGetCode(error);
 70  		if (err == EACCES) {
 71  			secerror("Notarization daemon found revoked hash: %@", hash);
 72  			is_revoked = true;
 73  		} else {
 74  			secerror("Error checking with notarization daemon: %ld", err);
 75  		}
 76  	}
 77  #endif
 78  
 79  	return is_revoked;
 80  }
 81  
 82  bool
 83  isNotarized(const Requirement::Context *context)
 84  {
 85  	CFRef<CFDataRef> cd;
 86  	CFRef<CFErrorRef> error;
 87  	bool is_notarized = false;
 88  	SecCSDigestAlgorithm hashType = kSecCodeSignatureNoHash;
 89  
 90  	if (context == NULL) {
 91  		is_notarized = false;
 92  		goto lb_exit;
 93  	}
 94  
 95  	if (context->directory) {
 96  		cd.take(context->directory->cdhash());
 97  		hashType = (SecCSDigestAlgorithm)context->directory->hashType;
 98  	} else if (context->packageChecksum) {
 99  		cd = context->packageChecksum;
100  		hashType = context->packageAlgorithm;
101  	}
102  
103  	if (cd.get() == NULL) {
104  		// No cdhash means we can't check notarization.
105  		is_notarized = false;
106  		goto lb_exit;
107  	}
108  
109  	secinfo("notarization", "checking notarization on %d, %@", hashType, cd.get());
110  
111  #if TARGET_OS_OSX
112  	if (SecAssessmentTicketLookup(cd, hashType, kSecAssessmentTicketFlagDefault, NULL, &error.aref())) {
113  		is_notarized = true;
114  	} else {
115  		is_notarized = false;
116  		if (error.get() != NULL) {
117  			secerror("Error checking with notarization daemon: %ld", CFErrorGetCode(error));
118  		}
119  	}
120  #endif
121  
122  lb_exit:
123  	secinfo("notarization", "isNotarized = %d", is_notarized);
124  	return is_notarized;
125  }
126  
127  void
128  registerStapledTicketInPackage(const std::string& path)
129  {
130  	int fd = 0;
131  	package_trailer_t trailer;
132  	off_t readOffset = 0;
133  	size_t bytesRead = 0;
134  	off_t backSeek = 0;
135  	uint8_t *ticketData = NULL;
136  	boolean_t ticketTrailerFound = false;
137  	CFRef<CFDataRef> data;
138  
139  	secinfo("notarization", "Extracting ticket from package: %s", path.c_str());
140  
141  	fd = open(path.c_str(), O_RDONLY);
142  	if (fd <= 0) {
143  		secerror("cannot open package for reading");
144  		goto lb_exit;
145  	}
146  
147  	bzero(&trailer, sizeof(trailer));
148  	readOffset = lseek(fd, -sizeof(trailer), SEEK_END);
149  	if (readOffset <= 0) {
150  		secerror("could not scan for first trailer on package (error - %d)", errno);
151  		goto lb_exit;
152  	}
153  
154  	while (!ticketTrailerFound) {
155  		bytesRead = read(fd, &trailer, sizeof(trailer));
156  		if (bytesRead != sizeof(trailer)) {
157  			secerror("could not read next trailer from package (error - %d)", errno);
158  			goto lb_exit;
159  		}
160  
161  		if (memcmp(trailer.magic, TrailerMagic, strlen(TrailerMagic)) != 0) {
162  			// Most packages will not be stapled, so this isn't really an error.
163  			secdebug("notarization", "package did not end in a trailer");
164  			goto lb_exit;
165  		}
166  
167  		switch (trailer.type) {
168  			case TrailerTypeTicket:
169  				ticketTrailerFound = true;
170  				break;
171  			case TrailerTypeTerminator:
172  				// Found a terminator before a trailer, so just exit.
173  				secinfo("notarization", "package had a trailer, but no ticket trailers");
174  				goto lb_exit;
175  			case TrailerTypeInvalid:
176  				secinfo("notarization", "package had an invalid trailer");
177  				goto lb_exit;
178  			default:
179  				// it's an unsupported trailer type, so skip it.
180  				break;
181  		}
182  
183  		// If we're here, it's either a ticket or an unknown trailer.  In both cases we can definitely seek back to the
184  		// beginning of the data pointed to by this trailer, which is the length of its data and the size of the trailer itself.
185  		backSeek = -1 * (sizeof(trailer) + trailer.length);
186  		if (!ticketTrailerFound) {
187  			// If we didn't find a ticket, we're about to iterate again and want to read the next trailer so seek back an additional
188  			// trailer blob to prepare for reading it.
189  			backSeek -= sizeof(trailer);
190  		}
191  		readOffset = lseek(fd, backSeek, SEEK_CUR);
192  		if (readOffset <= 0) {
193  			secerror("could not scan backwards (%lld) for next trailer on package (error - %d)", backSeek, errno);
194  			goto lb_exit;
195  		}
196  	}
197  
198  	// If we got here, we have a valid ticket trailer and already seeked back to the beginning of its data.
199  	ticketData = (uint8_t*)malloc(trailer.length);
200  	if (ticketData == NULL) {
201  		secerror("could not allocate memory for ticket");
202  		goto lb_exit;
203  	}
204  
205  	bytesRead = read(fd, ticketData, trailer.length);
206  	if (bytesRead != trailer.length) {
207  		secerror("unable to read entire ticket (error - %d)", errno);
208  		goto lb_exit;
209  	}
210  
211  	data.take(makeCFDataMalloc(ticketData, trailer.length));
212  	if (data.get() == NULL) {
213  		secerror("unable to create cfdata for notarization");
214  		goto lb_exit;
215  	}
216  
217  	secinfo("notarization", "successfully found stapled ticket for: %s", path.c_str());
218  	registerStapledTicketWithSystem(data);
219  
220  lb_exit:
221  	if (fd) {
222  		close(fd);
223  	}
224  }
225  
226  void
227  registerStapledTicketInBundle(const std::string& path)
228  {
229  	int fd = 0;
230  	struct stat st;
231  	uint8_t *ticketData = NULL;
232  	size_t ticketLength = 0;
233  	size_t bytesRead = 0;
234  	CFRef<CFDataRef> data;
235  	std::string ticketLocation = path + "/Contents/CodeResources";
236  
237  	secinfo("notarization", "Extracting ticket from bundle: %s", path.c_str());
238  
239  	fd =  open(ticketLocation.c_str(), O_RDONLY);
240  	if (fd <= 0) {
241  		// Only print an error if the file exists, otherwise its an expected early exit case.
242  		if (errno != ENOENT) {
243  			secerror("cannot open stapled file for reading: %d", errno);
244  		}
245  		goto lb_exit;
246  	}
247  
248  	if (fstat(fd, &st)) {
249  		secerror("unable to stat stapling file: %d", errno);
250  		goto lb_exit;
251  	}
252  
253  	if ((st.st_mode & S_IFREG) != S_IFREG) {
254  		secerror("stapling is not a regular file");
255  		goto lb_exit;
256  	}
257  
258  	if (st.st_size <= INT_MAX) {
259  		ticketLength = (size_t)st.st_size;
260  	} else {
261  		secerror("ticket size was too large: %lld", st.st_size);
262  		goto lb_exit;
263  	}
264  
265  	ticketData = (uint8_t*)malloc(ticketLength);
266  	if (ticketData == NULL) {
267  		secerror("unable to allocate data for ticket");
268  		goto lb_exit;
269  	}
270  
271  	bytesRead = read(fd, ticketData, ticketLength);
272  	if (bytesRead != ticketLength) {
273  		secerror("unable to read entire ticket from bundle");
274  		goto lb_exit;
275  	}
276  
277  	data.take(makeCFDataMalloc(ticketData, ticketLength));
278  	if (data.get() == NULL) {
279  		secerror("unable to create cfdata for notarization");
280  		goto lb_exit;
281  	}
282  
283  	secinfo("notarization", "successfully found stapled ticket for: %s", path.c_str());
284  	registerStapledTicketWithSystem(data);
285  
286  lb_exit:
287  	if (fd) {
288  		close(fd);
289  	}
290  }
291  
292  void
293  registerStapledTicketInDMG(CFDataRef ticketData)
294  {
295  	if (ticketData == NULL) {
296  		return;
297  	}
298  	secinfo("notarization", "successfully found stapled ticket in DMG");
299  	registerStapledTicketWithSystem(ticketData);
300  }
301  
302  void
303  registerStapledTicketInMachO(CFDataRef ticketData)
304  {
305  	if (ticketData == NULL) {
306  		return;
307  	}
308  	secinfo("notarization", "successfully found stapled ticket in MachO");
309  	registerStapledTicketWithSystem(ticketData);
310  }
311  
312  }
313  }