5

Our Java 7 application needs to listen for HTTPS requests on localhost. It must accept connections on https://localhost:8112 and https://127.0.0.1:8112.

To do so we have programmatically built an auto-signed X509v3 certificate, and we have installed this certificate in the Windows-ROOT keystore, as follows:

KeyStore.TrustedCertificateEntry trustedCert = ...;
KeyStore ks = KeyStore.getInstance("Windows-ROOT");
ks.load(null, null);
ks.setEntry("xxxx_localhost", trustedCert, null);

This makes the certificate accepted by Chrome 36 in both cases (localhost and 127.0.0.1), but IE 11 does not recognize the certificate as valid when accessing 127.0.0.1 while it does when accessing localhost:

Test ok with Chrome (top), KO with IE (bottom)

Why? The certificate is built as follows, with BasicConstraints, ExtendedKeyUsage and SubjectAlternativeName extensions, by using the sun.security.x509 package:

KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair kp = generator.generateKeyPair();

X509Certificate cert = generateCertificate("CN=localhost, OU=XXX, O=XXX", kp,
    1825, "SHA256withRSA", "ip:127.0.0.1,dns:localhost,uri:https://127.0.0.1:8112");

/**
 * Create a self-signed X.509 Certificate.
 * @param dn the X.509 Distinguished Name
 * @param pair the KeyPair
 * @param days how many days from now the Certificate is valid for
 * @param algorithm the signing algorithm, eg "SHA256withRSA"
 * @param san SubjectAlternativeName extension (optional)
 */
private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san) 
    throws GeneralSecurityException, IOException {
    PrivateKey privkey = pair.getPrivate();
    X509CertInfo info = new X509CertInfo();
    Date from = new Date();
    Date to = new Date(from.getTime() + days * 86400000l);
    CertificateValidity interval = new CertificateValidity(from, to);
    BigInteger sn = new BigInteger(64, new SecureRandom());
    X500Name owner = new X500Name(dn);

    info.set(X509CertInfo.VALIDITY, interval);
    info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
    info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
    info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
    info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
    info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
    AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
    info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));

    CertificateExtensions ext = new CertificateExtensions();
    // Critical: Not CA, max path len 0
    ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, false, 0));
    // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
    ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(true,
            new Vector<ObjectIdentifier>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));

    if (san != null) {
        int colonpos;
        String[] ps = san.split(",");
        GeneralNames gnames = new GeneralNames();
        for(String item: ps) {
            colonpos = item.indexOf(':');
            if (colonpos < 0) {
                throw new IllegalArgumentException("Illegal item " + item + " in " + san);
            }
            String t = item.substring(0, colonpos);
            String v = item.substring(colonpos+1);
            gnames.add(createGeneralName(t, v));
        }
        // Non critical
        ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(false, gnames));
    }

    info.set(X509CertInfo.EXTENSIONS, ext);

    // Sign the cert to identify the algorithm that's used.
    X509CertImpl cert = new X509CertImpl(info);
    cert.sign(privkey, algorithm);

    // Update the algorithm, and resign.
    algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG);
    info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
    cert = new X509CertImpl(info);
    cert.sign(privkey, algorithm);
    return cert;
}

By following this tutorial on CAPI2 Diagnostics, I have found the error reported by IE:

<CertVerifyCertificateChainPolicy>
    <Policy type="CERT_CHAIN_POLICY_SSL" constant="4" /> 
    <Certificate fileRef="XXX.cer" subjectName="127.0.0.1" /> 
    <CertificateChain chainRef="{XXX}" /> 
    <Flags value="0" /> 
    <SSLAdditionalPolicyInfo authType="server" serverName="127.0.0.1">
        <IgnoreFlags value="0" /> 
    </SSLAdditionalPolicyInfo>
    <Status chainIndex="0" elementIndex="0" /> 
    <EventAuxInfo ProcessName="iexplore.exe" /> 
    <CorrelationAuxInfo TaskId="{XXX}" SeqNumber="4" /> 
    <Result value="800B010F">The certificate's CN name does not match the passed value.</Result> 
</CertVerifyCertificateChainPolicy>

The documentation on CertVerifyCertificateChainPolicy and CERT_CHAIN_POLICY_STATUS doesn't help me much: it looks like IE expects a CN equal to server name, but I have tried to change my CN to CN=127.0.0.1 without success (same behaviour).

