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 }