Commit e1b57677 authored by George Nachman's avatar George Nachman

Add SignedArchive project

parent 22230e31
SignedArchive.xcodeproj/xcuserdata
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:SignedArchive.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "53F5894F21C9B527004A372B"
BuildableName = "extract"
BlueprintName = "extract"
ReferencedContainer = "container:SignedArchive.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "53F5894F21C9B527004A372B"
BuildableName = "extract"
BlueprintName = "extract"
ReferencedContainer = "container:SignedArchive.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "53F5894F21C9B527004A372B"
BuildableName = "extract"
BlueprintName = "extract"
ReferencedContainer = "container:SignedArchive.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "/tmp/passwd.sigzip /tmp/passwd.txt"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "53F5894F21C9B527004A372B"
BuildableName = "extract"
BlueprintName = "extract"
ReferencedContainer = "container:SignedArchive.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 George Nachman. All rights reserved.</string>
</dict>
</plist>
//
// SIGArchiveBuilder.h
// SignedArchive
//
// Created by George Nachman on 12/16/18.
// Copyright © 2018 George Nachman. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SIGArchiveChunk.h"
NS_ASSUME_NONNULL_BEGIN
@class SIGIdentity;
@interface SIGArchiveBuilder : NSObject
@property (nonatomic, readonly) NSURL *payloadFileURL;
@property (nonatomic, readonly) SIGIdentity *identity;
- (instancetype)initWithPayloadFileURL:(NSURL *)payloadFileURL
identity:(SIGIdentity *)identity NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (BOOL)writeToURL:(NSURL *)url
error:(out NSError **)error;
@end
NS_ASSUME_NONNULL_END
//
// SIGArchiveBuilder.m
// SignedArchive
//
// Created by George Nachman on 12/16/18.
// Copyright © 2018 George Nachman. All rights reserved.
//
#import "SIGArchiveBuilder.h"
#import "SIGArchiveChunk.h"
#import "SIGArchiveVerifier.h"
#import "SIGCertificate.h"
#import "SIGError.h"
#import "SIGIdentity.h"
#import "SIGKey.h"
#import "SIGSHA2SigningAlgorithm.h"
@implementation SIGArchiveBuilder {
long long _offset;
}
- (instancetype)initWithPayloadFileURL:(NSURL *)url
identity:(SIGIdentity *)identity {
self = [super init];
if (self) {
_payloadFileURL = url;
_identity = identity;
}
return self;
}
#pragma mark - API
- (BOOL)writeToURL:(NSURL *)url
error:(out NSError * _Nullable __autoreleasing *)error {
NSData *signature = [self signature:error];
if (!signature) {
return NO;
}
NSData *certificate = _identity.signingCertificate.data;
if (!certificate) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeNoCertificate];
}
return NO;
}
NSOutputStream *writeStream = [NSOutputStream outputStreamWithURL:url append:NO];
if (!writeStream) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeIOWrite detail:@"Could not create write stream"];
}
return NO;
}
[writeStream open];
if (![self writeHeaderToStream:writeStream error:error]) {
return NO;
}
if (![self writeMetadataToStream:writeStream error:error]) {
return NO;
}
if (![self writePayloadToStream:writeStream error:error]) {
return NO;
}
if (![self writeSignature:signature toStream:writeStream error:error]) {
return NO;
}
if (![self writeCertificate:certificate toStream:writeStream error:error]) {
return NO;
}
[writeStream close];
return YES;
}
#pragma mark - Write Chunks
- (BOOL)writeHeaderToStream:(NSOutputStream *)stream error:(out NSError * _Nullable __autoreleasing *)error {
NSData *data = [SIGArchiveHeaderMagicString dataUsingEncoding:NSUTF8StringEncoding];
const NSInteger desiredLength = data.length;
SIGArchiveChunkWriter *chunkWriter = [[SIGArchiveChunkWriter alloc] initWithTag:SIGArchiveTagHeader
length:desiredLength
offset:_offset];
const BOOL ok = [chunkWriter writeData:data
toStream:stream
error:error];
_offset += chunkWriter.chunkLength;
return ok;
}
- (BOOL)writePayloadToStream:(NSOutputStream *)writeStream error:(out NSError * _Nullable __autoreleasing *)errorOut {
NSInputStream *readStream = [[NSInputStream alloc] initWithURL:_payloadFileURL];
if (!readStream) {
return NO;
}
[readStream open];
NSError *error = nil;
NSInteger length = [[NSFileManager defaultManager] attributesOfItemAtPath:_payloadFileURL.path error:&error].fileSize;
if (length <= 0 || error != nil) {
if (errorOut) {
*errorOut = [SIGError errorWrapping:error
code:SIGErrorCodeIORead
detail:@"Error checking file size"];
}
return NO;
}
SIGArchiveChunkWriter *chunkWriter = [[SIGArchiveChunkWriter alloc] initWithTag:SIGArchiveTagPayload
length:length
offset:_offset];
const BOOL ok = [chunkWriter writeStream:readStream
toStream:writeStream
error:errorOut];
_offset += chunkWriter.chunkLength;
return ok;
}
- (BOOL)writeMetadataToStream:(NSOutputStream *)writeStream error:(out NSError * _Nullable __autoreleasing *)error {
NSArray<NSString *> *fields = @[ @"version=1",
@"digest-type=SHA2" ];
NSString *metadata = [fields componentsJoinedByString:@"\n"];
NSData *data = [metadata dataUsingEncoding:NSUTF8StringEncoding];
SIGArchiveChunkWriter *chunkWriter = [[SIGArchiveChunkWriter alloc] initWithTag:SIGArchiveTagMetadata
length:data.length
offset:_offset];
const BOOL ok = [chunkWriter writeData:data
toStream:writeStream
error:error];
_offset += chunkWriter.chunkLength;
return ok;
}
- (BOOL)writeSignature:(NSData *)signature toStream:(NSOutputStream *)writeStream error:(out NSError * _Nullable __autoreleasing *)error {
SIGArchiveChunkWriter *chunkWriter = [[SIGArchiveChunkWriter alloc] initWithTag:SIGArchiveTagSignature
length:signature.length
offset:_offset];
const BOOL ok = [chunkWriter writeData:signature
toStream:writeStream
error:error];
_offset += chunkWriter.chunkLength;
return ok;
}
- (BOOL)writeCertificate:(NSData *)certificate toStream:(NSOutputStream *)writeStream error:(out NSError * _Nullable __autoreleasing *)error {
SIGArchiveChunkWriter *chunkWriter = [[SIGArchiveChunkWriter alloc] initWithTag:SIGArchiveTagCertificate
length:certificate.length
offset:_offset];
const BOOL ok = [chunkWriter writeData:certificate
toStream:writeStream
error:error];
_offset += chunkWriter.chunkLength;
return ok;
}
#pragma mark - Signing
- (id<SIGSigningAlgorithm>)signingAlgorithm:(out NSError **)error {
id<SIGSigningAlgorithm> algorithm = [[SIGSHA2SigningAlgorithm alloc] init];
if (!algorithm) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeAlgorithmCreationFailed];
}
}
return algorithm;
}
- (NSData *)signature:(out NSError **)error {
id<SIGSigningAlgorithm> algorithm = [self signingAlgorithm:error];
if (!algorithm) {
return nil;
}
NSInputStream *readStream = [NSInputStream inputStreamWithFileAtPath:_payloadFileURL.path];
if (!readStream) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeIORead];
}
return nil;
}
return [algorithm signatureForInputStream:readStream
usingIdentity:_identity
error:error];
}
@end
//
// SIGArchiveChunk.h
// SignedArchive
//
// Created by George Nachman on 12/17/18.
// Copyright © 2018 George Nachman. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(long long, SIGArchiveTag) {
SIGArchiveTagHeader = 0,
SIGArchiveTagPayload = 1,
SIGArchiveTagMetadata = 2,
SIGArchiveTagSignature = 3,
SIGArchiveTagCertificate = 4,
};
extern NSString *const SIGArchiveHeaderMagicString;
@interface SIGArchiveChunk : NSObject
@property (nonatomic, readonly) SIGArchiveTag tag;
@property (nonatomic, readonly) long long payloadLength;
@property (nonatomic, readonly) long long chunkLength;
@property (nonatomic, readonly) long long payloadOffset;
@property (nonatomic, readonly) NSFileHandle *fileHandle;
+ (instancetype)chunkFromFileHandle:(NSFileHandle *)fileHandle
atOffset:(long long)offset
error:(out NSError **)error;
- (instancetype)initWithTag:(SIGArchiveTag)tag
length:(long long)length
offset:(long long)offset NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (NSData *)data:(out NSError **)error;
@end
@interface SIGArchiveChunkWriter : SIGArchiveChunk
- (BOOL)writeData:(NSData *)data
toStream:(NSOutputStream *)stream
error:(out NSError **)error;
- (BOOL)writeStream:(NSInputStream *)readStream
toStream:(NSOutputStream *)writeStream
error:(out NSError **)error;
@end
NS_ASSUME_NONNULL_END
//
// SIGArchiveChunk.m
// SignedArchive
//
// Created by George Nachman on 12/17/18.
// Copyright © 2018 George Nachman. All rights reserved.
//
#import "SIGArchiveChunk.h"
#import "SIGError.h"
static const NSInteger SIGArchiveChunkOverhead = sizeof(long long) * 2;
NSString *const SIGArchiveHeaderMagicString = @"signed-archive";
@implementation SIGArchiveChunk {
NSData *_data;
}
+ (instancetype)chunkFromFileHandle:(NSFileHandle *)fileHandle
atOffset:(long long)offset
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
@try {
[fileHandle seekToFileOffset:offset];
} @catch (NSException *exception) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeIORead detail:exception.reason];
}
return nil;
}
SIGArchiveTag tag;
if (![self readIntegerFromFileHandle:fileHandle fromOffset:offset value:(long long *)&tag error:error]) {
return nil;
}
long long length;
if (![self readIntegerFromFileHandle:fileHandle fromOffset:offset + sizeof(long long) value:(long long *)&length error:error]) {
return nil;
}
SIGArchiveChunk *chunk = [[SIGArchiveChunk alloc] initWithTag:tag
length:length
offset:offset + SIGArchiveChunkOverhead];
if (!chunk) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeUnknown
detail:@"Failed to create chunk object"];
}
return nil;
}
chunk->_fileHandle = fileHandle;
return chunk;
}
+ (NSData *)readDataFromFileHandle:(NSFileHandle *)fileHandle
expectedLength:(long long)expectedLength
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
NSError *readError = nil;
NSData *data = nil;
@try {
data = [fileHandle readDataOfLength:expectedLength];
} @catch (NSException *exception) {
readError = [SIGError errorWithCode:SIGErrorCodeIORead
detail:[exception reason]];
}
if (!readError && data.length != expectedLength) {
readError = [SIGError errorWithCode:SIGErrorCodeIORead
detail:@"Short read"];
}
if (error) {
*error = readError;
}
if (readError) {
return nil;
}
return data;
}
+ (NSData *)readDataFromFileHandle:(NSFileHandle *)fileHandle
fromOffset:(NSInteger)offset
expectedLength:(long long)expectedLength
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
@try {
[fileHandle seekToFileOffset:offset];
} @catch (NSException *exception) {
NSError *readError = [SIGError errorWithCode:SIGErrorCodeIORead
detail:[exception reason]];
if (error) {
*error = readError;
}
return nil;
}
return [self readDataFromFileHandle:fileHandle
expectedLength:expectedLength
error:error];
}
+ (BOOL)readIntegerFromFileHandle:(NSFileHandle *)fileHandle
fromOffset:(NSInteger)offset
value:(out long long *)valuePointer
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
NSData *data = [self readDataFromFileHandle:fileHandle
fromOffset:offset
expectedLength:sizeof(long long)
error:error];
if (data) {
long long networkOrder;
assert(data.length == sizeof(networkOrder));
memmove(&networkOrder, data.bytes, data.length);
*valuePointer = ntohll(networkOrder);
return YES;
}
return NO;
}
- (instancetype)initWithTag:(SIGArchiveTag)tag
length:(long long)length
offset:(long long)offset {
self = [super init];
if (self) {
_tag = tag;
_payloadLength = length;
_payloadOffset = offset;
}
return self;
}
- (long long)chunkLength {
return self.payloadLength + SIGArchiveChunkOverhead;
}
- (NSData *)data:(out NSError * _Nullable __autoreleasing * _Nullable)error {
if (_data) {
return _data;
}
NSError *readError = nil;
_data = [SIGArchiveChunk readDataFromFileHandle:_fileHandle
fromOffset:self.payloadOffset
expectedLength:self.payloadLength
error:&readError];
if (readError) {
if (error) {
*error = readError;
}
_data = nil;
_fileHandle = nil;
return nil;
}
return _data;
}
#pragma mark - Reading
@end
@implementation SIGArchiveChunkWriter
- (BOOL)writeData:(NSData *)data
toStream:(NSOutputStream *)stream
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
if (![self writeInteger:self.tag toStream:stream error:error]) {
return NO;
}
if (data.length != self.payloadLength) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeConsistency
detail:@"File changed during signing"];
}
return NO;
}
if (![self writeInteger:data.length toStream:stream error:error]) {
return NO;
}
const long long length = [stream write:data.bytes
maxLength:data.length];
if (length != data.length) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeIOWrite
detail:@"Short write"];
}
return NO;
}
return YES;
}
- (BOOL)writeStream:(NSInputStream *)readStream
toStream:(NSOutputStream *)writeStream
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
if (![self writeInteger:self.tag toStream:writeStream error:error]) {
return NO;
}
if (![self writeInteger:self.payloadLength toStream:writeStream error:error]) {
return NO;
}
NSInteger totalNumberOfBytes = 0;
while (readStream.hasBytesAvailable) {
uint8_t buffer[4096];
const NSInteger numberOfBytesRead = [readStream read:buffer
maxLength:sizeof(buffer)];
if (numberOfBytesRead == 0) {
break;
}
if (numberOfBytesRead < 0) {
if (error) {
*error = [SIGError errorWrapping:readStream.streamError
code:SIGErrorCodeIORead
detail:@"Error reading file"];
}
return NO;
}
totalNumberOfBytes += numberOfBytesRead;
if (totalNumberOfBytes > self.payloadLength) {
if (error) {
*error = [SIGError errorWithCode:SIGErrorCodeConsistency
detail:@"File changed while reading"];
}
return NO;
}
NSInteger numberOfBytesWritten = [writeStream write:buffer maxLength:numberOfBytesRead];
if (numberOfBytesWritten < numberOfBytesRead) {
*error = [SIGError errorWithCode:SIGErrorCodeIOWrite
detail:@"Short write"];
return NO;
}
}
return totalNumberOfBytes == self.payloadLength;
}
#pragma mark - Private
- (BOOL)writeInteger:(long long)integer
toStream:(NSOutputStream *)stream
error:(out NSError * _Nullable __autoreleasing * _Nullable)error {
const long long networkOrder = htonll(integer);
uint8_t buffer[sizeof(networkOrder)];
memmove(&buffer, (void *)&networkOrder, sizeof(networkOrder));
const NSInteger length = [stream write:buffer
maxLength:sizeof(buffer)];
if (error) {
if (length < 0) {
*error = [SIGError errorWrapping:stream.streamError
code:SIGErrorCodeIOWrite
detail:@"Error writing to file"];
} else if (length != sizeof(buffer)) {
*error = [SIGError errorWithCode:SIGErrorCodeIOWrite
detail:@"Short write"];
}
}
return length == sizeof(buffer);