vip
  • 1,707
  • 2
  • 16
  • 46
  • "CN=localhost" - Maybe not. That's deprecated by both the IETF and more importantly, the [CA/Browser Forums](https://cabforum.org/baseline-requirements-documents/). See Section 9.2.2 of the CA/B Baseline Requirements: *"Deprecated (Discouraged, but not prohibited)"*. Make the CN something friendly like "XXX IT" (personally, I don't use legal names). The SAN looks OK since it includes both `localhost` and `127.0.0.1`. See Section 9.2.1 of CA/B Baseline Requirements. – jww Jul 26 '14 at 16:38
  • I don't believe end entity certificates (server or user certificates) should be placed in the *Trusted Root* store (its actually called the *Trusted Root Certificate Authorities* store). As odd as it sounds, they could be used to sign other certificates. Instead, they go in *Personal -> Certificates*). – jww Jul 26 '14 at 16:44
  • Are you sure you haven't swapped IE and Chrome in your title? This looks like a Chrome screenshot to me (the one rejecting the cert). Just to be sure, did you check that your cert had a SAN entry for 127.0.0.1 of *iPAddress* type? (You could export it and have a look with `openssl x509 -text ...`.) – Bruno Jul 27 '14 at 15:14
  • Just wondering, what kind of attacks are you trying to protect against using HTTPS on localhost? – Bruno Jul 27 '14 at 15:15
  • jww: Changing the CN doesn't help. I also tried to store the certificate into "Windows-MY" store but it doesn't work, it seems it's only designed to add user private keys, not a root certificate. Bruno: Yes I'm sure. You have Chrome on top of the image and IE at the bottom. I'm not trying to protect anything. We have an HTTP service available on a fixed port that can be used by external websites. The problem comes from websites accessed in HTTPS: recent browsers do not allow them anymore to redirect users to a non-secure connection, even if it's for localhost, that's why we need HTTPS now. – vip Jul 27 '14 at 20:06
  • Does your certificate have a Subject Alternative name? If yes what is its value? – Yuvika Jul 27 '14 at 20:29
  • Yes the SAN is in the code: "ip:127.0.0.1,dns:localhost,uri:https://127.0.0.1:8112" – vip Jul 27 '14 at 20:44
  • I'm still not sure why you'd need HTTPS from localhost to localhost. Can you not access your local machine from its external name? If you've set it up for dual purposes, this shouldn't be a problem. I'd also look at tweaking the Internet Options security policies for IE. (By the way, I meant checking the actual cert for the SAN, just to check the code you're using actually worked.) – Bruno Jul 28 '14 at 16:57
  • We're talking of a desktop app used by thousands of people, it's not only "my" local machine but everyone's. See http://josm.openstreetmap.de/wiki/Help/Preferences/RemoteControl if you're curious about the why, but that's off-topic. Concerning the SAN, yes, I have checked into Windows keystore manager and the entries are listed as expected: DNS Name=localhost IP Address=127.0.0.1 URL=https://127.0.0.1:8112 I have tried to set https://127.0.0.1 to the trusted sites and local intranet sections of IE but it does not change anything. – vip Jul 28 '14 at 17:19
  • As per https://stackoverflow.com/questions/13418923/cant-access-my-local-site-through-ie-using-https-and-self-signed-certificate, changing from RSA to DSA helped. Do you think you can try this? – Yuvika Jul 28 '14 at 18:48
  • This is unrelated, the problem with RSA only concerned keys below 1024 bits, I use 2048. I have found the issue: IE does not use IP addresses values in SAN, only DNS entries. dns:127.0.0.1 is the correct way to handle this issue, I'm writing an answer. – vip Jul 28 '14 at 22:40
  • I'm sure it's not just *your* local machine, but HTTPS from 127.0.0.1 will only ever secure the connection from the local machine where it's running to the local machine where it's running, for each desktop app user. It hardly makes sense to use HTTPS there at all. – Bruno Jul 29 '14 at 11:05
  • As said in my first comment we were forced to add https support to make it work with websites calling our service from an https session, because recent browsers do not allow to switch between secure and insecure modes. See https://lists.openstreetmap.org/pipermail/talk/2014-February/069028.html and https://josm.openstreetmap.de/ticket/9720 for background. – vip Jul 31 '14 at 14:35
  • This seems to be one of the very few use cases where it makes sense indeed. – Bruno Aug 03 '14 at 22:48

2 Answers2

6

IE does not support IP addresses values in Subject Alternative Name (SAN), only DNS entries.

This is a known limitation that won't be fixed, according to Microsoft:

We do not support using the IP choice in the Subject Alternative name to match the server name. You can work around this by adding the IP address as a string for a DNS name choice. At this time we do not plan on fixing this issue.

So the correct way to handle it is to add a DNS entry containing the IP address:

"dns:127.0.0.1"

Unfortunately, this is not possible using keytool or programmatically with sun.security.x509 classes because of Java bug 8016345.

It is however possible to fix this bug by yourself, just by copying the latest version of DNSName.java and remove this check:

//DNSName components must begin with a letter A-Z or a-z
if (alpha.indexOf(name.charAt(startIndex)) < 0)
    throw new IOException("DNSName components must begin with a letter");
vip
  • 1,707
  • 2
  • 16
  • 46
  • Perhaps you could try with BouncyCastle or [OpenSSL (replacing "IP" with "DNS")](http://stackoverflow.com/a/8444863/372643). (Using the private API from `sun.*` is generally not a good idea, but it's up to you.) – Bruno Jul 29 '14 at 11:02
  • Thanks, but we try to keep our use of COTS to the bare minimum. It is currently fine for us to use this package as long it works with supported versions of Oracle JRE and OpenJDK/IcedTea. – vip Aug 01 '14 at 21:11
  • I guess you can ship a "localhost" certificate and its private key that you have already generated with this, instead of trying to integrate the certificate generation in your application. Of course, in general private key material should never be shared, but in this case sharing a certificate and private key to `127.0.0.1` between all your users probably doesn't present a major risk, since they'd also have to share the same machine anyway. – Bruno Aug 03 '14 at 22:48
-1

Why not just issue a HTTP 301 redirect when users use 127.0.0.1 sending them to localhost instead?

Jeremy
  • 2,321
  • 3
  • 21
  • 24
  • Redirections only work **after** the initial request/response exchange was performed successfully. You won't get any response if the cert is invalid for the initial request. – Bruno Aug 03 '14 at 22:49