Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,30 +661,32 @@ func (m *activateOneContainerStateMachine) tryWithUserAuthKeyslots(ctx context.C
break
}

cred, err := authRequestor.RequestUserCredential(ctx, name, m.container.Path(), authType)
cred, credAuthType, err := authRequestor.RequestUserCredential(ctx, name, m.container.Path(), authType)
if err != nil {
return fmt.Errorf("cannot request user credential: %w", err)
}

credAuthType &= authType

// We have a user credential.
// 1) Try it against every keyslot with a passphrase.
// 2) See if it decodes as a PIN and try it against every keyslot with a passphrase.
// 3) See if it decodes as a recovery key, and try it against every recovery keyslot.
// 1) If it's a passphrase, try it against every keyslot with a passphrase.
// 2) If it's a PIN, try it against every keyslot with a passphrase.
// 3) If it's a recovery key, try it against every recovery keyslot.

var (
unlockKey DiskUnlockKey
primaryKey PrimaryKey
)

if passphraseTries > 0 {
if credAuthType&UserAuthTypePassphrase > 0 {
passphraseTries -= 1
if uk, pk, success := m.tryPassphraseKeyslotsHelper(ctx, passphraseSlotRecords, cred); success {
unlockKey = uk
primaryKey = pk
}
}

if m.status == activationIncomplete && pinTries > 0 {
if m.status == activationIncomplete && credAuthType&UserAuthTypePIN > 0 {
pin, err := ParsePIN(cred)
switch {
case err != nil && authType == UserAuthTypePIN:
Expand All @@ -707,7 +709,7 @@ func (m *activateOneContainerStateMachine) tryWithUserAuthKeyslots(ctx context.C
}
}

if m.status == activationIncomplete && recoveryKeyTries > 0 {
if m.status == activationIncomplete && credAuthType&UserAuthTypeRecoveryKey > 0 {
recoveryKey, err := ParseRecoveryKey(cred)
switch {
case err != nil && authType == UserAuthTypeRecoveryKey:
Expand Down
112 changes: 112 additions & 0 deletions activate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4843,6 +4843,62 @@ Error with keyslot "default": cannot recover keys from keyslot: user authorizati
c.Check(err, Equals, ErrCannotActivate)
}

func (s *activateSuite) TestActivateContainerAuthModePassphraseAuthRequestorOnlyReturnsRecoveryKey(c *C) {
// Test a simple case with 2 keyslots with passphrase auth and
// a recovery keyslot. Unlocking happens with a recovery keyslot
// after initially entering what looks like a correct passphrase
// but the AuthRequestor indicated it was only a recovery key.
primaryKey := testutil.DecodeHexString(c, "ed988fada3dbf68e13862cfc52b6d6205c862dd0941e643a81dcab106a79ce6a")
kd1, unlockKey1 := s.makeKeyDataBlobWithPassphrase(c, primaryKey, testutil.DecodeHexString(c, "4d8b57f05f0e70a73768c1d9f1078b8e9b0e9c399f555342e1ac4e675fea122e"), "run+recover", "secret")
kd2, unlockKey2 := s.makeKeyDataBlobWithPassphrase(c, primaryKey, testutil.DecodeHexString(c, "d72501b0b558c3119e036d5585629a026e82c05b6a4f19511daa3f12cc37902f"), "recover", "foo")

recoveryKey := testutil.DecodeHexString(c, "9124e9a56e40c65424c5f652127f8d18")

authRequestor := &mockAuthRequestor{
responses: []any{
mockAuthRequestorResponse{response: "secret", authTypes: UserAuthTypeRecoveryKey},
mockAuthRequestorResponse{response: makeRecoveryKey(c, recoveryKey), authTypes: UserAuthTypeRecoveryKey},
},
}

err := s.testActivateContextActivateContainer(c, &testActivateContextActivateContainerParams{
contextOpts: []ActivateContextOption{
WithAuthRequestor(authRequestor),
WithPassphraseTries(3),
WithRecoveryKeyTries(3),
},
authRequestor: authRequestor,
container: newMockStorageContainer(
withStorageContainerPath("/dev/sda1"),
withStorageContainerCredentialName("sda1"),
withStorageContainerKeyslot("default", unlockKey1, KeyslotTypePlatform, 0, kd1),
withStorageContainerKeyslot("default-fallback", unlockKey2, KeyslotTypePlatform, 0, kd2),
withStorageContainerKeyslot("default-recovery", recoveryKey, KeyslotTypeRecovery, 0, nil),
),
opts: []ActivateOption{
WithAuthRequestorUserVisibleName("data"),
},
expectedAuthRequestName: "data",
expectedAuthRequestPath: "/dev/sda1",
expectedAuthRequestTypes: []UserAuthType{
UserAuthTypePassphrase | UserAuthTypeRecoveryKey,
UserAuthTypePassphrase | UserAuthTypeRecoveryKey,
},
expectedActivateConfig: map[any]any{
AuthRequestorKey: authRequestor,
PassphraseTriesKey: uint(3),
RecoveryKeyTriesKey: uint(3),
AuthRequestorUserVisibleNameKey: "data",
},
expectedUnlockKey: recoveryKey,
expectedState: &ContainerActivateState{
Status: ActivationSucceededWithRecoveryKey,
Keyslot: "default-recovery",
},
})
c.Check(err, IsNil)
}

func (s *activateSuite) TestActivateContainerAuthModePIN(c *C) {
// Test a simple case with 2 keyslots with PIN auth.
primaryKey := testutil.DecodeHexString(c, "ed988fada3dbf68e13862cfc52b6d6205c862dd0941e643a81dcab106a79ce6a")
Expand Down Expand Up @@ -5536,6 +5592,62 @@ Error with keyslot "default": cannot recover keys from keyslot: user authorizati
c.Check(err, Equals, ErrCannotActivate)
}

func (s *activateSuite) TestActivateContainerAuthModePINAuthRequestorOnlyReturnsRecoveryKey(c *C) {
// Test a simple case with 2 keyslots with PIN auth and
// a recovery keyslot. Unlocking happens with a recovery keyslot
// after initially entering what looks like a correct PIN
// but the AuthRequestor indicated it was only a recovery key.
primaryKey := testutil.DecodeHexString(c, "ed988fada3dbf68e13862cfc52b6d6205c862dd0941e643a81dcab106a79ce6a")
kd1, unlockKey1 := s.makeKeyDataBlobWithPIN(c, primaryKey, testutil.DecodeHexString(c, "4d8b57f05f0e70a73768c1d9f1078b8e9b0e9c399f555342e1ac4e675fea122e"), "run+recover", makePIN(c, "1234"))
kd2, unlockKey2 := s.makeKeyDataBlobWithPIN(c, primaryKey, testutil.DecodeHexString(c, "d72501b0b558c3119e036d5585629a026e82c05b6a4f19511daa3f12cc37902f"), "recover", makePIN(c, "5678"))

recoveryKey := testutil.DecodeHexString(c, "9124e9a56e40c65424c5f652127f8d18")

authRequestor := &mockAuthRequestor{
responses: []any{
mockAuthRequestorResponse{response: "1234", authTypes: UserAuthTypeRecoveryKey},
mockAuthRequestorResponse{response: makeRecoveryKey(c, recoveryKey), authTypes: UserAuthTypeRecoveryKey},
},
}

err := s.testActivateContextActivateContainer(c, &testActivateContextActivateContainerParams{
contextOpts: []ActivateContextOption{
WithAuthRequestor(authRequestor),
WithPINTries(3),
WithRecoveryKeyTries(3),
},
authRequestor: authRequestor,
container: newMockStorageContainer(
withStorageContainerPath("/dev/sda1"),
withStorageContainerCredentialName("sda1"),
withStorageContainerKeyslot("default", unlockKey1, KeyslotTypePlatform, 0, kd1),
withStorageContainerKeyslot("default-fallback", unlockKey2, KeyslotTypePlatform, 0, kd2),
withStorageContainerKeyslot("default-recovery", recoveryKey, KeyslotTypeRecovery, 0, nil),
),
opts: []ActivateOption{
WithAuthRequestorUserVisibleName("data"),
},
expectedAuthRequestName: "data",
expectedAuthRequestPath: "/dev/sda1",
expectedAuthRequestTypes: []UserAuthType{
UserAuthTypePIN | UserAuthTypeRecoveryKey,
UserAuthTypePIN | UserAuthTypeRecoveryKey,
},
expectedActivateConfig: map[any]any{
AuthRequestorKey: authRequestor,
PinTriesKey: uint(3),
RecoveryKeyTriesKey: uint(3),
AuthRequestorUserVisibleNameKey: "data",
},
expectedUnlockKey: recoveryKey,
expectedState: &ContainerActivateState{
Status: ActivationSucceededWithRecoveryKey,
Keyslot: "default-recovery",
},
})
c.Check(err, IsNil)
}

func (s *activateSuite) TestDeactivateContainer(c *C) {
state := &ActivateState{
Activations: map[string]*ContainerActivateState{
Expand Down
4 changes: 3 additions & 1 deletion auth_requestor.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ type AuthRequestor interface {
// and can be supplied via the ActivateContext API using the
// WithAuthRequestorUserVisibleName option. The authTypes argument is used
// to indicate what types of credential are being requested.
RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, error)
// The implementation returns the requested credential and its type, which
// may be a subset of the requested credential types.
RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, UserAuthType, error)
}
10 changes: 5 additions & 5 deletions auth_requestor_plymouth.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ type plymouthAuthRequestor struct {
stringer PlymouthAuthRequestorStringer
}

func (r *plymouthAuthRequestor) RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, error) {
func (r *plymouthAuthRequestor) RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, UserAuthType, error) {
fmtString, err := r.stringer.RequestUserCredentialFormatString(authTypes)
if err != nil {
return "", fmt.Errorf("cannot request format string for requested auth types: %w", err)
return "", 0, fmt.Errorf("cannot request format string for requested auth types: %w", err)
}
msg := fmt.Sprintf(fmtString, name, path)

Expand All @@ -59,15 +59,15 @@ func (r *plymouthAuthRequestor) RequestUserCredential(ctx context.Context, name,
cmd.Stdout = out
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("cannot execute plymouth ask-for-password: %w", err)
return "", 0, fmt.Errorf("cannot execute plymouth ask-for-password: %w", err)
}
result, err := io.ReadAll(out)
if err != nil {
// The only error returned from bytes.Buffer.Read should be io.EOF,
// which io.ReadAll filters out.
return "", fmt.Errorf("unexpected error: %w", err)
return "", 0, fmt.Errorf("unexpected error: %w", err)
}
return string(result), nil
return string(result), authTypes, nil
}

// NewPlymouthAuthRequestor creates an implementation of AuthRequestor that
Expand Down
9 changes: 5 additions & 4 deletions auth_requestor_plymouth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ func (s *authRequestorPlymouthSuite) testRequestUserCredential(c *C, params *tes
requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer))
c.Assert(err, IsNil)

passphrase, err := requestor.RequestUserCredential(params.ctx, params.name, params.path, params.authTypes)
passphrase, passphraseType, err := requestor.RequestUserCredential(params.ctx, params.name, params.path, params.authTypes)
c.Check(err, IsNil)
c.Check(passphrase, Equals, params.passphrase)
c.Check(passphraseType, Equals, params.authTypes)

c.Check(s.mockPlymouth.Calls(), HasLen, 1)
c.Check(s.mockPlymouth.Calls()[0], DeepEquals, []string{"plymouth", "ask-for-password", "--prompt", params.expectedMsg})
Expand Down Expand Up @@ -231,15 +232,15 @@ func (s *authRequestorPlymouthSuite) TestRequestUserCredentialObtainFormatString
})
c.Assert(err, IsNil)

_, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, `cannot request format string for requested auth types: some error`)
}

