Re: NSURL and certificates
Re: NSURL and certificates
- Subject: Re: NSURL and certificates
- From: Bill Monk <email@hidden>
- Date: Wed, 16 Apr 2008 02:49:57 -0500
On Apr 16, 2008, at 12:03 AM, Stephane Huaulme
<email@hidden> wrote:
when I try to fetch a web page on an internal server using:
[NSString stringWithContentsOfURL:[NSURL URLWithString: theURLString]
encoding:NSASCIIStringEncoding error:&theError];
I get the following error:
<CFString 0xa00daec8 [0xa01d71a0]>{contents = "NSUnderlyingError"} =
Error Domain=NSURLErrorDomain Code=-1203 UserInfo=0x15579210 "bad
server certificate
what can I do about this?
Don't know if it's related, but after installing the release version
of Safari 3.1 and some security update or other, I'm seeing this a lot.
Amusingly, one the sites 3.1 has complained about is
developer.apple.com...
In any case, I often have to grab many files off a certain
bastardized download site used by a client - a site which forces you
to click on every single URL. That got old fast, so I'd hacked
together something with NSURLDownload to automate it all. Suddenly on
3/18, not only was developer.apple.com getting certificate errors, so
was this particular site, in both Safari and my app.
The archives turned up a little info about the private method
[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:hostName].
Which I guess is not a great thing to use in production code, but if
you have to get around the problem somehow, it does work.
However I didn't want to just blow past the certificates without
taking a look at them. Google turned up some code (from Mike Ash I
believe?) showing how to use the Security framework to put up a
confirmation dialog with details for the bad certificates. I fixed
some typos and switched to a sheet rather than a modal dialog. Ripped
directly out of the app, Mail is going to mangle it for you below...
Posted with the usual caveats about using private API. (In this case
it was an app for my private use, so who cares; sounds like you may
be in a similar situation.)
// NSURLDownload delegate method
- (void)download:(NSURLDownload *)download didFailWithError:(NSError
*)error
{
NSURL *url = [[download request] URL];
NSString *path = [url absoluteString];
NSString *description = [error localizedDescription];
NSString *reason = [error localizedFailureReason];
int errorCode = [error code];
// handle sudden onset certificate errors
//
// first saw NSURLErrorServerCertificateHasUnknownRoot == -1203 on
3/18/08
// after update to Safari 3.1 and a security update.
if ( (errorCode <= NSURLErrorServerCertificateHasBadDate) &&
(errorCode >= NSURLErrorClientCertificateRejected) )
{
NSURL *failingURL = [[error userInfo]
objectForKey:@"NSErrorFailingURLKey"];
NSArray *badCerts = [[error userInfo]
objectForKey:@"NSErrorPeerCertificateChainKey"];
SecPolicySearchRef policySearch = NULL;
if (SecPolicySearchCreate(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_SSL,
NULL, &policySearch) == noErr)
{
SecPolicyRef policy = NULL;
OSStatus status;
// original code says this while-loop will only loop once, but on
my problem
// site it looped twice per failed URL - evidently the site had -
two- bad
// certificates per URL. (If it's even their fault and not
Safari's...)
while ((status = SecPolicySearchCopyNext(policySearch, &policy))
== noErr)
{
SecTrustRef trust = NULL;
if (SecTrustCreateWithCertificates((CFArrayRef)badCerts, policy,
&trust) == noErr)
{
SFCertificateTrustPanel *panel = [SFCertificateTrustPanel
sharedCertificateTrustPanel];
NSString *host = [failingURL host];
//NSString *host = [failingURL _web_hostString];
[panel setDefaultButtonTitle:@"Continue"];
[panel setAlternateButtonTitle:@"Cancel"];
if ([panel respondsToSelector:@selector
(setInformativeText:)]) // this method is in Tiger but is undocumented
{
//NSString *informativeText = [NSString
stringWithFormat:@"Security certificate is invalid for item %d, host %
@, failed host %@.", (downloadIndex + 1), [url host], host];
// the problem with this is that once you set it, subsequent
attempts to change it do nothing; the originally-set text remains
unchanged. Must be doing something wrong. For now, just hardcoded to
the problem site.
//[panel performSelector:@selector(setInformativeText:)
withObject:informativeText];
[panel performSelector:@selector(setInformativeText:)
withObject:@"DownloadYouSendItItems"];
}
[panel setShowsHelp:YES];
// docs say we won't receive any further delegate messages for
this failed download.
// But note this method's "download" param and our
currentDownload instance
// var are actually the same object, which we want to release -
but only once.
[self setCurrentDownload:NULL];
NSString *sheetText;
sheetText = [NSString stringWithFormat:@"Security certificate is
invalid for item %d, host %@, failed host %@.",
(downloadIndex + 1),
[url host],
host];
[panel beginSheetForWindow:mainWindow
modalDelegate:self
didEndSelector:@selector
(certificateTrustSheetDidEnd:returnCode:contextInfo:)
contextInfo:[host retain] // didEndSelector will release
trust:trust
message:sheetText];
CFRelease(trust);
trust = NULL;
}
CFRelease(policy);
policy = NULL;
} // end while
CFRelease(policySearch);
}
}
else // Handle other "normal" download errors
{
NSLog( @"Download of '%@' failed with error:%d, %@.\n", path,
errorCode, description );
// docs say we won't receive any further delegate messages for this
failed download.
// Note this method's "download" param and the app's
currentDownload instance
// var are actually the same object, which we want to release, but
only once.
[self setCurrentDownload:NULL];
NSString *failedReason = [NSString stringWithFormat:@" failed with
error:%d, %@ %@", [error code], description, reason];
// cause bindings to update filename status display in window
if ( [self currentDownloadName] )
[self setCurrentDownloadName:[[self currentDownloadName]
stringByAppendingString:failedReason]];
else
[self setCurrentDownloadName:[path
stringByAppendingString:failedReason]];
// continue next download
[self performSelector:@selector(initiateNextDownload)
withObject:NULL afterDelay:0.];
}
}
- (void)certificateTrustSheetDidEnd:(NSWindow *)sheet returnCode:(int)
returnCode contextInfo:(void *)contextInfo
{
// we set the contextInfo tp the retained host name of the failing
URL, obtained
// from [[error userInfo] objectForKey:@"NSErrorFailingURLKey"].
// we'll need to release this before returning; done below
NSString *hostName = (NSString *)contextInfo;
// note to self: SecTrustGetResult() useful?
if (returnCode == NSOKButton)
{
NSLog( @"Allowing invalid certificate for host:%@", hostName );
// unfortunately we have to use a private API to get past this
certificate
[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:hostName];
// having set the certificate to be ignored, start this download again
[self performSelector:@selector(downloadItem:) withObject:[NSNumber
numberWithInt:downloadIndex] afterDelay:0.0];
}
else // user clicked Cancel
{
NSLog( @"Disallowing invalid certificate for host:%@", hostName );
// skip to next download
[self performSelector:@selector(initiateNextDownload)
withObject:NULL afterDelay:0.0];
}
[hostName release];
}
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden