golang开发及数字证书研究分享

2022-07-20,,,,

目录
  • 私钥的生成
    • rsa
    • ecdsa
    • dsa
    • 给私钥上锁(加访问密码)
  • 生成证书请求
    • go对dsa类型的证书
  • 生成证书
      • 设置ca
      • 签名算法的选择
    • 代码时间

      在go语言提供的系统包中包含了大量和数字证书有关的方法。在这些方法中就有私钥生成的方法、私钥解析的方法、证书请求生成的方法、证书生成的方法等等。通过这些方法应该能够实现和openssl命令类似的功能。

      仿照openssl生成证书的流程(从私钥的生成—>证书请求的生成—>证书的生成)用go语言进行模拟。

      私钥的生成

      在go的x509包下有go定义的证书的结构,该结构如下:

              raw                     []byte // complete asn.1 der content (certificate, signature algorithm and signature).
              rawtbscertificate       []byte // certificate part of raw asn.1 der content.
              rawsubjectpublickeyinfo []byte // der encoded subjectpublickeyinfo.
              rawsubject              []byte // der encoded subject
              rawissuer               []byte // der encoded issuer
              signature          []byte
              signaturealgorithm signaturealgorithm
              publickeyalgorithm publickeyalgorithm
              publickey          interface{}
              version             int
              serialnumber        *big.int
              issuer              pkix.name
              subject             pkix.name
              notbefore, notafter time.time // validity bounds.
              keyusage            keyusage
              extensions []pkix.extension
              extraextensions []pkix.extension
              unhandledcriticalextensions []asn1.objectidentifier
              extkeyusage        []extkeyusage           // sequence of extended key usages.
              unknownextkeyusage []asn1.objectidentifier // encountered extended key usages unknown to this package.
              basicconstraintsvalid bool // if true then the next two fields are valid.
              isca                  bool
              maxpathlen            int
              maxpathlenzero bool
       
              subjectkeyid   []byte
              authoritykeyid []byte
              ocspserver            []string
              issuingcertificateurl []string
       
              // subject alternate name values
              dnsnames       []string
              emailaddresses []string
              ipaddresses    []net.ip
              permitteddnsdomainscritical bool // if true then the name constraints are marked critical.
              permitteddnsdomains         []string
              crldistributionpoints []string
              policyidentifiers []asn1.objectidentifier

      在该结构中有publickeyalgorithm字段,该字段用来表示生成公钥的算法。该字段的变量中可使用的字段如下:

      const (
              unknownpublickeyalgorithm publickeyalgorithm = iota
              rsa
              dsa
              ecdsa
      )

      一共定义了4中情况。除去unknown的情况。剩下的三种的实现分别在crypto/rsacrypto/dsacrypto/ecdsa这三个包中定义了实现。

      rsa

      使用rsa方法生成公私钥的方式非常简单。在crypto/rsa包中直接提供了生成方法。

      func generatekey(random io.reader, bits int) (*privatekey, error)

      该方法生成一个rsa的私钥。查找整个包所提供的方法并没有什么方法能够生成公钥。但在包中有公钥的结构说明。查看私钥的结构:

      type privatekey struct {
              publickey            // public part.
              d         *big.int   // private exponent
              primes    []*big.int // prime factors of n, has >= 2 elements.
              precomputed precomputedvalues
      }

      赫然发现,公钥包含在私钥的结构中。换句话说,只要生成的私钥,公钥就同时拥有了(ecdsa和dsa的公钥也是如此)。

      ecdsa

      使用ecdsa生成公私钥的方式和rsa的方式非常类似:

      func generatekey(c elliptic.curve, rand io.reader) (*privatekey, error)

      crypto/elliptic为参数c提供了4中实现方式。分别为:

       func p224() curve
       func p256() curve
       func p384() curve
       func p521() curve

      dsa

      使用dsa生成公私钥的方式和上面两种有些不同:

      func generatekey(priv *privatekey, rand io.reader) error

      私钥并不是作为结果返回,而是作为参数传入。那很简单,我直接初始化一个dsa的私钥,然后把该私钥作为参数传入不就可以了嘛。事实是,仅仅是实例化了一个dsa的私钥是无法完成公私钥的生成的。生成的结果如下:

      priv:&{publickey:{parameters:{p:<nil> q:<nil> g:<nil>} y:<nil>} x:<nil>}

      可以发现公钥中的所有内容都是为nil(空),由此可以说明无法只通过generatekey()方法生成dsa的私钥。

      crypto/dsa包中还提供了:

      func generateparameters(params *parameters, rand io.reader, sizes parametersizes) error

      通过该方法的描述,可以了解到该方法是为dsa设置参数。那又如何和公私钥有关呢?,在dsa的私钥结构中包含公钥,在公钥的结构中就包含该方法所需要传入的参数parameters。由此,我便想到可以先使用该方法对一些参数进行初始化,然后再生成私钥。

      priv := &dsa.privatekey{}
      dsa.generateparameters(&priv.parameters, rand.reader, dsa.l1024n160)
      dsa.generatekey(priv, rand.reader)

      生成的私钥内容如下:

      priv:&{publickey:{parameters:{p:+91268520972047344779510472614939006285152176630742165979533208518526258287540244526987668731096217967904150874969731516661412604963023247030101570715552650277776208098462838867711078025572452557692674802977527475661989210578136725258241385474445330497234586673407237238372329018550727884900161895964574509801 q:+767580094855879488293276223470508701563202760721 g:+42393651221310072390273970570719382707264443685255379637082820177806079494092036767507554061381644533127114802103872901363724639317297276457243780033980909021336576570837756106975221868617534717069925676009421223798208864916837561389117514471387385853288499961716794226875046226553216578582138687489881455573} y:+68767508229940365112562020548287141674708444377336699267991474890690034611201698420418573204906537903040876819582645033160073997940957577512216430788561800033703926395782022182868300960590402743043934344374390498368316144177816214923367214895567903510165216432049170686626889267028482641530556275670781873053} x:+628682865942164859869306394087148223993136336500}

      注意:golang 对dsa证书没有完整的支持。

      给私钥上锁(加访问密码)

      在使用openssl进行私钥生成的时候,openssl需要我提供私钥的访问密码。那使用go进行私钥时,应该也有该功能。那应该在什么时候添加这个密码呢?是在生成私钥的时候,还是在生成pem文件的时候。我首先想到的是在生成秘密的时候,但是在crypto/rsacrypto/dsacrypto/ecdsa这三个包中查找时并没有发现任何和密码有关的词眼。那就应该在生成pem文件的时候加上密码。生成pem文件的方法在encoding/pem这个包中。但该包中只有两个编码,一个解码的方法,和密码有没有任何关系,唯一的存在的关系就是block结构中的header字段。

      使用openssl生成的私钥文件中会存在这样的字段:

      proc-type: 4,encrypted
      dek-info: des-ede3-cbc,02a0ba59e8cfd431

      使用该字段来说明使用加密方式和提供用于解密的初始值向量。

      在生成私钥和生成文件都无法把密码添加进去。那我就在想是否是在得到私钥的时候对私钥的byte数组进行加密。但这样就需要自己实现了。讲道理的话,go应该会为这种普遍性的东西提供已经封装好的方法。来回重新看api文档。发现自己漏看一个非常重要的包crypto/x509。在该包提供的方法中。很轻松的就找到了如下两个方法:

      func decryptpemblock(b *pem.block, password []byte) ([]byte, error)
      func encryptpemblock(rand io.reader, blocktype string, data, password []byte, alg pemcipher) (*pem.block, error)

      在这两个方法中又要pem,password,恩应该就是这两个方法了,正好一个生成一个解析。

      同在x509包下提供了:

      func marshalpkcs1privatekey(key *rsa.privatekey) []byte
      func marshalecprivatekey(key *ecdsa.privatekey) ([]byte, error)

      把rsa和ecdsa私钥转换成byte数组的方法,但是没有找到把dsa私钥转换成byte数组的方法。

      生成证书请求

      证书请求生成很简单在crypto/x509中直接提供了现成的方法。

      func createcertificaterequest(rand io.reader, template *certificaterequest, priv interface{}) (csr []byte, err error)

      但使用用该方法有一个限制条件:

      all keys types that are implemented via crypto.signer are supported (this includes *rsa.publickey and *ecdsa.publickey.)

      无法使用*dsa.publickey类型的公钥。而传入的参数是一个私钥,因此无法使用dsa类型的私钥。

      go对dsa类型的证书

      该方法需要通过一个证书请求的模板,在go中certificaterequest是如下定义的:

      raw                      []byte // complete asn.1 der content (csr, signature algorithm and signature).
      rawtbscertificaterequest []byte // certificate request info part of raw asn.1 der content.
      rawsubjectpublickeyinfo  []byte // der encoded subjectpublickeyinfo.
      rawsubject               []byte // der encoded subject.
      version            int
      signature          []byte
      signaturealgorithm signaturealgorithm
      publickeyalgorithm publickeyalgorithm
      publickey          interface{}
      subject pkix.name
      attributes []pkix.attributetypeandvalueset
      extensions []pkix.extension
      extraextensions []pkix.extension
      dnsnames       []string
      emailaddresses []string
      ipaddresses    []net.ip

      有一些内容可以不用填写。如果填写了,在后面生成证书时将作为内容直接填入,我就根据openssl生成证书请求时在控制台所展现的内容进行填写。即添加subject中的内容。subject是这样定义的:

      type name struct {
              country, organization, organizationalunit []string
              locality, province                        []string
              streetaddress, postalcode                 []string
              serialnumber, commonname                  string
       
              names      []attributetypeandvalue
              extranames []attributetypeandvalue
      }

      生成证书

      在go提供的crypto/x509包下并没有生成ca的方法,生成证书的方法也只有一个方法:

      func createcertificate(rand io.reader, template, parent *certificate, pub, priv interface{}) (cert []byte, err error)

      它的参数中使用的是两个证书,和我们之前生成的certificaterequest没有关系,而且在整个crypto/x509中的方法中都没有找到把certificaterequest转换成certificate的方法,而且certificaterequest和certificate中的部分数据结构是一样的,因此猜想是通过把certificaterequest中的部分内容复制到certificate中。然后再通过createcertificate进行签发。

      如果传入的两个证书参数是一样的,那么生成的证书是一张自签发的根证书。如果传入的两张证书不同,生成的就是普通的证书了。使用的公钥和私钥是签发者的公私钥即参数parent的公私钥。和生成certificaterequest一样,在这个方法中使用的公私钥不能是dsa类型的。

      设置ca

      在certificate这个结构体中有isca这个字段。用来标识该证书是ca证书,但是在设置该字段为true后生成的证书在扩展中并没有显示这个证书是ca证书的。原因是在如果要使isca生效,需要设置basicconstraintsvalid也为true。同样的也适用于maxpathlen这个字段。

      签名算法的选择

      在go中为证书的签名算法提供了常见的类型:

      unknownsignaturealgorithm signaturealgorithm = iota
      md2withrsa
      md5withrsa
      sha1withrsa
      sha256withrsa
      sha384withrsa
      sha512withrsa
      dsawithsha1
      dsawithsha256
      ecdsawithsha1
      ecdsawithsha256
      ecdsawithsha384
      ecdsawithsha512

      在生成证书的时候我直接选择的sha1withrsa,应为我的私钥是通过rsa算法生成的,没有任何问题,但是在看go的源码中有一段生成自签名证书的测试方法。在该方法中使用了其他的签名算法。因此我想,这些签名算法的应该如何选择。当我把签名算法改成ecdsawithsha1的时候,在进行签名的时候,出现了签名错误。

      因此我猜猜签名算法的选择需要和签署者的公私钥的生成方式有关。

      代码时间

      一切用代码说话。

      和生成私钥有关:

      func genrsapriv(filename, passwd string, len int) error {
          priv, err := rsa.generatekey(rand.reader, len)
          if err != nil {
              return err
          }
       
          data := x509.marshalpkcs1privatekey(priv)
          err = encodeprivpemfile(filename, passwd, data)
          return err
      }
      //genecdsapriv 生成ecdsa私钥文件
      func genecdsapriv(filename, passwd string) error {
          priv, err := ecdsa.generatekey(elliptic.p224(), rand.reader)
          if err != nil {
              return err
          }
          data, err := x509.marshalecprivatekey(priv)
          if err != nil {
              return err
          }
          err = encodeprivpemfile(filename, passwd, data)
          return err
      }
      //gendsapriv 生成dsa私钥(用于演示)
      func gendsapriv() {
          priv := &dsa.privatekey{}
          dsa.generateparameters(&priv.parameters, rand.reader, dsa.l1024n160)
          dsa.generatekey(priv, rand.reader)
          fmt.printf("priv:%+v\n", priv)
      } 
      //decodepriv 解析私钥文件生成私钥,(rsa,和ecdsa两种私钥格式)
      func decodepriv(filename, passwd string) (pubkey, priv interface{}, err error) {
          data, err := ioutil.readfile(filename)
          if err != nil {
              return nil, nil, errors.new("读取私钥文件错误")
          }
          block, _ := pem.decode(data)
          data, err = x509.decryptpemblock(block, []byte(passwd))
          if err != nil {
              return nil, nil, err
          }
       
          privkey, err := x509.parsepkcs1privatekey(data) //解析成rsa私钥
          if err != nil {
              priv, err = x509.parseecprivatekey(data) //解析成ecdsa私钥
              if err != nil {
                  return nil, nil, errors.new("支持持rsa和ecdsa格式的私钥")
              }
          }
          priv = privkey
          pubkey = &privkey.publickey
          return
      } 
      //生成私钥的pem文件
      func encodeprivpemfile(filename, passwd string, data []byte) error {
          block, err := x509.encryptpemblock(rand.reader, "rsa private key", data, []byte(passwd), x509.pemcipher3des)
          if err != nil {
              return err
          }
          file, err := os.create(filename)
          if err != nil {
              return err
          }
          err = pem.encode(file, block)
          if err != nil {
              return err
          }
          return nil
      }

      在这个代码用有一些问题:使用ecdsa生成私钥后加密的type不知道填什么,暂时使用了”rsa private key”。

      和certificaterequest有关的代码:

      // encodecsr 生成证书请求
      func encodecsr(country, organization, organizationlunit, locality, province, streetaddress, postallcode []string, commonname, filename string, priv interface{}) error {
          req := &x509.certificaterequest{
              subject: pkix.name{
                  country:            country,
                  organization:       organization,
                  organizationalunit: organizationlunit,
                  locality:           locality,
                  province:           province,
                  streetaddress:      streetaddress,
                  postalcode:         postallcode,
                  commonname:         commonname,
              },
          } 
          data, err := x509.createcertificaterequest(rand.reader, req, priv)
          if err != nil {
              return err
          }
          err = util.encodepemfile(filename, "certificate request", data)
          return err
      } 
      //decodecsr 解析csrpem文件
      func decodecsr(filename string) (*x509.certificaterequest, error) {
          data, err := util.decodepemfile(filename)
          if err != nil {
              return nil, err
          } 
          req, err := x509.parsecertificaterequest(data)
          return req, err
      }

      和生成certificate有关的代码:

      //gensignselfcertificate 生成自签名证书
      func gensignselfcertificate(req *x509.certificaterequest, publickey, privkey interface{}, filename string, maxpath int, days time.duration) error {
          template := &x509.certificate{
              serialnumber:          big.newint(random.int63n(time.now().unix())),
              subject:               req.subject,
              notbefore:             time.now(),
              notafter:              time.now().add(days * 24 * time.hour),
              basicconstraintsvalid: true,
              isca:               true,
              signaturealgorithm: x509.sha1withrsa, // 签名算法选择sha1withrsa
              keyusage:           x509.keyusagecertsign | x509.keyusagecrlsign | x509.keyusagedataencipherment,
              subjectkeyid:       []byte{1, 2, 3},
          }
          if maxpath > 0 { //如果长度超过0则设置了 最大的路径长度
              template.maxpathlen = maxpath
          }
          cert, err := x509.createcertificate(rand.reader, template, template, publickey, privkey)
          if err != nil {
              return errors.new("签发自签名证书失败")
          }
          err = util.encodepemfile(filename, "certificate", cert)
          if err != nil {
              return err
          }
          return nil
      } 
      //gencertificate 生成非自签名证书
      func gencertificate(req *x509.certificaterequest, parentcert *x509.certificate, pubkey, parentprivkey interface{}, filename string, isca bool, days time.duration) error {
          template := &x509.certificate{
              serialnumber: big.newint(random.int63n(time.now().unix())),
              subject:      req.subject,
              notbefore:    time.now(),
              notafter:     time.now().add(days * 24 * time.hour),
              // extkeyusage: []x509.extkeyusage{ //额外的使用
              //  x509.extkeyusageclientauth,
              //  x509.extkeyusageserverauth,
              // },
              //
       
              signaturealgorithm: x509.sha1withrsa,
          }
       
          if isca {
              template.basicconstraintsvalid = true
              template.isca = true
          } 
          cert, err := x509.createcertificate(rand.reader, template, parentcert, pubkey, parentprivkey)
          if err != nil {
              return errors.new("签署证书失败")
          }
          err = util.encodepemfile(filename, "certificate", cert)
          if err != nil {
              return err
          }
          return nil
      }

      在生成证书这方法,由于可设置的内容过多,不应该使用参数来对证书内容进行控制。应该和openssl一样使用配置文件的方式来对证书中的内容进行配置。

      以上就是golang开发及数字证书研究分享的详细内容,更多关于golang的资料请关注其它相关文章!

      《golang开发及数字证书研究分享.doc》

      下载本文的Word格式文档,以方便收藏与打印。