-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PoC: allow renewing factory root CA #51
base: main
Are you sure you want to change the base?
Conversation
if err != nil { | ||
return err | ||
} | ||
if len(estCerts) > 1 { | ||
return fmt.Errorf("Unexpected more than one certificate in response: %d", len(estCerts)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick. Maybe "Unexpectedly received more than one certificate in response" or "Expected one ..., but received %d"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, your second suggestion looks good to me... I'll take it.
} | ||
|
||
var content bytes.Buffer | ||
for _, c := range certs { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect a certain number of certs? If so, then maybe it makes sense to check the expectation (e.g. len(certs) == 2). Or we actually don't know, since customers may put any number of certs into it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect any number of certs here.
Our API will only ever send 1 or 3, but the custom EST server may send a different amount.
That said, I was thinking about some hard limit to protect the device.
Maybe smth like a maximum of 10 CAs sounds reasonable.
At the final call, an EST server should normally wind up sending just one CA.
|
||
log.Printf("Performing root certificate update") | ||
if err = handler.Update(); err == nil { | ||
log.Print("Root certificate update sequence complete") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: "is completed" or "completed"?
@@ -184,6 +206,14 @@ func main() { | |||
}, | |||
}, | |||
}, | |||
{ | |||
Name: "renew-root", | |||
HelpName: "renew-root <EST Server> [<rotation-id>]", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like inconcistency between the cli parameter name rotation-id
and the state field handler.State.CorrelationId
. I suppose that's because it comes from the base class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted this to be correlation-id
, but it must be rotation-id
for backward compatibility (required for renew-cert
).
So, in this PR I ended up renaming the runtime field name, but leaving the state file property on disk the old way.
That said, I tend to agree with you that the command-line argument needs to be renamed too, as it is a positional argument, especially for this new command.
exit 0 | ||
} | ||
|
||
# We'll execute under two conditions: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are those conditions prevent from updating root CA while there is ongoing root CA update?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These conditions were copied from rotate-cert
, and IIUC they are here to prevent an accidental CA re-renewal unrelated config changes.
Maybe @doanac has a better explaination, as an original author of this (borrowed) idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to re-read the the cert verification logic another 5 times and then look at the unit tests a couple times but this is how I was hoping this would pan out.
func (s fetchRootStep) Execute(h *rootRenewalContext) error { | ||
caFile := h.app.sota.GetOrDie("import.tls_cacert_path") | ||
url := h.State.EstServer + "/cacerts" | ||
res, err := h.client.Get(url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can use these helpers which will help with retry and backoff- https://github.com/foundriesio/fioconfig/blob/main/internal/http.go#L84
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you are right... I just didn't pay enough attention as to why those helpers are not used for the cert rotation.
Maybe, in that case a "plain retry" would be dangerous because of the POST which is not applicable for a simple fetch of new CAs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was probably just me forgetting to use my own helper functions
} | ||
for len(data) > 0 { | ||
if block, data = pem.Decode(data); block == nil { | ||
return nil, fmt.Errorf("Malformed PEM block at %s", data[:100]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe put some ...
in the error message to indicate the string has been truncated for display purposes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
LGTM, will need to go through it for the second time to understand better. |
I actually needed to change the decodeEstResponse method signature, so that I could reuse it for the root CA update. This turned out to be a separate tiny improvement. Signed-off-by: Volodymyr Khoroz <[email protected]>
There is only one step in the state machine. But, a machine itself is still useful for robustness, retries, notifications, and restarting services. Signed-off-by: Volodymyr Khoroz <[email protected]>
It uses a command added in the previous commit. Signed-off-by: Volodymyr Khoroz <[email protected]>
Each new certificate must pass all of the below checks: 1. It is a valid certificate authority. 2. Its subject is exactly the same as a subject of one of the current CAs. 3. One of the following conditions is met: 3.1. It is signed by one of the current CAs. 3.2. It has the same public key as one of the current CAs. 3.3. It has the same public key as any certificate satisfying 3.1. Signed-off-by: Volodymyr Khoroz <[email protected]>
Some customers would want to allow unconditional root CA renewal. This can also be used as a way to recover from the root CA key loss. Signed-off-by: Volodymyr Khoroz <[email protected]>
Signed-off-by: Volodymyr Khoroz <[email protected]>
I need to reuse the EST server mock for the root CA renewal test. For that, I need to customize its handler func. While doing that I noticed a few interesting things: 1. An EST server mock client is not used. 2. An cert rotation EST step test does not verify what it received from the server. So, I also improved that test in scope of my work. Signed-off-by: Volodymyr Khoroz <[email protected]>
Signed-off-by: Volodymyr Khoroz <[email protected]>
Signed-off-by: Volodymyr Khoroz <[email protected]>
Signed-off-by: Volodymyr Khoroz <[email protected]>
Signed-off-by: Volodymyr Khoroz <[email protected]>
4b1ab2a
to
8b44b0a
Compare
Just rebased on main |
|
||
[ -z "$CONFIG_FILE" ] && (echo "No CONFIG_FILE specified"; exit 1) | ||
[ -f "$CONFIG_FILE" ] || (echo "$CONFIG_FILE does not exist"; exit 1) | ||
[ -z "$SOTA_DIR" ] && (echo "No SOTA_DIR specified"; exit 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable does not seem to be used anymore, or is it supposed to be STORAGE_DIR
that is references later on ? 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then do we need to check if it is actually a directory with [ -d $SOTA_DIR ] ||
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a copy paste from the other script: https://github.com/foundriesio/fioconfig/blob/main/contrib/renew-client-cert#L8
I think you are right and this code needs to check for the STORAGE_DIR instead.
It needs to be fixed in both the original and the copy.
It is a minor fix, and things just work in old versions (with this mistake); so no need to backport.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! 🚀
} | ||
caCerts, err := loadCertsFromPem(caCertBuf) | ||
if err != nil { | ||
log.Fatal("Failed to parse root CA file", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should return an error and not log.Fatal, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A root renewal script is not able to execute if it cannot parse CA certs.
So, it's a fatal error.
This is currently in the PoC stage i.e. fully untested.
However, it clearly shows the primary idea, and is ready for an early feedback.
The easiest way to grasp this is commit-by-commit.