Search code examples
ssl-certificatex509certificatebouncycastlecsrjava-security

Adding a new Extension to my generated certificate


I need to add a new Extension of OID 1.3.6.1.5.5.7.1.26 in my certificate. I got this OID extension in my certificate but with the following error:

Certificate Extensions: 10 [1]: ObjectId: 1.3.6.1.5.5.7.1.26 Criticality=false
Extension unknown: DER encoded OCTET string =
0000: 04 0C 30 0A 13 08 33 39 20 64 63 20 32 62 ..0...
39 dc 2b

I want this OID to be recognized similar to other extensions like AuthorityInfoAccess, etc.

Do I need to edit the jar of Bouncy Castle X509 class?

Im using ACME4j as a client and Letsencrypt Boulder as my server.

Here is the CSR Builder code for signing up the certificate.

public void sign(KeyPair keypair) throws IOException {
    //Security.addProvider(new BouncyCastleProvider());
    Objects.requireNonNull(keypair, "keypair");
    if (namelist.isEmpty()) {
        throw new IllegalStateException("No domain was set");
    }

    try {
        GeneralName[] gns = new GeneralName[namelist.size()];
        for (int ix = 0; ix < namelist.size(); ix++) {
            gns[ix] = new GeneralName(GeneralName.dNSName,namelist.get(ix));
        }
        SignatureAlgorithmIdentifierFinder algFinder = new 
                DefaultSignatureAlgorithmIdentifierFinder();
        GeneralNames subjectAltName = new GeneralNames(gns);


        PKCS10CertificationRequestBuilder p10Builder = new     JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());

        ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
        extensionsGenerator.addExtension(Extension.subjectAlternativeName,     false, subjectAltName);
        //extensionsGenerator.addExtension(Extension.authorityInfoAccess,         true, subjectAltName);
        //extensionsGenerator.addExtension(new ASN1ObjectIdentifier("TBD"),     false, subjectAltName);
        //extensionsGenerator.addExtension(new     ASN1ObjectIdentifier("1.3.6.1.5.5.7.1.24"), false, subjectAltName);
        extensionsGenerator.addExtension(new     ASN1ObjectIdentifier("1.3.6.1.5.5.7.1.26").intern(), false, subjectAltName);
        //extentionsGenerator.addExtension();
            p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest,     extensionsGenerator.generate());


        PrivateKey pk = keypair.getPrivate();
        /*JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(
                        pk instanceof ECKey ? EC_SIGNATURE_ALG :     EC_SIGNATURE_ALG);
        ContentSigner signer = csBuilder.build(pk);*/

        if(pk instanceof ECKey)
        {
            AlgorithmIdentifier sigAlg = algFinder.find("SHA1withECDSA");
              AlgorithmIdentifier digAlg = new     DefaultDigestAlgorithmIdentifierFinder().
                    find(sigAlg);
            ContentSigner signer = new     JcaContentSignerBuilder("SHA256with"+pk.getAlgorithm()).setProvider(BOUNCY_CASTL    E_PROVIDER).build(keypair.getPrivate());

            csr=p10Builder.build(signer);
            System.out.println("ZIPED CSR ECDSA: "+csr);
        }
        else
        {
            ContentSigner signer = new     JcaContentSignerBuilder("SHA256with"+pk.getAlgorithm()).build(keypair.getPrivate    ()); 
            csr = p10Builder.build(signer);
            System.out.println("ZIPED CSR RSA: "+csr);
        }

        //csr = p10Builder.build(signer);
    } catch (Exception ex) {
        ex.printStackTrace();;
    }
}

