source: advance the checked projection

Source-Sha: 194a59adfd6c901635da7a9c18253bb2210e2426
Policy-Sha: b9b0be0ee74e55f561a803b8aef6be3c2134f5a83b46816a069a372f2eb04f4d
Tree-Digest: 4697f2e281c1c6ecafff17cf6eeb391a0a1ac0fa6e5fc429f5b473f4579c252c
This commit is contained in:
Source publisher 2026-09-21 14:39:08 +00:00
commit ffdccba10f
42 changed files with 2516 additions and 554 deletions

View file

@ -29,35 +29,35 @@ type RegistrationTokenQueries struct {
}
type RegistrationToken struct {
ID uuid.UUID `json:"id" db:"id"`
TokenHash string `json:"-" db:"token_hash"`
ID uuid.UUID `json:"id" db:"id"`
TokenHash string `json:"-" db:"token_hash"`
// TokenEncrypted is the AES-256-GCM ciphertext (nonce||ct) of the plaintext
// token. Never serialised; decrypted into Token for live tokens only.
TokenEncrypted []byte `json:"-" db:"token_encrypted"`
TokenEncrypted []byte `json:"-" db:"token_encrypted"`
// Token is the decrypted plaintext, populated only for live tokens so the
// operator UI can rebuild the install one-liner. Empty for spent/expired/
// revoked tokens and for legacy one-way-hashed tokens.
Token string `json:"token,omitempty" db:"-"`
Label *string `json:"label" db:"label"`
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UsedAt *time.Time `json:"used_at" db:"used_at"`
UsedByAgentID *uuid.UUID `json:"used_by_agent_id" db:"used_by_agent_id"`
Revoked bool `json:"revoked" db:"revoked"`
RevokedAt *time.Time `json:"revoked_at" db:"revoked_at"`
RevokedReason *string `json:"revoked_reason" db:"revoked_reason"`
Status string `json:"status" db:"status"`
CreatedBy string `json:"created_by" db:"created_by"`
Metadata json.RawMessage `json:"metadata" db:"metadata"`
MaxSeats int `json:"max_seats" db:"max_seats"`
SeatsUsed int `json:"seats_used" db:"seats_used"`
Token string `json:"token,omitempty" db:"-"`
Label *string `json:"label" db:"label"`
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UsedAt *time.Time `json:"used_at" db:"used_at"`
UsedByAgentID *uuid.UUID `json:"used_by_agent_id" db:"used_by_agent_id"`
Revoked bool `json:"revoked" db:"revoked"`
RevokedAt *time.Time `json:"revoked_at" db:"revoked_at"`
RevokedReason *string `json:"revoked_reason" db:"revoked_reason"`
Status string `json:"status" db:"status"`
CreatedBy string `json:"created_by" db:"created_by"`
Metadata json.RawMessage `json:"metadata" db:"metadata"`
MaxSeats int `json:"max_seats" db:"max_seats"`
SeatsUsed int `json:"seats_used" db:"seats_used"`
// TOTPSeedEncrypted is the TOTP seed for fleet-join 2FA (SEC-025), stored
// as AES-256-GCM ciphertext (nonce||ct) of the base32 seed. Nil for
// standard registration tokens; set only when the operator creates a
// fleet-join token that requires the standalone host to prove possession
// of the seed via a TOTP code. Never serialised; decrypted only at join
// validation. The join request carries the code, never the seed.
TOTPSeedEncrypted []byte `json:"-" db:"totp_seed_encrypted"`
TOTPSeedEncrypted []byte `json:"-" db:"totp_seed_encrypted"`
}
// isLive reports whether a token is still usable for enrollment — the only state
@ -81,17 +81,17 @@ func (q *RegistrationTokenQueries) revealPlaintext(t *RegistrationToken) {
}
type TokenRequest struct {
Label string `json:"label"`
ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d"
MaxSeats int `json:"max_seats"` // Number of agents that can use this token (default: 1)
Metadata map[string]interface{} `json:"metadata"`
Label string `json:"label"`
ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d"
MaxSeats int `json:"max_seats"` // Number of agents that can use this token (default: 1)
Metadata map[string]interface{} `json:"metadata"`
}
type TokenResponse struct {
Token string `json:"token"`
Label string `json:"label"`
ExpiresAt time.Time `json:"expires_at"`
InstallCommand string `json:"install_command"`
Token string `json:"token"`
Label string `json:"label"`
ExpiresAt time.Time `json:"expires_at"`
InstallCommand string `json:"install_command"`
}
// NewRegistrationTokenQueries builds the query handle. encKeyB64 is the base64
@ -205,6 +205,45 @@ func (q *RegistrationTokenQueries) ValidateRegistrationToken(token string) (*Reg
return &regToken, nil
}
// GetFleetJoinToken resolves the raw join secret even after its one seat was
// consumed. The fleet-join handler uses that spent state only to recover the
// same Agent ID after a response was lost; standard enrollment never calls it.
func (q *RegistrationTokenQueries) GetFleetJoinToken(token string) (*RegistrationToken, error) {
var regToken RegistrationToken
query := `
SELECT id, token_hash, label, expires_at, created_at, used_at, used_by_agent_id,
revoked, revoked_at, revoked_reason, status, created_by, metadata,
max_seats, seats_used, totp_seed_encrypted
FROM registration_tokens
WHERE token_hash = $1
AND status IN ('active', 'used')
AND NOT revoked
AND expires_at > NOW()
AND totp_seed_encrypted IS NOT NULL
`
if err := q.db.Get(&regToken, query, HashRegistrationToken(token)); err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("invalid, expired, or revoked fleet join token")
}
return nil, fmt.Errorf("failed to resolve fleet join token: %w", err)
}
return &regToken, nil
}
func (q *RegistrationTokenQueries) FleetJoinTokenUsedBy(tokenID, agentID uuid.UUID) (bool, error) {
var used bool
err := q.db.Get(&used, `
SELECT EXISTS (
SELECT 1 FROM registration_token_usage
WHERE token_id = $1 AND agent_id = $2
)
`, tokenID, agentID)
if err != nil {
return false, fmt.Errorf("failed to resolve fleet join token use: %w", err)
}
return used, nil
}
// MarkTokenUsed marks a token as used by an agent.
// The caller passes the plaintext token; this function hashes it before calling
// the stored procedure (which now matches on token_hash).
@ -448,4 +487,4 @@ func (q *RegistrationTokenQueries) GetTokenUsageStats() (map[string]int, error)
}
return stats, nil
}
}