Skip to content

Commit

Permalink
Support creating assertions from existing X509 certs and TLS servers
Browse files Browse the repository at this point in the history
  • Loading branch information
bwesterb committed Feb 20, 2025
1 parent c1070bb commit ef9831b
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 60 deletions.
205 changes: 145 additions & 60 deletions cmd/mtc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/bwesterb/mtc"
"github.com/bwesterb/mtc/ca"
"github.com/bwesterb/mtc/umbilical"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/cryptobyte"

Expand Down Expand Up @@ -88,6 +89,19 @@ func assertionFlags(inFile bool) []cli.Flag {
Category: "Assertion",
Usage: "Only proceed if assertion matches checksum",
},

&cli.StringFlag{
Name: "from-x509-pem",
Category: "Assertion",
Aliases: []string{"x"},
Usage: "Suggest assertion from X509 PEM encoded certificate (chain)",
},
&cli.StringFlag{
Name: "from-x509-server",
Category: "Assertion",
Aliases: []string{"X"},
Usage: "Suggest assertion for TLS server with existing X509 chain",
},
}
if inFile {
ret = append(
Expand Down Expand Up @@ -149,6 +163,8 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) {
"ip6",
"tls-der",
"tls-pem",
"from-x509-server",
"from-x509-pem",
} {
if cc.IsSet(flag) {
return nil, fmt.Errorf(
Expand All @@ -174,84 +190,153 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) {
}, nil
}

cs := mtc.Claims{
DNS: cc.StringSlice("dns"),
DNSWildcard: cc.StringSlice("dns-wildcard"),
}
var (
a mtc.Assertion
scheme mtc.SignatureScheme
)

for _, ip := range cc.StringSlice("ip4") {
cs.IPv4 = append(cs.IPv4, net.ParseIP(ip))
if cc.IsSet("tls-scheme") {
scheme = mtc.SignatureSchemeFromString(cc.String("tls-scheme"))
if scheme == 0 {
return nil, fmt.Errorf("Unknown TLS signature scheme: %s", scheme)
}
}

for _, ip := range cc.StringSlice("ip6") {
cs.IPv6 = append(cs.IPv6, net.ParseIP(ip))
}
if cc.IsSet("from-x509-pem") || cc.IsSet("from-x509-server") {
var certs []*x509.Certificate

if (cc.String("tls-pem") == "" &&
cc.String("tls-der") == "") ||
(cc.String("tls-pem") != "" &&
cc.String("tls-der") != "") {
return nil, errors.New("Expect either tls-pem or tls-der flag")
}
if cc.IsSet("from-x509-server") {
tlsAddr := cc.String("from-x509-server")
certs, err = umbilical.GetChainFromTLSServer(tlsAddr)
if err != nil {
return nil, fmt.Errorf("from-x509-server: %v", err)
}
}

usingPem := false
subjectPath := cc.String("tls-der")
if cc.String("tls-pem") != "" {
usingPem = true
subjectPath = cc.String("tls-pem")
}
if cc.IsSet("from-x509-pem") {
x509Path := cc.String("from-x509-pem")
x509Buf, err := os.ReadFile(x509Path)
if err != nil {
return nil, fmt.Errorf("reading x509 chain %s: %w", x509Path, err)
}

subjectBuf, err := os.ReadFile(subjectPath)
if err != nil {
return nil, fmt.Errorf("reading subject %s: %w", subjectPath, err)
}
rest := []byte(x509Buf)
for i := 0; true; i++ {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}

if usingPem {
block, _ := pem.Decode([]byte(subjectBuf))
if block == nil {
return nil, fmt.Errorf(
"reading subject %s: failed to parse PEM block",
subjectPath,
)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf(
"parsing x509 certificate %d in %s: %s",
i, x509Path, err,
)
}

certs = append(certs, cert)
}

if len(certs) == 0 {
return nil, fmt.Errorf(
"reading x509 chain %s: no (PEM encoded) certificates found",
x509Path,
)
}
}
subjectBuf = block.Bytes
}

pub, err := x509.ParsePKIXPublicKey(subjectBuf)
if err != nil {
return nil, fmt.Errorf("Parsing subject %s: %w", subjectPath, err)
a, err = umbilical.SuggestedAssertionFromX509(certs[0], scheme)
if err != nil {
return nil, fmt.Errorf("from-x509: %s", err)
}
}

var scheme mtc.SignatureScheme
if cc.String("tls-scheme") != "" {
scheme = mtc.SignatureSchemeFromString(cc.String("tls-scheme"))
if scheme == 0 {
return nil, fmt.Errorf("Unknown TLS signature scheme: %s", scheme)
// Setting any claim will overwrite those suggested by the
// X509 certificate.
if cc.IsSet("dns") || cc.IsSet("dns-wildcard") || cc.IsSet("ip4") ||
cc.IsSet("ip6") {

a.Claims = mtc.Claims{
DNS: cc.StringSlice("dns"),
DNSWildcard: cc.StringSlice("dns-wildcard"),
}
} else {
schemes := mtc.SignatureSchemesFor(pub)
if len(schemes) == 0 {
return nil, fmt.Errorf(
"No matching signature scheme for that public key",
)

for _, ip := range cc.StringSlice("ip4") {
a.Claims.IPv4 = append(a.Claims.IPv4, net.ParseIP(ip))
}
if len(schemes) >= 2 {
return nil, fmt.Errorf(
"Specify --tls-scheme with one of %s",
schemes,
)

for _, ip := range cc.StringSlice("ip6") {
a.Claims.IPv6 = append(a.Claims.IPv6, net.ParseIP(ip))
}
scheme = schemes[0]
}

subj, err := mtc.NewTLSSubject(scheme, pub)
if err != nil {
return nil, fmt.Errorf("creating subject: %w", err)
subjectFlagCount := 0
for _, flag := range []string{"tls-pem", "tls-der", "from-x509-pem",
"from-x509-server"} {
if cc.IsSet(flag) {
subjectFlagCount++
}
}
if subjectFlagCount != 1 {
return nil, errors.New(
"Expect exactly one of tls-pem, tls-der, from-x509-server," +
" or from-x509-pem flags",
)
}

a := mtc.Assertion{
Claims: cs,
Subject: subj,
if a.Subject == nil {
usingPem := false
subjectPath := cc.String("tls-der")
if cc.String("tls-pem") != "" {
usingPem = true
subjectPath = cc.String("tls-pem")
}

subjectBuf, err := os.ReadFile(subjectPath)
if err != nil {
return nil, fmt.Errorf("reading subject %s: %w", subjectPath, err)
}

if usingPem {
block, _ := pem.Decode([]byte(subjectBuf))
if block == nil {
return nil, fmt.Errorf(
"reading subject %s: failed to parse PEM block",
subjectPath,
)
}
subjectBuf = block.Bytes
}

pub, err := x509.ParsePKIXPublicKey(subjectBuf)
if err != nil {
return nil, fmt.Errorf("Parsing subject %s: %w", subjectPath, err)
}

if !cc.IsSet("tls-scheme") {
schemes := mtc.SignatureSchemesFor(pub)
if len(schemes) == 0 {
return nil, fmt.Errorf(
"No matching signature scheme for that public key",
)
}
if len(schemes) >= 2 {
return nil, fmt.Errorf(
"Specify --tls-scheme with one of %s",
schemes,
)
}
scheme = schemes[0]
}

subj, err := mtc.NewTLSSubject(scheme, pub)
if err != nil {
return nil, fmt.Errorf("creating subject: %w", err)
}

a.Subject = subj
}

return &ca.QueuedAssertion{
Expand Down
85 changes: 85 additions & 0 deletions umbilical/x509.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Package umbilical has the temporary logic to back an MTC with an existing
// X509 certificate chain.
package umbilical

import (
"github.com/bwesterb/mtc"

"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"sync"
)

// Suggests an Assertion from an existing X509 certificate.
//
// If non-zero, uses the given signature scheme.
func SuggestedAssertionFromX509(cert *x509.Certificate,
scheme mtc.SignatureScheme) (
a mtc.Assertion, err error) {
for _, name := range cert.DNSNames {
wildcard := false

if strings.HasPrefix(name, "*.") {
wildcard = true
name = name[2:]
}

if strings.Contains(name, "*") {
continue
}

if wildcard {
a.Claims.DNSWildcard = append(a.Claims.DNSWildcard, name)
} else {
a.Claims.DNS = append(a.Claims.DNS, name)
}
}

for _, ip := range cert.IPAddresses {
ip4 := ip.To4()
if ip4 != nil {
a.Claims.IPv4 = append(a.Claims.IPv4, ip4)
} else {
a.Claims.IPv6 = append(a.Claims.IPv6, ip)
}
}

if scheme == 0 {
schemes := mtc.SignatureSchemesFor(cert.PublicKey)
if len(schemes) == 0 {
err = errors.New("Unsupported public key type")
return
}
scheme = schemes[0]
}

a.Subject, err = mtc.NewTLSSubject(scheme, cert.PublicKey)

return
}

// Pulls certificate chain served by TLS server.
func GetChainFromTLSServer(addr string) (chain []*x509.Certificate, err error) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
conn, err2 := tls.Dial("tcp", addr, &tls.Config{
InsecureSkipVerify: true,
VerifyConnection: func(state tls.ConnectionState) error {
chain = state.PeerCertificates
return nil
},
})
if err2 != nil {
err = fmt.Errorf("tls.Dial(%s): %v", addr, err2)
} else {
conn.Close()
}
wg.Done()
}()
wg.Wait()
return
}

0 comments on commit ef9831b

Please sign in to comment.