向我生成的证书添加新扩展

Adding a new Extension to my generated certificate

我需要在我的证书中添加 OID 1.3.6.1.5.5.7.1.26 的新扩展。我的证书中有此 OID 扩展名,但出现以下错误:

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

我希望此 OID 能够像 AuthorityInfoAccess 等其他扩展一样被识别

我需要编辑 Bouncy Castle X509 class 的 jar 吗?

我使用 ACME4j 作为客户端,使用 Letsencrypt Boulder 作为我的服务器。

这是用于注册证书的 CSR Builder 代码。

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

由于 OID 1.3.6.1.5.5.7.1.26 仍然是草稿,我相信像 Let's Encrypt[ 这样的工具和系统是不太可能的=44=] 认可此延期(他们可能会在此延期正式生效后这样做,我真的不知道此类批准背后的官僚程序)。

这意味着您可能必须对其进行编码。我已经使用 Bouncy Castle 几年了,但从未创建新的 ASN1 结构。但如果必须的话,我会看一下它的源代码作为初步指导。

考虑到此扩展的 ASN1 结构:

 TNAuthorizationList ::= SEQUENCE SIZE (1..MAX) OF TNEntry

 TNEntry ::= CHOICE {
   spc   [0] ServiceProviderCodeList,
   range [1] TelephoneNumberRange,
   one       E164Number
   }

 ServiceProviderCodeList ::= SEQUENCE SIZE (1..3) OF IA5String

 -- Service Provider Codes may be OCNs, various SPIDs, or other
 -- SP identifiers from the telephone network

 TelephoneNumberRange ::= SEQUENCE {
   start E164Number,
   count INTEGER
   }

 E164Number ::= IA5String (SIZE (1..15)) (FROM ("0123456789#*"))

扩展值必须是 TNEntrySEQUENCE。所以你可以使用ASN1Sequence(或者它的subclass DERSequence)并且把TNEntry的实例放在里面。

要创建 TNEntry,您需要实现 ASN1Choice(查看 GeneralName class 的源代码并执行类似的操作)。

依此类推,直到你将整个结构映射到它们各自的classes,使用Bouncy Castle内置classes来支持你(IA5StringDERIA5StringINTEGERDERInteger,可以用在ServiceProviderCodeListTelephoneNumberRange中)

之后你可以构建自己的解析器,它可以识别这个扩展。但正如我所说,不要指望其他工具能够识别它。

现在,出于测试目的,我只是从我的 CA Boulder 传递一个字符串值。所以要阅读,这是 TNAUthList 的自定义 ASN1 对象结构。

public class TNAuthorizationList extends ASN1Object implements ASN1Choice{

public static final int spc                     = 0;
public static final int range                   = 1;

private ASN1Encodable obj;
private int           tag;

public TNAuthorizationList(
        int           tag,
        ASN1Encodable name)
    {
        this.obj = name;
        this.tag = tag;
    }

public TNAuthorizationList(
        int       tag,
        String    name)
    {
        this.tag = tag;

        if (tag == spc)
        {
            this.obj = new DERIA5String(name);
        }
        else if (tag == range)
        {
            this.obj = new ASN1ObjectIdentifier(name);
        }
        else
        {
            throw new IllegalArgumentException("can't process String for tag: " + tag);
        }
    }

public static TNAuthorizationList getInstance(
        Object obj)
    {
        if (obj == null || obj instanceof TNAuthorizationList)
        {
            return (TNAuthorizationList)obj;
        }

        if (obj instanceof ASN1TaggedObject)
        {
            ASN1TaggedObject    tagObj = (ASN1TaggedObject)obj;
            int                 tag = tagObj.getTagNo();

            switch (tag)
            {
            case spc:
                return new TNAuthorizationList(tag, DERIA5String.getInstance(tagObj, false));
            }
        }

        if (obj instanceof byte[])
        {
            try
            {
                return getInstance(ASN1Primitive.fromByteArray((byte[])obj));
            }
            catch (IOException e)
            {
                throw new IllegalArgumentException("unable to parse encoded general name");
            }
        }

        throw new IllegalArgumentException("unknown object in getInstance: " + obj.getClass().getName());
    }

public static TNAuthorizationList getInstance(
        ASN1TaggedObject tagObj,
        boolean          explicit)
    {
        return TNAuthorizationList.getInstance(ASN1TaggedObject.getInstance(tagObj, true));
    }

