orchard/internal/bootstraptoken/bootstraptoken.go

154 lines
4.0 KiB
Go

package bootstraptoken
import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"strings"
)
var (
ErrFailedToCreateBootstrapToken = errors.New("failed to create bootstrap token")
ErrInvalidBootstrapTokenFormat = errors.New("invalid bootstrap token format")
encoding = base64.RawURLEncoding
)
const (
versionPrefix = "orchard-bootstrap-token-v"
version = 0
)
type BootstrapToken struct {
version int
certificate *x509.Certificate
rawCertificate []byte
serviceAccountName string
serviceAccountToken string
}
func New(rawCertificate []byte, serviceAccountName string, serviceAccountToken string) (*BootstrapToken, error) {
if serviceAccountName == "" {
return nil, fmt.Errorf("%w: empty service account name", ErrFailedToCreateBootstrapToken)
}
if serviceAccountToken == "" {
return nil, fmt.Errorf("%w: empty service account token", ErrFailedToCreateBootstrapToken)
}
// Optionally parse a certificate
var certificate *x509.Certificate
var err error
if len(rawCertificate) != 0 {
block, _ := pem.Decode(rawCertificate)
if block == nil {
return nil, fmt.Errorf("%w: failed to parse certificate: expected a PEM format",
ErrFailedToCreateBootstrapToken)
}
certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("%w: failed to parse certificate: %v",
ErrFailedToCreateBootstrapToken, err)
}
}
return &BootstrapToken{
version: version,
certificate: certificate,
serviceAccountName: serviceAccountName,
serviceAccountToken: serviceAccountToken,
rawCertificate: rawCertificate,
}, nil
}
func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) {
splits := strings.Split(rawBootstrapToken, ".")
currentVersionString := fmt.Sprintf("%s%d", versionPrefix, version)
if splits[0] != currentVersionString {
return nil, fmt.Errorf("%w: invalid version string or unsupported version",
ErrInvalidBootstrapTokenFormat)
}
if len(splits) < 3 {
return nil, fmt.Errorf("%w: missing service account credentials", ErrInvalidBootstrapTokenFormat)
}
if len(splits) > 4 {
return nil, fmt.Errorf("%w: extraneous data", ErrInvalidBootstrapTokenFormat)
}
serviceAccountName, err := encoding.DecodeString(splits[1])
if err != nil {
return nil, fmt.Errorf("%w: failed to decode service account name: %v",
ErrInvalidBootstrapTokenFormat, err)
}
serviceAccountToken, err := encoding.DecodeString(splits[2])
if err != nil {
return nil, fmt.Errorf("%w: failed to decode service account token: %v",
ErrInvalidBootstrapTokenFormat, err)
}
// Optionally parse the certificate
var certificate *x509.Certificate
var rawCertificate []byte
if len(splits) == 4 {
rawCertificate, err = encoding.DecodeString(splits[3])
if err != nil {
return nil, fmt.Errorf("%w: failed to decode certificate: %v",
ErrInvalidBootstrapTokenFormat, err)
}
block, _ := pem.Decode(rawCertificate)
certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("%w: failed to parse certificate: %v",
ErrFailedToCreateBootstrapToken, err)
}
}
return &BootstrapToken{
version: version,
certificate: certificate,
rawCertificate: rawCertificate,
serviceAccountName: string(serviceAccountName),
serviceAccountToken: string(serviceAccountToken),
}, nil
}
func (bt *BootstrapToken) String() string {
var certificatePart string
// Certificate is optional
if len(bt.rawCertificate) != 0 {
certificatePart = fmt.Sprintf(".%s", encoding.EncodeToString(bt.rawCertificate))
}
return fmt.Sprintf("%s%d.%s.%s%s",
versionPrefix,
version,
encoding.EncodeToString([]byte(bt.serviceAccountName)),
encoding.EncodeToString([]byte(bt.serviceAccountToken)),
certificatePart,
)
}
func (bt *BootstrapToken) ServiceAccountName() string {
return bt.serviceAccountName
}
func (bt *BootstrapToken) ServiceAccountToken() string {
return bt.serviceAccountToken
}
func (bt *BootstrapToken) Certificate() *x509.Certificate {
return bt.certificate
}