Skip to content

Commit

Permalink
Basic commit GPG sign
Browse files Browse the repository at this point in the history
Supported operations:
* Commit
* Merge
* Cherry-pick
  • Loading branch information
OdNairy committed Sep 26, 2022
1 parent 0d3b71b commit 7e17d4e
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 1 deletion.
42 changes: 41 additions & 1 deletion GitUpKit/Core/GCRepository+Bare.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#endif

#import "GCPrivate.h"
#import "GPGKeys.h"

@implementation GCRepository (Bare)

Expand Down Expand Up @@ -365,19 +366,58 @@ - (GCCommit*)createCommitFromTree:(git_tree*)tree
error:(NSError**)error {
GCCommit* commit = nil;
git_signature* signature = NULL;
const char *gpgSignature = NULL;

git_oid oid;

GCConfigOption* shouldSignOption = [self readConfigOptionForVariable:@"commit.gpgsign" error:nil];
BOOL shouldSign = [shouldSignOption.value isEqualToString:@"true"];

CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_signature_default, &signature, self.private);
CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_commit_create, &oid, self.private, NULL, author ? author : signature, signature, NULL, GCCleanedUpCommitMessage(message).bytes, tree, count, parents);

git_buf commitBuffer = GIT_BUF_INIT;
CALL_LIBGIT2_FUNCTION_GOTO(cleanupBuffer, git_commit_create_buffer, &commitBuffer, self.private, author ? author : signature, signature, NULL, GCCleanedUpCommitMessage(message).bytes, tree, count, parents);

if (shouldSign) {
GCConfigOption* signingKeyOption = [self readConfigOptionForVariable:@"user.signingkey" error:nil];
gpgSignature = [self gpgSig:commitBuffer.ptr keyId:signingKeyOption.value];
}

CALL_LIBGIT2_FUNCTION_GOTO(cleanupBuffer, git_commit_create_with_signature, &oid, self.private, commitBuffer.ptr, gpgSignature, NULL);

git_commit* newCommit = NULL;
CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_commit_lookup, &newCommit, self.private, &oid);
commit = [[GCCommit alloc] initWithRepository:self commit:newCommit];

cleanupBuffer:
git_buf_dispose(&commitBuffer);

cleanup:
git_signature_free(signature);
return commit;
}

-(const char*)gpgSig:(const char*)body keyId:(NSString*)keyId {
GPGKey *key = nil;

if (keyId.length > 0) {
key = [GPGKey secretKeyForId:keyId];
}

if (key == nil) {
key = [[GPGKey allSecretKeys] firstObject];
}

if (key == nil) {
return NULL;
}

NSString* plainToSign = [[NSString alloc] initWithCString:body encoding:NSUTF8StringEncoding];
NSString* signature = [key signSignature:plainToSign];

return [signature UTF8String];
}

- (GCCommit*)createCommitFromIndex:(git_index*)index
withParents:(const git_commit**)parents
count:(NSUInteger)count
Expand Down
24 changes: 24 additions & 0 deletions GitUpKit/Core/GPGContext+Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (C) 2015-2022 Pierre-Olivier Latour <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import "GPGContext.h"

NS_ASSUME_NONNULL_BEGIN

@interface GPGContext()
@property (nonatomic, assign) gpgme_ctx_t gpgContext;
@end

NS_ASSUME_NONNULL_END
24 changes: 24 additions & 0 deletions GitUpKit/Core/GPGContext.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (C) 2015-2022 Pierre-Olivier Latour <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import <Foundation/Foundation.h>
#include <gpgme.h>

NS_ASSUME_NONNULL_BEGIN

@interface GPGContext : NSObject
@end

NS_ASSUME_NONNULL_END
41 changes: 41 additions & 0 deletions GitUpKit/Core/GPGContext.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (C) 2015-2022 Pierre-Olivier Latour <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import "GPGContext.h"
#import "GPGContext+Private.h"
#import "XLFacilityMacros.h"

@implementation GPGContext
-(instancetype)init {
self = [super init];
if (self) {
static dispatch_once_t initializeThreadInfo;
dispatch_once(&initializeThreadInfo, ^{
gpgme_check_version(NULL);
});

gpgme_error_t initError = gpgme_new(&_gpgContext);
if (initError) {
XLOG_ERROR(@"Failed to initialize GPGME context");
return nil;
}
}
return self;
}

-(void)dealloc {
gpgme_release(_gpgContext);
}
@end
33 changes: 33 additions & 0 deletions GitUpKit/Core/GPGKeys.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (C) 2015-2022 Pierre-Olivier Latour <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import <Foundation/Foundation.h>
#include <gpgme.h>

NS_ASSUME_NONNULL_BEGIN

@interface GPGKey : NSObject
@property (readonly) NSString* email;
@property (readonly) NSString* name;
@property (readonly) NSString* keyId;

+(NSArray<GPGKey *> *)allSecretKeys;
+(nullable instancetype)secretKeyForId:(NSString*)keyId;

-(NSString*)signSignature:(NSString*)document;

