javaandroidcertificateasn.1csr

How to generate CSR manually in java


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.


Solution

  • 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);
      }