Bug 2036462 - rh1991003 patch breaks sun.security.pkcs11.wrapper.PKCS11.getInstance() [NEEDINFO]
Summary: rh1991003 patch breaks sun.security.pkcs11.wrapper.PKCS11.getInstance()
Keywords:
Status: CLOSED ERRATA
Alias: None
Product: Fedora
Classification: Fedora
Component: java-1.8.0-openjdk
Version: 35
Hardware: Unspecified
OS: Unspecified
unspecified
unspecified
Target Milestone: ---
Assignee: Andrew John Hughes
QA Contact: Fedora Extras Quality Assurance
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2022-01-02 00:11 UTC by Francisco de la Peña
Modified: 2022-08-03 01:48 UTC (History)
11 users (show)

Fixed In Version: java-1.8.0-openjdk-1.8.0.342.b07-1.fc35
Doc Type: If docs needed, set a value
Doc Text:
Clone Of:
Environment:
Last Closed: 2022-08-03 01:48:54 UTC
Type: Bug
mmillson: needinfo? (mbalao)


Attachments (Terms of Use)
Test class to reproduce the building/running issue (678 bytes, text/x-modelica)
2022-01-02 00:11 UTC, Francisco de la Peña
no flags Details
Fixed version with 4 args as intended (1.30 KB, application/x-java)
2022-01-02 19:59 UTC, Francisco de la Peña
no flags Details
Fixed version with 4 args as intended (ignore the previous attachment, it was a .class file) (672 bytes, text/x-modelica)
2022-01-02 20:01 UTC, Francisco de la Peña
no flags Details

Description Francisco de la Peña 2022-01-02 00:11:07 UTC
Created attachment 1848563 [details]
Test class to reproduce the building/running issue

Description of problem:

The sun.security.pkcs11.wrapper.PKCS11 class contains a public method, getInstance with 4 arguments. The patch related to bug 1991003, modifies this method by adding an additional 5th argument, fipsKeyImporter, of type MethodHandle.

https://src.fedoraproject.org/rpms/java-1.8.0-openjdk/blob/f35/f/rh1991003-enable_fips_keys_import.patch#_446

Possibly due to an oversight, the patch lacks public API compatible method with the same argument count to use with existing software. Due to this, existing applications could fail, showing exceptions like the following:

java.lang.NoSuchMethodError: sun.security.pkcs11.wrapper.PKCS11.getInstance(Ljava/lang/String;Ljava/lang/String;Lsun/security/pkcs11/wrapper/CK_C_INITIALIZE_ARGS;Z)Lsun/security/pkcs11/wrapper/PKCS11;


Version-Release number of selected component (if applicable):

java-1.8.0-openjdk-1.8.0.312.b07-2.fc35

And likely the equivalent in other versions (11, etc.).


How reproducible:

Always.


Steps to Reproduce:
1. Create a file Test.java with the following:

class Test {
    public static void main(String[] args) throws Exception {
        sun.security.pkcs11.wrapper.CK_C_INITIALIZE_ARGS initArgs = new sun.security.pkcs11.wrapper.CK_C_INITIALIZE_ARGS();
        initArgs.flags = 0;
        sun.security.pkcs11.wrapper.PKCS11 pkcs11 = sun.security.pkcs11.wrapper.PKCS11.getInstance("/usr/lib64/opensc-pkcs11.so", "C_GetFunctionList", initArgs, false);

        long[] slotList = pkcs11.C_GetSlotList(true);
        for (long slot : slotList) {
            sun.security.pkcs11.wrapper.CK_TOKEN_INFO tokenInfo = pkcs11.C_GetTokenInfo(slot);
            System.out.println("Slot info:\n" + tokenInfo.toString());
        }
    }
}

2. save and compile with: javac Test.java


Actual results:

test.java:5: error: method getInstance in class PKCS11 cannot be applied to given types;
        sun.security.pkcs11.wrapper.PKCS11 pkcs11 = sun.security.pkcs11.wrapper.PKCS11.getInstance("/usr/lib64/opensc-pkcs11.so", "C_GetFunctionList", initArgs, false);
                                                                                      ^
  required: String,String,CK_C_INITIALIZE_ARGS,boolean,MethodHandle
  found: String,String,CK_C_INITIALIZE_ARGS,boolean
  reason: actual and formal argument lists differ in length
