/* * 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