ObjOpenSSL  X509Certificate.m at trunk

File src/X509Certificate.m artifact f6177e4bd6 on branch trunk


/*
 * Copyright (c) 2011, Florian Zeitz <florob@babelmonkeys.de>
 * Copyright (c) 2011, 2012, 2013, 2015, 2021, Jonathan Schleifer <js@nil.im>
 *
 * https://fossil.nil.im/objopenssl
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice is present in all copies.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#import "X509Certificate.h"

#import <ObjFW/OFArray.h>
#import <ObjFW/OFData.h>
#import <ObjFW/OFDictionary.h>
#import <ObjFW/OFFile.h>
#import <ObjFW/OFInitializationFailedException.h>
#import <ObjFW/OFInvalidEncodingException.h>
#import <ObjFW/OFList.h>
#import <ObjFW/OFMutableDictionary.h>
#import <ObjFW/OFString.h>

#import <ObjFW/macros.h>

#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wdocumentation"
#endif

#ifdef X509_NAME
/* wincrypt.h has a conflicting define. */
# undef X509_NAME
#endif

#include <openssl/crypto.h>
#include <openssl/x509v3.h>

#if defined(__clang__)
# pragma clang diagnostic pop
#endif

OF_ASSUME_NONNULL_BEGIN

@interface X509Certificate ()
- (bool)X509_isAssertedDomain: (OFString *)asserted
		  equalDomain: (OFString *)domain;
- (OFDictionary *)X509_dictionaryFromX509Name: (X509_NAME *)name;
- (X509OID *)X509_stringFromASN1Object: (ASN1_OBJECT *)obj;
- (OFString *)X509_stringFromASN1String: (ASN1_STRING *)str;
@end

OF_ASSUME_NONNULL_END

@implementation X509Certificate
- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)initWithFile: (OFString *)path
{
	self = [super init];

	@try {
		void *pool = objc_autoreleasePoolPush();
		OFData *data = [OFData dataWithContentsOfFile: path];
		const unsigned char *dataItems = data.items;

		_certificate = d2i_X509(NULL, &dataItems, data.count);
		if (_certificate == NULL)
			@throw [OFInitializationFailedException
			    exceptionWithClass: self.class];

		objc_autoreleasePoolPop(pool);
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)initWithX509Struct: (X509 *)certificate
{
	self = [super init];

	@try {
		if ((_certificate = X509_dup(certificate)) == NULL)
			@throw [OFInitializationFailedException
			    exceptionWithClass: self.class];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_issuer release];
	[_subject release];
	[_subjectAlternativeName release];

	if (_certificate != NULL)
		X509_free(_certificate);

	[super dealloc];
}

- (OFString *)description
{
	OFString *issuer = [self.issuer.description
	    stringByReplacingOccurrencesOfString: @"\n"
				      withString: @"\n\t"];

	return [OFString stringWithFormat:
	    @"<%@\n"
	    @"\tIssuer: %@\n"
	    @"\tSubject: %@\n"
	    @"\tSANs: %@\n"
	    @">",
	    self.class, issuer, self.subject, self.subjectAlternativeName];
}

- (OFDictionary *)issuer
{
	X509_NAME *name;

	if (_issuer != nil)
		return [[_issuer copy] autorelease];

	name = X509_get_issuer_name(_certificate);
	_issuer = [[self X509_dictionaryFromX509Name: name] retain];

	return _issuer;
}

- (OFDictionary *)subject
{
	X509_NAME *name;

	if (_subject != nil)
		return [[_subject copy] autorelease];

	name = X509_get_subject_name(_certificate);
	_subject = [[self X509_dictionaryFromX509Name: name] retain];

	return _subject;
}

- (OFDictionary *)subjectAlternativeName
{
	OFMutableDictionary *ret;
	int i;

	if (_subjectAlternativeName != nil)
		return [[_subjectAlternativeName copy] autorelease];

	ret = [OFMutableDictionary dictionary];

	i = -1;
	while ((i = X509_get_ext_by_NID(_certificate,
	    NID_subject_alt_name, i)) != -1) {
		void *pool = objc_autoreleasePoolPush();
		X509_EXTENSION *extension;
		STACK_OF(GENERAL_NAME) *values;
		int j, count;

		if ((extension = X509_get_ext(_certificate, i)) == NULL)
			break;

		if ((values = X509V3_EXT_d2i(extension)) == NULL)
			break;

		count = sk_GENERAL_NAME_num(values);
		for (j = 0; j < count; j++) {
			GENERAL_NAME *generalName;
			OFList *list;

			generalName = sk_GENERAL_NAME_value(values, j);

			switch(generalName->type) {
			case GEN_OTHERNAME:;
				OTHERNAME *otherName = generalName->d.otherName;
				OFMutableDictionary *types;
				X509OID *key;

				types = [ret objectForKey: @"otherName"];
				if (types == nil) {
					types =
					    [OFMutableDictionary dictionary];
					[ret setObject: types
						forKey: @"otherName"];
				}

				key = [self X509_stringFromASN1Object:
					otherName->type_id];
				list = [types objectForKey: key];
				if (list == nil) {
					list = [OFList list];
					[types setObject: list forKey: key];
				}

				[list appendObject:
				    [self X509_stringFromASN1String:
					otherName->value->value.asn1_string]];
				break;
			case GEN_EMAIL:
				list = [ret objectForKey: @"rfc822Name"];
				if (list == nil) {
					list = [OFList list];
					[ret setObject: list
						forKey: @"rfc822Name"];
				}

				[list appendObject:
				    [self X509_stringFromASN1String:
					generalName->d.rfc822Name]];
				break;
			case GEN_DNS:
				list = [ret objectForKey: @"dNSName"];
				if (list == nil) {
					list = [OFList list];
					[ret setObject: list
						forKey: @"dNSName"];
				}
				[list appendObject:
				    [self X509_stringFromASN1String:
					generalName->d.dNSName]];
				break;
			case GEN_URI:
				list = [ret objectForKey:
				    @"uniformResourceIdentifier"];
				if (list == nil) {
					list = [OFList list];
					[ret setObject: list
						forKey: @"uniformResource"
							@"Identifier"];
				}
				[list appendObject:
				    [self X509_stringFromASN1String:
				    generalName->d.uniformResourceIdentifier]];
				break;
			case GEN_IPADD:
				list = [ret objectForKey: @"iPAddress"];
				if (list == nil) {
					list = [OFList list];
					[ret setObject: list
						forKey: @"iPAddress"];
				}
				[list appendObject: [self
				    X509_stringFromASN1String:
				    generalName->d.iPAddress]];
				break;
			default:
				break;
			}
		}

		i++; /* Next extension */
		objc_autoreleasePoolPop(pool);
	}


	[ret makeImmutable];
	_subjectAlternativeName = [ret retain];

	return ret;
}

- (bool)hasCommonNameMatchingDomain: (OFString *)domain
{
	void *pool = objc_autoreleasePoolPush();

	for (OFString *name in [self.subject objectForKey: OID_commonName]) {
		if ([self X509_isAssertedDomain: name equalDomain: domain]) {
			objc_autoreleasePoolPop(pool);
			return true;
		}
	}

	objc_autoreleasePoolPop(pool);
	return false;
}

- (bool)hasDNSNameMatchingDomain: (OFString *)domain
{
	void *pool = objc_autoreleasePoolPush();

	for (OFString *name in
	    [self.subjectAlternativeName objectForKey: @"dNSName"]) {
		if ([self X509_isAssertedDomain: name equalDomain: domain]) {
			objc_autoreleasePoolPop(pool);
			return true;
		}
	}

	objc_autoreleasePoolPop(pool);
	return false;
}

- (bool)hasSRVNameMatchingDomain: (OFString *)domain
			 service: (OFString *)service
{
	size_t serviceLength;
	void *pool = objc_autoreleasePoolPush();
	OFDictionary *SANs = self.subjectAlternativeName;
	OFList *assertedNames = [[SANs objectForKey: @"otherName"]
	    objectForKey: OID_SRVName];

	if (![service hasPrefix: @"_"])
		service = [service stringByPrependingString: @"_"];

	service = [service stringByAppendingString: @"."];
	serviceLength = service.length;

	for (OFString *name in assertedNames) {
		if ([name hasPrefix: service]) {
			OFString *asserted;
			asserted = [name substringWithRange: OFRangeMake(
			    serviceLength, name.length - serviceLength)];
			if ([self X509_isAssertedDomain: asserted
					    equalDomain: domain]) {
				objc_autoreleasePoolPop(pool);
				return true;
			}
		}
	}

	objc_autoreleasePoolPop(pool);
	return false;
}

- (bool)X509_isAssertedDomain: (OFString *)asserted
		  equalDomain: (OFString *)domain
{
	/*
	 * In accordance with RFC 6125 this only allows a wildcard as the
	 * left-most label and matches only the left-most label with it.
	 * E.g. *.example.com matches foo.example.com,
	 * but not foo.bar.example.com
	 */

	size_t firstDot;

	if ([asserted caseInsensitiveCompare: domain] == OFOrderedSame)
		return true;

	if (![asserted hasPrefix: @"*."])
		return false;

	asserted = [asserted substringWithRange:
	    OFRangeMake(2, asserted.length - 2)];

	firstDot = [domain rangeOfString: @"."].location;
	if (firstDot == OFNotFound)
		return false;

	domain = [domain substringWithRange:
	    OFRangeMake(firstDot + 1, domain.length - firstDot - 1)];

	if ([asserted caseInsensitiveCompare: domain] == 0)
		return true;

	return false;
}

- (OFDictionary *)X509_dictionaryFromX509Name: (X509_NAME *)name
{
	OFMutableDictionary *dict = [OFMutableDictionary dictionary];
	int i, count = X509_NAME_entry_count(name);

	for (i = 0; i < count; i++) {
		void *pool = objc_autoreleasePoolPush();
		X509OID *key;
		OFString *value;
		X509_NAME_ENTRY *entry = X509_NAME_get_entry(name, i);
		ASN1_OBJECT *obj = X509_NAME_ENTRY_get_object(entry);
		ASN1_STRING *str = X509_NAME_ENTRY_get_data(entry);
		key = [self X509_stringFromASN1Object: obj];

		if ([dict objectForKey: key] == nil)
			[dict setObject: [OFList list] forKey: key];

		value = [self X509_stringFromASN1String: str];
		[[dict objectForKey: key] appendObject: value];

		objc_autoreleasePoolPop(pool);
	}

	[dict makeImmutable];
	return dict;
}


- (X509OID *)X509_stringFromASN1Object: (ASN1_OBJECT *)object
{
	X509OID *ret;
	int length, bufferLength = 256;
	char *buffer = OFAllocMemory(1, bufferLength);

	@try {
		while ((length = OBJ_obj2txt(buffer, bufferLength, object,
		    1)) > bufferLength) {
			bufferLength = length;
			buffer = OFResizeMemory(buffer, 1, bufferLength);
		}

		ret = [[[X509OID alloc]
		    initWithUTF8String: buffer] autorelease];
	} @finally {
		OFFreeMemory(buffer);
	}

	return ret;
}

- (OFString *)X509_stringFromASN1String: (ASN1_STRING *)str
{
	OFString *ret;
	char *buffer;

	if (ASN1_STRING_to_UTF8((unsigned char **)&buffer, str) < 0)
		@throw [OFInvalidEncodingException exception];

	@try {
		ret = [OFString stringWithUTF8String: buffer];
	} @finally {
		OPENSSL_free(buffer);
	}

	return ret;
}
@end

@implementation X509OID
- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)initWithUTF8String: (const char *)string
{
	self = [super init];

	@try {
		_string = [[OFString alloc] initWithUTF8String: string];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_string release];
	[super dealloc];
}

- (OFString *)description
{
	char tmp[1024];
	OBJ_obj2txt(tmp, sizeof(tmp), OBJ_txt2obj(_string.UTF8String, 1), 0);
	return [OFString stringWithUTF8String: tmp];
}

- (bool)isEqual: (id)object
{
	if ([object isKindOfClass: [X509OID class]]) {
		X509OID *OID = object;

		return [OID->_string isEqual: _string];
	}

	if ([object isKindOfClass: [OFString class]])
		return [_string isEqual: object];

	return false;
}

- (unsigned long)hash
{
	return _string.hash;
}

- (id)copy
{
	return [self retain];
}
@end