Skip to content
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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Conversation

vkhoroz
Copy link
Member

@vkhoroz vkhoroz commented Jun 12, 2024

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.

@vkhoroz vkhoroz requested review from doanac and mike-sul June 12, 2024 19:22
@vkhoroz vkhoroz self-assigned this Jun 12, 2024
if err != nil {
return err
}
if len(estCerts) > 1 {
return fmt.Errorf("Unexpected more than one certificate in response: %d", len(estCerts))

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"?

Copy link
Member Author

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 {

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?

Copy link
Member Author

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")

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>]",

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.

Copy link
Member Author

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:

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?

Copy link
Member Author

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.

Copy link
Member

@doanac doanac left a 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)
Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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])
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

internal/renew_root.go Show resolved Hide resolved
main.go Show resolved Hide resolved
@mike-sul
Copy link

LGTM, will need to go through it for the second time to understand better.

vkhoroz added 11 commits June 19, 2024 20:40
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]>
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]>
@vkhoroz vkhoroz force-pushed the vkhoroz-renew-root-ca branch from 4b1ab2a to 8b44b0a Compare June 19, 2024 19:15
@vkhoroz
Copy link
Member Author

vkhoroz commented Jun 19, 2024

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)
Copy link
Member

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 ? 👍

Copy link
Member

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 ] || ?

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! 🚀

contrib/renew-root-cert Show resolved Hide resolved
}
caCerts, err := loadCertsFromPem(caCertBuf)
if err != nil {
log.Fatal("Failed to parse root CA file", err)
Copy link
Member

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?

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants