1

I am trying to generate a Certificate Signing Request (CSR) in Java without using third-party libraries like BouncyCastle. Below is the code I am using to generate the CSR:

public void createCSR(@NonNull KeyPair keyPair, @NonNull CSRCallback csrCallback) {
    try {
        byte[] subject = encodeSubject("CN=MyClient, O=MyOrganization, L=City, C=US");
        byte[] publicKey = encodePublicKey(keyPair.getPublic());
        byte[] algorithmIdentifier = encodeAlgorithmIdentifier();
        byte[] dataToSign = concatenateData(subject, publicKey, algorithmIdentifier);
        byte[] signature = signData(dataToSign, keyPair.getPrivate());
        byte[] csrBytes = createCSR(subject, publicKey, algorithmIdentifier, signature);
        String pemCSR = toPEM(csrBytes);

        csrCallback.onCSRGenerated(pemCSR);
    } catch (Exception e) {
        csrCallback.onCSRGenerationFailed(e);
    }
}

byte[] encodeSubject(String subjectDN) throws Exception {
    // Example subject: "CN=MyClient, O=MyOrganization, L=City, C=US"
    String[] rdnPairs = subjectDN.split(",\\s*");
    ByteArrayOutputStream subjectStream = new ByteArrayOutputStream();

    for (String rdn : rdnPairs) {
        String[] keyValue = rdn.split("=");
        String rdnType = keyValue[0];
        String rdnValue = keyValue[1];

        ByteArrayOutputStream rdnStream = new ByteArrayOutputStream();
        rdnStream.write(0x0C); // UTF8String tag
        rdnStream.write(rdnValue.length());
        rdnStream.write(rdnValue.getBytes(StandardCharsets.UTF_8));

        ByteArrayOutputStream rdnPairStream = new ByteArrayOutputStream();
        rdnPairStream.write(0x31); // SET tag
        rdnPairStream.write(rdnStream.size());
        rdnPairStream.write(rdnStream.toByteArray());

        subjectStream.write(0x30); // SEQUENCE tag
        subjectStream.write(rdnPairStream.size());
        subjectStream.write(rdnPairStream.toByteArray());
    }

    ByteArrayOutputStream finalSubjectStream = new ByteArrayOutputStream();
    finalSubjectStream.write(0x30); // SEQUENCE tag
    finalSubjectStream.write(subjectStream.size());
    finalSubjectStream.write(subjectStream.toByteArray());

    byte[] res = new byte[]{
            (byte) 0x30, (byte) 0x56, (byte) 0x31, (byte) 0x0B, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03,
            (byte) 0x55, (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31,
            (byte) 0x0D, (byte) 0x30, (byte) 0x0B, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x07,
            (byte) 0x13, (byte) 0x04, (byte) 0x43, (byte) 0x69, (byte) 0x74, (byte) 0x79, (byte) 0x31, (byte) 0x18,
            (byte) 0x30, (byte) 0x16, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x0A, (byte) 0x13,
            (byte) 0x0F, (byte) 0x4D, (byte) 0x79, (byte) 0x20, (byte) 0x4F, (byte) 0x72, (byte) 0x67, (byte) 0x61,
            (byte) 0x6E, (byte) 0x69, (byte) 0x7A, (byte) 0x61, (byte) 0x74, (byte) 0x69, (byte) 0x6F, (byte) 0x6E,
            (byte) 0x31, (byte) 0x1E, (byte) 0x30, (byte) 0x1C, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
            (byte) 0x03, (byte) 0x13, (byte) 0x15, (byte) 0x4D, (byte) 0x79, (byte) 0x20, (byte) 0x43, (byte) 0x6C,
            (byte) 0x69, (byte) 0x65, (byte) 0x6E, (byte) 0x74, (byte) 0x20, (byte) 0x43, (byte) 0x65, (byte) 0x72,
            (byte) 0x74, (byte) 0x69, (byte) 0x66, (byte) 0x69, (byte) 0x63, (byte) 0x61, (byte) 0x74, (byte) 0x65
    };
    return res;
}


byte[] encodePublicKey(PublicKey publicKey) {
    return publicKey.getEncoded(); // Returns the public key in X.509 DER format
}

byte[] encodeAlgorithmIdentifier() {
    return new byte[]{
            0x30, 0x0D, 0x06, 0x09,
            0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x01, 0x0B,
            0x05, 0x00
    };
}

byte[] concatenateData(byte[] subject, byte[] publicKey, byte[] algorithmIdentifier) throws Exception {
    ByteArrayOutputStream data = new ByteArrayOutputStream();
    data.write(subject);
    data.write(publicKey);
    data.write(algorithmIdentifier);
    return data.toByteArray();
}

byte[] signData(byte[] data, PrivateKey privateKey) throws Exception {
    Signature signature = Signature.getInstance("SHA256withRSA/PSS");
    signature.initSign(privateKey);
    signature.update(data);
    return signature.sign();
}

void addSequence(ByteArrayOutputStream stream, byte[] data) throws IOException {
    stream.write(0x30);
    encodeLength(stream, data.length);
    stream.write(data);
};

void addByteStringASN(ByteArrayOutputStream stream, byte[] data) throws IOException {
    stream.write(0x03);
    encodeLength(stream, data.length);
    stream.write(data);
};

void addIntASN(ByteArrayOutputStream stream, byte[] data) throws IOException {
    stream.write(0x02);
    encodeLength(stream, data.length);
    stream.write(data);
};

public static byte[] mergeArrays(byte[]... arrays) {
    return Stream.of(arrays)
            .flatMapToInt(array -> java.util.stream.IntStream.range(0, array.length).map(i -> array[i]))
            .collect(ByteArrayOutputStream::new,
                    ByteArrayOutputStream::write,
                    (a, b) -> {}).toByteArray();
}

byte[] createCSR(byte[] subject, byte[] publicKey, byte[] algorithmIdentifier, byte[] signature) throws Exception {

    ByteArrayOutputStream certificateRequest = new ByteArrayOutputStream();

    ByteArrayOutputStream certificateRequestInfo = new ByteArrayOutputStream();

    byte[] version = new byte[] {0x02, 0x01, 0x00};

    ByteArrayOutputStream pkInfo = new ByteArrayOutputStream();
    ByteArrayOutputStream subjectPublicKey = new ByteArrayOutputStream();
    addByteStringASN(subjectPublicKey, publicKey);
    addSequence(pkInfo, mergeArrays(algorithmIdentifier, subjectPublicKey.toByteArray()));

    byte[] attribute = new byte[] {(byte) 0xA0, 0x00};

    addSequence(certificateRequestInfo, mergeArrays(version, subject, pkInfo.toByteArray(), attribute));

    byte[] signatureAlgorithm = new byte[] {0x30, 0x0D, 0x06, 0x09, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00};

    ByteArrayOutputStream signatureBitString = new ByteArrayOutputStream();
    addByteStringASN(signatureBitString, signature);

    addSequence(certificateRequest, mergeArrays(certificateRequestInfo.toByteArray(), signatureAlgorithm, signatureBitString.toByteArray()));

    return certificateRequest.toByteArray();
}

private void encodeLength(ByteArrayOutputStream stream, int length) {
    if (length < 128) {
        stream.write(length); // Short form
    } else {
        // Long form
        int numBytes = 0;
        int tempLength = length;
        while (tempLength > 0) {
            tempLength >>= 8;
            numBytes++;
        }
        stream.write(0x80 | numBytes); // Long form indicator
        for (int i = numBytes - 1; i >= 0; i--) {
            stream.write((length >> (8 * i)) & 0xFF);
        }
    }
}

String toPEM(byte[] csrBytes) {
    String base64CSR = Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(csrBytes);
    return "-----BEGIN CERTIFICATE REQUEST-----\n" + base64CSR + "\n-----END CERTIFICATE REQUEST-----";
}

Generated certificate

-----BEGIN CERTIFICATE REQUEST-----
MIICsTCCAZoCAQAwVjELMAkGA1UEBhMCVVMxDTALBgNVBAcTBENpdHkxGDAWBgNV
BAoTD015IE9yZ2FuaXphdGlvbjEeMBwGA1UEAxMVTXkgQ2xpZW50IENlcnRpZmlj
YXRlMIIBOTANBgkqhkiG9w0BAQsFAAOCASYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQCl0DKXmjTTGCSQzUkHue+XJtBVza2+DQu82yAizQFMhDG1zdZw
crCOl46gvkcGYnOjBfaVq6VlLahTMu9FGLnEq+FOnL+n+EVKSIRCw/RCAB/R/eYw
SE4htgoqFK+zWFwjq3DvZkF7CmuZaGSilSh1ifcesANBg0qh6PwGLs2i4WuOy67P
52Oeg986gMsCB4UA7tUSHALbEPZs7UajsGB16yU0s6b+8shECXsbb/yrA4czfkgc
BFYeTW6h+nUIgGnXQ6zdeEHqIxdLGagT5iRndypnX0YVH3Wue5nbG7Mal4Yg8v3N
6wzMr54FhKcHwln5s033Ucbu1QnNJkjXjbolAgMBAAGgADANBgkqhkiG9w0BAQsF
AAOCAQBZ7x8XmDk19tjHAHA4edQYxhjtEGYDnnymEsh6zBVMP+Gkzd5HeEgb23Fl
oWeQXFm2pw9p6SXqQkYq8oB3LaaSPnUOKIR84ZQl3dTrn+vdEb2iJ2HkCmPA5BCp
V1+qULItQvLEYBpR8N9dmGW6/m/ErwO32y3WLxAbRKy7mDrUKAAl6gAicU6y5QQ8
isDRcFqCCuOPfKMNnuC1ViAmM4DSDj6UePH8R0uogPshIlpXaY2K+boaK6bgGNfa
+mReFP4LMIqiTLrABirCmr6lygUWinsBOzgtAD9OeWWzhG7/5l2mB0jlzc2y+e8V
iL0llf7wwXEHPMcOPid1lFvMha8G
-----END CERTIFICATE REQUEST-----

when I run: openssl req -in request.csr -noout -text then I get below error:

C08F6E51F87F0000:error:068000A8:asn1 encoding routines:asn1_check_tlen:wrong tag:crypto/asn1/tasn_dec.c:1194:
C08F6E51F87F0000:error:0688010A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error:crypto/asn1/tasn_dec.c:349:Type=X509_REQ
error: unable to load X509 request from file 'request.csr'

My key pair generation code -

public static KeyPair generateRSAKeyPairWithAuth(String alias) throws Exception {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
    keyGen.initialize(new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
            .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationValidityDurationSeconds(120)
            .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1, KeyProperties.SIGNATURE_PADDING_RSA_PSS)
            .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512).build());
    return keyGen.generateKeyPair();
}

NOTE: I don't want to use BouncyCastle or SpongyCastle for this.

4
  • Have you checked if your CSR is valid ASN.1 at all, eg. By parsing it on lapo.it/asn1js Commented Dec 6, 2024 at 14:49
  • @Robert it fails... mentioning "Input contains 622 more bytes to decode". How do I fix it Commented Dec 9, 2024 at 9:38
  • In general if you want to implement something without library first implement it with a library, create multiple sample data and then try to recreate those samo,e data without library byte by byte. Commented Dec 9, 2024 at 13:21
  • @Robert, I tried generating certificate using BouncyCastle and then matching the exact pattern for this. In above I think Signature and Signature Algorithm are getting generated wrong. Commented Dec 10, 2024 at 7:50

2 Answers 2

1

First thing I notice: You are not reserving space for encoding the lenght of your outer sequence, so the statement csrBytes[startPos - 1] = (byte) length is actually overwriting your 0x30 tag which you put at the beginning. You also assume that the length is less than 128 bytes, as 127 is the maximum you can encode in one byte. Fixing this is a bit tricky as you don't know how many bytes you need to reserve for encoding the length, but you can get this running based on your test data.

Encoding of subject, public key and algorithm identifier looks ok, but encoding of the signature does not, as signature.length + 1 seems to be 257 which you try to encode as 1 byte, so length of 1 is encoded. As you write the signature after this, it is interpreted as ASN.1 TLV and of course that kills any further decoding.

