Testing push notifications sometimes can be a problematic process. In one of the previous posts I covered base practices and how we can do that.

Another moment that was covered in that post - I show my dev tool for testing: pushHandle. It was a raw tool with limited functionality (only p8 key support) and poor UI.

Some time passed and I faced again the need to test push notifications. For this time, I completely redesign the app and add a ton of new functionality:

  • Auth with p8 key
  • Auth with p12 cert
  • Auth with keychain
  • Custom push payload
  • Payload JSON validation
  • Predefined payloads
  • Full customization of push
  • Build in hints
  • Multiply sessions
  • Session persistants
  • History with detailed info
  • Inspection of request with option to use curl for p8 auth
  • Rich failure description
  • Light/Dark mode
  • Written in Swift

All this is combined into the new tool - Antumbra - OS X app for sending pushes with Apple Push Notification service (APNs) over HTTP/2 API.

If u wondering what is the word Antumbra means - check this wiki page for more.

the app

I decided to make a universal free and open-source tool that covers push notifications test purposes from different prospects.

The idea was taken originally from Pusher app - a great tool, that is not supported anymore. After checking alternatives I found PushHero app - a good one, but u need to pay 15$ for that.

The overall app UI looks like this

design



The link to my tool here

I spend some free time in the evening creating this tool.

The good source of information was original Apple posts about push work. I strongly recommend this to u if u want to learn to push in details. unusually detailed posts.

During creating this tool, I also learn a few new stuff that I haven’t work before. The most interesting are:

  • work with Security framework and keychain
  • an inner structure of certificates
  • details of file protection system (sandbox) on macOS

The most interesting one - is working with API from Security framework.

Knowledge about keychains and certificates is needed to be able to accomplish a part of the functionality that uses installed certificates for APNS from an inside keychain.

keychain and certificates

I won’t cover all the theory about certificates and keychains here, but here is the link for doc related to this part.

The interesting for our purposes part - is to get the list of certificates and fetch needed details from each entity, later use that to sign our APNS request.

certificate

The certificate is represented via SecCertificate. We then can ispect the certificate for various values - name, issuer, validity date, etc.

For purpose of this app, I created an object that represents a certificate - KeychainCertificate:

import Security

struct KeychainCertificate: Equatable, Hashable {
  let certificate: SecCertificate
}

Yep, that’s easy :). SecCertificate contains all the info we need later. We can check the SecIdentity header for convenient methods of getting specific info from the certificate.

The certificate can provide us with different values. For example, to get SecIdentity:

var identity: SecIdentity? {
	var identityInst: SecIdentity?
	let copyStatus = SecIdentityCreateWithCertificate(
	  nil,
	  certificate,
	  &identityInst
	)
	if copyStatus == errSecSuccess,
	   let identityInst = identityInst {
	  return identityInst
	}
	
	return nil
}

We also can use the SecCertificateOIDs header to fetch some specific data from a key-value collection of data provided by the certificate. Part of the data extracted from the certificate:

