snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
110
selfservice/strategy/deviceauthn/attestation.go
Normal file
110
selfservice/strategy/deviceauthn/attestation.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package deviceauthn
|
||||
|
||||
import (
|
||||
"encoding/asn1"
|
||||
)
|
||||
|
||||
// Attestation holds the platform-specific attestation parsed at enrollment.
|
||||
// Exactly one of `android` or `ios` is set, matching the key's device_type.
|
||||
//
|
||||
// swagger:model deviceAuthnAttestation
|
||||
type Attestation struct {
|
||||
Android *AndroidKeyDescription `json:"android,omitempty"`
|
||||
IOS *IOSAttestation `json:"ios,omitempty"`
|
||||
}
|
||||
|
||||
// AndroidKeyDescription matches the ASN.1 sequence defined by Android.
|
||||
// It is stored in the leaf certificate's x509 extension.
|
||||
//
|
||||
// Schema: https://source.android.com/docs/security/features/keystore/attestation#schema
|
||||
//
|
||||
// swagger:model deviceAuthnAndroidKeyDescription
|
||||
type AndroidKeyDescription struct {
|
||||
AttestationVersion int `json:"attestation_version,omitempty"`
|
||||
AttestationSecurityLevel asn1.Enumerated `json:"attestation_security_level,omitempty"`
|
||||
KeymasterVersion int `json:"keymaster_version,omitempty"`
|
||||
KeymasterSecurityLevel asn1.Enumerated `json:"keymaster_security_level,omitempty"`
|
||||
// AttestationChallenge is verified at enrollment time and not re-used
|
||||
// later, so we don't persist it.
|
||||
AttestationChallenge []byte `json:"-"`
|
||||
// UniqueID is a hardware-tied identifier; we don't persist it for
|
||||
// privacy reasons.
|
||||
UniqueID []byte `json:"-"`
|
||||
SoftwareEnforced AndroidAuthorizationList `json:"software_enforced"`
|
||||
TeeEnforced AndroidAuthorizationList `json:"tee_enforced"`
|
||||
}
|
||||
|
||||
// AndroidAuthorizationList maps the ASN.1 tags to the fields defined in KeyMint 4.
|
||||
// Schema: https://source.android.com/docs/security/features/keystore/attestation#schema
|
||||
//
|
||||
// swagger:model deviceAuthnAndroidAuthorizationList
|
||||
type AndroidAuthorizationList struct {
|
||||
Purpose []int `asn1:"tag:1,explicit,set,optional" json:"purpose,omitempty"`
|
||||
Algorithm int `asn1:"tag:2,explicit,optional" json:"algorithm,omitempty"`
|
||||
KeySize int `asn1:"tag:3,explicit,optional" json:"key_size,omitempty"`
|
||||
Digest []int `asn1:"tag:5,explicit,set,optional" json:"digest,omitempty"`
|
||||
EcCurve int `asn1:"tag:10,explicit,optional" json:"ec_curve,omitempty"`
|
||||
ActiveDateTime int `asn1:"tag:400,explicit,optional" json:"active_date_time,omitempty"`
|
||||
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional" json:"origination_expire_date_time,omitempty"`
|
||||
UsageExpireDateTime int `asn1:"tag:402,explicit,optional" json:"usage_expire_date_time,omitempty"`
|
||||
NoAuthRequired asn1.RawValue `asn1:"tag:503,explicit,optional" json:"-"`
|
||||
UserAuthType int `asn1:"tag:504,explicit,optional" json:"user_auth_type,omitempty"`
|
||||
AuthTimeout int `asn1:"tag:505,explicit,optional" json:"auth_timeout,omitempty"`
|
||||
AllowWhileOnBody asn1.RawValue `asn1:"tag:506,explicit,optional" json:"-"`
|
||||
TrustedUserPresenceRequired asn1.RawValue `asn1:"tag:507,explicit,optional" json:"-"`
|
||||
TrustedConfirmationRequired asn1.RawValue `asn1:"tag:508,explicit,optional" json:"-"`
|
||||
UnlockedDeviceRequired asn1.RawValue `asn1:"tag:509,explicit,optional" json:"-"`
|
||||
ApplicationID []byte `asn1:"tag:601,explicit,optional" json:"application_id,omitempty"`
|
||||
CreationDateTime int `asn1:"tag:701,explicit,optional" json:"creation_date_time,omitempty"`
|
||||
Origin int `asn1:"tag:702,explicit,optional" json:"origin,omitempty"`
|
||||
RootOfTrust AndroidRootOfTrust `asn1:"tag:704,explicit,optional" json:"root_of_trust"`
|
||||
OsVersion int `asn1:"tag:705,explicit,optional" json:"os_version,omitempty"`
|
||||
OsPatchLevel int `asn1:"tag:706,explicit,optional" json:"os_patch_level,omitempty"`
|
||||
AttestationChallenge []byte `asn1:"tag:708,explicit,optional" json:"-"`
|
||||
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional" json:"attestation_application_id,omitempty"`
|
||||
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional" json:"attestation_id_brand,omitempty"`
|
||||
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional" json:"attestation_id_device,omitempty"`
|
||||
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional" json:"attestation_id_product,omitempty"`
|
||||
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional" json:"attestation_id_serial,omitempty"`
|
||||
AttestationIDImei []byte `asn1:"tag:714,explicit,optional" json:"attestation_id_imei,omitempty"`
|
||||
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional" json:"attestation_id_meid,omitempty"`
|
||||
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional" json:"attestation_id_manufacturer,omitempty"`
|
||||
AttestationIDModel []byte `asn1:"tag:717,explicit,optional" json:"attestation_id_model,omitempty"`
|
||||
VendorPatchLevel int `asn1:"tag:718,explicit,optional" json:"vendor_patch_level,omitempty"`
|
||||
BootPatchLevel int `asn1:"tag:719,explicit,optional" json:"boot_patch_level,omitempty"`
|
||||
DeviceUniqueAttestation asn1.RawValue `asn1:"tag:720,explicit,optional" json:"-"`
|
||||
}
|
||||
|
||||
// swagger:model deviceAuthnAndroidRootOfTrust
|
||||
type AndroidRootOfTrust struct {
|
||||
VerifiedBootKey []byte `asn1:"optional" json:"verified_boot_key,omitempty"`
|
||||
DeviceLocked bool `json:"device_locked,omitempty"`
|
||||
// Actually it is `AndroidVerifiedBootState` but due to ASN1 quirks we define it so.
|
||||
VerifiedBootState asn1.Enumerated `json:"verified_boot_state,omitempty"`
|
||||
VerifiedBootHash []byte `asn1:"optional" json:"verified_boot_hash,omitempty"`
|
||||
}
|
||||
|
||||
// IOSAttStmt is the attestation statement of an Apple App Attest attestation.
|
||||
// Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation .
|
||||
//
|
||||
// swagger:model deviceAuthnIOSAttStmt
|
||||
type IOSAttStmt struct {
|
||||
// Receipt is several KB of proprietary opaque data; not persisted.
|
||||
Receipt []byte `cbor:"receipt" json:"-"`
|
||||
// X5c is the leaf-first certificate chain encoded as base64(DER), per
|
||||
// the JOSE convention defined in RFC 7515 §4.1.6.
|
||||
X5c [][]byte `cbor:"x5c" json:"x5c,omitempty"`
|
||||
}
|
||||
|
||||
// IOSAttestation is an Apple App Attest attestation.
|
||||
// Defined in https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server#Verify-the-attestation .
|
||||
//
|
||||
// swagger:model deviceAuthnIOSAttestation
|
||||
type IOSAttestation struct {
|
||||
Fmt string `cbor:"fmt" json:"fmt,omitempty"`
|
||||
AttStmt IOSAttStmt `cbor:"attStmt" json:"att_stmt"`
|
||||
AuthData []byte `cbor:"authData" json:"auth_data,omitempty"`
|
||||
}
|
||||
44
selfservice/strategy/deviceauthn/attestation_test.go
Normal file
44
selfservice/strategy/deviceauthn/attestation_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package deviceauthn_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/selfservice/strategy/deviceauthn"
|
||||
)
|
||||
|
||||
func TestAndroidKeyDescriptionJSON_dropsChallengeAndUniqueID(t *testing.T) {
|
||||
desc := deviceauthn.AndroidKeyDescription{
|
||||
AttestationVersion: 3,
|
||||
AttestationSecurityLevel: 1, // TEE-backed.
|
||||
AttestationChallenge: []byte("nonce"),
|
||||
UniqueID: []byte("hardware-id"),
|
||||
TeeEnforced: deviceauthn.AndroidAuthorizationList{
|
||||
AttestationIDManufacturer: []byte("Google"),
|
||||
},
|
||||
}
|
||||
|
||||
out, err := json.Marshal(desc)
|
||||
require.NoError(t, err)
|
||||
|
||||
res := gjson.ParseBytes(out)
|
||||
assert.Equal(t, int64(3), res.Get("attestation_version").Int())
|
||||
assert.Equal(t, base64.StdEncoding.EncodeToString([]byte("Google")), res.Get("tee_enforced.attestation_id_manufacturer").String())
|
||||
assert.False(t, res.Get("attestation_challenge").Exists(), "challenge is verified at enrollment, not serialized")
|
||||
assert.False(t, res.Get("unique_id").Exists(), "unique id is privacy-sensitive, not serialized")
|
||||
}
|
||||
|
||||
func TestKeyAttestationOmitemptyWhenNil(t *testing.T) {
|
||||
k := deviceauthn.Key{ClientKeyID: "abc"}
|
||||
out, err := json.Marshal(k)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, gjson.ParseBytes(out).Get("attestation").Exists())
|
||||
}
|
||||
198
selfservice/strategy/deviceauthn/key.go
Normal file
198
selfservice/strategy/deviceauthn/key.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package deviceauthn
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// The valid values (Android, iOS) are pinned as an enum via the OpenAPI patch
|
||||
// in .schema/openapi/patches/schema.yaml. (Detached from the doc comment to
|
||||
// stay out of the generated API description.)
|
||||
|
||||
// DeviceType is the platform the key was enrolled on.
|
||||
//
|
||||
// "Android" and "iOS" use different attestation and signature encodings.
|
||||
type DeviceType string
|
||||
|
||||
const (
|
||||
DeviceTypeAndroid DeviceType = "Android"
|
||||
DeviceTypeIOS DeviceType = "iOS"
|
||||
)
|
||||
|
||||
// The valid values (initial, confirmed, locked) are pinned as an enum via the
|
||||
// OpenAPI patch in .schema/openapi/patches/schema.yaml. (Detached from the doc
|
||||
// comment to stay out of the generated API description.)
|
||||
|
||||
// KeyState is the lifecycle state of an enrolled key.
|
||||
//
|
||||
// Only a "confirmed" key can be used to authenticate. A "locked" key was
|
||||
// disabled after too many wrong-PIN attempts; rotating its pin_secret unlocks
|
||||
// it.
|
||||
type KeyState string
|
||||
|
||||
const (
|
||||
KeyStateInitial KeyState = "initial"
|
||||
KeyStateConfirmed KeyState = "confirmed"
|
||||
// KeyStateLocked marks a PIN key disabled after too many wrong-PIN attempts.
|
||||
KeyStateLocked KeyState = "locked"
|
||||
)
|
||||
|
||||
// The schema name stays UpperCamelCase on purpose: it is part of the published
|
||||
// DeviceAuthn schema family (DeviceAuthnKey, DeviceType, KeyState, PINConfig),
|
||||
// which all use UpperCamelCase. Renaming it to the lowerCamelCase the core
|
||||
// naming convention prefers would be a breaking change to the public SDK and
|
||||
// would split the family's casing, so the deliberate exception is kept.
|
||||
// The valid values (pin, platform, none) are pinned as an enum via the OpenAPI
|
||||
// patch in .schema/openapi/patches/schema.yaml. (Detached from the doc comment
|
||||
// to stay out of the generated API description.)
|
||||
|
||||
// UserVerification records how the key's holder is verified at use time.
|
||||
//
|
||||
// "pin" means the holder proves knowledge of a PIN via pin_proof at login;
|
||||
// "platform" means the device gates key use behind platform biometrics or an
|
||||
// equivalent lock screen; "none" marks a possession-only key, usable as a
|
||||
// second factor but never as the sole first factor. The empty value marks a
|
||||
// legacy key from before user verification existed; such keys are rejected at
|
||||
// login and must be re-enrolled.
|
||||
type UserVerification string
|
||||
|
||||
const (
|
||||
UserVerificationPIN UserVerification = "pin"
|
||||
UserVerificationPlatform UserVerification = "platform"
|
||||
// UserVerificationNone marks a possession-only key: usable as a second
|
||||
// factor (step-up) but never as a first factor, which login enforces.
|
||||
// Unlike JWT's alg=none the value strictly reduces what the key may do —
|
||||
// it bypasses no verification. It covers keys deliberately enrolled
|
||||
// without PIN or platform verification.
|
||||
UserVerificationNone UserVerification = "none"
|
||||
)
|
||||
|
||||
// PINConfig holds only scalar fields, so it is safe to shallow copy; the pointer
|
||||
// on Key.PIN encodes optionality (nil means no PIN), not copy safety.
|
||||
//
|
||||
// The schema name stays UpperCamelCase: it belongs to the published DeviceAuthn
|
||||
// schema family (DeviceAuthnKey, DeviceType, KeyState, UserVerification), and
|
||||
// renaming it would break the public SDK.
|
||||
// (Detached from the doc comment to stay out of the generated API description.)
|
||||
|
||||
// PINConfig is the per-key PIN state.
|
||||
//
|
||||
// The pin_secret field holds only the at-rest ciphertext; the plaintext
|
||||
// exists transiently in server memory during verification and is invalidated
|
||||
// once the key locks.
|
||||
type PINConfig struct {
|
||||
// pin_secret holds ciphertext of the kratos cipher (hex) and must never be
|
||||
// returned to or logged by clients; the OpenAPI patch in
|
||||
// .schema/openapi/patches/schema.yaml marks it write-only.
|
||||
// (Detached from the doc comment to stay out of the API description.)
|
||||
|
||||
// The at-rest ciphertext of the pin_secret. It never leaves the server and
|
||||
// is cleared once the key locks.
|
||||
PINSecret string `json:"pin_secret"`
|
||||
// A negative stored value must be a decode error instead of silently
|
||||
// extending the attempt budget, hence uint. (Detached from the doc comment
|
||||
// to stay out of the generated API description.)
|
||||
|
||||
// The number of consecutive wrong-PIN attempts so far; the key locks when
|
||||
// it reaches the configured maximum (pin_max_attempts, default 5).
|
||||
FailedAttempts uint `json:"failed_attempts"`
|
||||
// When the pin_secret was first issued.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// When the pin_secret was last rotated. Omitted if the secret was never
|
||||
// rotated.
|
||||
RotatedAt time.Time `json:"rotated_at,omitzero"`
|
||||
}
|
||||
|
||||
// Key is safe to shallow copy as long as the shared reference fields (PublicKey,
|
||||
// PIN, Attestation, RelaxedAttestationExpiresAt) are treated as read-only:
|
||||
// in-place mutation goes through a *Key into the backing slice, and the merge
|
||||
// copies PIN rather than aliasing it. Kept out of the doc comment so it stays
|
||||
// out of the generated API description.
|
||||
|
||||
// An enrolled device authentication (DeviceAuthn) key.
|
||||
//
|
||||
// Represents a hardware-backed signing key enrolled from a mobile device.
|
||||
// The private key resides inside the device and never exists on the server.
|
||||
//
|
||||
// To list the identity's enrolled keys, fetch a settings flow: each key's
|
||||
// remove button (a `ui.nodes` entry named `deviceauthn_remove` in group
|
||||
// `deviceauthn`) carries the key, with its PIN state redacted, in the node
|
||||
// label's `context`.
|
||||
//
|
||||
// swagger:model DeviceAuthnKey
|
||||
type Key struct {
|
||||
// We intentionally avoid storing the cryptographic algorithm a la JWT/TLS to
|
||||
// avoid security issues and algorithm negotiation; a future v2 may use
|
||||
// ML-DSA (post-quantum) once Android/iOS support it. (Detached from the doc
|
||||
// comment to stay out of the generated API description.)
|
||||
|
||||
// The cryptography version of the key. Version 1 uses ECDSA with SHA-256
|
||||
// on an elliptic curve (P-224, P-256, P-384, or P-521); further versions
|
||||
// are reserved for future signature suites.
|
||||
Version int `json:"version"`
|
||||
|
||||
// A human-readable name for the device, helping the user tell this key
|
||||
// apart from others.
|
||||
DeviceName string `json:"device_name"`
|
||||
// The device's public key (an elliptic-curve key on P-224, P-256, P-384,
|
||||
// or P-521 in version 1) in PKIX, ASN.1 DER (SubjectPublicKeyInfo) form,
|
||||
// base64-encoded. Signatures are verified against this key.
|
||||
//
|
||||
// swagger:strfmt byte
|
||||
PublicKey []byte `json:"public_key,omitempty"`
|
||||
|
||||
// The key's stable id, unique per identity. Submit it as the
|
||||
// `client_key_id` when logging in with the key, deleting it, or rotating
|
||||
// its pin_secret.
|
||||
//
|
||||
// The device can also compute the id without reading it back from the
|
||||
// server: it is the lowercase-hex SHA-256 of `public_key` (the key's
|
||||
// PKIX, ASN.1 DER encoding). Keys enrolled before the server derived the
|
||||
// id keep their original client-chosen value, so prefer reading this
|
||||
// field over recomputing it for older keys.
|
||||
ClientKeyID string `json:"client_key_id"`
|
||||
// When the key was enrolled. Only used for troubleshooting and UI.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// The platform the key was enrolled on, "Android" or "iOS". The two
|
||||
// platforms use different attestation and signature encodings.
|
||||
DeviceType DeviceType `json:"device_type"`
|
||||
|
||||
// Auto-confirmation vs. an admin confirmation call (for out-of-band KYC,
|
||||
// e.g. a physical letter with a code) is not configurable yet; keys are
|
||||
// auto-confirmed at creation time. (Detached from the doc comment to stay
|
||||
// out of the generated API description.)
|
||||
|
||||
// The lifecycle state of the key. Only a "confirmed" key can be used to
|
||||
// authenticate; a "locked" key was disabled after too many wrong-PIN
|
||||
// attempts.
|
||||
State KeyState `json:"state"`
|
||||
|
||||
// How the key's holder is verified at use time: "pin", "platform", or
|
||||
// "none". Empty for legacy keys enrolled before user verification existed;
|
||||
// such keys are rejected at login and must be re-enrolled.
|
||||
UserVerification UserVerification `json:"user_verification,omitempty"`
|
||||
// The per-key PIN state. Absent unless the key is PIN-protected.
|
||||
PIN *PINConfig `json:"pin,omitempty"`
|
||||
|
||||
// The platform-specific attestation captured at enrollment. Useful for
|
||||
// telling keys apart in the UI (e.g. OS version, manufacturer) and for
|
||||
// forensics.
|
||||
Attestation *Attestation `json:"attestation,omitempty"`
|
||||
|
||||
// Set only when the key's attestation chain was accepted under relaxed
|
||||
// rules (software roots, expired certificates, software security level)
|
||||
// rather than strict hardware attestation. Such keys are refused at login
|
||||
// after this time, or immediately once relaxed attestation is turned off.
|
||||
// Absent for hardware-attested keys that pass strict validation.
|
||||
RelaxedAttestationExpiresAt *time.Time `json:"relaxed_attestation_expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialsDeviceAuthnConfig is the JSON shape stored in
|
||||
// `identity_credentials.config` for the `deviceauthn` credential type.
|
||||
type CredentialsDeviceAuthnConfig struct {
|
||||
Credentials []Key `json:"credentials"`
|
||||
}
|
||||
44
selfservice/strategy/deviceauthn/key_test.go
Normal file
44
selfservice/strategy/deviceauthn/key_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package deviceauthn_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/selfservice/strategy/deviceauthn"
|
||||
)
|
||||
|
||||
func TestKeyVersion2RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := deviceauthn.Key{
|
||||
Version: 2,
|
||||
ClientKeyID: "abc123",
|
||||
State: deviceauthn.KeyStateConfirmed,
|
||||
UserVerification: deviceauthn.UserVerificationPIN,
|
||||
PIN: &deviceauthn.PINConfig{
|
||||
PINSecret: "ciphertext-hex",
|
||||
FailedAttempts: 2,
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(k)
|
||||
require.NoError(t, err)
|
||||
|
||||
var got deviceauthn.Key
|
||||
require.NoError(t, json.Unmarshal(b, &got))
|
||||
assert.Equal(t, k, got)
|
||||
}
|
||||
|
||||
func TestKeyVersion1ParsesAsLegacy(t *testing.T) {
|
||||
t.Parallel()
|
||||
legacy := []byte(`{"version":1,"client_key_id":"old","state":"confirmed"}`)
|
||||
var got deviceauthn.Key
|
||||
require.NoError(t, json.Unmarshal(legacy, &got))
|
||||
assert.Equal(t, 1, got.Version)
|
||||
assert.Nil(t, got.PIN)
|
||||
assert.Equal(t, deviceauthn.UserVerification(""), got.UserVerification)
|
||||
}
|
||||
Reference in New Issue
Block a user