-
Notifications
You must be signed in to change notification settings - Fork 498
Add android-key attestation format support #2322
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
base: master
Are you sure you want to change the base?
Conversation
Add a new attestationFormat for ACME device-attest-01 challenge to support Android attestation (android-key) as defined by WebAuthn and modify by RFC (sig use key authorization). The implementation involve adding a CRL. ACME provider support a new configuration key called RootCRLs (rootCRLs in json). When 'android-key' is specified in attestationFormat and the list is not provided by the configuration, the list will be populated and updated automatically based on the validation implementation procedure. Other ACME challenge could use IsRootRevoked and RootCRLs in the future independantly to android-key or device-attest-01 challenge.
Hi,
However, this PR will require an update to linkedca for two reason:
The CRL List is cached for 24h and refreshed when android-key attestation validation occurred passed this delay. I will be happy to reply to any comments or suggestions. |
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.
Hey @jbpin,
Looking quite good overall. Many suggestions are stylistic; feel free to combine them into your own commit(s).
I'd like to test this myself with a device too to inspect the full X5C that's provided. Or, alternatively, it would be nice to get a chain from your testing. There's some surprising thing in the code and the Google docs, so I just want to make sure things are correct.
Functionally, and kind of expected, the biggest thing that needs some attention/changes is fetching and refreshing the revoked attestation certificate serials. Eventually that logic needs to become "more pluggable" because of the way we integrate changes into our hosted version. Concretely, it would not be necessary to refetch the revocation list for each hosted authority, as it would simply be retrieving the same list for each. I think it'll come down to going with a basic version for now, and then we may do some changes to make it compatible with our platform ourselves, but we'll have to see how things roll.
"go.step.sm/crypto/x509util" | ||
"golang.org/x/exp/slices" | ||
|
||
"github.com/mbreban/attestation" |
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.
Given that this is a new package, and not used in other projects (yet), it might be better to copy/fork its code over, and maintain it internally. Are you familiar with the author?
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.
No I'm not familiar with the author. We can manage to rewrite this lib. The lib support version 1 to 300. I saw there is version 400 now.
authority/provisioner/acme.go
Outdated
// APPLE is the format used to enable device-attest-01 on Apple devices. | ||
ANDROID ACMEAttestationFormat = "android-key" | ||
|
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.
// APPLE is the format used to enable device-attest-01 on Apple devices. | |
ANDROID ACMEAttestationFormat = "android-key" | |
// ANDROIDKEY is the format used to enable device-attest-01 for Android | |
// devices using Android Key Attestation. | |
ANDROIDKEY ACMEAttestationFormat = "android-key" | |
authority/provisioner/acme.go
Outdated
func (f ACMEAttestationFormat) Validate() error { | ||
switch ACMEAttestationFormat(f.String()) { | ||
case APPLE, STEP, TPM: | ||
case APPLE, STEP, TPM, ANDROID: |
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.
case APPLE, STEP, TPM, ANDROID: | |
case APPLE, STEP, TPM, ANDROIDKEY: |
authority/provisioner/acme.go
Outdated
return fmt.Errorf("failed initializing Wire options: %w", err) | ||
} | ||
|
||
if slices.Contains(p.AttestationFormats, "android-key") && len(p.RootCRLs) == 0 { |
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.
if slices.Contains(p.AttestationFormats, "android-key") && len(p.RootCRLs) == 0 { | |
if slices.Contains(p.AttestationFormats, ANDROIDKEY) && len(p.RootCRLs) == 0 { |
authority/provisioner/acme.go
Outdated
if slices.Contains(p.AttestationFormats, "android-key") && !p.androidCRLTimeout.IsZero() && time.Now().After(p.androidCRLTimeout) { | ||
p.initializeAndroidCRL() | ||
} | ||
return len(p.RootCRLs) > 0 && slices.Contains(p.RootCRLs, serialNumber) |
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's sufficient to just check using Contains
, as it's backed by range
:
return len(p.RootCRLs) > 0 && slices.Contains(p.RootCRLs, serialNumber) | |
return slices.Contains(p.RootCRLs, serialNumber) |
authority/provisioner/acme.go
Outdated
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL) | ||
if err != nil { | ||
return fmt.Errorf("client: error making Android CRL request: %s\n", 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.
Can you change this into using the http.Client
that's available on the Controller
?
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've try but got an invalid dereference p.ctl.GetHttpClient().Get
authority/provisioner/acme.go
Outdated
Reason string `json:"reason"` | ||
} `json:"entries"` | ||
} | ||
res, err := http.Get(ANDROID_ATTESTATION_STATUS_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.
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL) | |
res, err := http.Get(androidAttestationStatusURL) |
authority/provisioner/acme.go
Outdated
} | ||
|
||
if slices.Contains(p.AttestationFormats, "android-key") && len(p.RootCRLs) == 0 { | ||
p.initializeAndroidCRL() |
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.
p.initializeAndroidCRL() | |
p.fetchAndroidCRL() |
acme/challenge.go
Outdated
return nil, NewDetailedError(ErrorBadAttestationStatementType, "Root certificate not signed by Android") | ||
} | ||
default: | ||
return nil, NewDetailedError(ErrorBadAttestationStatementType, "Invalid root certificate signature algorithm") |
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.
We could also opt to allow other public key types. This would make testing with a different signing chain more flexible. It would now require someone to always use an RSA root. Assuming a change is made that allowed the attestation root(s) to be overridden, the assumption that it always is an RSA key can be limiting. Your logic already verifies that the root certificate is part of the chain that the device sends, and that key can thus be compared against the one configured as the attestation root.
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
Hi @hslatman, We will be able to implement the |
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
authority/provisioner/acme.go
Outdated
for k := range CRLResponse.Entries { | ||
keys = append(keys, k) | ||
} | ||
p.RootCRLs = keys |
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.
As for now is on Provisioner and is configurable, I proposed RevokedCertificateSerials
acme/challenge.go
Outdated
// 2. hardwareEnforced | ||
if ch.Value != string(data.Attestation.TeeEnforced.AttestationIdSerial) { |
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 line ensure that the hardware identifier match the permanent-identifier. I've updated the comment.
There are actually severals others checks that can be done, like verified boot, but seems interested to leave that to a webhook validation. What do you think ?
acme/challenge.go
Outdated
// Android Root CA | ||
// https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate | ||
const AndroidRootCAPubKey = `-----BEGIN PUBLIC KEY----- | ||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU |
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.
That's correct. I've update this part. Also note that Google issue new certificate. So now, there are 2 RSA and 1 ECDSA on the code. If customer want to validate older devices they can rely on attestationRoots
field and provide them all.
authority/provisioner/acme.go
Outdated
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL) | ||
if err != nil { | ||
return fmt.Errorf("client: error making Android CRL request: %s\n", 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.
I've try but got an invalid dereference p.ctl.GetHttpClient().Get
acme/challenge.go
Outdated
// if err != nil { | ||
// return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "") | ||
// } | ||
attCert := leaf |
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.
actually this is not safe. Documentation alert say:
Caution: Don't assume that the key attestation certificate extension is in the leaf certificate of the chain. Only the first occurrence of the extension in the chain can be trusted. Any further instances of the extension have not been issued by the secure hardware and might have been issued by an attacker extending the chain while attempting to create fake attestations for untrusted keys.
Add a new attestationFormat for ACME device-attest-01 challenge to support Android attestation (android-key) as defined by WebAuthn and modify by RFC (sig use key authorization).
The implementation involve adding a CRL. ACME provider support a new configuration key called RootCRLs (rootCRLs in json). When 'android-key' is specified in attestationFormat and the list is not provided by the configuration, the list will be populated and updated
automatically based on the validation implementation procedure. Other ACME challenge could use IsRootRevoked and RootCRLs in the future independantly to android-key or device-attest-01 challenge.
Name of feature:
android-key attestation support for device-attest-01 challenge
Pain or issue this feature alleviates:
All to use device identifier certificate issuing on Android devices
Why is this important to the project (if not answered above):
The project already support Apple, so supporting Android is a plus.
Is there documentation on how to use this feature? If so, where?
This PR only provide a new attestationFormat called android-key
In what environments or workflows is this feature supported?
ACME provisionner with device_attest_01 and attestationFormat android-key. The device need to be managed by a MDM and an ACME client for android should be deployed
In what environments or workflows is this feature explicitly NOT supported (if any)?
Supporting links/other PRs/issues:
💔Thank you!