SCEP Client Support

This page provides code samples and information required when creating a SCEP client. These code samples are not maintained, and may not be in accordance with future versions of BouncyCastle. 

All classes found in this page, not in the standard Java libraries, are either from BouncyCastle or in EJBCA. 

Generating a PKCSREQ Message

A basic PKCSREQ (certificate request) is a standard PKCS10 certificate request wrapped in a PKCS7 envelope. To do so, the following code can be found in the EJBCA protocol tests:

    public byte[] generateCertReq(String dn, String password, String transactionId, X509Certificate ca, Extensions exts,
            final X509Certificate senderCertificate, final PrivateKey signatureKey, ASN1ObjectIdentifier encryptionAlg) throws OperatorCreationException, CertificateException,
            IOException, CMSException {
        // Generate keys
        // Create challenge password attribute for PKCS10
        // Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }}
        //
        // Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE {
        //    type    ATTRIBUTE.&id({IOSet}),
        //    values  SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{\@type})
        // }
        ASN1EncodableVector challpwdattr = new ASN1EncodableVector();
        // Challenge password attribute
        challpwdattr.add(PKCSObjectIdentifiers.pkcs_9_at_challengePassword); 
        ASN1EncodableVector pwdvalues = new ASN1EncodableVector();
        pwdvalues.add(new DERUTF8String(password));
        challpwdattr.add(new DERSet(pwdvalues));
        ASN1EncodableVector extensionattr = new ASN1EncodableVector();
        extensionattr.add(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
        extensionattr.add(new DERSet(exts));
        // Complete the Attribute section of the request, the set (Attributes) contains two sequences (Attribute)
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(new DERSequence(challpwdattr));
        v.add(new DERSequence(extensionattr));
        DERSet attributes = new DERSet(v);
        // Create PKCS#10 certificate request
        final PKCS10CertificationRequest p10request = CertTools.genPKCS10CertificationRequest("SHA256WithRSA",
                CertTools.stringToBcX500Name(reqdn), keys.getPublic(), attributes, keys.getPrivate(), null);    
        // wrap message in pkcs#7
        return wrap(p10request.getEncoded(), Integer.toString(ScepRequestMessage.SCEP_TYPE_PKCSREQ), transactionId, senderCertificate, signatureKey, encryptionAlg);
    }
    private byte[] wrap(byte[] envBytes, String messageType, String transactionId, final X509Certificate senderCertificate,
            final PrivateKey signatureKey, ASN1ObjectIdentifier encryptionAlg) throws CertificateEncodingException, CMSException, IOException {
        // Create inner enveloped data
        CMSEnvelopedData ed = envelope(new CMSProcessableByteArray(envBytes), encryptionAlg);
        log.debug("Enveloped data is " + ed.getEncoded().length + " bytes long");
        CMSTypedData msg = new CMSProcessableByteArray(ed.getEncoded());
        // Create the outer signed data
        CMSSignedData s = sign(msg, messageType, transactionId, senderCertificate, signatureKey);
        byte[] ret = s.getEncoded();
        return ret;
    }

Generating a GetCRL Message 

    public byte[] generateCrlReq(String dn, String transactionId, X509Certificate ca, final X509Certificate senderCertificate,
            final PrivateKey signatureKey, ASN1ObjectIdentifier encryptionAlg) throws CertificateEncodingException, CMSException, IOException {
        X500Name name = CertTools.stringToBcX500Name(cacert.getIssuerDN().getName());
        IssuerAndSerialNumber ias = new IssuerAndSerialNumber(name, cacert.getSerialNumber());       
        // wrap message in pkcs#7
        return wrap(ias.getEncoded(), Integer.toString(ScepRequestMessage.SCEP_TYPE_GETCRL), transactionId, senderCertificate, signatureKey, encryptionAlg);        
    }

Generating a GetCertInitial Message

The GetCertInitial message is used when polling EJBCA in RA mode while waiting for issuance of a certificate requiring approval from an administrator. 

    public byte[] generateGetCertInitial(String dn, String transactionId, X509Certificate caCertificate, final X509Certificate senderCertificate,
            final PrivateKey signatureKey, ASN1ObjectIdentifier encryptionAlg) throws CertificateEncodingException, CMSException, IOException {
        this.cacert = caCertificate;
        this.reqdn = dn;

        // pkcsGetCertInitial issuerAndSubject ::= { 
        //	    issuer "the certificate authority issuer name" 
        //	    subject "the requester subject name as given in PKCS#10" 
        //	} 
        ASN1EncodableVector vec = new ASN1EncodableVector();
        vec.add(CertTools.stringToBcX500Name(caCertificate.getIssuerDN().getName()));
        vec.add(CertTools.stringToBcX500Name(dn));
        DERSequence seq = new DERSequence(vec);

        // wrap message in pkcs#7
        return wrap(seq.getEncoded(), Integer.toString(ScepRequestMessage.SCEP_TYPE_GETCERTINITIAL), transactionId, senderCertificate, signatureKey, encryptionAlg);
    }

Parsing a SCEP Response

The following sample code from EJBCA's protocol tests verifies and extracts the contents of a successful SCEP Response

private void checkSuccesfulScepResponse(byte[] retMsg, X509Certificate caCertificate, String userDN, String _senderNonce, String _transId, boolean crlRep,
            String digestOid, boolean noca, KeyPair keyPair)
            throws CMSException, NoSuchProviderException, NoSuchAlgorithmException, CertStoreException, InvalidKeyException, CertificateException,
            SignatureException, CRLException, OperatorCreationException {
        // Parse response message
        CMSSignedData s = new CMSSignedData(retMsg);
        // The signer, i.e. the CA, check it's the right CA
        SignerInformationStore signers = s.getSignerInfos();
        Collection<SignerInformation> col = signers.getSigners();
        assertTrue(col.size() > 0);
        Iterator<SignerInformation> iter = col.iterator();
        SignerInformation signerInfo = iter.next();
        // Check that the message is signed with the correct digest alg
        assertEquals(signerInfo.getDigestAlgOID(), digestOid);
        SignerId sinfo = signerInfo.getSID();
        // Check that the signer is the expected CA
        assertEquals(CertTools.stringToBCDNString(caCertificate.getIssuerDN().getName()), CertTools.stringToBCDNString(sinfo.getIssuer().toString()));
        // Verify the signature
        JcaDigestCalculatorProviderBuilder calculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME);
        JcaSignerInfoVerifierBuilder jcaSignerInfoVerifierBuilder = new JcaSignerInfoVerifierBuilder(calculatorProviderBuilder.build()).setProvider(BouncyCastleProvider.PROVIDER_NAME);
        assertTrue("Response was not correctly signed by CA.", signerInfo.verify(jcaSignerInfoVerifierBuilder.build(caCertificate.getPublicKey())));
        // Get authenticated attributes
        AttributeTable tab = signerInfo.getSignedAttributes();
        // --Fail info
        Attribute attr = tab.get(new ASN1ObjectIdentifier(ScepRequestMessage.id_failInfo));
        // No failInfo on this success message
        assertNull(attr);
        // --Message type
        attr = tab.get(new ASN1ObjectIdentifier(ScepRequestMessage.id_messageType));
        assertNotNull(attr);
        ASN1Set values = attr.getAttrValues();
        assertEquals(values.size(), 1);
        ASN1String str = DERPrintableString.getInstance((values.getObjectAt(0)));
        String messageType = str.getString();
        assertEquals("3", messageType);
        // --Success status
        attr = tab.get(new ASN1ObjectIdentifier(ScepRequestMessage.id_pkiStatus));
        assertNotNull(attr);
        values = attr.getAttrValues();
        assertEquals(values.size(), 1);
        str = DERPrintableString.getInstance((values.getObjectAt(0)));
        assertEquals("Response status was not as expected.", ResponseStatus.SUCCESS.getStringValue(), str.getString());
        // --SenderNonce
        attr = tab.get(new ASN1ObjectIdentifier(ScepRequestMessage.id_senderNonce));
        assertNotNull(attr);
        values = attr.getAttrValues();
        assertEquals(values.size(), 1);
        ASN1OctetString octstr = ASN1OctetString.getInstance(values.getObjectAt(0));
        // SenderNonce is something the server came up with, but it should be 16 chars
        assertEquals("Expected nonce length was 16.", 16, octstr.getOctets().length);
        // --Recipient Nonce
        attr = tab.get(new ASN1ObjectIdentifier(ScepRequestMessage.id_recipientNonce));
        assertNotNull(attr);
        values = attr.getAttrValues();
        assertEquals(values.size(), 1);
        octstr = ASN1OctetString.getInstance(values.getObjectAt(0));
        // recipient nonce should be the same as we sent away as sender nonce
        String recipientNonce = new String(Base64.encode(octstr.getOctets()));
        assertEquals("Incorrect nonce was received back.", _senderNonce, recipientNonce);
        // --Transaction ID
        attr = tab.get(new ASN1ObjectIdentifier(ScepRequestMessage.id_transId));
        assertNotNull(attr);
        values = attr.getAttrValues();
        assertEquals(values.size(), 1);
        str = DERPrintableString.getInstance((values.getObjectAt(0)));
        // transid should be the same as the one we sent
        assertEquals(_transId, str.getString());
        // First we extract the encrypted data from the CMS enveloped data contained within the CMS signed data
        final CMSProcessable sp = s.getSignedContent();
        final byte[] content = (byte[]) sp.getContent();
        final CMSEnvelopedData ed = new CMSEnvelopedData(content);
        final RecipientInformationStore recipients = ed.getRecipientInfos();
        Store<X509CertificateHolder> certstore;
        Collection<RecipientInformation> c = recipients.getRecipients();
        assertEquals(c.size(), 1);
        Iterator<RecipientInformation> it = c.iterator();
        byte[] decBytes = null;
        RecipientInformation recipient = it.next();
        JceKeyTransEnvelopedRecipient rec = new JceKeyTransEnvelopedRecipient(keyPair.getPrivate());
        rec.setProvider(BouncyCastleProvider.PROVIDER_NAME);
        rec.setContentProvider(BouncyCastleProvider.PROVIDER_NAME);
        // Option we must set to prevent Java PKCS#11 provider to try to make the symmetric decryption in the HSM,
        // even though we set content provider to BC. Symm decryption in HSM varies between different HSMs and at least for this case is known
        // to not work in SafeNet Luna (JDK behavior changed in JDK 7_75 where they introduced imho a buggy behavior)
        rec.setMustProduceEncodableUnwrappedKey(true);
        decBytes = recipient.getContent(rec);
        // This is yet another CMS signed data
        CMSSignedData sd = new CMSSignedData(decBytes);
        // Get certificates from the signed data
        certstore = sd.getCertificates();
        assertNotNull(certstore);
        if (crlRep) {
            // We got a reply with a requested CRL
            final List<X509CRLHolder> crls = (List<X509CRLHolder>) sd.getCRLs().getMatches(null);
            assertEquals(crls.size(), 1);
            // CRL is first (and only)
            final X509CRL retCrl = new JcaX509CRLConverter().getCRL(crls.get(0));
            log.info("Got CRL with DN: " + retCrl.getIssuerDN().getName());
            // check the returned CRL
            assertEquals(caCertificate.getSubjectDN().getName(), retCrl.getIssuerDN().getName());
            retCrl.verify(caCertificate.getPublicKey());
        } else {
            // We got a reply with a requested certificate
            final Collection<X509CertificateHolder> certs = certstore.getMatches(null);
            // EJBCA returns the issued cert and the CA cert (cisco vpn client requires that the ca cert is included)
            if (noca) {
                assertEquals(certs.size(), 1);
            } else {
                assertEquals(certs.size(), 2);
            }
            // Issued certificate must be first
            boolean verified = false;
            boolean gotcacert = false;
            for (X509CertificateHolder x509CertificateHolder : certs) {
                X509Certificate retcert = new JcaX509CertificateConverter().getCertificate(x509CertificateHolder);
                log.info("Got cert with DN: " + retcert.getSubjectDN().getName());
                // check the returned certificate
                String subjectdn = CertTools.getSubjectDN(retcert);
                if (CertTools.stringToBCDNString(userDN).equals(subjectdn)) {
                    // issued certificate
                    assertEquals(CertTools.stringToBCDNString(userDN), subjectdn);
                    assertEquals(CertTools.getSubjectDN(caCertificate), CertTools.getIssuerDN(retcert));
                    retcert.verify(caCertificate.getPublicKey());
                    assertTrue(checkKeys(keyPair.getPrivate(), retcert.getPublicKey()));
                    verified = true;
                } else {
                    // ca certificate
                    assertEquals(CertTools.getSubjectDN(caCertificate), CertTools.getSubjectDN(retcert));
                    gotcacert = true;
                }
            }
            assertTrue(verified);
            if (noca) {
                assertFalse(gotcacert);
            } else {
                assertTrue(gotcacert);
            }
        }
    }