func (s *authRequestorPlymouthSuite) TestRequestUserCredentialFailure(c *C) {
requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer))
c.Assert(err, IsNil)

_, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, "cannot execute plymouth ask-for-password: exit status 1")
}

Expand All @@ -252,7 +253,7 @@ func (s *authRequestorPlymouthSuite) TestRequestUserCredentialCanceledContext(c
ctx, cancel := context.WithCancel(context.Background())
cancel()

_, err = requestor.RequestUserCredential(ctx, "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(ctx, "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, "cannot execute plymouth ask-for-password: context canceled")
c.Check(errors.Is(err, context.Canceled), testutil.IsTrue)
}
10 changes: 5 additions & 5 deletions auth_requestor_systemd.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ type systemdAuthRequestor struct {
formatStringFn func(UserAuthType) (string, error)
}

func (r *systemdAuthRequestor) RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, error) {
func (r *systemdAuthRequestor) RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, UserAuthType, error) {
fmtString, err := r.formatStringFn(authTypes)
if err != nil {
return "", fmt.Errorf("cannot request format string for requested auth types: %w", err)
return "", 0, fmt.Errorf("cannot request format string for requested auth types: %w", err)
}
msg := fmt.Sprintf(fmtString, name, path)

Expand All @@ -50,14 +50,14 @@ func (r *systemdAuthRequestor) RequestUserCredential(ctx context.Context, name,
cmd.Stdout = out
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("cannot execute systemd-ask-password: %w", err)
return "", 0, fmt.Errorf("cannot execute systemd-ask-password: %w", err)
}
result, err := out.ReadString('\n')
if err != nil {
// The only error returned from bytes.Buffer.ReadString is io.EOF.
return "", errors.New("systemd-ask-password output is missing terminating newline")
return "", 0, errors.New("systemd-ask-password output is missing terminating newline")
}
return strings.TrimRight(result, "\n"), nil
return strings.TrimRight(result, "\n"), authTypes, nil
}