1 error


Expected results:

It builds as is.


Additional info:

The affected API is widely used by applications needing to handle available tokens and their slots also for handle public certificates on tokens without requiring login.

If you add a null as a 5th argument, it builds. But will fail to run on JREs not affected by the patch.

If you build the source from a JDK not affected by the patch and run the class with java Test on an affected JRE, it will fail as mentioned in the "Description of problem" section.

Comment 1 Francisco de la Peña 2022-01-02 00:36:12 UTC
A possible simple fix for this would be method overloading by creating a method with four arguments calling the one with five arguments with a null for the fifth argument.

Comment 2 Francisco de la Peña 2022-01-02 19:56:12 UTC
Comment on attachment 1848563 [details]
Test class to reproduce the building/running issue

>class Test {
>    public static void main(String[] args) throws Exception {
>        sun.security.pkcs11.wrapper.CK_C_INITIALIZE_ARGS initArgs = new sun.security.pkcs11.wrapper.CK_C_INITIALIZE_ARGS();
>        initArgs.flags = 0;
>        sun.security.pkcs11.wrapper.PKCS11 pkcs11 = sun.security.pkcs11.wrapper.PKCS11.getInstance("/usr/lib64/opensc-pkcs11.so", "C_GetFunctionList", initArgs, false);
>
>        long[] slotList = pkcs11.C_GetSlotList(true);
>        for (long slot : slotList) {
>            sun.security.pkcs11.wrapper.CK_TOKEN_INFO tokenInfo = pkcs11.C_GetTokenInfo(slot);
>            System.out.println("Slot info:\n" + tokenInfo.toString());
>        }
>    }
>}

Comment 3 Francisco de la Peña 2022-01-02 19:59:46 UTC
Created attachment 1848630 [details]
Fixed version with 4 args as intended

Comment 4 Francisco de la Peña 2022-01-02 20:01:43 UTC
Created attachment 1848631 [details]
Fixed version with 4 args as intended (ignore the previous attachment, it was a .class file)

Comment 5 Francisco de la Peña 2022-01-26 03:00:50 UTC
To clarify, the issue breaks programs even without enabling FIPS mode. RHEL 8/9 is affected, too. Confirmed the issue affects java-11-openjdk and java-latest-openjdk, too.

Comment 6 Andrew John Hughes 2022-02-08 17:36:16 UTC
Yes, looks like this should be easily fixable. I'll get a fix into the JDKs.

Comment 7 Martin Balao 2022-06-08 14:48:50 UTC
As commented in an internal email:

The sun.security.pkcs11.wrapper package is not public. This API is internal and not available for applications or libraries direct use. In JDK-8, that can be inferred from the namespace. In later releases, that is blocked by module encapsulation. The method "getInstance" is package-public because the class SunPKCS11 (which is in a different package, sun.security.pkcs11) needs access.

While we can overload the method with a backward-compatible signature (as suggested in comment 1 [1]), relying on internal APIs is strongly discouraged.

With that said, can we get a pointer to the code that uses this API? I wonder what's the reason behind, which JDK releases does the code support and if it's still done that way in its latest release.

Martin.-

--
[1] - https://bugzilla.redhat.com/show_bug.cgi?id=2036462#c1

Comment 8 Francisco de la Peña 2022-06-08 15:51:32 UTC
Java public PKCS#11 API does not allow enumerating certificates without login. At most, you can get a lot list number, but not checking contents to determinate which slot should use. This is an issue when more than one slot is connected and increases the risk of using a PIN for the wrong token and getting locked. 

Unfortunately, we are aware of multiple implementations (mostly from Government from several countries) using sun wrapper because it allows this without JNI. There are multiple sites with smart card access and java web start for accessing or signing on websites using them and they likely can't fix their software.

Since Java 16, it is still possible to access sun classes by passing the command line argument: --add-exports jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED

There are even cases using it this way to circumvent the restriction on newer Java versions for launching standalone JARs without passing args, e.g. for Web Start. It could even work on (currently unmaintained) icedtea-web as the latest F35 version didn't provide the upstream fix for supporting relevant fixes such multi-release JAR support or prefixed jnlp.*/javaws.* for -Dargs to match those supported by Oracle's javaws:

