[iOS] 앱에서 인증서 만료일 조회하는 방법

B2B에서는 꽤나 유용하게 쓸 수 있을 것 같아 공유드립니다.

 

알아본 바로 앱 내에서 인증서의 만료일을 가져올 수 있는 API는 존재하지 않습니다.

그렇기에 빌드 시 생성되는 embedded.mobileprovision 파일을 이용해서 만료일을 가져와 보겠습니다.

 

코드만으로 앱에서 만료일을 조회할 수도 있지만, 원리를 설명드리기 위해 아카이브를 통해 진행해보도록 하겠습니다.

바로 코드에 적용하실 분들은 1~3번은 건너뛰어도 무방합니다.

 

1. ipa 준비

앱을 아카이브하여 ipa 파일을 준비해줍니다.

 

2. ipa unzip

unzip -q TEST.ipa

터미널에서 해당 경로로 이동 후 ipa 파일을 unzip 해주도록 합니다.

 

그러면 Palyoad 폴더안에 *.app 파일이 생겼을거에요

 

마우스 우클릭 후 패키지 내용 보기를 눌러보면 앱의 리소스들이 전부 들어가있는 것을 볼 수 있습니다.

여기서 우리가 눈여겨 볼 파일은 embedded.mobileprovision 입니다.

 

3. embedded.mobileprovision

다시 터미널에서 Payload/*.app 경로로 이동합니다.

이제 embedded 파일을 조회해 볼건데 암호화되어 있기 때문에 일반적으로는 볼 수 없습니다.

security cms -D -i embedded.mobileprovision 명령어를 입력하면 앱 정보가 Key:Value 쌍으로 들어있음을 알 수 있습니다.

여기서 ExpirationDate 키 값을 통해 인증서 만료일을 가져와볼거에요

 

4. 앱에서 embedded.mobileprovision 정보 가져오기

위 과정으로 대략적인 과정을 파악하셨을텐데 이제 앱에서 정보를 가져와보도록 하죠

 

4-1. Objective-C

더보기
- (NSString*) getExpiry{

    NSString *profilePath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    // Check provisioning profile existence
    if (profilePath)
    {
        // Get hex representation
        NSData *profileData = [NSData dataWithContentsOfFile:profilePath];
        NSString *profileString = [NSString stringWithFormat:@"%@", profileData];

        // Remove brackets at beginning and end
        profileString = [profileString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
        profileString = [profileString stringByReplacingCharactersInRange:NSMakeRange(profileString.length - 1, 1) withString:@""];

        // Remove spaces
        profileString = [profileString stringByReplacingOccurrencesOfString:@" " withString:@""];


        // Convert hex values to readable characters
        NSMutableString *profileText = [NSMutableString new];
        for (int i = 0; i < profileString.length; i += 2)
        {
            NSString *hexChar = [profileString substringWithRange:NSMakeRange(i, 2)];
            int value = 0;
            sscanf([hexChar cStringUsingEncoding:NSASCIIStringEncoding], "%x", &value);
            [profileText appendFormat:@"%c", (char)value];
        }

        // Remove whitespaces and new lines characters
        NSArray *profileWords = [profileText componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

        //There must be a better word to search through this as a structure! Need 'date' sibling to <key>ExpirationDate</key>, or use regex
        BOOL sibling = false;
        for (NSString* word in profileWords){
            if ([word isEqualToString:@"<key>ExpirationDate</key>"]){
                NSLog(@"Got to the key, now need the date!");
                sibling = true;
            }
            if (sibling && ([word rangeOfString:@"<date>"].location != NSNotFound)) {
                NSLog(@"Found it, you win!");
                NSLog(@"Expires: %@",word);
                return word;
            }
        }

    }

    return @"";
}

 

4-2. Swift

더보기
// Swift
private func getProvisioningProfileExpirationDateAsString() -> String? {
    guard
        let profilePath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision"),
        let profileData = try? Data(contentsOf: URL(fileURLWithPath: profilePath)),
        // Note: We use `NSString` instead of `String`, because it makes it easier working with regex, ranges, substring etc.
        let profileNSString = NSString(data: profileData, encoding: String.Encoding.ascii.rawValue)
        else {
        print("WARNING: Could not find or read `embedded.mobileprovision`. If running on Simulator, there are no provisioning profiles.")
        return nil
    }


    // NOTE: We have the `[\\W]*?` check to make sure that variations in number of tabs or new lines in the future does not influence the result.
    guard let regex = try? NSRegularExpression(pattern: "<key>ExpirationDate</key>[\\W]*?<date>(.*?)</date>", options: []) else {
        print("Warning: Could not create regex.")
        return nil
    }

    let regExMatches = regex.matches(in: profileNSString as String, options: [], range: NSRange(location: 0, length: profileNSString.length))

    // NOTE: range `0` corresponds to the full regex match, so to get the first capture group, we use range `1`
    guard let rangeOfCapturedGroupForDate = regExMatches.first?.range(at: 1) else {
        print("Warning: Could not find regex match or capture group.")
        return nil
    }

    let dateAsString = profileNSString.substring(with: rangeOfCapturedGroupForDate)
    return dateAsString
}

해당 함수를 원하는 구간에서 호출해주면 됩니다.

 

참고글

https://stackoverflow.com/questions/18961267/get-the-expiration-date-of-a-provisioning-profile-at-run-time

 

Get the EXPIRATION date of a Provisioning Profile at Run-time?

I have an app that I routinely pass out to testers via the ad-hoc distribution method. Some of these testers are 'on the ball' and know enough about provisioning profiles and the quarterly expirati...

stackoverflow.com