    public int getTagNo()
    {
        return tag;
    }

    public ASN1Encodable getSpc()
    {
        return obj;
    }

    public String toString()
    {
        StringBuffer buf = new StringBuffer();

        buf.append(tag);
        buf.append(": ");
        switch (tag)
        {
        case spc:
            buf.append(DERIA5String.getInstance(obj).getString());
            break;
        default:
            buf.append(obj.toString());
        }
        return buf.toString();
    }



/**
*TNEntry ::= CHOICE {
*       spc   [0] ServiceProviderCodeList,
*       range [1] TelephoneNumberRange,
*       one       E164Number
*       }
*/
@Override
public ASN1Primitive toASN1Primitive() {
    // TODO Auto-generated method stub
    return new DERTaggedObject(false, tag, obj);
}

}

按照您的建议,我已将 OID 值传递给 X509Util class 并打印输出。

ASN1Object o = X509ExtensionUtil.fromExtensionValue(cert.getExtensionValue("1.3.6.1.5.5.7.1.26"));
    System.out.println("ASN1 Object: "+o);
    System.out.println("get Class "+o.getClass());

而 O/P 是

ASN1 Object: [SPID : 39 dc 2b]
get Class class org.bouncycastle.asn1.DLSequence

这样可以吗。我如何使用我的自定义 ASN1 结构解析它?

注意:对于这些代码,我使用了 bcprov-jdk15on 1.56

对您的代码的一些评论。首先,注意ASN1结构:

TNAuthorizationList ::= SEQUENCE SIZE (1..MAX) OF TNEntry

TNEntry ::= CHOICE {
  spc   [0] ServiceProviderCodeList,
  range [1] TelephoneNumberRange,
  one       E164Number
}

注意TNEntry选择TNAuthorizationList是[=17=的序列 ] 对象。所以应该改成TNEntry。在下面的代码中,请记住我已将 class 名称更改为 TNEntry

我也改变了这个 class 中的一些东西。在getInstance(Object obj)方法中,spcrange字段的类型不正确(根据ASN1定义,它们都是序列):

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
}

我只是不知道如何处理 one 字段,因为它没有标记。也许它应该是 DERIA5String,或者可能还有其他类型供 "untagged" 选择。

在同一个 class 中(记住,我已将其名称更改为 TNEntry),我还删除了构造函数 public TNEntry(int tag, String name),因为我不确定它是否适用(至少我不需要使用它,但如果你愿意,你可以保留它),我已经将 toString 方法更改为 return 一个更具可读性的字符串:

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

而且我还创建了一个 TNAuthorizationList class,其中包含 TNEntry 个对象的 序列 (记住我已经更改了你的class 命名为 TNEntry,所以这个 TNAuthorizationList class 是不同的)。请注意,我还创建了一个常量来保存 OID(只是为了让事情变得更简单):

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

现在,为了将这个扩展添加到证书,我已经完成了这段代码(使用一些示例数据,因为我不知道在现实世界的情况下每个字段应该是什么):

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

获得证书对象(在我的示例中为 X509Certificate)后,您可以:

// 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());

输出将是:

TNAuthorizationList:
  TNEntry [0]: ServiceProviderCodeList: 
    spc1
    spc2

  TNEntry [1]: TelephoneNumberRange: 
    start: 123456
    count: 1

备注:

  • 如我所说,此代码不完整,因为我不确定如何处理 TNEntryone 字段,因为它没有标记(我不知道不知道它是否必须是 DERIA5String 或者 "untagged" 字段是否有其他类型的对象)。
  • 您还可以做一些改进:
    • ServiceProviderCodeList 可以有 1 到 3 个元素,所以你可以验证它的大小
    • TelephoneNumberRangestart 字段具有特定格式(FROM ("0123456789#*"),我认为这意味着只接受这些字符),因此您也可以验证它
    • 为了创建 ServiceProviderCodeListTelephoneNumberRange 的值,我手动创建了 DERSequence 对象,但您可以为它们创建自定义 classes,如果你想要:ServiceProviderCodeList 可以包含 DERIA5String 的列表并在其构造函数中执行适当的验证(大小从 1 到 3),并且 TelephoneNumberRange 可以有 startcount 字段(正确验证 start 值)- toASN1Primitive 只需要 return DERSequence 字段的正确顺序

