Files
rustfs/protocol/agent/v1/fixtures/offline-enrollment/error-codes.json
T
overtrue 3b5164032a feat(connect): add device identity store and registration proof
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.
2026-08-19 12:54:50 +08:00

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."
}
]
}