Skip to content

Conversation

jbpin
Copy link

@jbpin jbpin commented Jun 30, 2025

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!

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.
@github-actions github-actions bot added the needs triage Waiting for discussion / prioritization by team label Jun 30, 2025
@jbpin
Copy link
Author

jbpin commented Jun 30, 2025

Hi,
This PR can be test with a provisioner config like this :

{
            "attestationFormats": [
              "android-key"
            ],
            "challenges": [
              "device-attest-01"
            ],
            "claims": {
              "defaultTLSCertDuration": "2h",
              "maxTLSCertDuration": "8h"
            },
            "name": "android-acme",
            "type": "ACME"
          },

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.

@hslatman hslatman self-assigned this Jul 1, 2025
@hslatman hslatman added this to the v0.28.5 milestone Jul 28, 2025
Copy link
Member

@hslatman hslatman left a 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"
Copy link
Member

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?

Copy link
Author

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.

Comment on lines 61 to 63
// APPLE is the format used to enable device-attest-01 on Apple devices.
ANDROID ACMEAttestationFormat = "android-key"

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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"

func (f ACMEAttestationFormat) Validate() error {
switch ACMEAttestationFormat(f.String()) {
case APPLE, STEP, TPM:
case APPLE, STEP, TPM, ANDROID:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
case APPLE, STEP, TPM, ANDROID:
case APPLE, STEP, TPM, ANDROIDKEY:

return fmt.Errorf("failed initializing Wire options: %w", err)
}

if slices.Contains(p.AttestationFormats, "android-key") && len(p.RootCRLs) == 0 {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if slices.Contains(p.AttestationFormats, "android-key") && len(p.RootCRLs) == 0 {
if slices.Contains(p.AttestationFormats, ANDROIDKEY) && len(p.RootCRLs) == 0 {

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

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:

Suggested change
return len(p.RootCRLs) > 0 && slices.Contains(p.RootCRLs, serialNumber)
return slices.Contains(p.RootCRLs, serialNumber)

Comment on lines 248 to 251
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL)
if err != nil {
return fmt.Errorf("client: error making Android CRL request: %s\n", err)
}
Copy link
Member

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?

Copy link
Author

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

Reason string `json:"reason"`
} `json:"entries"`
}
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL)
res, err := http.Get(androidAttestationStatusURL)

}

if slices.Contains(p.AttestationFormats, "android-key") && len(p.RootCRLs) == 0 {
p.initializeAndroidCRL()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
p.initializeAndroidCRL()
p.fetchAndroidCRL()

return nil, NewDetailedError(ErrorBadAttestationStatementType, "Root certificate not signed by Android")
}
default:
return nil, NewDetailedError(ErrorBadAttestationStatementType, "Invalid root certificate signature algorithm")
Copy link
Member

@hslatman hslatman Aug 1, 2025

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.

Jean-Baptiste Pin and others added 3 commits August 26, 2025 16:07
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
@jbpin
Copy link
Author

jbpin commented Aug 26, 2025

Hi @hslatman,
I'm back from vacation, I will implement the suggestion and change a bit the code. It looks like there are some changes since this implementation on Google side : https://developer.android.com/privacy-and-security/security-key-attestation

We will be able to implement the attestationRoots instead of key and support ECDSA as well.

for k := range CRLResponse.Entries {
keys = append(keys, k)
}
p.RootCRLs = keys
Copy link
Author

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

Comment on lines 866 to 867
// 2. hardwareEnforced
if ch.Value != string(data.Attestation.TeeEnforced.AttestationIdSerial) {
Copy link
Author

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 ?

Comment on lines 1402 to 1405
// Android Root CA
// https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate
const AndroidRootCAPubKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU
Copy link
Author

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.

Comment on lines 248 to 251
res, err := http.Get(ANDROID_ATTESTATION_STATUS_URL)
if err != nil {
return fmt.Errorf("client: error making Android CRL request: %s\n", err)
}
Copy link
Author

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

// if err != nil {
// return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "")
// }
attCert := leaf
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs triage Waiting for discussion / prioritization by team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants