Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions api/client/joinservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,20 @@ func (c *JoinServiceClient) RegisterUsingOracleMethod(
return certs, nil
}

// BoundKeypairRegistrationResponse is the response on a successful registration attempt.
type BoundKeypairRegistrationResponse struct {
// Certs is the generated certificate bundle.
Certs *proto.Certs

// BoundPublicKey is the public key bound at the completion of the joining
// process, in ssh authorized_hosts format.
BoundPublicKey string

// JoinState is a compact serialized JWT containing join state, to be stored
// by the client and verified on subsequent join attempts.
JoinState []byte
}

// RegisterUsingBoundKeypairMethod attempts to register the caller using
// bound-keypair join method. If successful, the public key registered with auth
// and a certificate bundle is returned, or an error. Clients must provide a
Expand All @@ -277,13 +291,13 @@ func (c *JoinServiceClient) RegisterUsingBoundKeypairMethod(
ctx context.Context,
initReq *proto.RegisterUsingBoundKeypairInitialRequest,
challengeFunc RegisterUsingBoundKeypairChallengeResponseFunc,
) (*proto.Certs, string, error) {
) (*BoundKeypairRegistrationResponse, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

stream, err := c.grpcClient.RegisterUsingBoundKeypairMethod(ctx)
if err != nil {
return nil, "", trace.Wrap(err)
return nil, trace.Wrap(err)
}
defer stream.CloseSend()

Expand All @@ -293,7 +307,7 @@ func (c *JoinServiceClient) RegisterUsingBoundKeypairMethod(
},
})
if err != nil {
return nil, "", trace.Wrap(err, "sending initial request")
return nil, trace.Wrap(err, "sending initial request")
}

// Unlike other methods, the server may send multiple challenges,
Expand All @@ -304,15 +318,15 @@ func (c *JoinServiceClient) RegisterUsingBoundKeypairMethod(
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, "", trace.Wrap(err, "receiving intermediate bound keypair join response")
return nil, trace.Wrap(err, "receiving intermediate bound keypair join response")
}

switch kind := res.GetResponse().(type) {
case *proto.RegisterUsingBoundKeypairMethodResponse_Certs:
// If we get certs, we're done, so just return the result.
certs := kind.Certs.GetCerts()
if certs == nil {
return nil, "", trace.BadParameter("expected Certs, got %T", kind.Certs.Certs)
return nil, trace.BadParameter("expected Certs, got %T", kind.Certs.Certs)
}

// If we receive a cert bundle, we can return early. Even if we
Expand All @@ -322,23 +336,27 @@ func (c *JoinServiceClient) RegisterUsingBoundKeypairMethod(
// raise an error if rotation fails or is otherwise skipped or not
// allowed.

return certs, kind.Certs.GetPublicKey(), nil
return &BoundKeypairRegistrationResponse{
Certs: certs,
BoundPublicKey: kind.Certs.GetPublicKey(),
JoinState: kind.Certs.JoinState,
}, nil
default:
// Forward all other responses to the challenge handler.
nextRequest, err := challengeFunc(res)
if err != nil {
return nil, "", trace.Wrap(err, "solving challenge")
return nil, trace.Wrap(err, "solving challenge")
}

if err := stream.Send(nextRequest); err != nil {
return nil, "", trace.Wrap(err, "sending solution")
return nil, trace.Wrap(err, "sending solution")
}
}
}

// Ideally the server will emit a proper error instead of just hanging up on
// us.
return nil, "", trace.AccessDenied("server declined to send certs during bound-keypair join attempt")
return nil, trace.AccessDenied("server declined to send certs during bound-keypair join attempt")
}

// RegisterUsingToken registers the caller using a token and returns signed
Expand Down
6 changes: 5 additions & 1 deletion api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6197,7 +6197,7 @@ message RegisterUsingTokenRequest {
(gogoproto.jsontag) = "expires,omitempty"
];
// BotInstanceID is a trusted instance identifier for a Machine ID bot,
// provided when rejoining. This parameters may only be provided by the join
// provided when rejoining. This parameter may only be provided by the join
// service and is ignored otherwise; bots should otherwise rejoin with their
// existing client certificate to prove their instance identity.
string BotInstanceID = 13 [(gogoproto.jsontag) = "bot_instance_id"];
Expand All @@ -6206,6 +6206,10 @@ message RegisterUsingTokenRequest {
// join method. Rejoining bots supply this value via a client certificate
// extension; it is ignored from other sources.
int32 BotGeneration = 14 [(gogoproto.jsontag) = "bot_generation"];
// PreviousBotInstanceID is a trusted previous instance identifier for a
// Machine ID bot. This parameter may only be set internally during certain
// join processes and is ignored otherwise.
string PreviousBotInstanceID = 15 [(gogoproto.jsontag) = "previous_bot_instance_id"];
}

// RecoveryCodes holds a user's recovery code information. Recovery codes allows users to regain
Expand Down
14 changes: 14 additions & 0 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,9 @@ func (a *ProvisionTokenSpecV2AzureDevops) checkAndSetDefaults() error {
}

func (a *ProvisionTokenSpecV2BoundKeypair) checkAndSetDefaults() error {
// Note: don't attempt to initialize onboarding - at least for now - as it
// has required keys. This behavior may be relaxed when we add
// server-generated joining secrets.
if a.Onboarding == nil {
return trace.BadParameter("spec.bound_keypair.onboarding is required")
}
Expand All @@ -1061,5 +1064,16 @@ func (a *ProvisionTokenSpecV2BoundKeypair) checkAndSetDefaults() error {
"initial_public_key] is required in spec.bound_keypair.onboarding")
}

if a.Recovery == nil {
a.Recovery = &ProvisionTokenSpecV2BoundKeypair_RecoverySpec{}
}

if a.Recovery.Limit == 0 {
a.Recovery.Limit = 1
}

// Note: Recovery.Mode will be interpreted at joining time; it's zero value
// ("") is mapped to RecoveryModeStandard.

return nil
}
4 changes: 4 additions & 0 deletions api/types/provisioning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,10 @@ func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
Onboarding: &ProvisionTokenSpecV2BoundKeypair_OnboardingSpec{
InitialPublicKey: "asdf",
},
Recovery: &ProvisionTokenSpecV2BoundKeypair_RecoverySpec{
Limit: 1,
Mode: "",
},
},
},
},
Expand Down
6 changes: 6 additions & 0 deletions api/types/trust.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const (
// OktaCA identifies the certificate authority that will be used by the
// integration with Okta.
OktaCA CertAuthType = "okta"
// BoundKeypairCA identifies the CA used to sign bound keypair client state
// documents.
BoundKeypairCA CertAuthType = "bound_keypair"
)

// CertAuthTypes lists all certificate authority types.
Expand All @@ -78,6 +81,7 @@ var CertAuthTypes = []CertAuthType{
OIDCIdPCA,
SPIFFECA,
OktaCA,
BoundKeypairCA,
}

// NewlyAdded should return true for CA types that were added in the current
Expand All @@ -102,6 +106,8 @@ func (c CertAuthType) addedInMajorVer() int64 {
return 15
case OktaCA:
return 17
case BoundKeypairCA:
return 17
Comment on lines +109 to +110
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constant is 18 for branch/v18, and adjusted to 17 for this branch. See discussion for context: https://gravitational.slack.com/archives/C0DF0TPMY/p1755281229605639

default:
// We don't care about other CAs added before v4.0.0
return 4
Expand Down
Loading
Loading