Optional({
    "1.2.840.143635.100.6.1.12" =     {
        label = "1.2.840.113435.100.6.1.12";
        "localized label" = "1.2.840.113435.100.6.1.12";
        type = section;
        value =         (
                        {
                label = Critical;
                "localized label" = Critical;
                type = string;
                value = Yes;
            },
                        {
                label = "Unparsed Data";
                "localized label" = "Unparsed Data";
                type = data;
                value = {length = 2, bytes = 0x0500};
            }
        );
    };
    "2.5.4.3" =     {
        label = CN;
        "localized label" = CN;
        type = array;
        value =         (
            "Apple Development: Kyryl Horbushko (XXXXXXXX)"
        );
    };
    ... 
    // much more

What we need - this is a date. To get this, we can do the following:

  var expireAtDate: Date? {
    let data = SecCertificateCopyValues(certificate, nil, nil)
    let valueRaw = CFDictionaryGetValue(
      data,
      unsafeBitCast(kSecOIDX509V1ValidityNotAfter, to: UnsafeRawPointer.self)
    )
    let value = unsafeBitCast(
      valueRaw,
      to: NSDictionary.self
    )
    if let timeInterval = value["value"] as? TimeInterval {
      let date = Date(timeIntervalSinceReferenceDate: timeInterval)
      return date
    }

    return nil
  }

I think there must be some type existing to cast a value not to NSDictionary.self but to it… but, didn’t find it.

All the next - u can extend this type as u wish, adding more and more values via computed properties.

full code for `KeychainCertificate`

import Foundation
import Security

struct KeychainCertificate: Equatable, Hashable {
  let certificate: SecCertificate

  var name: String? {
    SecCertificateCopySubjectSummary(certificate) as? String
  }

  var summary: String? {
    SecCertificateCopySubjectSummary(certificate) as? String
  }

  var data: CFData {
    SecCertificateCopyData(certificate)
  }

  var identity: SecIdentity? {
    var identityInst: SecIdentity?
    let copyStatus = SecIdentityCreateWithCertificate(
      nil,
      certificate,
      &identityInst
    )
    if copyStatus == errSecSuccess,
       let identityInst = identityInst {
      return identityInst
    }

    return nil
  }

  var key: SecKey? {
    var keyInst: SecKey?
    if let identity = identity {
      let keystat: OSStatus = SecIdentityCopyPrivateKey(identity, &keyInst)
      if keystat == errSecSuccess,
         let keyInst = keyInst {
        return keyInst
      }
    }

    return nil
  }

  var expireAtDate: Date? {
    let data = SecCertificateCopyValues(certificate, nil, nil)
    let valueRaw = CFDictionaryGetValue(
      data,
      unsafeBitCast(kSecOIDX509V1ValidityNotAfter, to: UnsafeRawPointer.self)
    )
    let value = unsafeBitCast(
      valueRaw,
      to: NSDictionary.self
    )
    if let timeInterval = value["value"] as? TimeInterval {
      let date = Date(timeIntervalSinceReferenceDate: timeInterval)
      return date
    }

    return nil
  }

  var isAPNS: Bool {
    name?.contains("Push") == true
  }
}

extension KeychainCertificate {
  var expireAtReadableString: String? {
    if let expireAtDate = expireAtDate {
      let formatter = DateFormatter()
      formatter.dateFormat = "dd MMM YYYY"
      let value = formatter.string(from: expireAtDate)
      return value

    }
    return nil
  }
}


fetch certificates

The first part is done. Now we must somehow fetch all the certificates from the keychain.

To do so, we can check doc and see, that there is a function SecItemCopyMatching(::) that can help us.

import Foundation
import Security
import Combine

final class KeychainCertificateExtractor {

  static func extractAllCertificates() -> [KeychainCertificate] {
    var certificates: [KeychainCertificate] = []

    var copyResult: CFTypeRef?
    let extractItemsErr = SecItemCopyMatching(
      [
        kSecClass: kSecClassIdentity,
        kSecMatchLimit: kSecMatchLimitAll,
        kSecReturnRef: true
      ] as NSDictionary,
      &copyResult
    )

    if extractItemsErr == errSecSuccess,
       let identities = copyResult as? [SecIdentity] {
      for identity in identities {
        var certificate: SecCertificate?
        let certCopyErr = SecIdentityCopyCertificate(identity, &certificate)
        if certCopyErr == errSecSuccess,
           let certificate = certificate {

          let certificate = KeychainCertificate(
            certificate: certificate
          )

          certificates.append(certificate)
        }
      }
    }

    return certificates
  }

  static func fetchAll() -> AnyPublisher<[KeychainCertificate], Never> {
    Deferred {
      Future { promise in
        let certs = Self.extractAllCertificates()
        promise(.success(certs))
      }
    }
    .eraseToAnyPublisher()
  }
}

credentials

And last but not least part - use certificate as a credential in URLSession.

A sign can be done via URLCredential - the entity that is used by a delegate of URLSession and able to provide authentication info for it.

extension KeychainCertificate {
  var urlCredentials: URLCredential? {
    if let identity = identity {
      let credentials = URLCredential(
        identity: identity,
        certificates: [certificate],
        persistence: .forSession
      )
      return credentials
    }

    return nil
  }
}

Using these 3 step and crafting some UI, I got this:

design



conclusion

Always try to improve the process. This gives u at least 2 benefits - u learn something, u improve something.

Check out my tool here. Feel free to open pr for improving it or open issue for reporting a problem.

resources