+/*
+ * Copyright (c) 2018 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+#include "SecAssessment.h"
+#include "notarization.h"
+#include "unix++.h"
+
+typedef struct __attribute__((packed)) _package_trailer {
+ uint8_t magic[4];
+ uint16_t version;
+ uint16_t type;
+ uint32_t length;
+ uint8_t reserved[4];
+} package_trailer_t;
+
+enum TrailerType {
+ TrailerTypeInvalid = 0,
+ TrailerTypeTerminator,
+ TrailerTypeTicket,
+};
+
+static const char *TrailerMagic = "t8lr";
+
+namespace Security {
+namespace CodeSigning {
+
+static void
+registerStapledTicketWithSystem(CFDataRef data)
+{
+ secinfo("notarization", "Registering stapled ticket with system");
+
+#if TARGET_OS_OSX
+ CFErrorRef error = NULL;
+ if (!SecAssessmentTicketRegister(data, &error)) {
+ secerror("Error registering stapled ticket: %@", data);
+ }
+#endif // TARGET_OS_OSX
+}
+
+bool
+checkNotarizationServiceForRevocation(CFDataRef hash, SecCSDigestAlgorithm hashType, double *date)
+{
+ bool is_revoked = false;
+
+ secinfo("notarization", "checking with online notarization service for hash: %@", hash);
+
+#if TARGET_OS_OSX
+ CFRef<CFErrorRef> error;
+ if (!SecAssessmentTicketLookup(hash, hashType, kSecAssessmentTicketFlagForceOnlineCheck, date, &error.aref())) {
+ CFIndex err = CFErrorGetCode(error);
+ if (err == EACCES) {
+ secerror("Notarization daemon found revoked hash: %@", hash);
+ is_revoked = true;
+ } else {
+ secerror("Error checking with notarization daemon: %ld", err);
+ }
+ }
+#endif
+
+ return is_revoked;
+}
+
+bool
+isNotarized(const Requirement::Context *context)
+{
+ CFRef<CFDataRef> cd;
+ CFRef<CFErrorRef> error;
+ bool is_notarized = false;
+ SecCSDigestAlgorithm hashType = kSecCodeSignatureNoHash;
+
+ if (context == NULL) {
+ is_notarized = false;
+ goto lb_exit;
+ }
+
+ if (context->directory) {
+ cd.take(context->directory->cdhash());
+ hashType = (SecCSDigestAlgorithm)context->directory->hashType;
+ } else if (context->packageChecksum) {
+ cd = context->packageChecksum;
+ hashType = context->packageAlgorithm;
+ }
+
+ if (cd.get() == NULL) {
+ // No cdhash means we can't check notarization.
+ is_notarized = false;
+ goto lb_exit;
+ }
+
+ secinfo("notarization", "checking notarization on %d, %@", hashType, cd.get());
+
+#if TARGET_OS_OSX
+ if (SecAssessmentTicketLookup(cd, hashType, kSecAssessmentTicketFlagDefault, NULL, &error.aref())) {
+ is_notarized = true;
+ } else {
+ is_notarized = false;
+ if (error.get() != NULL) {
+ secerror("Error checking with notarization daemon: %ld", CFErrorGetCode(error));
+ }
+ }
+#endif
+
+lb_exit:
+ secinfo("notarization", "isNotarized = %d", is_notarized);
+ return is_notarized;
+}
+
+void
+registerStapledTicketInPackage(const std::string& path)
+{
+ int fd = 0;
+ package_trailer_t trailer;
+ off_t readOffset = 0;
+ size_t bytesRead = 0;
+ off_t backSeek = 0;
+ uint8_t *ticketData = NULL;
+ boolean_t ticketTrailerFound = false;
+ CFRef<CFDataRef> data;
+
+ secinfo("notarization", "Extracting ticket from package: %s", path.c_str());
+
+ fd = open(path.c_str(), O_RDONLY);
+ if (fd <= 0) {
+ secerror("cannot open package for reading");
+ goto lb_exit;
+ }
+
+ bzero(&trailer, sizeof(trailer));
+ readOffset = lseek(fd, -sizeof(trailer), SEEK_END);
+ if (readOffset <= 0) {
+ secerror("could not scan for first trailer on package (error - %d)", errno);
+ goto lb_exit;
+ }
+
+ while (!ticketTrailerFound) {
+ bytesRead = read(fd, &trailer, sizeof(trailer));
+ if (bytesRead != sizeof(trailer)) {
+ secerror("could not read next trailer from package (error - %d)", errno);
+ goto lb_exit;
+ }
+
+ if (memcmp(trailer.magic, TrailerMagic, strlen(TrailerMagic)) != 0) {
+ // Most packages will not be stapled, so this isn't really an error.
+ secdebug("notarization", "package did not end in a trailer");
+ goto lb_exit;
+ }
+
+ switch (trailer.type) {
+ case TrailerTypeTicket:
+ ticketTrailerFound = true;
+ break;
+ case TrailerTypeTerminator:
+ // Found a terminator before a trailer, so just exit.
+ secinfo("notarization", "package had a trailer, but no ticket trailers");
+ goto lb_exit;
+ case TrailerTypeInvalid:
+ secinfo("notarization", "package had an invalid trailer");
+ goto lb_exit;
+ default:
+ // it's an unsupported trailer type, so skip it.
+ break;
+ }
+
+ // If we're here, it's either a ticket or an unknown trailer. In both cases we can definitely seek back to the
+ // beginning of the data pointed to by this trailer, which is the length of its data and the size of the trailer itself.
+ backSeek = -1 * (sizeof(trailer) + trailer.length);
+ if (!ticketTrailerFound) {
+ // 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
+ // trailer blob to prepare for reading it.
+ backSeek -= sizeof(trailer);
+ }
+ readOffset = lseek(fd, backSeek, SEEK_CUR);
+ if (readOffset <= 0) {
+ secerror("could not scan backwards (%lld) for next trailer on package (error - %d)", backSeek, errno);
+ goto lb_exit;
+ }
+ }
+
+ // If we got here, we have a valid ticket trailer and already seeked back to the beginning of its data.
+ ticketData = (uint8_t*)malloc(trailer.length);
+ if (ticketData == NULL) {
+ secerror("could not allocate memory for ticket");
+ goto lb_exit;
+ }
+
+ bytesRead = read(fd, ticketData, trailer.length);
+ if (bytesRead != trailer.length) {
+ secerror("unable to read entire ticket (error - %d)", errno);
+ goto lb_exit;
+ }
+
+ data = CFDataCreateWithBytesNoCopy(NULL, ticketData, trailer.length, NULL);
+ if (data.get() == NULL) {
+ secerror("unable to create cfdata for notarization");
+ goto lb_exit;
+ }
+
+ secinfo("notarization", "successfully found stapled ticket for: %s", path.c_str());
+ registerStapledTicketWithSystem(data);
+
+lb_exit:
+ if (fd) {
+ close(fd);
+ }
+ if (ticketData) {
+ free(ticketData);
+ }
+}
+
+void
+registerStapledTicketInBundle(const std::string& path)
+{
+ int fd = 0;
+ struct stat st;
+ uint8_t *ticketData = NULL;
+ size_t ticketLength = 0;
+ size_t bytesRead = 0;
+ CFRef<CFDataRef> data;
+ std::string ticketLocation = path + "/Contents/CodeResources";
+
+ secinfo("notarization", "Extracting ticket from bundle: %s", path.c_str());
+
+ fd = open(ticketLocation.c_str(), O_RDONLY);
+ if (fd <= 0) {
+ // Only print an error if the file exists, otherwise its an expected early exit case.
+ if (errno != ENOENT) {
+ secerror("cannot open stapled file for reading: %d", errno);
+ }
+ goto lb_exit;
+ }
+
+ if (fstat(fd, &st)) {
+ secerror("unable to stat stapling file: %d", errno);
+ goto lb_exit;
+ }
+
+ if ((st.st_mode & S_IFREG) != S_IFREG) {
+ secerror("stapling is not a regular file");
+ goto lb_exit;
+ }
+
+ if (st.st_size <= INT_MAX) {
+ ticketLength = (size_t)st.st_size;
+ } else {
+ secerror("ticket size was too large: %lld", st.st_size);
+ goto lb_exit;
+ }
+
+ ticketData = (uint8_t*)malloc(ticketLength);
+ if (ticketData == NULL) {
+ secerror("unable to allocate data for ticket");
+ goto lb_exit;
+ }
+
+ bytesRead = read(fd, ticketData, ticketLength);
+ if (bytesRead != ticketLength) {
+ secerror("unable to read entire ticket from bundle");
+ goto lb_exit;
+ }
+
+ data = CFDataCreateWithBytesNoCopy(NULL, ticketData, ticketLength, NULL);
+ if (data.get() == NULL) {
+ secerror("unable to create cfdata for notarization");
+ goto lb_exit;
+ }
+
+ secinfo("notarization", "successfully found stapled ticket for: %s", path.c_str());
+ registerStapledTicketWithSystem(data);
+
+lb_exit:
+ if (fd) {
+ close(fd);
+ }
+ if (ticketData) {
+ free(ticketData);
+ }
+}
+
+void
+registerStapledTicketInDMG(CFDataRef ticketData)
+{
+ if (ticketData == NULL) {
+ return;
+ }
+ secinfo("notarization", "successfully found stapled ticket in DMG");
+ registerStapledTicketWithSystem(ticketData);
+}
+
+}
+}