Solution

  • Note: for these codes I used bcprov-jdk15on 1.56

    Some comments about your code. First of all, note the ASN1 structure:

    TNAuthorizationList ::= SEQUENCE SIZE (1..MAX) OF TNEntry
    
    TNEntry ::= CHOICE {
      spc   [0] ServiceProviderCodeList,
      range [1] TelephoneNumberRange,
      one       E164Number
    }
    

    Note that TNEntry is a choice, and TNAuthorizationList is a sequence of TNEntry objects. So your class name should be changed to TNEntry. In the code below, please remember that I've changed the class name to TNEntry.

    I've also changed some things in this class. In getInstance(Object obj) method, the types of spc and range fields are incorrect (according to ASN1 definition, they are both sequences):

    switch (tag) {
        case spc:
        case range: // both are sequences
            return new TNEntry(tag, ASN1Sequence.getInstance(tagObj, false));
        // not sure about "one" field, as it's not tagged
    }
    

    I just don't know how to handle the one field, as it's not tagged. Maybe it should be a DERIA5String, or maybe there's another type for "untagged" choices.

    In this same class (remember, I've changed its name to TNEntry), I also removed the constructor public TNEntry(int tag, String name) because I'm not sure if it applies (at least I didn't need to use it, but you can keep it if you want), and I've changed toString method to return a more readable string:

    public String toString() {
        String sep = System.getProperty("line.separator");
        StringBuffer buf = new StringBuffer();
    
        buf.append(this.getClass().getSimpleName());
        buf.append(" [").append(tag);
        buf.append("]: ");
        switch (tag) {
            case spc:
                buf.append("ServiceProviderCodeList: ").append(sep);
                ASN1Sequence seq = (ASN1Sequence) this.obj;
                int size = seq.size();
                for (int i = 0; i < size; i++) {
                    // all elements are DERIA5Strings
                    DERIA5String str = (DERIA5String) seq.getObjectAt(i);
                    buf.append("    ");
                    buf.append(str.getString());
                    buf.append(sep);
                }
                break;
    
            case range:
                buf.append("TelephoneNumberRange: ").append(sep);
    
                // there are always 2 elements in TelephoneNumberRange
                ASN1Sequence s = (ASN1Sequence) this.obj;
                DERIA5String str = (DERIA5String) s.getObjectAt(0);
                buf.append("    start: ");
                buf.append(str.getString());
                buf.append(sep);
                ASN1Integer count = (ASN1Integer) s.getObjectAt(1);
                buf.append("    count: ");
                buf.append(count.toString());
                buf.append(sep);
                break;
    
            default:
                buf.append(obj.toString());
        }
    
        return buf.toString();
    }
    

    And I also created a TNAuthorizationList class, which holds the sequence of TNEntry objects (remember that I've changed your class name to TNEntry, so this TNAuthorizationList class is a different one). Note that I also created a constant to hold the OID (just to make things a little bit easier):

    public class TNAuthorizationList extends ASN1Object {
        // put OID in a constant, so I don't have to remember it all the time
        public static final ASN1ObjectIdentifier TN_AUTH_LIST_OID = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.1.26");
    
        private TNEntry[] entries;
    
        public TNAuthorizationList(TNEntry[] entries) {
            this.entries = entries;
        }
    
        public static TNAuthorizationList getInstance(Object obj) {
            if (obj instanceof TNAuthorizationList) {
                return (TNAuthorizationList) obj;
            }
            if (obj != null) {
                return new TNAuthorizationList(ASN1Sequence.getInstance(obj));
            }
    
            return null;
        }
    
        public static TNAuthorizationList getInstance(ASN1TaggedObject obj, boolean explicit) {
            return getInstance(ASN1Sequence.getInstance(obj, explicit));
        }
    
        private TNAuthorizationList(ASN1Sequence seq) {
            this.entries = new TNEntry[seq.size()];
    
            for (int i = 0; i != seq.size(); i++) {
                entries[i] = TNEntry.getInstance(seq.getObjectAt(i));
            }
        }
    
        public TNEntry[] getEntries() {
            TNEntry[] tmp = new TNEntry[entries.length];
            System.arraycopy(entries, 0, tmp, 0, entries.length);
            return tmp;
        }
    
        @Override
        public ASN1Primitive toASN1Primitive() {
            return new DERSequence(entries);
        }
    
        public String toString() {
            String sep = System.getProperty("line.separator");
            StringBuffer buf = new StringBuffer();
    
            buf.append(this.getClass().getSimpleName());
            buf.append(":").append(sep);
            for (TNEntry tnEntry : entries) {
                buf.append("  ");
                buf.append(tnEntry.toString());
                buf.append(sep);
            }
            return buf.toString();
        }
    }
    

    Now, to add this extension to a certificate, I've done this code (with some sample data as I don't know what should be in each field in a real world situation):

    X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(etc...);
    
    // create TNEntries for TNAuthorizationList
    TNEntry[] entries = new TNEntry[2];
    
    // create a "spc" entry
    DERIA5String[] cList = new DERIA5String[] { new DERIA5String("spc1"), new DERIA5String("spc2") };
    DERSequence spc = new DERSequence(cList);
    entries[0] = TNEntry.getInstance(new DERTaggedObject(false, TNEntry.spc, spc));
    
    // create a "range" entry
    DERSequence range = new DERSequence(new ASN1Encodable[] { new DERIA5String("123456"), new ASN1Integer(1) });
    entries[1] = TNEntry.getInstance(new DERTaggedObject(false, TNEntry.range, range));
    
    TNAuthorizationList tnAuthList = new TNAuthorizationList(entries);
    builder.addExtension(TNAuthorizationList.TN_AUTH_LIST_OID, false, tnAuthList);
    

    Once you have the certificate object (a X509Certificate in my example), you can do:

    // cert is a X509Certificate instance
    ASN1Primitive value = X509ExtensionUtil.fromExtensionValue(cert.getExtensionValue(TNAuthorizationList.TN_AUTH_LIST_OID.getId()));
    TNAuthorizationList authList = TNAuthorizationList.getInstance(value);
    System.out.println(authList.toString());
    

    The output will be:

    TNAuthorizationList:
      TNEntry [0]: ServiceProviderCodeList: 
        spc1
        spc2
    
      TNEntry [1]: TelephoneNumberRange: 
        start: 123456
        count: 1
    

    Notes:

    • As I said, this code is incomplete because I'm not sure how to handle the one field of TNEntry, because it's not tagged (I don't know if it must be a DERIA5String or if there's another type of object for an "untagged" field).
    • You could also do some improvements:
      • ServiceProviderCodeList can have 1 to 3 elements, so you could validate its size
      • TelephoneNumberRange: the start field has a specific format (FROM ("0123456789#*") which I think it means only these characters are accepted), so you could also validate it
      • To create the values for ServiceProviderCodeList and TelephoneNumberRange, I've created the DERSequence objects by hand, but you can create custom classes for them if you want: ServiceProviderCodeList could hold a list of DERIA5String and perform proper validations in its constructor (size from 1 to 3), and TelephoneNumberRange could have start and count fields (with proper validation of start value) - and toASN1Primitive just need to return a DERSequence of its fields in the right order

    For your parsing issues, I've checked acme4j code and it uses a java.security.cert.X509Certificate class. The toString() method of this class (when using Sun's default provider) is generating this "extension unknown" output (according to the corresponding code).

    So, in order to parse it correctly (show the formatted output as described above), you'll probably have to change acme4j's code (or write your own), creating a new toString() method and include the new TNAuthorizationList classes in this method.

    When you provide the code showing how you're using acme4j, I'll update this answer accordingly, if needed.