为了你的 , 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).

因此,为了正确解析它(显示如上所述的格式化输出),您可能必须更改 acme4j 的代码(或编写您自己的代码),创建一个新的 toString() 方法并包含此方法中的新 TNAuthorizationList classes。

当您提供显示您如何使用 acme4j 的代码时,如果需要,我会相应地更新此答案。

这就是我使用 ACME4j 的方式。

public class RSASignedCertificate {

private static final int KEY_SIZE = 2048;

private static final Logger LOG = Logger.getLogger(CCIDClient.class);

@SuppressWarnings("unused")
public void fetchCertificate(String domain,String spid, String email, int port,
        String username, String password, String certPath) throws Exception {
    // Load or create a key pair for the user's account
    boolean createdNewKeyPair = true;
    KeyPair domainKeyPair = null;

    DomainKeyStore details = null;
    KeyPair userKeyPair = null;

    userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);

    DateFormat dateTime = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    Date date;
    details = new DomainKeyStore();

    // Create Hibernate Util class Object

    // dao=new HibernateDAO();
    boolean isDomainExist = new HibernateDAO().isDomainExist(domain);
    if (isDomainExist) {

        details.setDomain(domain);
        details.setEmail(email);
        date = new Date();
        details.setUpdatedOn(dateTime.parse(dateTime.format(date)));
        boolean updateresult = new HibernateDAO().updateDetails(details);

        LOG.info("User Details Updated ");
    }

    else {

        date = new Date();
        // Date currentDateTime = dateTime.parse(dateTime.format(date));
        details.setEmail(email);
        details.setDomain(domain);
        details.setStatus("Not Registered");
        details.setCreatedOn(dateTime.parse(dateTime.format(date)));
        details.setUpdatedOn(dateTime.parse(dateTime.format(date)));

        boolean isInserted = new HibernateDAO().insertDetails(details);
        if (!isInserted) {
            throw new AcmeException("Unable to insert details");
        }
        LOG.info("User Details inserted ");
    }
    // details=dao.getDetails(domain);

    Session session = null;
    if (userKeyPair != null) {
        session = new Session("http://192.168.1.143:4000/directory", userKeyPair);
        System.out.println(session.getServerUri().toString());
        System.out.println(session.resourceUri(Resource.NEW_REG));
    }
    Registration reg = null;
    try {
        reg = new RegistrationBuilder().create(session);
        LOG.info("Registered a new user, URI: " + reg.getLocation());
    } catch (AcmeConflictException ex) {
        reg = Registration.bind(session, ex.getLocation());
        LOG.info("Account does already exist, URI: " + reg.getLocation());
    }
    date = new Date();
    details.setStatus("Registered");
    details.setRegistrationDate(dateTime.parse(dateTime.format(date)));
    details.setUpdatedOn(dateTime.parse(dateTime.format(date)));

    new HibernateDAO().updateRegistration(details);

    URI agreement = reg.getAgreement();
    LOG.info("Terms of Service: " + agreement);

    if (createdNewKeyPair) {
        boolean accepted = acceptAgreement(reg, agreement);
        if (!accepted) {
            return;
        }
    }

    Authorization auth = null;
    try {
        auth = reg.authorizeDomain(spid);
    } catch (AcmeUnauthorizedException ex) {
        // Maybe there are new T&C to accept?
        boolean accepted = acceptAgreement(reg, agreement);
        if (!accepted) {
            return;
        }
        // Then try again...
        auth = reg.authorizeDomain(spid);
    }
    LOG.info("New authorization for domain " + spid);
    LOG.info("Authorization " + auth);

    Challenge challenge = tokenChallenge(auth);
    // System.out.println("Challendg status before trigger :"+challenge.getStatus());
    if (challenge == null) {
        throw new AcmeException("No Challenge found");
    }