class Test {
    public static void doMain() { /* "real" main content goes here */ }

    public static void main(String[] args) throws Throwable {
        if (System.getProperty("java.version").startsWith("1.")) doMain();
        else {
            for(String s : args) {
                if(s.equals("run")) {
                    doMain();
                    return;
                }
            }
            new ProcessBuilder().inheritIO().command("java",
                "--add-exports", "jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED",
                "-jar", MethodHandles.lookup().lookupClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath(),
                "run").start().waitFor();
        }
    }
}

Here's a complete example with Sun's wrapper features:

import java.io.ByteArrayInputStream;
import java.lang.invoke.MethodHandles;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import sun.security.pkcs11.wrapper.CK_ATTRIBUTE;
import sun.security.pkcs11.wrapper.CK_C_INITIALIZE_ARGS;
import sun.security.pkcs11.wrapper.CK_INFO;
import sun.security.pkcs11.wrapper.CK_SLOT_INFO;
import sun.security.pkcs11.wrapper.CK_TOKEN_INFO;
import sun.security.pkcs11.wrapper.PKCS11;
import sun.security.pkcs11.wrapper.PKCS11Exception;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKA_CLASS;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKA_ID;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKA_VALUE;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKF_OS_LOCKING_OK;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKF_SERIAL_SESSION;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKF_TOKEN_PRESENT;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKO_CERTIFICATE;
import static sun.security.pkcs11.wrapper.PKCS11Constants.CKR_TOKEN_NOT_RECOGNIZED;

class Test {
    public static void doMain() throws Throwable {
        String lib = "/usr/lib64/opensc-pkcs11.so";
        String functionList = "C_GetFunctionList";
        CK_C_INITIALIZE_ARGS pInitArgs = new CK_C_INITIALIZE_ARGS();
        PKCS11 pkcs11;
        try {
            pInitArgs.flags = CKF_OS_LOCKING_OK;
            pkcs11 = PKCS11.getInstance(lib, functionList, pInitArgs, false);
        } catch (PKCS11Exception e) {
            pInitArgs.flags = 0;
            pkcs11 = PKCS11.getInstance(lib, functionList, pInitArgs, false);
        }
        CK_INFO info = pkcs11.C_GetInfo();
        System.out.println("Interface: " + new String(info.libraryDescription).trim());
        Boolean tokenPresent = true;
        for (long slotID : pkcs11.C_GetSlotList(tokenPresent)) {
            CK_SLOT_INFO slotInfo = pkcs11.C_GetSlotInfo(slotID);
            System.out.println("Slot " + slotID + ": " + new String(slotInfo.slotDescription).trim());
            if ((slotInfo.flags & CKF_TOKEN_PRESENT) != 0) { // Not required if tokenPresent = true, condition could be removed if true, it's just for testing empty slot enumeration
                try { // TODO: check if slotID could be reused after switching card. Try CK_SESSION_INFO sessionInfo = pkcs11.C_GetSessionInfo(hSession); and catch PCKCS11Exception meaning invalid session instead.
                    CK_TOKEN_INFO tokenInfo = pkcs11.C_GetTokenInfo(slotID);
                    System.out.println("Token: " + new String(tokenInfo.label).trim() + " (" + new String(tokenInfo.serialNumber).trim() + ")");
                    CK_ATTRIBUTE[] pTemplate = { new CK_ATTRIBUTE(CKA_CLASS, CKO_CERTIFICATE) };
                    long ulMaxObjectCount = 32;
                    long hSession = pkcs11.C_OpenSession(slotID, CKF_SERIAL_SESSION, null, null); // TODO verify slot session just after getting PIN but just before login
                    pkcs11.C_FindObjectsInit(hSession, pTemplate);
                    long[] phObject = pkcs11.C_FindObjects(hSession, ulMaxObjectCount);
                    pkcs11.C_FindObjectsFinal(hSession);
                    for (long object : phObject) {
                        CK_ATTRIBUTE[] pTemplate2 = { new CK_ATTRIBUTE(CKA_VALUE), new CK_ATTRIBUTE(CKA_ID) }; // if you add more attributes, update the iterator jump
                        pkcs11.C_GetAttributeValue(hSession, object, pTemplate2);
                        for (int i = 0; i < pTemplate2.length; i = i + 2) { // iterator jump value to read just certificates at pTemplate[0], pTemplate[2]...
                            X509Certificate certificate = (X509Certificate)CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream((byte[])pTemplate2[i].pValue));
                            boolean[] keyUsage = certificate.getKeyUsage();
                            if (certificate.getBasicConstraints() == -1 && keyUsage[0] && keyUsage[1]) {
                                LdapName ldapName = new LdapName(certificate.getSubjectX500Principal().getName("RFC1779"));
                                String firstName = "", lastName = "", identification = "";
                                for (Rdn rdn : ldapName.getRdns()) {
                                    if (rdn.getType().equals("OID.2.5.4.5")) identification = rdn.getValue().toString();
                                    if (rdn.getType().equals("OID.2.5.4.4")) lastName = rdn.getValue().toString();
                                    if (rdn.getType().equals("OID.2.5.4.42")) firstName = rdn.getValue().toString();
                                }
                                String expires = new SimpleDateFormat("yyyy-MM-dd").format(certificate.getNotAfter());
                                System.out.println(firstName + " " + lastName + " (" + identification + ") " + certificate.getSerialNumber().toString(16) + " [Token serial number: " + new String(tokenInfo.serialNumber) + "] (Expires: " + expires+ ")");
                                Object keyIdentifier = pTemplate2[i + 1]/* .pValue */; // TODO use pValue to get the value for comparison when using it to match with private key!
                                System.out.println("Public/Private key pair identifier: " + keyIdentifier); // After logging in with PIN, find the matching private key pValue.
                            }
                            // TODO Don't assume there's a single valid certificate per token
                        }
                    }
                    pkcs11.C_CloseSession(hSession);
                } catch (PKCS11Exception e) {
                    if (e.getErrorCode() == CKR_TOKEN_NOT_RECOGNIZED) {
                        System.err.println("Slot reports token is present but not recognized by the cryptoki library");
                    } else throw e;
                }
            } else System.out.println("No token present in this slot"); // Not required if tokenPresent = true, condition could be removed
        }
    }

    public static void main(String[] args) throws Throwable {
        if (System.getProperty("java.version").startsWith("1.")) doMain();
        else {
            for(String s : args) {
                if(s.equals("run")) {
                    doMain();
                    return;
                }
            } // Launch JAR from JAR with own args for Java 16+ compatibility
            new ProcessBuilder().inheritIO().command("java",
                "--add-exports", "jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED",
                "-jar", MethodHandles.lookup().lookupClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath(),
                "run").start().waitFor(); // TODO test with icedtea-web (2.0 alpha builds) with Java > 8
        }
    }
}

