blob: 758fb34c60fd719db427af2c585b5617db371151 [file] [log] [blame]
// 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
}