diff --git a/acme/challenge.go b/acme/challenge.go index f328a25ef..5d7a4fbf6 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -31,6 +31,7 @@ import ( "github.com/fxamacker/cbor/v2" "github.com/google/go-tpm/legacy/tpm2" + attestation "github.com/smallstep/android-attestation" "github.com/smallstep/go-attestation/attest" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" @@ -838,7 +839,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose format := att.Format prov := MustProvisionerFromContext(ctx) if !prov.IsAttestationFormatEnabled(ctx, provisioner.ACMEAttestationFormat(format)) { - if format != "apple" && format != "step" && format != "tpm" { + if format != "apple" && format != "step" && format != "tpm" && format != "android-key" { return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", format)) } @@ -847,6 +848,36 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose } switch format { + case "android-key": + data, err := doAndroidKeyAttestationFormat(ctx, prov, ch, jwk, &att) + if err != nil { + var acmeError *Error + if errors.As(err, &acmeError) { + if acmeError.Status == 500 { + return acmeError + } + return storeError(ctx, db, ch, true, acmeError) + } + return WrapErrorISE(err, "error validating attestation") + } + + // Enforce hardware security level (TrustedEnvironment or StrongBox; Software not allowed) + if data.Attestation.AttestationSecurityLevel < 1 { + return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "insufficient security level: %d", data.Attestation.AttestationSecurityLevel)) + } + + // Enforce hardware backed device serial + if ch.Value != string(data.Attestation.TeeEnforced.AttestationIdSerial) { + subproblem := NewSubproblemWithIdentifier( + ErrorRejectedIdentifierType, + Identifier{Type: "permanent-identifier", Value: ch.Value}, + "challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, []string{string(data.Attestation.TeeEnforced.AttestationIdSerial)}, + ) + return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem)) + } + + // Update attestation key fingerprint to compare against the CSR + az.Fingerprint = data.Fingerprint case "apple": data, err := doAppleAttestationFormat(ctx, prov, ch, &att) if err != nil { @@ -1370,6 +1401,264 @@ func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge, return data, nil } +// Android RSA Root CA +// https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate +var androidRSARootCA = `-----BEGIN CERTIFICATE----- +MIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgw +NzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS +Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7 +tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj +nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq +C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ +oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O +JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg +sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi +igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M +RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E +aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um +AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud +IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7 +174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGIC +W/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2G +tkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkx +oSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG +1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mF +mr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPz +lHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVw +n6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1Eu +zbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHo +vaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHn +w1IdYIg2Wxg7yHcQZemFQg== +-----END CERTIFICATE-----` + +// Android ECDSA (secp384r1) Root CA +var androidECDSARootCA = `-----BEGIN CERTIFICATE----- +MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4 +WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex +MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG +EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt +G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5 +Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA +MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH +EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY +uR2zh/80lQyu9vAFCj6E4AXc+osmRg== +-----END CERTIFICATE-----` + +// OID for Android Key Attestation +// https://source.android.com/docs/security/features/keystore/attestation#id-attestation +var oidAndroidAttestation = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17} + +type androidKeyAttestationData struct { + Certificate *x509.Certificate + Fingerprint string + Attestation *attestation.KeyDescription +} + +// findAndroidAttestationCert traverses the slice of [*x509.Certificate] from +// end to start (root -> intermediate -> leaf) to locate the certificate closest +// to the root carrying an Android Key Attestation extension. +// +// TODO(hs): implement the optional step of locating the provisioning information +// extension? That should immediately precede the cert with the key attestation +// extension. +func findAndroidAttestationCert(certs []*x509.Certificate) *x509.Certificate { + for i := len(certs) - 1; i >= 0; i-- { + cert := certs[i] + for _, ext := range cert.Extensions { + if ext.Id.Equal(oidAndroidAttestation) { + return cert + } + } + } + + return nil +} + +// doAndroidKeyAttestationFormat handles ACME Device Attestation requests for +// the "android-key" attestation format. Its verification logic is based on +// the documentation at https://developer.android.com/privacy-and-security/security-key-attestation +// +// This function performs the below steps: +// - Verifies that the root public certificate is trustworthy and that each certificate signs +// the next certificate in the chain. +// - Checks each certificate's revocation status to ensure that none of the certificates have +// been revoked. +// - Find the nearest certificate to the root that contains the key attestation certificate +// extension. +// - Check the extension data that you've retrieved in the previous steps for consistency and +// compare with the set of values that you expect the hardware-backed key to contain. +func doAndroidKeyAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*androidKeyAttestationData, error) { + acmeProv, ok := prov.(*provisioner.ACME) + if !ok { + return nil, NewErrorISE("provisioner in context is not an ACME provisioner") + } + + roots, ok := prov.GetAttestationRoots() + if !ok { + rsaRoot, err := pemutil.ParseCertificate([]byte(androidRSARootCA)) + if err != nil { + return nil, WrapErrorISE(err, "error parsing Android RSA root CA") + } + ecdsaRoot, err := pemutil.ParseCertificate([]byte(androidECDSARootCA)) + if err != nil { + return nil, WrapErrorISE(err, "error parsing Android ECDSA root CA") + } + roots = x509.NewCertPool() + roots.AddCert(rsaRoot) + roots.AddCert(ecdsaRoot) + } + + // extract x5c and verify certificate + x5c, ok := att.AttStatement["x5c"].([]any) + if !ok { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c not present") + } + if len(x5c) == 0 { + return nil, NewDetailedError(ErrorRejectedIdentifierType, "x5c is empty") + } + + // Parse leaf, intermediates and root + intermediates := x509.NewCertPool() + var leaf, root *x509.Certificate + for i, v := range x5c { + der, dok := v.([]byte) + if !dok { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c element is malformed") + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed parsing certificate at index %d", i) + } + + // Verify certificate serial number against CRL + revoked, err := acmeProv.IsAndroidCertificateRevoked(ctx, cert) + if err != nil { + return nil, WrapDetailedError(ErrorServerInternalType, err, "failed checking certificate revocation status") + } + if revoked { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c element contains a revoked certificate") + } + + switch i { + case 0: // leaf + leaf = cert + case len(x5c) - 1: // root + root = cert + default: // intermediates + intermediates.AddCert(cert) + } + } + + // Require a leaf in the chain + if leaf == nil { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "missing leaf certificate in x5c chain") + } + + // Require a root in the chain + if root == nil { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "missing root certificate in x5c chain") + } + + // Verify the root is one of the trusted roots + _, err := root.Verify(x509.VerifyOptions{ + Roots: roots, + CurrentTime: time.Now().Truncate(time.Second), + }) + if err != nil { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "root certificate in chain is not trusted") + } + + // Validate the full chain including root as trust anchor + chains, err := leaf.Verify(x509.VerifyOptions{ + Intermediates: intermediates, + Roots: roots, + CurrentTime: time.Now().Truncate(time.Second), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + if err != nil { + return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c chain verification failed") + } + + switch { + case len(chains) == 0: + return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c does not contain a valid certificate chain") + case len(chains) > 1: + // currently we strictly prohibit multiple valid signing chains + return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c contains multiple (%d) valid certificate chains", len(chains)) + } + + // Get signature + sig, ok := att.AttStatement["sig"].([]byte) + if !ok { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "sig not present") + } + + keyAuth, err := KeyAuthorization(ch.Token, jwk) + if err != nil { + return nil, err + } + + // Find the attestation certificate + attCert := findAndroidAttestationCert(chains[0]) + if attCert == nil { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "no attestation certificate with OID 1.3.6.1.4.1.11129.2.1.17 found in the cert chain") + } + + switch pub := attCert.PublicKey.(type) { + case *ecdsa.PublicKey: + if pub.Curve != elliptic.P256() { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "unsupported elliptic curve %s", pub.Curve) + } + sum := sha256.Sum256([]byte(keyAuth)) + if !ecdsa.VerifyASN1(pub, sum[:], sig) { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed validating signature") + } + case *rsa.PublicKey: + sum := sha256.Sum256([]byte(keyAuth)) + if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig); err != nil { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed validating signature") + } + case ed25519.PublicKey: + if !ed25519.Verify(pub, []byte(keyAuth), sig) { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed validating signature") + } + default: + return nil, NewDetailedError(ErrorBadAttestationStatementType, "unsupported public key type %T", pub) + } + + data := &androidKeyAttestationData{ + Certificate: attCert, + } + if data.Fingerprint, err = keyutil.Fingerprint(attCert.PublicKey); err != nil { + return nil, WrapErrorISE(err, "error calculating key fingerprint") + } + + for _, ext := range attCert.Extensions { + if !ext.Id.Equal(oidAndroidAttestation) { + continue + } + keyDesc, err := attestation.ParseExtension(ext.Value) + if err != nil { + return nil, WrapError(ErrorBadAttestationStatementType, err, "error parsing attestation") + } + data.Attestation = keyDesc + break + } + + // Validate key authorization + if string(data.Attestation.AttestationChallenge) != keyAuth { + return nil, NewDetailedError(ErrorBadAttestationStatementType, "challenge mismatch; expected %q, got %q", keyAuth, string(data.Attestation.AttestationChallenge)) + } + + return data, nil +} + // Yubico PIV Root CA Serial 263751 // https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem const yubicoPIVRootCA = `-----BEGIN CERTIFICATE----- @@ -1504,7 +1793,7 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, switch pub := leaf.PublicKey.(type) { case *ecdsa.PublicKey: if pub.Curve != elliptic.P256() { - return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "unsupported elliptic curve %s", pub.Curve) + return nil, NewDetailedError(ErrorBadAttestationStatementType, "unsupported elliptic curve %s", pub.Curve) } sum := sha256.Sum256([]byte(keyAuth)) if !ecdsa.VerifyASN1(pub, sum[:], sig) { diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 50af568c3..0cd29a463 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -25,12 +25,14 @@ import ( "net/http" "net/http/httptest" "reflect" + "slices" "strconv" "strings" "testing" "time" "github.com/fxamacker/cbor/v2" + attestation "github.com/smallstep/android-attestation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,6 +44,7 @@ import ( "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/provisioner/androidkey" wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire" ) @@ -98,6 +101,31 @@ func mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner { return prov } +type fakeAndroidKeyCRLChecker []string + +func (p fakeAndroidKeyCRLChecker) IsRevoked(_ context.Context, cert *x509.Certificate) (bool, error) { + return slices.Contains(p, cert.SerialNumber.String()), nil +} + +func mustAndroidAttestationProvisioner(t *testing.T, roots []byte, androidKeyCRLChecker androidkey.CRLChecker) Provisioner { + t.Helper() + + prov := &provisioner.ACME{ + Type: "ACME", + Name: "acme", + Challenges: []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01}, + AttestationFormats: []provisioner.ACMEAttestationFormat{provisioner.ANDROIDKEY}, + AttestationRoots: roots, + } + if err := prov.Init(provisioner.Config{ + Claims: config.GlobalProvisionerClaims, + AndroidKeyCRLChecker: androidKeyCRLChecker, + }); err != nil { + t.Fatal(err) + } + return prov +} + func mustAccountAndKeyAuthorization(t *testing.T, token string) (*jose.JSONWebKey, string) { t.Helper() @@ -109,6 +137,63 @@ func mustAccountAndKeyAuthorization(t *testing.T, token string) (*jose.JSONWebKe return jwk, keyAuth } +func mustAttestAndroid(t *testing.T, keyAuthorization string) ([]byte, *x509.Certificate, *x509.Certificate) { + t.Helper() + + ca, err := minica.New() + require.NoError(t, err) + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + keyAuthSum := sha256.Sum256([]byte(keyAuthorization)) + require.NoError(t, err) + + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + require.NoError(t, err) + + atts := attestation.KeyDescription{ + AttestationVersion: 300, + AttestationSecurityLevel: 1, + AttestationChallenge: []byte(keyAuthorization), + TeeEnforced: attestation.AuthorizationList{ + AttestationIdSerial: []byte("serial-number"), + }, + } + attestByte, err := attestation.CreateKeyDescription(&atts) + require.NoError(t, err) + + leaf, err := ca.Sign(&x509.Certificate{ + Subject: pkix.Name{CommonName: "attestation cert"}, + PublicKey: signer.Public(), + ExtraExtensions: []pkix.Extension{ + {Id: oidAndroidAttestation, Value: attestByte}, + }, + }) + require.NoError(t, err) + + attObj, err := cbor.Marshal(struct { + Format string `json:"fmt"` + AttStatement map[string]any `json:"attStmt,omitempty"` + }{ + Format: "android-key", + AttStatement: map[string]any{ + "x5c": []any{leaf.Raw, ca.Intermediate.Raw, ca.Root.Raw}, + "sig": sig, + }, + }) + require.NoError(t, err) + + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + require.NoError(t, err) + + return payload, leaf, ca.Root +} + func mustAttestApple(t *testing.T, nonce string) ([]byte, *x509.Certificate, *x509.Certificate) { t.Helper() @@ -4503,6 +4588,95 @@ func Test_deviceAttest01Validate(t *testing.T) { wantErr: nil, } }, + "ok/doAndroidAttestationFormat": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, _, root := mustAttestAndroid(t, keyAuth) + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) + ctx := NewProvisionerContext(context.Background(), mustAndroidAttestationProvisioner(t, caRoot, nil)) + return test{ + args: args{ + ctx: ctx, + jwk: jwk, + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "serial-number", + }, + payload: payload, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + assert.Equal(t, "serial-number", updch.Value) + assert.NotNil(t, updch.Payload) // TODO: validate payload? + assert.Equal(t, "android-key", updch.PayloadFormat) + + return nil + }, + }, + }, + wantErr: nil, + } + }, + "ok/doAndroidAttestationFormat-invalid-root": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, _, root := mustAttestAndroid(t, keyAuth) + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) + androidKeyCRLChecker := fakeAndroidKeyCRLChecker([]string{root.SerialNumber.String()}) + ctx := NewProvisionerContext(context.Background(), mustAndroidAttestationProvisioner(t, caRoot, androidKeyCRLChecker)) + return test{ + args: args{ + ctx: ctx, + jwk: jwk, + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + Token: "nonce", + Type: "device-attest-01", + Status: StatusPending, + Value: "serial-number", + }, + payload: payload, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "nonce", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + assert.Equal(t, "serial-number", updch.Value) + assert.NotNil(t, updch.Payload) // TODO: validate payload? + assert.Empty(t, updch.PayloadFormat) + + err := NewDetailedError(ErrorBadAttestationStatementType, "x5c element contains a revoked certificate") + + assert.EqualError(t, updch.Error.Err, err.Err.Error()) + assert.Equal(t, err.Type, updch.Error.Type) + assert.Equal(t, err.Detail, updch.Error.Detail) + assert.Equal(t, err.Status, updch.Error.Status) + assert.Equal(t, err.Subproblems, updch.Error.Subproblems) + + return nil + }, + }, + }, + wantErr: nil, + } + }, "ok/doStepAttestationFormat-storeError": func(t *testing.T) test { ca, err := minica.New() require.NoError(t, err) @@ -4918,14 +5092,15 @@ func Test_deviceAttest01Validate(t *testing.T) { t.Run(name, func(t *testing.T) { tc := run(t) - if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil { - if assert.Error(t, tc.wantErr) { + err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload) + if tc.wantErr != nil { + if assert.Error(t, err) { assert.ErrorContains(t, err, tc.wantErr.Error()) } return } - assert.Nil(t, tc.wantErr) + assert.NoError(t, err) }) } } diff --git a/authority/authority.go b/authority/authority.go index 98dd68968..8dd6a0b8e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -30,6 +30,7 @@ import ( "github.com/smallstep/certificates/authority/internal/constraints" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/provisioner/androidkey" "github.com/smallstep/certificates/cas" casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" @@ -41,17 +42,18 @@ import ( // Authority implements the Certificate Authority internal interface. type Authority struct { - config *config.Config - keyManager kms.KeyManager - provisioners *provisioner.Collection - admins *administrator.Collection - db db.AuthDB - adminDB admin.DB - templates *templates.Templates - linkedCAToken string - wrapTransport httptransport.Wrapper - webhookClient provisioner.HTTPClient - httpClient provisioner.HTTPClient + config *config.Config + keyManager kms.KeyManager + provisioners *provisioner.Collection + admins *administrator.Collection + db db.AuthDB + adminDB admin.DB + templates *templates.Templates + linkedCAToken string + wrapTransport httptransport.Wrapper + webhookClient provisioner.HTTPClient + httpClient provisioner.HTTPClient + androidKeyCRLChecker androidkey.CRLChecker // X509 CA password []byte diff --git a/authority/options.go b/authority/options.go index a5e50df1b..b12f83887 100644 --- a/authority/options.go +++ b/authority/options.go @@ -14,6 +14,7 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/provisioner/androidkey" "github.com/smallstep/certificates/cas" casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" @@ -103,6 +104,15 @@ func WithWebhookClient(c provisioner.HTTPClient) Option { } } +// WithAndroidKeyCRLChecker sets the [androidkey.CRLChecker] to be +// used to check Android Key Attestation certificate revocation status. +func WithAndroidKeyCRLChecker(c androidkey.CRLChecker) Option { + return func(a *Authority) error { + a.androidKeyCRLChecker = c + return nil + } +} + // Wrapper wraps the set of functions mapping [http.Transport] references to [http.RoundTripper]. type TransportWrapper = httptransport.Wrapper diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index b014e6754..d571ab400 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -3,15 +3,22 @@ package provisioner import ( "context" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" "net" + "net/http" + "slices" "strings" + "sync/atomic" "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/acme/wire" + "golang.org/x/sync/singleflight" + "github.com/smallstep/linkedca" + + "github.com/smallstep/certificates/acme/wire" ) // ACMEChallenge represents the supported acme challenges. @@ -53,6 +60,9 @@ func (c ACMEChallenge) Validate() error { type ACMEAttestationFormat string const ( + // ANDROIDKEY is the format used to enable device-attest-01 on Android devices. + ANDROIDKEY ACMEAttestationFormat = "android-key" + // APPLE is the format used to enable device-attest-01 on Apple devices. APPLE ACMEAttestationFormat = "apple" @@ -74,7 +84,7 @@ func (f ACMEAttestationFormat) String() string { // Validate returns an error if the attestation format is not a valid one. func (f ACMEAttestationFormat) Validate() error { switch ACMEAttestationFormat(f.String()) { - case APPLE, STEP, TPM: + case APPLE, STEP, TPM, ANDROIDKEY: return nil default: return fmt.Errorf("acme attestation format %q is not supported", f) @@ -120,10 +130,25 @@ type ACME struct { AttestationRoots []byte `json:"attestationRoots,omitempty"` Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` + androidCRL *androidCRLCache attestationRootPool *x509.CertPool ctl *Controller } +// androidCRLCache caches the Android attestation revocation list. +type androidCRLCache struct { + current atomic.Pointer[androidCRL] + group singleflight.Group +} + +// androidCRL is an immutable snapshot of the Android attestation revocation +// list. A new snapshot is swapped in atomically on each refresh, so readers +// never observe a partially-updated set. +type androidCRL struct { + serials map[string]struct{} // set of revoked serial numbers + fetchedAt time.Time +} + // GetID returns the provisioner unique identifier. func (p ACME) GetID() string { if p.ID != "" { @@ -217,10 +242,89 @@ func (p *ACME) Init(config Config) (err error) { return fmt.Errorf("failed initializing Wire options: %w", err) } + p.androidCRL = &androidCRLCache{} + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } +const ( + androidAttestationStatusURL = "https://android.googleapis.com/attestation/status" // TODO(hs): make configurable through options? + androidCRLTTL = 24 * time.Hour // TODO(hs): make configurable through options and/or Cache-Control header? +) + +// snapshot returns a fresh-enough snapshot of the Android revocation list, +// fetching a new one if the cached copy is missing or older than androidCRLTTL. +// Concurrent refreshes are coalesced into a single upstream request, so a burst +// of validations results in at most one call to the Android status endpoint. +func (c *androidCRLCache) snapshot(ctx context.Context, client HTTPClient) (*androidCRL, error) { + if crl := c.current.Load(); crl != nil && time.Since(crl.fetchedAt) < androidCRLTTL { + return crl, nil + } + + // The cache is missing or stale. Coalesce concurrent refreshes so that only + // one upstream request is performed; all callers share its result. + v, err, _ := c.group.Do("android-crl", func() (any, error) { + // Re-check under the singleflight leader: another goroutine may have + // refreshed the snapshot while we were queued behind it. + if crl := c.current.Load(); crl != nil && time.Since(crl.fetchedAt) < androidCRLTTL { + return crl, nil + } + + // load the CRL. We choose to let all followers get a context.Canceled when the + // leader's ctx is canceled. Clients will retry after. + return c.load(ctx, client) + }) + if err != nil { + return nil, err + } + + return v.(*androidCRL), nil +} + +// load fetches the CRL at https://android.googleapis.com/attestation/status, +// builds the set of revoked serial numbers, and atomically publishes it as the +// current snapshot. +func (c *androidCRLCache) load(ctx context.Context, client HTTPClient) (*androidCRL, error) { + var crlResponse struct { + Entries map[string]struct { + Status string `json:"status"` + Reason string `json:"reason"` + } `json:"entries"` + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, androidAttestationStatusURL, http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed creating Android CRL request: %w", err) + } + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed performing Android CRL request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected Android CRL response %d", res.StatusCode) + } + + if err := json.NewDecoder(res.Body).Decode(&crlResponse); err != nil { + return nil, fmt.Errorf("error decoding Android CRL JSON: %w", err) + } + + // Build the set of revoked serials. Currently no distinction is being made + // based on the status of the certificate. The status can be "REVOKED" and + // "SUSPENDED"; both statuses result in a certificate being considered + // revoked. + serials := make(map[string]struct{}, len(crlResponse.Entries)) + for k := range crlResponse.Entries { + serials[k] = struct{}{} + } + + crl := &androidCRL{serials: serials, fetchedAt: time.Now()} + c.current.Store(crl) + + return crl, nil +} + // initializeWireOptions initializes the options for the ACME Wire // integration. It'll return early if no Wire challenge types are // enabled. @@ -372,7 +476,7 @@ func (p *ACME) IsChallengeEnabled(_ context.Context, challenge ACMEChallenge) bo // AttestationFormat provisioner property should have at least one element. func (p *ACME) IsAttestationFormatEnabled(_ context.Context, format ACMEAttestationFormat) bool { enabledFormats := []ACMEAttestationFormat{ - APPLE, STEP, TPM, + APPLE, STEP, TPM, ANDROIDKEY, } if len(p.AttestationFormats) > 0 { enabledFormats = p.AttestationFormats @@ -393,3 +497,39 @@ func (p *ACME) IsAttestationFormatEnabled(_ context.Context, format ACMEAttestat func (p *ACME) GetAttestationRoots() (*x509.CertPool, bool) { return p.attestationRootPool, p.attestationRootPool != nil } + +// IsAndroidCertificateRevoked returns whether the serial number is +// revoked or not. +func (p *ACME) IsAndroidCertificateRevoked(ctx context.Context, cert *x509.Certificate) (bool, error) { + // the check only has to be performed when the "android-key" attestation + // format is enabled + if !slices.Contains(p.AttestationFormats, ANDROIDKEY) { + return false, nil + } + + if p.ctl.androidKeyCRLChecker != nil { + revoked, err := p.ctl.androidKeyCRLChecker.IsRevoked(ctx, cert) + if err != nil { + return true, fmt.Errorf("failed checking certificate against Android CRL: %w", err) + } + + return revoked, nil + } + + // No CRL check is performed using Google's CRL if custom roots + // are configured. + if p.attestationRootPool != nil { + return false, nil + } + + // Fall back to the built-in CRL. A fetch error fails closed: the + // certificate is reported as revoked so the attestation is rejected. + crl, err := p.androidCRL.snapshot(ctx, p.ctl.GetHTTPClient()) + if err != nil { + return true, err + } + + _, revoked := crl.serials[cert.SerialNumber.String()] + + return revoked, nil +} diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index f51698091..3f30feea1 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -6,15 +6,22 @@ import ( "crypto/x509" "errors" "fmt" + "io" + "math/big" "net/http" "os" + "slices" + "strings" + "sync" + "sync/atomic" "testing" "time" - "github.com/smallstep/certificates/api/render" - "github.com/smallstep/certificates/authority/provisioner/wire" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/provisioner/wire" ) func TestACMEChallenge_Validate(t *testing.T) { @@ -51,6 +58,7 @@ func TestACMEAttestationFormat_Validate(t *testing.T) { f ACMEAttestationFormat wantErr bool }{ + {"android-key", ANDROIDKEY, false}, {"apple", APPLE, false}, {"step", STEP, false}, {"tpm", TPM, false}, @@ -201,7 +209,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Name: "foo", Type: "ACME", Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01}, - AttestationFormats: []ACMEAttestationFormat{APPLE, STEP}, + AttestationFormats: []ACMEAttestationFormat{APPLE, STEP, ANDROIDKEY}, AttestationRoots: bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n")), }, } @@ -429,14 +437,16 @@ func TestACME_IsAttestationFormatEnabled(t *testing.T) { args args want bool }{ - {"ok", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, TPM}, true}, + {"ok", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM, ANDROIDKEY}}, args{ctx, TPM}, true}, {"ok empty apple", fields{nil}, args{ctx, APPLE}, true}, {"ok empty step", fields{nil}, args{ctx, STEP}, true}, {"ok empty tpm", fields{[]ACMEAttestationFormat{}}, args{ctx, "tpm"}, true}, + {"ok empty android", fields{[]ACMEAttestationFormat{}}, args{ctx, "android-key"}, true}, {"ok uppercase", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, "STEP"}, true}, {"fail apple", fields{[]ACMEAttestationFormat{STEP, TPM}}, args{ctx, APPLE}, false}, {"fail step", fields{[]ACMEAttestationFormat{APPLE, TPM}}, args{ctx, STEP}, false}, {"fail step", fields{[]ACMEAttestationFormat{APPLE, STEP}}, args{ctx, TPM}, false}, + {"fail android", fields{[]ACMEAttestationFormat{APPLE, STEP}}, args{ctx, ANDROIDKEY}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -448,3 +458,177 @@ func TestACME_IsAttestationFormatEnabled(t *testing.T) { }) } } + +// countingHTTPClient is an HTTPClient that records how many upstream requests +// it serves and returns a fixed Android CRL response after an optional delay. +type countingHTTPClient struct { + calls atomic.Int64 + body string + delay time.Duration +} + +func (c *countingHTTPClient) Get(string) (*http.Response, error) { + return c.Do(&http.Request{}) +} + +func (c *countingHTTPClient) Do(*http.Request) (*http.Response, error) { + c.calls.Add(1) + if c.delay > 0 { + time.Sleep(c.delay) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(c.body)), + Header: make(http.Header), + }, nil +} + +func certWithSerial(serial int64) *x509.Certificate { + return &x509.Certificate{SerialNumber: big.NewInt(serial)} +} + +func TestACME_IsAndroidCertificateRevoked(t *testing.T) { + // The CRL reports serial number 42 as revoked. The delay ensures a burst + // of concurrent lookups overlaps on the initial fetch so that the + // singleflight coalescing is actually exercised. + client := &countingHTTPClient{ + body: `{"entries":{"42":{"status":"REVOKED"}}}`, + delay: 50 * time.Millisecond, + } + + p := &ACME{ + Type: "ACME", + Name: "acme", + AttestationFormats: []ACMEAttestationFormat{ANDROIDKEY}, + } + require.NoError(t, p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + HTTPClient: client, + })) + + ctx := t.Context() + + // Fire a burst of concurrent lookups against the cold cache. This must not + // race (run with -race) and, thanks to singleflight, must result in a + // single upstream request. + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines) + for range goroutines { + go func() { + defer wg.Done() + revoked, err := p.IsAndroidCertificateRevoked(ctx, certWithSerial(42)) + assert.NoError(t, err) + assert.True(t, revoked) + }() + } + wg.Wait() + + assert.Equal(t, int64(1), client.calls.Load(), "concurrent refreshes should coalesce into a single upstream request") + + // A non-revoked serial is served from the same cached snapshot without a + // new upstream request. + revoked, err := p.IsAndroidCertificateRevoked(ctx, certWithSerial(7)) + assert.NoError(t, err) + assert.False(t, revoked) + assert.Equal(t, int64(1), client.calls.Load(), "cached snapshot should not trigger another request") +} + +func TestACME_IsAndroidCertificateRevoked_controller(t *testing.T) { + p := &ACME{ + Type: "ACME", + Name: "acme", + AttestationFormats: []ACMEAttestationFormat{ANDROIDKEY}, + } + require.NoError(t, p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + AndroidKeyCRLChecker: fakeAndroidKeyCRLChecker([]string{"42"}), + })) + + revoked, err := p.IsAndroidCertificateRevoked(t.Context(), certWithSerial(42)) + assert.NoError(t, err) + assert.True(t, revoked) + + revoked, err = p.IsAndroidCertificateRevoked(t.Context(), certWithSerial(7)) + assert.NoError(t, err) + assert.False(t, revoked) +} + +type fakeAndroidKeyCRLChecker []string + +func (p fakeAndroidKeyCRLChecker) IsRevoked(_ context.Context, cert *x509.Certificate) (bool, error) { + return slices.Contains(p, cert.SerialNumber.String()), nil +} + +func TestACME_IsAndroidCertificateRevoked_controllerError(t *testing.T) { + p := &ACME{ + Type: "ACME", + Name: "acme", + AttestationFormats: []ACMEAttestationFormat{ANDROIDKEY}, + } + require.NoError(t, p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + AndroidKeyCRLChecker: fakeAndroidKeyCRLCheckerWithError{}, + })) + + revoked, err := p.IsAndroidCertificateRevoked(t.Context(), certWithSerial(42)) + assert.Error(t, err) + assert.True(t, revoked) +} + +type fakeAndroidKeyCRLCheckerWithError []string + +func (p fakeAndroidKeyCRLCheckerWithError) IsRevoked(_ context.Context, cert *x509.Certificate) (bool, error) { + return true, errors.New("fail!") +} + +func TestACME_IsAndroidCertificateRevoked_disabled(t *testing.T) { + client := &countingHTTPClient{body: `{"entries":{}}`} + p := &ACME{ + Type: "ACME", + Name: "acme", + AttestationFormats: []ACMEAttestationFormat{APPLE}, + } + require.NoError(t, p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + HTTPClient: client, + })) + + // android-key is not enabled, so no CRL lookup should happen at all. + revoked, err := p.IsAndroidCertificateRevoked(t.Context(), certWithSerial(42)) + assert.NoError(t, err) + assert.False(t, revoked) + assert.Equal(t, int64(0), client.calls.Load()) +} + +func TestACME_IsAndroidCertificateRevoked_failsClosed(t *testing.T) { + // An upstream error must fail closed: the certificate is reported revoked + // and the error is surfaced. + p := &ACME{ + Type: "ACME", + Name: "acme", + AttestationFormats: []ACMEAttestationFormat{ANDROIDKEY}, + } + require.NoError(t, p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + HTTPClient: &erroringHTTPClient{}, + })) + + revoked, err := p.IsAndroidCertificateRevoked(t.Context(), certWithSerial(42)) + assert.Error(t, err) + assert.True(t, revoked) +} + +// erroringHTTPClient is an HTTPClient whose requests always fail. +type erroringHTTPClient struct{} + +func (erroringHTTPClient) Get(string) (*http.Response, error) { return nil, errors.New("fail!") } + +func (erroringHTTPClient) Do(*http.Request) (*http.Response, error) { + return nil, errors.New("fail!") +} diff --git a/authority/provisioner/androidkey/crl.go b/authority/provisioner/androidkey/crl.go new file mode 100644 index 000000000..fb75bf40f --- /dev/null +++ b/authority/provisioner/androidkey/crl.go @@ -0,0 +1,10 @@ +package androidkey + +import ( + "context" + "crypto/x509" +) + +type CRLChecker interface { + IsRevoked(ctx context.Context, cert *x509.Certificate) (bool, error) +} diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index ae3b1a247..b03b297df 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -12,6 +12,7 @@ import ( "github.com/smallstep/linkedca" + "github.com/smallstep/certificates/authority/provisioner/androidkey" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/internal/cast" "github.com/smallstep/certificates/internal/httptransport" @@ -32,6 +33,7 @@ type Controller struct { webhookClient HTTPClient webhooks []*Webhook wrapTransport httptransport.Wrapper + androidKeyCRLChecker androidkey.CRLChecker } // NewController initializes a new provisioner controller. @@ -66,6 +68,7 @@ func NewController(p Interface, claims *Claims, config Config, options *Options) webhooks: options.GetWebhooks(), httpClient: config.HTTPClient, wrapTransport: wt, + androidKeyCRLChecker: config.AndroidKeyCRLChecker, }, nil } @@ -75,7 +78,10 @@ func (c *Controller) GetHTTPClient() HTTPClient { if c.httpClient != nil { return c.httpClient } - return &http.Client{} + + return &http.Client{ + Timeout: 10 * time.Second, + } } // GetIdentity returns the identity for a given email. diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 33d75fe9a..ae6e0f978 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -13,6 +13,7 @@ import ( kmsapi "go.step.sm/crypto/kms/apiv1" "golang.org/x/crypto/ssh" + "github.com/smallstep/certificates/authority/provisioner/androidkey" "github.com/smallstep/certificates/errs" ) @@ -281,6 +282,10 @@ type Config struct { // WrapTransport references the function that should wrap any [http.Transport] initialized // down the Config's chain. WrapTransport TransportWrapper + // AndroidKeyCRLChecker references an implementation of [androidkey.CRLChecker] + // that is responsible for checking revoked Android Key Attestation certificate + // serial numbers. + AndroidKeyCRLChecker androidkey.CRLChecker } type provisioner struct { diff --git a/authority/provisioners.go b/authority/provisioners.go index d01048564..3413ab309 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -205,6 +205,7 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner. HTTPClient: a.httpClient, WrapTransport: a.wrapTransport, SCEPKeyManager: a.scepKeyManager, + AndroidKeyCRLChecker: a.androidKeyCRLChecker, }, nil } @@ -1360,6 +1361,8 @@ func attestationFormatsToCertificates(formats []linkedca.ACMEProvisioner_Attesta ret := make([]provisioner.ACMEAttestationFormat, 0, len(formats)) for _, f := range formats { switch f { + case linkedca.ACMEProvisioner_ANDROID_KEY: + ret = append(ret, provisioner.ANDROIDKEY) case linkedca.ACMEProvisioner_APPLE: ret = append(ret, provisioner.APPLE) case linkedca.ACMEProvisioner_STEP: @@ -1377,6 +1380,8 @@ func attestationFormatsToLinkedca(formats []provisioner.ACMEAttestationFormat) [ ret := make([]linkedca.ACMEProvisioner_AttestationFormatType, 0, len(formats)) for _, f := range formats { switch provisioner.ACMEAttestationFormat(f.String()) { + case provisioner.ANDROIDKEY: + ret = append(ret, linkedca.ACMEProvisioner_ANDROID_KEY) case provisioner.APPLE: ret = append(ret, linkedca.ACMEProvisioner_APPLE) case provisioner.STEP: diff --git a/go.mod b/go.mod index 5a26552b9..241580b6e 100644 --- a/go.mod +++ b/go.mod @@ -28,16 +28,17 @@ require ( github.com/rs/xid v1.6.0 github.com/sirupsen/logrus v1.9.4 github.com/slackhq/nebula v1.10.3 + github.com/smallstep/android-attestation v0.0.0-20260722204520-a51b1564ebda github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/smallstep/cli-utils v0.12.2 github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9 - github.com/smallstep/linkedca v0.25.0 + github.com/smallstep/linkedca v0.26.0 github.com/smallstep/nosql v0.8.0 github.com/smallstep/pkcs7 v0.2.1 github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 github.com/stretchr/testify v1.11.1 github.com/urfave/cli v1.22.17 - go.step.sm/crypto v0.84.1 + go.step.sm/crypto v0.85.0 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.54.0 golang.org/x/net v0.57.0 @@ -66,23 +67,23 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect - github.com/ThalesGroup/crypto11 v1.6.0 // indirect + github.com/ThalesGroup/crypto11 v1.6.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.25 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.53.4 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect - github.com/aws/smithy-go v1.27.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.30 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 // indirect + github.com/aws/smithy-go v1.27.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect @@ -104,8 +105,8 @@ require ( github.com/golang/glog v1.2.5 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/certificate-transparency-go v1.1.7 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect github.com/google/go-tpm-tools v0.4.9 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect @@ -127,7 +128,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/klauspost/compress v1.19.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect @@ -171,6 +172,6 @@ require ( google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 47ea88d4c..91272588e 100644 --- a/go.sum +++ b/go.sum @@ -50,42 +50,42 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ThalesGroup/crypto11 v1.6.0 h1:Og9EMn44fBS4GNnGnH1aqHnF2wL6F7IU/RhpJajWX/4= -github.com/ThalesGroup/crypto11 v1.6.0/go.mod h1:H6LRjN5R5SHxTrLqGNteisLDI0/IC6+SGx1pHtbwizE= +github.com/ThalesGroup/crypto11 v1.6.2 h1:X+JsrOlKanaIlHgwV/3MJ/cFivbEaQ8kpXRPoiC2T+c= +github.com/ThalesGroup/crypto11 v1.6.2/go.mod h1:fQ61t02lJdXD2HrG7upt/VRlDECsipwtqpPcZJEjBKg= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= -github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= -github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= -github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= -github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= -github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= -github.com/aws/aws-sdk-go-v2/service/kms v1.53.4 h1:PEgVSsWtR8NNxsDxFL2Ywisi7R+1EFQARGsT4q3mWwI= -github.com/aws/aws-sdk-go-v2/service/kms v1.53.4/go.mod h1:3EeKyDGPGSCEphG2OolwNGNF45RvQIfm27AYYpfEWrw= -github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= -github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= -github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= -github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= +github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= +github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk= +github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ= +github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= +github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 h1:aeJAJyvWS3gQ679pJbz8ZdOh3MViD1zvEdoZMVEawbg= +github.com/aws/aws-sdk-go-v2/service/kms v1.54.1/go.mod h1:0RXNc6Yf3AvSMldGD6Lcch96Ojlw2TtGnHsqfD/L4u8= +github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE= +github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg= +github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk= +github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84= +github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw= +github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q= +github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= +github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/ccoveille/go-safecast/v2 v2.0.1 h1:2+mIu3gXtwmWelBia2kkxfB8eP4orTHDH7ClSlWkd6I= @@ -182,11 +182,11 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= -github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -262,8 +262,9 @@ github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= @@ -351,14 +352,16 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU= github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs= +github.com/smallstep/android-attestation v0.0.0-20260722204520-a51b1564ebda h1:IfbkxDvPbdr00RD5kgdzdCkPp4ANOLgXyVI8NkWjQ+I= +github.com/smallstep/android-attestation v0.0.0-20260722204520-a51b1564ebda/go.mod h1:WEOYAQ4qnt6LhEcreLGA0woxzE8k5e0vjGuIq72J5tw= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9 h1:n+X1wnMKJMcCRd98YKAo/56tMRSPUg+qjAvNNS1EZeM= github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo= -github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc= +github.com/smallstep/linkedca v0.26.0 h1:NsxTVo3zI3KwOFFiVodeHtuGgKq0b5kOUXVLjjNTvXY= +github.com/smallstep/linkedca v0.26.0/go.mod h1:Z8c7EgVrSHNhshIhRnUGfKfE796+TwF2eyErhqANmJQ= github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A= github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo= github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= @@ -420,8 +423,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.step.sm/crypto v0.84.1 h1:i0JNkcLT7LcXef00TNpckjoTTH0QP6REHcAvnY3qrNY= -go.step.sm/crypto v0.84.1/go.mod h1:T54MIdw42uZnz3+mjOIOpJbsbTZF+O3IOLhvU9lBoJk= +go.step.sm/crypto v0.85.0 h1:h3U9gzCcxP1fWbseqy+CwsTxzHFNuoblefy7YP4jBHk= +go.step.sm/crypto v0.85.0/go.mod h1:Rp/BdP0/ZJ7eYREhXcE4izCABLp0e8ziFqb0LBNG7Cc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -538,8 +541,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 h1:rgSNvqscFZ1JgV/4wH5GOsZFSFkR2Eua9As3KIr2LlM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2/go.mod h1:iMEtFwDlAhjDU9L5mY6U1XLwlIId/G3h+QcBHDIvrJ8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=