Below is the output from my ASN.1 tools trying to decode what you generated, illustrating the points above:

  1. Incorrect tag at offset 0x00
  2. Encoding with length 1 at offset 0x16f
  3. Failure to decode the signature starting at offset 0x172
0x00000000:|#>[APPLICATION 17](0x02)
0x00000002:   |->[UNIVERSAL 1](0x00) = ""
0x00000004:|#>[UNIVERSAL 16](0x34)
0x00000006:   |#>[UNIVERSAL 16](0x0c)
0x00000008:      |#>[UNIVERSAL 17](0x0a)
0x0000000a:         |->[UNIVERSAL 12](0x08) = "MyClient"
0x00000014:   |#>[UNIVERSAL 16](0x12)
0x00000016:      |#>[UNIVERSAL 17](0x10)
0x00000018:         |->[UNIVERSAL 12](0x0e) = "MyOrganization"
0x00000028:   |#>[UNIVERSAL 16](0x08)
0x0000002a:      |#>[UNIVERSAL 17](0x06)
0x0000002c:         |->[UNIVERSAL 12](0x04) = "City"
0x00000032:   |#>[UNIVERSAL 16](0x06)
0x00000034:      |#>[UNIVERSAL 17](0x04)
0x00000036:         |->[UNIVERSAL 12](0x02) = "US"
0x0000003a:|#>[UNIVERSAL 16](0x0122)
0x0000003e:   |#>[UNIVERSAL 16](0x0d)
0x00000040:      |->[UNIVERSAL 6](0x09) = "1.2.840.113549.1.1.1"
0x0000004b:      |->[UNIVERSAL 5](0x00) = ""
0x0000004d:   |->[UNIVERSAL 3](0x010f) = "001100001000001000000001000010100000001010000010000000010000000100000000101110001101111000111101111110111010101011111100010100001011001100000111100101001100010011110100000011101010100010101011101000000100111000011000010000010010111001111110101000011110011001010011010001011110110010011111110111011101101100011011110000011110010011100101100010100001001010000110011100010101000011100000011011000001011011110111001111010111111000110100111110100001110011111011010101000101100100111010100101111101001101001110010100100011000101110111101110010100011101101111110011101010101011100110111110111110101011101000000010000101100111110110011110111110110111000110010010010001000011100011110111010100000011011101101001100100011100100111000001000000001111011011000011011110101011110011111111101000100100011111111111110010000111101110010000111111001100011000101110011110101010110010011001010001101111011010100001001101111100100001111000100001110001000001011100001001110110010100101010000011011001000000100011101000000110100001110000000111010100100011010000011101110110101111001001110000010101100001001110010001100001111101010110010101010000100010000010110010101100110111000111111011110001010110111011010011110101000010110001011111010100000101101010000110101000011010110011111100001000100110010010010111111001011110110011011111010010011111000000110000000100001011011101101110111001010101100001011011100100110110010011100101111100110011100111001111111101111100001101000001011011111010011100100100000100001010110100100110000101011100010110001010111011100001110011001101111001110100001011000111010001100001110000111101101001100011111101011011010010110110010010001111011111100001001000010000101100000110001110101111101101101001101001011000111001000111001011010101011001101011110110001101111000101100110011001001110011101011011110110011110000100100001010110010110111101110111110010111111011111110100010110100010000101010100000000100101111110010110001000101010110101100111010100011001110010111001111111010101101011011000001101010010100001001011001101110110001000010001000010111110110100101110011111100100101111000001001100001001000010010000000110000001000000011000000010000000000000001"
0x00000160:|#>[UNIVERSAL 16](0x0d)
0x00000162:   |->[UNIVERSAL 6](0x09) = "1.2.840.113549.1.1.11"
0x0000016d:   |->[UNIVERSAL 5](0x00) = ""
0x0000016f:|->[UNIVERSAL 3](0x01) = ""
Error while reading the datafile.
Data error at offset 436 (0x000001b4)
The length read for an encoding is greater than 18446744073709551615,
which is the maximum allowed on this platform.