@end

NS_ASSUME_NONNULL_END
163 changes: 163 additions & 0 deletions GitUpKit/Core/GPGKeys.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (C) 2015-2022 Pierre-Olivier Latour <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import "GPGKeys.h"
#import "GPGContext.h"
#import "GPGContext+Private.h"
#import "XLFacilityMacros.h"

@interface GPGKey()
@property (nonatomic, assign) gpgme_key_t key;
@property (nonatomic, strong) GPGContext* gpgContext;
@property (nonatomic, strong, nullable) NSString* name;
@property (nonatomic, strong, nullable) NSString* email;
@property (nonatomic, strong, nullable) NSString* keyId;
@end

@interface GPGKeys : NSObject
-(instancetype)initWithContext:(GPGContext*)context;

-(NSArray<GPGKey*>*)allSecretKeys;
@end


NSString* helperGpgDataToString(gpgme_data_t data) {
gpgme_data_seek(data, 0, SEEK_SET);
char buffer[1024] = {0};
ssize_t readCount = gpgme_data_read(data, buffer, 1024);

NSData* readData = [[NSData alloc] initWithBytes:buffer length:readCount];
NSString* readString = [[NSString alloc] initWithData:readData encoding:NSUTF8StringEncoding];

return readString;
}

@implementation GPGKey
static dispatch_once_t initializeThreadInfo;

+(NSArray<GPGKey *> *)allSecretKeys {
dispatch_once(&initializeThreadInfo, ^{
gpgme_check_version(NULL);
});

GPGContext* contextWrapper = [[GPGContext alloc] init];
gpg_error_t keylistStartError = gpgme_op_keylist_start(contextWrapper.gpgContext, NULL, 1);

if (keylistStartError) {
XLOG_ERROR(@"Failed to start keylist: %s", gpg_strerror(keylistStartError));
return nil;
}

NSMutableArray<GPGKey*> *keys = [NSMutableArray array];
gpg_error_t err = 0;
while (!err) {
gpgme_key_t key;
err = gpgme_op_keylist_next (contextWrapper.gpgContext, &key);
if (err) {
break;
}

GPGKey* gpgKey = [[GPGKey alloc] initWithGPGKey:key context:contextWrapper];
[keys addObject:gpgKey];
}

if (gpg_err_code (err) != GPG_ERR_EOF) {
XLOG_ERROR(@"Cannot list keys: %s", gpg_strerror(err));
return nil;
}

return [keys copy];
}

+(instancetype)secretKeyForId:(NSString *)keyId {
NSArray<GPGKey *>* allKeys = [self allSecretKeys];
NSUInteger indexOfKey = [allKeys indexOfObjectPassingTest:^BOOL(GPGKey * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return [obj.keyId isEqualToString:keyId];
}];

if (indexOfKey == NSNotFound) {
return nil;
}
return allKeys[indexOfKey];
}

- (instancetype)initWithGPGKey:(gpgme_key_t)key context:(GPGContext*)context {
self = [super init];
if (self) {
// retain key on initializer, release on object dealloc.
gpgme_key_ref(key);
self.key = key;
self.gpgContext = context;

if (_key->uids) {
if (_key->uids->name) {
_name = [[NSString alloc] initWithCString:_key->uids->name
encoding:NSUTF8StringEncoding];
}
if (_key->uids->email) {
_email = [[NSString alloc] initWithCString:_key->uids->email
encoding:NSUTF8StringEncoding];
}
}

if (_key->subkeys) {
if (_key->subkeys->keyid) {
_keyId = [[NSString alloc] initWithCString:_key->subkeys->keyid
encoding:NSUTF8StringEncoding];
}
}
}
return self;
}

-(NSString *)description {
return [NSString stringWithFormat:@"<%@: %p 'keyId: %@' 'email: %@' 'name: %@'>", self.class, self, self.keyId, self.email, self.name];
}

-(void)dealloc {
gpgme_key_unref(_key);
}

-(NSString*)signSignature:(NSString*)document {
gpgme_signers_clear(_gpgContext.gpgContext);
gpgme_signers_add(_gpgContext.gpgContext, _key);

gpgme_data_t in, out;
gpgme_data_new(&out);

gpgme_error_t err = gpgme_data_new_from_mem(&in, [document UTF8String], document.length, 1);
if (err) {
XLOG_ERROR(@"Failed to initialize input data: %s", gpg_strerror(err));
return nil;
}

gpgme_set_textmode(_gpgContext.gpgContext, 0);
gpgme_set_armor(_gpgContext.gpgContext, 1);

err = gpgme_op_sign(_gpgContext.gpgContext, in, out, GPGME_SIG_MODE_DETACH);
if (err) {
XLOG_ERROR(@"Signing failed due: %s", gpg_strerror(err));
return nil;
}

NSString* signatureString = helperGpgDataToString(out);

gpgme_data_release(in);
gpgme_data_release(out);

return signatureString;
}

@end
Loading

0 comments on commit 7e17d4e

Please sign in to comment.