You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

helpers.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. // Package helpers implements utility functionality common to many
  2. // CFSSL packages.
  3. package helpers
  4. import (
  5. "bytes"
  6. "crypto"
  7. "crypto/ecdsa"
  8. "crypto/elliptic"
  9. "crypto/rsa"
  10. "crypto/tls"
  11. "crypto/x509"
  12. "crypto/x509/pkix"
  13. "encoding/asn1"
  14. "encoding/pem"
  15. "errors"
  16. "fmt"
  17. "io/ioutil"
  18. "os"
  19. "github.com/google/certificate-transparency-go"
  20. cttls "github.com/google/certificate-transparency-go/tls"
  21. ctx509 "github.com/google/certificate-transparency-go/x509"
  22. "golang.org/x/crypto/ocsp"
  23. "strings"
  24. "time"
  25. "github.com/cloudflare/cfssl/crypto/pkcs7"
  26. cferr "github.com/cloudflare/cfssl/errors"
  27. "github.com/cloudflare/cfssl/helpers/derhelpers"
  28. "github.com/cloudflare/cfssl/log"
  29. "golang.org/x/crypto/pkcs12"
  30. )
  31. // OneYear is a time.Duration representing a year's worth of seconds.
  32. const OneYear = 8760 * time.Hour
  33. // OneDay is a time.Duration representing a day's worth of seconds.
  34. const OneDay = 24 * time.Hour
  35. // InclusiveDate returns the time.Time representation of a date - 1
  36. // nanosecond. This allows time.After to be used inclusively.
  37. func InclusiveDate(year int, month time.Month, day int) time.Time {
  38. return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(-1 * time.Nanosecond)
  39. }
  40. // Jul2012 is the July 2012 CAB Forum deadline for when CAs must stop
  41. // issuing certificates valid for more than 5 years.
  42. var Jul2012 = InclusiveDate(2012, time.July, 01)
  43. // Apr2015 is the April 2015 CAB Forum deadline for when CAs must stop
  44. // issuing certificates valid for more than 39 months.
  45. var Apr2015 = InclusiveDate(2015, time.April, 01)
  46. // KeyLength returns the bit size of ECDSA or RSA PublicKey
  47. func KeyLength(key interface{}) int {
  48. if key == nil {
  49. return 0
  50. }
  51. if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok {
  52. return ecdsaKey.Curve.Params().BitSize
  53. } else if rsaKey, ok := key.(*rsa.PublicKey); ok {
  54. return rsaKey.N.BitLen()
  55. }
  56. return 0
  57. }
  58. // ExpiryTime returns the time when the certificate chain is expired.
  59. func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) {
  60. if len(chain) == 0 {
  61. return
  62. }
  63. notAfter = chain[0].NotAfter
  64. for _, cert := range chain {
  65. if notAfter.After(cert.NotAfter) {
  66. notAfter = cert.NotAfter
  67. }
  68. }
  69. return
  70. }
  71. // MonthsValid returns the number of months for which a certificate is valid.
  72. func MonthsValid(c *x509.Certificate) int {
  73. issued := c.NotBefore
  74. expiry := c.NotAfter
  75. years := (expiry.Year() - issued.Year())
  76. months := years*12 + int(expiry.Month()) - int(issued.Month())
  77. // Round up if valid for less than a full month
  78. if expiry.Day() > issued.Day() {
  79. months++
  80. }
  81. return months
  82. }
  83. // ValidExpiry determines if a certificate is valid for an acceptable
  84. // length of time per the CA/Browser Forum baseline requirements.
  85. // See https://cabforum.org/wp-content/uploads/CAB-Forum-BR-1.3.0.pdf
  86. func ValidExpiry(c *x509.Certificate) bool {
  87. issued := c.NotBefore
  88. var maxMonths int
  89. switch {
  90. case issued.After(Apr2015):
  91. maxMonths = 39
  92. case issued.After(Jul2012):
  93. maxMonths = 60
  94. case issued.Before(Jul2012):
  95. maxMonths = 120
  96. }
  97. if MonthsValid(c) > maxMonths {
  98. return false
  99. }
  100. return true
  101. }
  102. // SignatureString returns the TLS signature string corresponding to
  103. // an X509 signature algorithm.
  104. func SignatureString(alg x509.SignatureAlgorithm) string {
  105. switch alg {
  106. case x509.MD2WithRSA:
  107. return "MD2WithRSA"
  108. case x509.MD5WithRSA:
  109. return "MD5WithRSA"
  110. case x509.SHA1WithRSA:
  111. return "SHA1WithRSA"
  112. case x509.SHA256WithRSA:
  113. return "SHA256WithRSA"
  114. case x509.SHA384WithRSA:
  115. return "SHA384WithRSA"
  116. case x509.SHA512WithRSA:
  117. return "SHA512WithRSA"
  118. case x509.DSAWithSHA1:
  119. return "DSAWithSHA1"
  120. case x509.DSAWithSHA256:
  121. return "DSAWithSHA256"
  122. case x509.ECDSAWithSHA1:
  123. return "ECDSAWithSHA1"
  124. case x509.ECDSAWithSHA256:
  125. return "ECDSAWithSHA256"
  126. case x509.ECDSAWithSHA384:
  127. return "ECDSAWithSHA384"
  128. case x509.ECDSAWithSHA512:
  129. return "ECDSAWithSHA512"
  130. default:
  131. return "Unknown Signature"
  132. }
  133. }
  134. // HashAlgoString returns the hash algorithm name contains in the signature
  135. // method.
  136. func HashAlgoString(alg x509.SignatureAlgorithm) string {
  137. switch alg {
  138. case x509.MD2WithRSA:
  139. return "MD2"
  140. case x509.MD5WithRSA:
  141. return "MD5"
  142. case x509.SHA1WithRSA:
  143. return "SHA1"
  144. case x509.SHA256WithRSA:
  145. return "SHA256"
  146. case x509.SHA384WithRSA:
  147. return "SHA384"
  148. case x509.SHA512WithRSA:
  149. return "SHA512"
  150. case x509.DSAWithSHA1:
  151. return "SHA1"
  152. case x509.DSAWithSHA256:
  153. return "SHA256"
  154. case x509.ECDSAWithSHA1:
  155. return "SHA1"
  156. case x509.ECDSAWithSHA256:
  157. return "SHA256"
  158. case x509.ECDSAWithSHA384:
  159. return "SHA384"
  160. case x509.ECDSAWithSHA512:
  161. return "SHA512"
  162. default:
  163. return "Unknown Hash Algorithm"
  164. }
  165. }
  166. // StringTLSVersion returns underlying enum values from human names for TLS
  167. // versions, defaults to current golang default of TLS 1.0
  168. func StringTLSVersion(version string) uint16 {
  169. switch version {
  170. case "1.2":
  171. return tls.VersionTLS12
  172. case "1.1":
  173. return tls.VersionTLS11
  174. default:
  175. return tls.VersionTLS10
  176. }
  177. }
  178. // EncodeCertificatesPEM encodes a number of x509 certificates to PEM
  179. func EncodeCertificatesPEM(certs []*x509.Certificate) []byte {
  180. var buffer bytes.Buffer
  181. for _, cert := range certs {
  182. pem.Encode(&buffer, &pem.Block{
  183. Type: "CERTIFICATE",
  184. Bytes: cert.Raw,
  185. })
  186. }
  187. return buffer.Bytes()
  188. }
  189. // EncodeCertificatePEM encodes a single x509 certificates to PEM
  190. func EncodeCertificatePEM(cert *x509.Certificate) []byte {
  191. return EncodeCertificatesPEM([]*x509.Certificate{cert})
  192. }
  193. // ParseCertificatesPEM parses a sequence of PEM-encoded certificate and returns them,
  194. // can handle PEM encoded PKCS #7 structures.
  195. func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) {
  196. var certs []*x509.Certificate
  197. var err error
  198. certsPEM = bytes.TrimSpace(certsPEM)
  199. for len(certsPEM) > 0 {
  200. var cert []*x509.Certificate
  201. cert, certsPEM, err = ParseOneCertificateFromPEM(certsPEM)
  202. if err != nil {
  203. return nil, cferr.New(cferr.CertificateError, cferr.ParseFailed)
  204. } else if cert == nil {
  205. break
  206. }
  207. certs = append(certs, cert...)
  208. }
  209. if len(certsPEM) > 0 {
  210. return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
  211. }
  212. return certs, nil
  213. }
  214. // ParseCertificatesDER parses a DER encoding of a certificate object and possibly private key,
  215. // either PKCS #7, PKCS #12, or raw x509.
  216. func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) {
  217. certsDER = bytes.TrimSpace(certsDER)
  218. pkcs7data, err := pkcs7.ParsePKCS7(certsDER)
  219. if err != nil {
  220. var pkcs12data interface{}
  221. certs = make([]*x509.Certificate, 1)
  222. pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password)
  223. if err != nil {
  224. certs, err = x509.ParseCertificates(certsDER)
  225. if err != nil {
  226. return nil, nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
  227. }
  228. } else {
  229. key = pkcs12data.(crypto.Signer)
  230. }
  231. } else {
  232. if pkcs7data.ContentInfo != "SignedData" {
  233. return nil, nil, cferr.Wrap(cferr.CertificateError, cferr.DecodeFailed, errors.New("can only extract certificates from signed data content info"))
  234. }
  235. certs = pkcs7data.Content.SignedData.Certificates
  236. }
  237. if certs == nil {
  238. return nil, key, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
  239. }
  240. return certs, key, nil
  241. }
  242. // ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed.
  243. func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
  244. cert, err := ParseCertificatePEM(certPEM)
  245. if err != nil {
  246. return nil, err
  247. }
  248. if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil {
  249. return nil, cferr.Wrap(cferr.CertificateError, cferr.VerifyFailed, err)
  250. }
  251. return cert, nil
  252. }
  253. // ParseCertificatePEM parses and returns a PEM-encoded certificate,
  254. // can handle PEM encoded PKCS #7 structures.
  255. func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
  256. certPEM = bytes.TrimSpace(certPEM)
  257. cert, rest, err := ParseOneCertificateFromPEM(certPEM)
  258. if err != nil {
  259. // Log the actual parsing error but throw a default parse error message.
  260. log.Debugf("Certificate parsing error: %v", err)
  261. return nil, cferr.New(cferr.CertificateError, cferr.ParseFailed)
  262. } else if cert == nil {
  263. return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
  264. } else if len(rest) > 0 {
  265. return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PEM file should contain only one object"))
  266. } else if len(cert) > 1 {
  267. return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PKCS7 object in the PEM file should contain only one certificate"))
  268. }
  269. return cert[0], nil
  270. }
  271. // ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object,
  272. // either a raw x509 certificate or a PKCS #7 structure possibly containing
  273. // multiple certificates, from the top of certsPEM, which itself may
  274. // contain multiple PEM encoded certificate objects.
  275. func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) {
  276. block, rest := pem.Decode(certsPEM)
  277. if block == nil {
  278. return nil, rest, nil
  279. }
  280. cert, err := x509.ParseCertificate(block.Bytes)
  281. if err != nil {
  282. pkcs7data, err := pkcs7.ParsePKCS7(block.Bytes)
  283. if err != nil {
  284. return nil, rest, err
  285. }
  286. if pkcs7data.ContentInfo != "SignedData" {
  287. return nil, rest, errors.New("only PKCS #7 Signed Data Content Info supported for certificate parsing")
  288. }
  289. certs := pkcs7data.Content.SignedData.Certificates
  290. if certs == nil {
  291. return nil, rest, errors.New("PKCS #7 structure contains no certificates")
  292. }
  293. return certs, rest, nil
  294. }
  295. var certs = []*x509.Certificate{cert}
  296. return certs, rest, nil
  297. }
  298. // LoadPEMCertPool loads a pool of PEM certificates from file.
  299. func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) {
  300. if certsFile == "" {
  301. return nil, nil
  302. }
  303. pemCerts, err := ioutil.ReadFile(certsFile)
  304. if err != nil {
  305. return nil, err
  306. }
  307. return PEMToCertPool(pemCerts)
  308. }
  309. // PEMToCertPool concerts PEM certificates to a CertPool.
  310. func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) {
  311. if len(pemCerts) == 0 {
  312. return nil, nil
  313. }
  314. certPool := x509.NewCertPool()
  315. if !certPool.AppendCertsFromPEM(pemCerts) {
  316. return nil, errors.New("failed to load cert pool")
  317. }
  318. return certPool, nil
  319. }
  320. // ParsePrivateKeyPEM parses and returns a PEM-encoded private
  321. // key. The private key may be either an unencrypted PKCS#8, PKCS#1,
  322. // or elliptic private key.
  323. func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) {
  324. return ParsePrivateKeyPEMWithPassword(keyPEM, nil)
  325. }
  326. // ParsePrivateKeyPEMWithPassword parses and returns a PEM-encoded private
  327. // key. The private key may be a potentially encrypted PKCS#8, PKCS#1,
  328. // or elliptic private key.
  329. func ParsePrivateKeyPEMWithPassword(keyPEM []byte, password []byte) (key crypto.Signer, err error) {
  330. keyDER, err := GetKeyDERFromPEM(keyPEM, password)
  331. if err != nil {
  332. return nil, err
  333. }
  334. return derhelpers.ParsePrivateKeyDER(keyDER)
  335. }
  336. // GetKeyDERFromPEM parses a PEM-encoded private key and returns DER-format key bytes.
  337. func GetKeyDERFromPEM(in []byte, password []byte) ([]byte, error) {
  338. keyDER, _ := pem.Decode(in)
  339. if keyDER != nil {
  340. if procType, ok := keyDER.Headers["Proc-Type"]; ok {
  341. if strings.Contains(procType, "ENCRYPTED") {
  342. if password != nil {
  343. return x509.DecryptPEMBlock(keyDER, password)
  344. }
  345. return nil, cferr.New(cferr.PrivateKeyError, cferr.Encrypted)
  346. }
  347. }
  348. return keyDER.Bytes, nil
  349. }
  350. return nil, cferr.New(cferr.PrivateKeyError, cferr.DecodeFailed)
  351. }
  352. // ParseCSR parses a PEM- or DER-encoded PKCS #10 certificate signing request.
  353. func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error) {
  354. in = bytes.TrimSpace(in)
  355. p, rest := pem.Decode(in)
  356. if p != nil {
  357. if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" {
  358. return nil, rest, cferr.New(cferr.CSRError, cferr.BadRequest)
  359. }
  360. csr, err = x509.ParseCertificateRequest(p.Bytes)
  361. } else {
  362. csr, err = x509.ParseCertificateRequest(in)
  363. }
  364. if err != nil {
  365. return nil, rest, err
  366. }
  367. err = csr.CheckSignature()
  368. if err != nil {
  369. return nil, rest, err
  370. }
  371. return csr, rest, nil
  372. }
  373. // ParseCSRPEM parses a PEM-encoded certificate signing request.
  374. // It does not check the signature. This is useful for dumping data from a CSR
  375. // locally.
  376. func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) {
  377. block, _ := pem.Decode([]byte(csrPEM))
  378. if block == nil {
  379. return nil, cferr.New(cferr.CSRError, cferr.DecodeFailed)
  380. }
  381. csrObject, err := x509.ParseCertificateRequest(block.Bytes)
  382. if err != nil {
  383. return nil, err
  384. }
  385. return csrObject, nil
  386. }
  387. // SignerAlgo returns an X.509 signature algorithm from a crypto.Signer.
  388. func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm {
  389. switch pub := priv.Public().(type) {
  390. case *rsa.PublicKey:
  391. bitLength := pub.N.BitLen()
  392. switch {
  393. case bitLength >= 4096:
  394. return x509.SHA512WithRSA
  395. case bitLength >= 3072:
  396. return x509.SHA384WithRSA
  397. case bitLength >= 2048:
  398. return x509.SHA256WithRSA
  399. default:
  400. return x509.SHA1WithRSA
  401. }
  402. case *ecdsa.PublicKey:
  403. switch pub.Curve {
  404. case elliptic.P521():
  405. return x509.ECDSAWithSHA512
  406. case elliptic.P384():
  407. return x509.ECDSAWithSHA384
  408. case elliptic.P256():
  409. return x509.ECDSAWithSHA256
  410. default:
  411. return x509.ECDSAWithSHA1
  412. }
  413. default:
  414. return x509.UnknownSignatureAlgorithm
  415. }
  416. }
  417. // LoadClientCertificate load key/certificate from pem files
  418. func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) {
  419. if certFile != "" && keyFile != "" {
  420. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  421. if err != nil {
  422. log.Criticalf("Unable to read client certificate from file: %s or key from file: %s", certFile, keyFile)
  423. return nil, err
  424. }
  425. log.Debug("Client certificate loaded ")
  426. return &cert, nil
  427. }
  428. return nil, nil
  429. }
  430. // CreateTLSConfig creates a tls.Config object from certs and roots
  431. func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config {
  432. var certs []tls.Certificate
  433. if cert != nil {
  434. certs = []tls.Certificate{*cert}
  435. }
  436. return &tls.Config{
  437. Certificates: certs,
  438. RootCAs: remoteCAs,
  439. }
  440. }
  441. // SerializeSCTList serializes a list of SCTs.
  442. func SerializeSCTList(sctList []ct.SignedCertificateTimestamp) ([]byte, error) {
  443. list := ctx509.SignedCertificateTimestampList{}
  444. for _, sct := range sctList {
  445. sctBytes, err := cttls.Marshal(sct)
  446. if err != nil {
  447. return nil, err
  448. }
  449. list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes})
  450. }
  451. return cttls.Marshal(list)
  452. }
  453. // DeserializeSCTList deserializes a list of SCTs.
  454. func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) {
  455. var sctList ctx509.SignedCertificateTimestampList
  456. rest, err := cttls.Unmarshal(serializedSCTList, &sctList)
  457. if err != nil {
  458. return nil, err
  459. }
  460. if len(rest) != 0 {
  461. return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, errors.New("serialized SCT list contained trailing garbage"))
  462. }
  463. list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList))
  464. for i, serializedSCT := range sctList.SCTList {
  465. var sct ct.SignedCertificateTimestamp
  466. rest, err := cttls.Unmarshal(serializedSCT.Val, &sct)
  467. if err != nil {
  468. return nil, err
  469. }
  470. if len(rest) != 0 {
  471. return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, errors.New("serialized SCT contained trailing garbage"))
  472. }
  473. list[i] = sct
  474. }
  475. return list, nil
  476. }
  477. // SCTListFromOCSPResponse extracts the SCTList from an ocsp.Response,
  478. // returning an empty list if the SCT extension was not found or could not be
  479. // unmarshalled.
  480. func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTimestamp, error) {
  481. // This loop finds the SCTListExtension in the OCSP response.
  482. var SCTListExtension, ext pkix.Extension
  483. for _, ext = range response.Extensions {
  484. // sctExtOid is the ObjectIdentifier of a Signed Certificate Timestamp.
  485. sctExtOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5}
  486. if ext.Id.Equal(sctExtOid) {
  487. SCTListExtension = ext
  488. break
  489. }
  490. }
  491. // This code block extracts the sctList from the SCT extension.
  492. var sctList []ct.SignedCertificateTimestamp
  493. var err error
  494. if numBytes := len(SCTListExtension.Value); numBytes != 0 {
  495. var serializedSCTList []byte
  496. rest := make([]byte, numBytes)
  497. copy(rest, SCTListExtension.Value)
  498. for len(rest) != 0 {
  499. rest, err = asn1.Unmarshal(rest, &serializedSCTList)
  500. if err != nil {
  501. return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, err)
  502. }
  503. }
  504. sctList, err = DeserializeSCTList(serializedSCTList)
  505. }
  506. return sctList, err
  507. }
  508. // ReadBytes reads a []byte either from a file or an environment variable.
  509. // If valFile has a prefix of 'env:', the []byte is read from the environment
  510. // using the subsequent name. If the prefix is 'file:' the []byte is read from
  511. // the subsequent file. If no prefix is provided, valFile is assumed to be a
  512. // file path.
  513. func ReadBytes(valFile string) ([]byte, error) {
  514. switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) {
  515. case 1:
  516. return ioutil.ReadFile(valFile)
  517. case 2:
  518. switch splitVal[0] {
  519. case "env":
  520. return []byte(os.Getenv(splitVal[1])), nil
  521. case "file":
  522. return ioutil.ReadFile(splitVal[1])
  523. default:
  524. return nil, fmt.Errorf("unknown prefix: %s", splitVal[0])
  525. }
  526. default:
  527. return nil, fmt.Errorf("multiple prefixes: %s",
  528. strings.Join(splitVal[:len(splitVal)-1], ", "))
  529. }
  530. }