File Offset   Octets indicating the length
-----------   ----------------------------
 0x00000173   0x548c1c97aad66072a8fca62a8a4163a328f13112fa36e0742bce5368d7894d4c8f2fa803daa91b7a911b233a6ad2eb1e4132e05cfab85c692254afb4132fc4fb
Sign up to request clarification or add additional context in comments.

3 Comments

updated the code as per your suggestions and also some other changes. Still my signature and signature algorithm bits are generating wrong.
@Go For Pro You need to add a zero-byte at the beginning of the bit-strings. I think you had it there before, but it is missing now in your updated implementation.
Btw, your addByteStringASN function is named incorrectly, it encodes bit strings. "byte strings" in ASN.1 are called octet strings and have tag 4, not 3. The byte that you need to add (after the length) indicates the number of unused bits in the final octet.
0

I have noticed 2 issues in the code

  1. Not sure how android encodes it, but in java 17 publicKey.getEncoded() already returns a sequence which contains algorithm identifier and n, e. So you don't need to wrap in a sequence with algorithm again.

  2. According to https://luca.ntop.org/Teaching/Appunti/asn1.html section 5.4, in DER bit string is not just length followed by content. it is actually length, unused bits (should be from 0-8) and then content. this is to allow for arbitary length bit strings not just multiples of 8.
    For eg: 3 length bit string 111 is encoded as follows 03 02 05 E0 where last 5 bits of E0 are ignored. Since none of the signature bits should be ignored we can modify addByteStringASN

    void addByteStringASN(ByteArrayOutputStream stream, byte[] data) throws IOException {
        stream.write(0x03);
        encodeLength(stream, data.length + 1); // 1 extra for storing unused bits info
        stream.write(0); // there are 0 unused bits
        stream.write(data);
    }
    

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.