// NewSystemdAuthRequestor creates an implementation of AuthRequestor that
Expand Down
11 changes: 6 additions & 5 deletions auth_requestor_systemd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *test
})
c.Assert(err, IsNil)

passphrase, err := requestor.RequestUserCredential(params.ctx, params.name, params.path, params.authTypes)
passphrase, passphraseType, err := requestor.RequestUserCredential(params.ctx, params.name, params.path, params.authTypes)
c.Check(err, IsNil)
c.Check(passphrase, Equals, params.passphrase)
c.Check(passphraseType, Equals, params.authTypes)

c.Check(s.mockSdAskPassword.Calls(), HasLen, 1)
c.Check(s.mockSdAskPassword.Calls()[0], DeepEquals, []string{"systemd-ask-password", "--icon", "drive-harddisk",
Expand Down Expand Up @@ -223,7 +224,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialObtainFormatStringE
})
c.Assert(err, IsNil)

_, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, `cannot request format string for requested auth types: some error`)
}

Expand All @@ -235,7 +236,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialInvalidResponse(c *
})
c.Assert(err, IsNil)

_, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, "systemd-ask-password output is missing terminating newline")
}

Expand All @@ -245,7 +246,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialFailure(c *C) {
})
c.Assert(err, IsNil)

_, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, "cannot execute systemd-ask-password: exit status 1")
}

