| // Copyright 2025 The BoringSSL Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package runner |
| |
| import ( |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/ed25519" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/asn1" |
| "encoding/pem" |
| "errors" |
| "fmt" |
| "math/bits" |
| "os" |
| "sync/atomic" |
| "time" |
| |
| "golang.org/x/crypto/cryptobyte" |
| cbasn1 "golang.org/x/crypto/cryptobyte/asn1" |
| ) |
| |
| // A custom X.509 certificate generator. This file exists both to give more |
| // convenient ways to generate X.509 certificates, as well as add support for |
| // key types that upstream Go does not support. As a result, it does not reuse |
| // the x509.Certificate encoder. |
| |
| var ( |
| oidSHA256WithRSAEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} |
| oidECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} |
| oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} |
| oidPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} |
| |
| oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} |
| |
| oidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8} |
| |
| oidSubjectKeyID = []int{2, 5, 29, 14} |
| oidKeyUsage = []int{2, 5, 29, 15} |
| oidSubjectAltName = []int{2, 5, 29, 17} |
| oidBasicConstraints = []int{2, 5, 29, 19} |
| oidAuthorityKeyID = []int{2, 5, 29, 35} |
| |
| lastSerial atomic.Uint64 |
| |
| tmpDir string |
| ) |
| |
| type X509SignatureAlgorithm int |
| |
| const ( |
| X509SignDefault X509SignatureAlgorithm = iota |
| X509SignRSAWithSHA256 |
| X509SignECDSAWithSHA256 |
| X509SignEd25519 |
| ) |
| |
| func (alg X509SignatureAlgorithm) Marshal(bb *cryptobyte.Builder) error { |
| switch alg { |
| case X509SignRSAWithSHA256: |
| bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { |
| algID.AddASN1ObjectIdentifier(oidSHA256WithRSAEncryption) |
| algID.AddASN1NULL() |
| }) |
| case X509SignECDSAWithSHA256: |
| bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { |
| algID.AddASN1ObjectIdentifier(oidECDSAWithSHA256) |
| }) |
| case X509SignEd25519: |
| bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { |
| algID.AddASN1ObjectIdentifier(oidEd25519) |
| }) |
| default: |
| return errors.New("unknown algorithm") |
| } |
| return nil |
| } |
| |
| func chooseDefaultX509SignatureAlgorithm(signer crypto.Signer) (X509SignatureAlgorithm, error) { |
| if _, ok := signer.(*rsa.PrivateKey); ok { |
| return X509SignRSAWithSHA256, nil |
| } |
| if _, ok := signer.(*ecdsa.PrivateKey); ok { |
| return X509SignECDSAWithSHA256, nil |
| } |
| if _, ok := signer.(ed25519.PrivateKey); ok { |
| return X509SignEd25519, nil |
| } |
| return 0, fmt.Errorf("unsupported key type: %T", signer) |
| } |
| |
| func signX509(signer crypto.Signer, alg X509SignatureAlgorithm, in []byte) ([]byte, error) { |
| var opts crypto.SignerOpts |
| if _, ok := signer.(*rsa.PrivateKey); ok { |
| switch alg { |
| case X509SignRSAWithSHA256: |
| opts = crypto.SHA256 |
| default: |
| return nil, errors.New("unknown algorithm") |
| } |
| } else if _, ok := signer.(*ecdsa.PrivateKey); ok { |
| switch alg { |
| case X509SignECDSAWithSHA256: |
| opts = crypto.SHA256 |
| default: |
| return nil, errors.New("unknown algorithm") |
| } |
| } else if _, ok := signer.(ed25519.PrivateKey); ok { |
| switch alg { |
| case X509SignEd25519: |
| opts = crypto.Hash(0) |
| default: |
| return nil, errors.New("unknown algorithm") |
| } |
| } else { |
| return nil, fmt.Errorf("unsupported key type: %T", signer) |
| } |
| |
| digest := in |
| if hash := opts.HashFunc(); hash != crypto.Hash(0) { |
| h := hash.New() |
| h.Write(in) |
| digest = h.Sum(nil) |
| } |
| return signer.Sign(rand.Reader, digest, opts) |
| } |
| |
| func addX509Time(bb *cryptobyte.Builder, t time.Time) { |
| t = t.UTC() |
| if y := t.Year(); 1950 <= y && y <= 2049 { |
| bb.AddASN1UTCTime(t) |
| } else { |
| bb.AddASN1GeneralizedTime(t) |
| } |
| } |
| |
| func addASN1ImplicitString(bb *cryptobyte.Builder, tag cbasn1.Tag, b []byte) { |
| bb.AddASN1(tag, func(child *cryptobyte.Builder) { child.AddBytes(b) }) |
| } |
| |
| func addASN1ExplicitTag(bb *cryptobyte.Builder, outerTag, innerTag cbasn1.Tag, cb func(*cryptobyte.Builder)) { |
| bb.AddASN1(outerTag.Constructed().ContextSpecific(), func(child *cryptobyte.Builder) { |
| child.AddASN1(innerTag, cb) |
| }) |
| } |
| |
| func addRSAPSSSubjectPublicKeyInfo(bb *cryptobyte.Builder, key *rsa.PublicKey) { |
| bb.AddASN1(cbasn1.SEQUENCE, func(spki *cryptobyte.Builder) { |
| spki.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { |
| algID.AddASN1ObjectIdentifier(oidPSS) |
| algID.AddASN1(cbasn1.SEQUENCE, func(params *cryptobyte.Builder) { |
| addASN1ExplicitTag(params, 0, cbasn1.SEQUENCE, func(hash *cryptobyte.Builder) { |
| hash.AddASN1ObjectIdentifier(oidSHA256) |
| hash.AddASN1NULL() |
| }) |
| addASN1ExplicitTag(params, 1, cbasn1.SEQUENCE, func(mgf *cryptobyte.Builder) { |
| mgf.AddASN1ObjectIdentifier(oidMGF1) |
| mgf.AddASN1(cbasn1.SEQUENCE, func(hash *cryptobyte.Builder) { |
| hash.AddASN1ObjectIdentifier(oidSHA256) |
| hash.AddASN1NULL() |
| }) |
| }) |
| params.AddASN1(cbasn1.Tag(2).Constructed().ContextSpecific(), func(saltLen *cryptobyte.Builder) { |
| saltLen.AddASN1Uint64(32) |
| }) |
| }) |
| }) |
| spki.AddASN1BitString(x509.MarshalPKCS1PublicKey(key)) |
| }) |
| } |
| |
| type X509Info struct { |
| PrivateKey crypto.Signer |
| Name pkix.Name |
| DNSNames []string |
| IsCA bool |
| SubjectKeyID []byte |
| KeyUsage x509.KeyUsage |
| SignatureAlgorithm X509SignatureAlgorithm |
| // EncodeSPKIAsRSAPSS, if true, causes the subjectPublicKeyInfo field to be |
| // encoded as id-RSASSA-PSS with SHA-256 parameters, instead of |
| // id-rsaEncryption. This is sufficient for our purposes because we do not |
| // need real id-RSASSA-PSS support in the test runner. If we ever to, we |
| // can replace this with a real PSSPrivateKey type. |
| EncodeSPKIAsRSAPSS bool |
| } |
| |
| type X509ChainBuilder struct { |
| privateKey crypto.Signer |
| name pkix.Name |
| subjectKeyID []byte |
| rootCert []byte |
| rootPath string |
| chain [][]byte |
| } |
| |
| func x509ChainBuilderFromInfo(info X509Info) *X509ChainBuilder { |
| return &X509ChainBuilder{ |
| privateKey: info.PrivateKey, |
| name: info.Name, |
| subjectKeyID: info.SubjectKeyID, |
| } |
| } |
| |
| func NewX509Root(root X509Info) *X509ChainBuilder { |
| ret := x509ChainBuilderFromInfo(root).Issue(root) |
| ret.rootCert = ret.chain[0] |
| ret.rootPath = writeTempCertFile([][]byte{ret.rootCert}) |
| ret.chain = nil |
| return ret |
| } |
| |
| func (issuer *X509ChainBuilder) Issue(subject X509Info) *X509ChainBuilder { |
| serial := lastSerial.Add(1) |
| |
| sigAlg := subject.SignatureAlgorithm |
| if sigAlg == X509SignDefault { |
| var err error |
| sigAlg, err = chooseDefaultX509SignatureAlgorithm(issuer.privateKey) |
| if err != nil { |
| panic(err) |
| } |
| } |
| |
| notBefore := time.Now().Add(-time.Hour) |
| notAfter := time.Now().Add(time.Hour) |
| |
| bb := cryptobyte.NewBuilder(nil) |
| bb.AddASN1(cbasn1.SEQUENCE, func(tbs *cryptobyte.Builder) { |
| tbs.AddASN1(cbasn1.Tag(0).Constructed().ContextSpecific(), func(vers *cryptobyte.Builder) { |
| vers.AddASN1Uint64(2) // v3 |
| }) |
| tbs.AddASN1Uint64(serial) |
| tbs.AddValue(sigAlg) |
| issuerDER, err := asn1.Marshal(issuer.name.ToRDNSequence()) |
| if err != nil { |
| tbs.SetError(err) |
| return |
| } |
| tbs.AddBytes(issuerDER) |
| tbs.AddASN1(cbasn1.SEQUENCE, func(val *cryptobyte.Builder) { |
| addX509Time(val, notBefore) |
| addX509Time(val, notAfter) |
| }) |
| subjectDER, err := asn1.Marshal(subject.Name.ToRDNSequence()) |
| if err != nil { |
| tbs.SetError(err) |
| return |
| } |
| tbs.AddBytes(subjectDER) |
| if subject.EncodeSPKIAsRSAPSS { |
| addRSAPSSSubjectPublicKeyInfo(tbs, subject.PrivateKey.Public().(*rsa.PublicKey)) |
| } else { |
| spki, err := x509.MarshalPKIXPublicKey(subject.PrivateKey.Public()) |
| if err != nil { |
| tbs.SetError(err) |
| return |
| } |
| tbs.AddBytes(spki) |
| } |
| addASN1ExplicitTag(tbs, 3, cbasn1.SEQUENCE, func(exts *cryptobyte.Builder) { |
| if len(issuer.subjectKeyID) != 0 { |
| exts.AddASN1(cbasn1.SEQUENCE, func(ext *cryptobyte.Builder) { |
| ext.AddASN1ObjectIdentifier(oidAuthorityKeyID) |
| ext.AddASN1(cbasn1.OCTET_STRING, func(extVal *cryptobyte.Builder) { |
| extVal.AddASN1(cbasn1.SEQUENCE, func(akid *cryptobyte.Builder) { |
| addASN1ImplicitString(akid, cbasn1.Tag(0).ContextSpecific(), issuer.subjectKeyID) |
| }) |
| }) |
| }) |
| } |
| |
| if subject.KeyUsage != 0 { |
| exts.AddASN1(cbasn1.SEQUENCE, func(ext *cryptobyte.Builder) { |
| ext.AddASN1ObjectIdentifier(oidKeyUsage) |
| ext.AddASN1Boolean(true) // critical |
| ext.AddASN1(cbasn1.OCTET_STRING, func(extVal *cryptobyte.Builder) { |
| var b [2]byte |
| // DER orders the bits from most to least significant. |
| b[0] = bits.Reverse8(byte(subject.KeyUsage)) |
| b[1] = bits.Reverse8(byte(subject.KeyUsage >> 8)) |
| // If the final byte is all zeros, skip it. |
| var ku asn1.BitString |
| if b[1] == 0 { |
| ku.Bytes = b[:1] |
| } else { |
| ku.Bytes = b[:] |
| } |
| ku.BitLength = bits.Len16(uint16(subject.KeyUsage)) |
| der, err := asn1.Marshal(ku) |
| if err != nil { |
| extVal.SetError(err) |
| } else { |
| extVal.AddBytes(der) |
| } |
| }) |
| }) |
| } |
| |
| if len(subject.DNSNames) != 0 { |
| exts.AddASN1(cbasn1.SEQUENCE, func(ext *cryptobyte.Builder) { |
| ext.AddASN1ObjectIdentifier(oidSubjectAltName) |
| ext.AddASN1(cbasn1.OCTET_STRING, func(extVal *cryptobyte.Builder) { |
| extVal.AddASN1(cbasn1.SEQUENCE, func(names *cryptobyte.Builder) { |
| for _, dns := range subject.DNSNames { |
| addASN1ImplicitString(names, cbasn1.Tag(2).ContextSpecific(), []byte(dns)) |
| } |
| }) |
| }) |
| }) |
| } |
| |
| if subject.IsCA { |
| exts.AddASN1(cbasn1.SEQUENCE, func(ext *cryptobyte.Builder) { |
| ext.AddASN1ObjectIdentifier(oidBasicConstraints) |
| ext.AddASN1Boolean(true) // critical |
| ext.AddASN1(cbasn1.OCTET_STRING, func(extVal *cryptobyte.Builder) { |
| extVal.AddASN1(cbasn1.SEQUENCE, func(bcons *cryptobyte.Builder) { |
| bcons.AddASN1Boolean(true) |
| }) |
| }) |
| }) |
| } |
| |
| if len(subject.SubjectKeyID) != 0 { |
| exts.AddASN1(cbasn1.SEQUENCE, func(ext *cryptobyte.Builder) { |
| ext.AddASN1ObjectIdentifier(oidSubjectKeyID) |
| ext.AddASN1(cbasn1.OCTET_STRING, func(extVal *cryptobyte.Builder) { |
| extVal.AddASN1OctetString(subject.SubjectKeyID) |
| }) |
| }) |
| } |
| }) |
| }) |
| |
| tbs := bb.BytesOrPanic() |
| sig, err := signX509(issuer.privateKey, sigAlg, tbs) |
| if err != nil { |
| panic(err) |
| } |
| |
| bb = cryptobyte.NewBuilder(nil) |
| bb.AddASN1(cbasn1.SEQUENCE, func(cert *cryptobyte.Builder) { |
| cert.AddBytes(tbs) |
| cert.AddValue(sigAlg) |
| cert.AddASN1BitString(sig) |
| }) |
| cert := bb.BytesOrPanic() |
| |
| ret := x509ChainBuilderFromInfo(subject) |
| ret.rootCert = issuer.rootCert |
| ret.rootPath = issuer.rootPath |
| ret.chain = make([][]byte, len(issuer.chain)+1) |
| copy(ret.chain, issuer.chain) |
| ret.chain[len(ret.chain)-1] = cert |
| return ret |
| } |
| |
| func (b *X509ChainBuilder) ToCredential() Credential { |
| return Credential{ |
| Certificate: b.chain, |
| ChainPath: writeTempCertFile(b.chain), |
| PrivateKey: b.privateKey, |
| KeyPath: writeTempKeyFile(b.privateKey), |
| RootCertificate: b.rootCert, |
| RootPath: b.rootPath, |
| } |
| } |
| |
| func writeTempCertFile(certs [][]byte) string { |
| f, err := os.CreateTemp(tmpDir, "test-cert") |
| if err != nil { |
| panic(fmt.Sprintf("failed to create temp file: %s", err)) |
| } |
| for _, cert := range certs { |
| if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil { |
| panic(fmt.Sprintf("failed to write test certificate: %s", err)) |
| } |
| } |
| tmpCertPath := f.Name() |
| if err := f.Close(); err != nil { |
| panic(fmt.Sprintf("failed to close test certificate temp file: %s", err)) |
| } |
| return tmpCertPath |
| } |
| |
| func writeTempKeyFile(privKey crypto.PrivateKey) string { |
| f, err := os.CreateTemp(tmpDir, "test-key") |
| if err != nil { |
| panic(fmt.Sprintf("failed to create temp file: %s", err)) |
| } |
| keyDER, err := x509.MarshalPKCS8PrivateKey(privKey) |
| if err != nil { |
| panic(fmt.Sprintf("failed to marshal test key: %s", err)) |
| } |
| if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}); err != nil { |
| panic(fmt.Sprintf("failed to write test key: %s", err)) |
| } |
| tmpKeyPath := f.Name() |
| if err := f.Close(); err != nil { |
| panic(fmt.Sprintf("failed to close test key temp file: %s", err)) |
| } |
| return tmpKeyPath |
| } |