mtpublisher: Replace the stub with a tlog-mirror client - #8973
mtpublisher: Replace the stub with a tlog-mirror client#8973beautifulentropy wants to merge 1 commit into
Conversation
|
@beautifulentropy, this PR appears to contain configuration and/or SQL schema changes. Please ensure that a corresponding deployment ticket has been filed with the new values. |
| @@ -25,25 +25,29 @@ type Config struct { | |||
|
|
|||
| DebugAddr string `validate:"omitempty,hostname_port"` | |||
|
|
|||
| // PollInterval is how often the stub scans for checkpoints that still | |||
| // lack a mirror cosignature. | |||
| // PollInterval is how often the publisher scans for checkpoints that | |||
| // still lack a mirror cosignature. | |||
| PollInterval config.Duration `validate:"required"` | |||
|
|
|||
| // LogID identifies the issuance log this publisher operates on. It must | |||
| // match the mtca's. | |||
| LogID issuancelog.ID `validate:"required"` | |||
|
|
|||
| // MirrorID identifies the cosigner this publisher writes alongside each | |||
| // cosignature (e.g. "32473.9"). | |||
| MirrorID string `validate:"required"` | |||
| // MTCAPublicKeyFile holds the PEM-encoded ML-DSA-44 public key the mtca | |||
There was a problem hiding this comment.
It seems like we could alternately take the CA certificate file here, which would save us from having to manage a separate artifact containing just the CA public key. Even though our CA certificates are unsigned, Go can parse them - it just can't verify them.
| // MirrorBaseURL is the base URL of the mirror's tlog-mirror submission | ||
| // endpoints (e.g. "http://localhost:4700"). | ||
| MirrorBaseURL string `validate:"required,url"` |
There was a problem hiding this comment.
Does this belong in cmd.MirrorConfig instead?
Also, I was wondering: does this need to include some CA-specific prefix also, e.g. the CA ID?
Turns out, no: there's just one submission prefix for a given mirror. The relevant submission endpoints (add-checkpoint, add-entries, sign-subtree) carry the issuance log's origin in the request body. https://github.com/C2SP/C2SP/blob/main/tlog-mirror.md#introduction
| } | ||
| var proof []tlog.Hash | ||
| if oldSize > 0 && oldSize < tree.N { | ||
| treeProof, err := m.src.consistencyProof(ctx, tree, oldSize) |
There was a problem hiding this comment.
This means we post with an empty consistency proof for two cases:
- empty issuance log.
- pushing a checkpoint at the same size the mirror already has.
For (1) - I think we'll never have that, right? We always initialize our trees with a null_entry and sign a tree size of 1.
For (2): I had to think about this a bit, but I think it makes sense. It's possible for all mirrors to be fully up-to-date with our latest checkpoint, but have no MirrorSignature in the database, for instance if we previously sync'ed those mirrors but failed to write to the database. Of course we don't want to wedge in such a case, so we push the current checkpoint even though it's "old news", then send an empty add-entries request, and get the mirror's note signature line in the response.
Perhaps that's worth a comment in the addCheckpoint and Cosign doccomments? For instance "Makes requests even when the mirror is up-to-date, in order to get a mirror cosignature."
There was a problem hiding this comment.
It's also possible for a mirror to become unexpectedly up-to-date because someone else is mirroring faster than we are.
| if retried { | ||
| return errors.New("mirror rejected the old size twice") | ||
| } | ||
| retried = true | ||
| oldSize, err = mirror.ParseSizeResponse(respBody) |
There was a problem hiding this comment.
Let's move the error return below ParseSizeResponse so we can include the parsed size in the error message. E.g.:
return errors.New("add-checkpoint at tree size %d got 409 with mirror tree size %d (after retry)",
...)
| return err | ||
| } | ||
| default: | ||
| return fmt.Errorf("mirror returned status %d: %s", status, strings.TrimSpace(string(respBody))) |
There was a problem hiding this comment.
Let's trim the size of the response body even more aggressively than maxMirrorResponseSize. I think we probably don't want more than, say, 400 bytes of the response for debugging purposes.
| if info.TreeSize != tree.N { | ||
| return nil, fmt.Errorf("mirror wants upload_end %d, checkpoint size is %d", info.TreeSize, tree.N) | ||
| } | ||
| start = info.NextEntry |
There was a problem hiding this comment.
Let's do some bounds checking on the NextEntry the mirror sent us. It should not be greater than or equal to the issuance log's tree size. It should not be less than zero.
| return nil, err | ||
| } | ||
| if info.TreeSize != tree.N { | ||
| return nil, fmt.Errorf("mirror wants upload_end %d, checkpoint size is %d", info.TreeSize, tree.N) |
There was a problem hiding this comment.
https://github.com/C2SP/C2SP/blob/main/tlog-mirror.md#add-entries
When sending a "409 Conflict" or "202 Accepted" response, the response body MUST have a Content-Type of text/x.tlog.mirror-info and consist of three lines, each followed by a newline (U+000A):
The tree size of a valid pending checkpoint, in decimal
...
If the client's upload_end value was valid, the first line SHOULD contain upload_end. This allows the client to resume an interrupted upload without recomputing subtree consistency proofs. Otherwise, the first line SHOULD be the tree size of the current pending checkpoint.After receiving a "409 Conflict" or "202 Accepted" response, the client SHOULD retry setting upload_end to the tree size, upload_start to the advertised next entry value, and the ticket to the received ticket.
The spec wants us to retry with an updated upload_end.
However, if we were to do that, the eventual "200 Success" response would give us note signature lines over a checkpoint at the wrong tree size (the new upload_end), not the one we asked for.
Since we're violating a SHOULD, let's comment. E.g. "Don't set upload_start to the tree size from the response, because we need upload_end to be the tree size of the checkpoint we are seeking a cosignature for. A cosignature on a different tree size won't do."
But I think we don't have to error out in this case, and probably shouldn't. The log's obligations with regards to this value are also only "SHOULD."
Perhaps in a future revision we want to implement "retry with updated upload_end to try to advance the log, but then return an error instead of a cosignature."
| } | ||
| proof, err := subtree.ConsistencyProof(p.SubtreeStart, p.End, tree.N, s.hashReaderForTree(ctx, tree)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("proving subtree [%d, %d): %s", p.SubtreeStart, p.End, err) |
There was a problem hiding this comment.
| return nil, fmt.Errorf("proving subtree [%d, %d): %s", p.SubtreeStart, p.End, err) | |
| return nil, fmt.Errorf("fetching subtree consistency proof [%d, %d), tree size %d: %s", p.SubtreeStart, p.End, tree.N, err) |
Rationale: make it clear that it was a fetch error, not a verification error. Also, a subtree consistency proof is a function of start, end, and tree size, so include the tree size in the error.
| `SELECT id, checkpoints.mtcLogID, mtcaSignature, mirrorID, | ||
| mirrorSignature, treeSize, rootHash | ||
| `SELECT id, checkpoints.mtcLogID, mtcaSignature, | ||
| COALESCE(mirrorID, '') AS mirrorID, |
There was a problem hiding this comment.
Ah, this is a nice trick, and makes #8968 slightly redundant.
No description provided.