    if (challenge.getStatus() == Status.VALID) {
        return;
    }
    challenge.trigger();
    int attempts = 1;
    // System.out.println("Challendg status after trigger :"+challenge.getStatus());
    while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
        // System.out.println(challenge.getStatus());
        if (challenge.getStatus().equals(Status.PENDING)) {
            challenge.update();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                LOG.warn("interrupted", e);
                e.printStackTrace();
            }
        }
        if (challenge.getStatus() == Status.INVALID) {
            LOG.error("Challenge failed... Giving up.");
            throw new AcmeServerException("Challenge Failed");
        }
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException ex) {
            LOG.warn("interrupted", ex);
        }
        challenge.update();
    }
    if (challenge.getStatus() != Status.VALID) {
        LOG.error("Failed to pass the challenge... Giving up.");
        throw new AcmeServerException("Challenge Failed");
    }

    date = new Date();
    details.setStatus("Clallenge Completed");
    details.setUpdatedOn(dateTime.parse(dateTime.format(date)));
    new HibernateDAO().updateChallenge(details);

    domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);

    // Generate a CSR for the domain
    CSRBuilder csrb = new CSRBuilder();
    csrb.addDomains(spid);
    csrb.sign(domainKeyPair);

    // System.out.println("CSR:" +csrb.getCSR());

    LOG.info("Keys Algorithm: "
            + domainKeyPair.getPrivate().getAlgorithm());

    PrivateKeyStore privatekey = new PrivateKeyStore();
    privatekey.setDomain(spid);
    privatekey.setEmail(email);
    privatekey.setPrivateKey(domainKeyPair.getPrivate().getEncoded());

    PublicKeyStore publickey = new PublicKeyStore();
    publickey.setDomain(spid);
    publickey.setEmail(email);
    publickey.setPublicKey(domainKeyPair.getPublic().getEncoded());

        // Request a signed certificate
    Certificate certificate = reg.requestCertificate(csrb.getEncoded());
    LOG.info("Success! The certificate for spids " + spid
            + " has been generated!");
    LOG.info("Certificate URI: " + certificate.getLocation());

    String nameFile = spid.replace(".", "") + ".cer";

    X509Certificate sscert = CertificateUtils.createTlsSniCertificate(domainKeyPair,spid);

    System.out.println("Certificate :" +sscert);

    ASN1Primitive o = X509ExtensionUtil.fromExtensionValue(sscert.getExtensionValue(TNAuthorizationList.TN_AUTH_LIST_OID.getId()));
    System.out.println("ASN1:Object "+o+" class: "+o.getClass());
    TNAuthorizationList TNList = TNAuthorizationList.getInstance(o);
    System.out.println(TNList.toString());

    File createFile = new File(certPath + nameFile);
    if (!createFile.exists()) {
        createFile.createNewFile();
    }

    try (FileWriter fw = new FileWriter(createFile.getAbsoluteFile())) {
        CertificateUtils.writeX509Certificate(sscert, fw);
        System.out.println("Certificate " + sscert);
        System.out.println("Certificate Content" + fw);
    }

    date = new Date();
    Calendar c = Calendar.getInstance();
    c.setTime(new Date());
    c.add(Calendar.DATE, 90);
    details.setIssueDate(dateTime.parse(dateTime.format(date)));
    details.setUpdatedOn(dateTime.parse(dateTime.format(date)));
    details.setValidUntil(dateTime.parse(dateTime.format(c.getTime())));
    details.setStatus("Issued");



    details.setCertPath(certPath + nameFile);
    new HibernateDAO().updateCertificate(details);

}

public boolean acceptAgreement(Registration reg, URI agreement) throws AcmeException
         {

    reg.modify().setAgreement(agreement).commit();
    LOG.info("Updated user's ToS");

    return true;
}

public Challenge tokenChallenge(Authorization auth)
{
    TokenChallenge chall = auth.findChallenge(TokenChallenge.TYPE);

    LOG.info("File name: " + chall.getType());
    //LOG.info("Content: " + chall.`);
    return chall;

}