Comment 12 Fedora Update System 2022-07-19 10:03:18 UTC
FEDORA-2022-4d338824b7 has been submitted as an update to Fedora 35. https://bodhi.fedoraproject.org/updates/FEDORA-2022-4d338824b7

Comment 13 Fedora Update System 2022-07-20 01:53:04 UTC
FEDORA-2022-4d338824b7 has been pushed to the Fedora 35 testing repository.
Soon you'll be able to install the update with the following command:
`sudo dnf upgrade --enablerepo=updates-testing --refresh --advisory=FEDORA-2022-4d338824b7`
You can provide feedback for this update here: https://bodhi.fedoraproject.org/updates/FEDORA-2022-4d338824b7

See also https://fedoraproject.org/wiki/QA:Updates_Testing for more information on how to test updates.

Comment 14 Fedora Update System 2022-07-26 15:15:24 UTC
FEDORA-2022-80afe2304a has been pushed to the Fedora 35 testing repository.
Soon you'll be able to install the update with the following command:
`sudo dnf upgrade --enablerepo=updates-testing --refresh --advisory=FEDORA-2022-80afe2304a`
You can provide feedback for this update here: https://bodhi.fedoraproject.org/updates/FEDORA-2022-80afe2304a

See also https://fedoraproject.org/wiki/QA:Updates_Testing for more information on how to test updates.

Comment 15 Fedora Update System 2022-08-03 01:48:54 UTC
FEDORA-2022-80afe2304a has been pushed to the Fedora 35 stable repository.
If problem still persists, please make note of it in this bug report.


Note You need to log in before you can comment on or make changes to this bug.