Expand All @@ -260,7 +261,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialCanceledContext(c *
ctx, cancel := context.WithCancel(context.Background())
cancel()

_, err = requestor.RequestUserCredential(ctx, "data", "/dev/sda1", UserAuthTypePassphrase)
_, _, err = requestor.RequestUserCredential(ctx, "data", "/dev/sda1", UserAuthTypePassphrase)
c.Check(err, ErrorMatches, "cannot execute systemd-ask-password: context canceled")
c.Check(errors.Is(err, context.Canceled), testutil.IsTrue)
}
4 changes: 2 additions & 2 deletions crypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func (s *activateWithKeyDataState) run() (success bool, err error) {
// a maximum of 2 keys with passphrases enabled (Ubuntu Core based desktop on
// a UEFI+TPM platform with run+recovery and recovery-only protectors for
// ubuntu-data).
passphrase, err := s.authRequestor.RequestUserCredential(context.Background(), s.volumeName, s.sourceDevicePath, UserAuthTypePassphrase)
passphrase, _, err := s.authRequestor.RequestUserCredential(context.Background(), s.volumeName, s.sourceDevicePath, UserAuthTypePassphrase)
if err != nil {
passphraseErr = xerrors.Errorf("cannot obtain passphrase: %w", err)
continue
Expand Down Expand Up @@ -329,7 +329,7 @@ func activateWithRecoveryKey(volumeName, sourceDevicePath string, authRequestor
for ; tries > 0; tries-- {
lastErr = nil

keyString, err := authRequestor.RequestUserCredential(context.Background(), volumeName, sourceDevicePath, UserAuthTypeRecoveryKey)
keyString, _, err := authRequestor.RequestUserCredential(context.Background(), volumeName, sourceDevicePath, UserAuthTypeRecoveryKey)
if err != nil {
lastErr = xerrors.Errorf("cannot obtain recovery key: %w", err)
continue
Expand Down
Loading
Loading