mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 11:06:17 +00:00
3b5164032a
A RustFS cluster device needs a durable identity before it can exchange a one-time registration token for a certificate. This adds the device-side half of that exchange, which rustfs/connect already verifies. `connect::identity` builds the canonical registration transcript frozen by protocol/agent/v1/registration-proof.md, signs it as low-S ES256, and emits the PKCS#10 certificate request Connect consumes for its SubjectPublicKeyInfo. `connect::identity_store` seals the P-256 key at mode 0600 and publishes it through a no-clobber link, so a retry or a concurrent start returns the original identity rather than minting a second one, and a corrupt or widened key is refused rather than silently replaced. The protocol fixture set is copied here byte-identically because fixture-sets.json names this repository as the consumer copy; the tests verify it against its own manifests and cross-verify Connect-produced ECDSA proofs against transcripts rebuilt locally. Nothing starts a task or touches the S3 data path: an unenrolled deployment generates no key and holds no identity.
112 lines
4.9 KiB
JSON
112 lines
4.9 KiB
JSON
{
|
|
"protocolVersion": "v1",
|
|
"fixtureSet": "offline-enrollment",
|
|
"fixture": "error-codes",
|
|
"description": "Frozen ErrorInfo reasons for offline enrollment. Clients branch on status and reason, never on message.",
|
|
"domain": "rustfs.connect",
|
|
"detailType": "type.googleapis.com/google.rpc.ErrorInfo",
|
|
"disclosureRules": [
|
|
"A rejection never reveals whether a presented key or challenge belongs to another tenant.",
|
|
"A rejection never contains key material, signature octets, nonces, or document bytes.",
|
|
"A rejection never reports which of several failed checks failed first beyond the single frozen reason."
|
|
],
|
|
"reasons": [
|
|
{
|
|
"reason": "UNSUPPORTED_PROTOCOL",
|
|
"httpStatus": 400,
|
|
"status": "INVALID_ARGUMENT",
|
|
"meaning": "The protocolVersion is missing, malformed, or names an unsupported major version. Identical to the rule frozen in protocol/agent/v1/authentication.md."
|
|
},
|
|
{
|
|
"reason": "UNSUPPORTED_FORMAT",
|
|
"httpStatus": 400,
|
|
"status": "INVALID_ARGUMENT",
|
|
"meaning": "The formatVersion is not one of the frozen supported format versions."
|
|
},
|
|
{
|
|
"reason": "SIGNATURE_MALFORMED",
|
|
"httpStatus": 400,
|
|
"status": "INVALID_ARGUMENT",
|
|
"meaning": "The signature is not 64 octets of fixed-width r||s in unpadded base64url, or r or s is out of range."
|
|
},
|
|
{
|
|
"reason": "SIGNATURE_NOT_CANONICAL",
|
|
"httpStatus": 400,
|
|
"status": "INVALID_ARGUMENT",
|
|
"meaning": "The signature is well formed and verifies, but its s exceeds half the group order. Only the low-S form is accepted."
|
|
},
|
|
{
|
|
"reason": "SIGNATURE_INVALID",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "ECDSA verification over the received octets failed. The document was altered, or it was signed by another key."
|
|
},
|
|
{
|
|
"reason": "ENROLLMENT_ROOT_UNKNOWN",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "The first trust link is issued by a key that is not pinned in this build. There is no path from this to acceptance: the root is never learned."
|
|
},
|
|
{
|
|
"reason": "TRUST_CHAIN_INVALID",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "A trust link failed its own signature check, named the wrong issuer, carried an unknown role, or was outside its validity at the challenge issuedAt."
|
|
},
|
|
{
|
|
"reason": "CONNECT_KEY_UNCHAINED",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "The challenge connectKeyId is not the subject of the last trust link, so nothing under the pinned root vouches for the signing key."
|
|
},
|
|
{
|
|
"reason": "CHALLENGE_UNKNOWN",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "Connect has no issued challenge with this challengeId."
|
|
},
|
|
{
|
|
"reason": "CHALLENGE_NOT_YET_VALID",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "The evaluation time is more than the skew tolerance before issuedAt."
|
|
},
|
|
{
|
|
"reason": "CHALLENGE_EXPIRED",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "The evaluation time is more than the skew tolerance after expiresAt."
|
|
},
|
|
{
|
|
"reason": "CHALLENGE_PROOF_INVALID",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "The response nonce or challengeProof is not the one Connect issued for this challenge."
|
|
},
|
|
{
|
|
"reason": "DEVICE_PROOF_INVALID",
|
|
"httpStatus": 401,
|
|
"status": "UNAUTHENTICATED",
|
|
"meaning": "The response signature does not verify under the devicePublicKey it presents, or deviceKeyId is not that key fingerprint. Proof of possession failed."
|
|
},
|
|
{
|
|
"reason": "ENROLLMENT_REPLAYED",
|
|
"httpStatus": 409,
|
|
"status": "ABORTED",
|
|
"meaning": "The challenge was already consumed. A challenge is single use even when the replayed response is byte identical."
|
|
},
|
|
{
|
|
"reason": "ORGANIZATION_MISMATCH",
|
|
"httpStatus": 403,
|
|
"status": "PERMISSION_DENIED",
|
|
"meaning": "The response names a different organization than the challenge it answers."
|
|
},
|
|
{
|
|
"reason": "CLUSTER_MISMATCH",
|
|
"httpStatus": 403,
|
|
"status": "PERMISSION_DENIED",
|
|
"meaning": "The response names a different cluster than the challenge it answers."
|
|
}
|
|
]
|
|
}
|