diff --git a/ssh/certs.go b/ssh/certs.go index b003234..6f75d77 100644 --- a/ssh/certs.go +++ b/ssh/certs.go @@ -444,7 +444,17 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error { if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) { return fmt.Errorf("ssh: cert has expired") } - if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil { + // Match OpenSSH: the SK user-presence flag is never enforced on a + // certificate's CA signature. OpenSSH calls sshkey_verify with + // detailsp==NULL in sshkey.c:cert_parse, so the UP/UV flags are + // not even extracted. The UP bit on a CA signature reflects the + // CA operator's presence at signing time, which has no bearing on + // whether the user being authenticated is present now; enforcing + // it here would only break interop with certificates issued by + // non-interactive SK CAs. skKeyWithoutUP is a no-op for non-SK + // keys (the common case). + caKey := skKeyWithoutUP(cert.SignatureKey) + if err := caKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil { return fmt.Errorf("ssh: certificate signature does not verify") } diff --git a/ssh/certs_test.go b/ssh/certs_test.go index 51fbf88..43280e3 100644 --- a/ssh/certs_test.go +++ b/ssh/certs_test.go @@ -9,8 +9,10 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/sha256" "fmt" "io" + "math/big" "net" "reflect" "testing" @@ -128,6 +130,103 @@ func TestValidateCert(t *testing.T) { } } +// signSKCert builds an SK-ECDSA signature over cert.bytesForSigning() +// using caKey as the simulated hardware token. The SK signing flags +// byte (UP bit and others) is caller-controlled so tests can exercise +// the user-presence enforcement paths. caApp is the SK application +// string (typically "ssh:"). +func signSKCert(t *testing.T, cert *Certificate, caKey *ecdsa.PrivateKey, caApp string, flags byte) { + t.Helper() + cert.SignatureKey = &skECDSAPublicKey{ + application: caApp, + PublicKey: caKey.PublicKey, + } + cert.Nonce = make([]byte, 32) + if _, err := rand.Read(cert.Nonce); err != nil { + t.Fatal(err) + } + + h := sha256.New() + h.Write([]byte(caApp)) + appDigest := h.Sum(nil) + h.Reset() + h.Write(cert.bytesForSigning()) + dataDigest := h.Sum(nil) + + var counter uint32 = 1 + blob := struct { + ApplicationDigest []byte `ssh:"rest"` + Flags byte + Counter uint32 + MessageDigest []byte `ssh:"rest"` + }{appDigest, flags, counter, dataDigest} + h.Reset() + h.Write(Marshal(blob)) + digest := h.Sum(nil) + + r, s, err := ecdsa.Sign(rand.Reader, caKey, digest) + if err != nil { + t.Fatal(err) + } + cert.Signature = &Signature{ + Format: KeyAlgoSKECDSA256, + Blob: Marshal(struct{ R, S *big.Int }{r, s}), + Rest: Marshal(struct { + Flags byte + Counter uint32 + }{flags, counter}), + } +} + +// TestCheckCertSKAuthorityNoUPRequired pins the OpenSSH-parity +// behavior of CertChecker.CheckCert for SK CA signatures: a +// certificate signed by an SK CA that produced a UP=0 signature is +// accepted, because the UP bit on a CA signature has no bearing on +// the current user authentication (see sshkey.c:cert_parse, which +// passes detailsp==NULL to sshkey_verify). Without this, certs issued +// by non-interactive SK CAs would fail to verify. +func TestCheckCertSKAuthorityNoUPRequired(t *testing.T) { + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + userPub, err := NewPublicKey(&userKey.PublicKey) + if err != nil { + t.Fatal(err) + } + + mkCert := func(flags byte) *Certificate { + c := &Certificate{ + CertType: UserCert, + Key: userPub, + ValidBefore: CertTimeInfinity, + ValidPrincipals: []string{"user"}, + } + signSKCert(t, c, caKey, "ssh:", flags) + return c + } + + checker := CertChecker{ + IsUserAuthority: func(k PublicKey) bool { + caSK := &skECDSAPublicKey{application: "ssh:", PublicKey: caKey.PublicKey} + return bytes.Equal(k.Marshal(), caSK.Marshal()) + }, + } + + // Both UP=0 and UP=1 CA signatures verify: UP is not enforced + // here, matching OpenSSH. + if err := checker.CheckCert("user", mkCert(0)); err != nil { + t.Errorf("UP=0 CA signature must verify (OpenSSH parity): %v", err) + } + if err := checker.CheckCert("user", mkCert(flagUserPresence)); err != nil { + t.Errorf("UP=1 CA signature must verify: %v", err) + } +} + func TestValidateCertTime(t *testing.T) { cert := Certificate{ ValidPrincipals: []string{"user"}, diff --git a/ssh/keys.go b/ssh/keys.go index 2b53d95..3482c4d 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -901,11 +901,25 @@ type skFields struct { Counter uint32 } +// flagUserPresence is the "user present" bit (UP) in the SK signature +// flags, matching the FIDO CTAP2 authenticatorData UP flag. See +// openssh/PROTOCOL.u2f. +const flagUserPresence = 0x01 + +// errSKMissingUserPresence is returned by SK key Verify methods when +// the signature does not assert user presence and the key was not +// marked as no-touch-required. +var errSKMissingUserPresence = errors.New("ssh: signature missing required user presence flag") + type skECDSAPublicKey struct { // application is a URL-like string, typically "ssh:" for SSH. // see openssh/PROTOCOL.u2f for details. application string ecdsa.PublicKey + // noTouchRequired, when true, disables the default user-presence + // check in Verify. It is set by skKeyWithoutUP on a clone of the + // key, never on an instance shared across authentication attempts. + noTouchRequired bool } func (k *skECDSAPublicKey) Type() string { @@ -991,6 +1005,10 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error { return err } + if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired { + return errSKMissingUserPresence + } + blob := struct { ApplicationDigest []byte `ssh:"rest"` Flags byte @@ -1024,6 +1042,10 @@ type skEd25519PublicKey struct { // see openssh/PROTOCOL.u2f for details. application string ed25519.PublicKey + // noTouchRequired, when true, disables the default user-presence + // check in Verify. It is set by skKeyWithoutUP on a clone of the + // key, never on an instance shared across authentication attempts. + noTouchRequired bool } func (k *skEd25519PublicKey) Type() string { @@ -1098,6 +1120,10 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error { return err } + if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired { + return errSKMissingUserPresence + } + blob := struct { ApplicationDigest []byte `ssh:"rest"` Flags byte diff --git a/ssh/keys_test.go b/ssh/keys_test.go index 6fd5a4f..93a0a13 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -12,6 +12,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" @@ -841,6 +842,296 @@ func TestSKKeys(t *testing.T) { } } +// skTestHarness builds SK-formatted signatures over a fixed payload +// using a caller-supplied signing function, letting tests vary the UP +// flag byte without duplicating the wire-format scaffolding. +type skTestHarness struct { + format string + application string + data []byte + // sign takes the SHA-256 digest of the marshalled SK blob and + // returns the value to embed in Signature.Blob. For ECDSA keys the + // helper feeds the digest to ecdsa.Sign; for ed25519 the helper + // feeds the raw marshalled blob to ed25519.Sign. + signDigest func(digest []byte) []byte + signBlob func(blob []byte) []byte +} + +func (h skTestHarness) sign(t *testing.T, flags byte) *Signature { + t.Helper() + hsh := sha256.New() + hsh.Write([]byte(h.application)) + appDigest := hsh.Sum(nil) + + hsh.Reset() + hsh.Write(h.data) + dataDigest := hsh.Sum(nil) + + var counter uint32 = 1 + blob := struct { + ApplicationDigest []byte `ssh:"rest"` + Flags byte + Counter uint32 + MessageDigest []byte `ssh:"rest"` + }{appDigest, flags, counter, dataDigest} + marshalled := Marshal(blob) + + var sigBlob []byte + if h.signDigest != nil { + hsh.Reset() + hsh.Write(marshalled) + sigBlob = h.signDigest(hsh.Sum(nil)) + } else { + sigBlob = h.signBlob(marshalled) + } + + return &Signature{ + Format: h.format, + Blob: sigBlob, + Rest: Marshal(struct { + Flags byte + Counter uint32 + }{flags, counter}), + } +} + +func TestSKUserPresence(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + h := skTestHarness{ + format: "sk-ecdsa-sha2-nistp256@openssh.com", + application: "ssh:", + data: []byte("test data"), + signDigest: func(digest []byte) []byte { + r, s, err := ecdsa.Sign(rand.Reader, ecKey, digest) + if err != nil { + t.Fatal(err) + } + return Marshal(struct{ R, S *big.Int }{r, s}) + }, + } + + pk := &skECDSAPublicKey{ + application: h.application, + PublicKey: ecKey.PublicKey, + } + + // Valid signature with UP=1 should pass. + if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil { + t.Errorf("Verify failed with UP=1: %v", err) + } + + // Valid signature with UP=0 should fail with the user-presence sentinel. + sigNoUP := h.sign(t, 0) + if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) { + t.Errorf("expected errSKMissingUserPresence, got: %v", err) + } + + // UV set but UP clear must still fail: we only waive UP, never UV-only. + if err := pk.Verify(h.data, h.sign(t, 0x04)); !errors.Is(err, errSKMissingUserPresence) { + t.Errorf("UV-only (flags=0x04): expected errSKMissingUserPresence, got: %v", err) + } + + // With noTouchRequired, UP=0 passes; UP=1+UV=1 also passes. + pk.noTouchRequired = true + if err := pk.Verify(h.data, sigNoUP); err != nil { + t.Errorf("Verify with noTouchRequired failed: %v", err) + } + if err := pk.Verify(h.data, h.sign(t, flagUserPresence|0x04)); err != nil { + t.Errorf("Verify UP|UV with noTouchRequired failed: %v", err) + } +} + +func TestSKKeyWithoutUP(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + edPub, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + skEC := &skECDSAPublicKey{PublicKey: ecKey.PublicKey} + skED := &skEd25519PublicKey{PublicKey: edPub} + certEC := &Certificate{Key: &skECDSAPublicKey{PublicKey: ecKey.PublicKey}} + certED := &Certificate{Key: &skEd25519PublicKey{PublicKey: edPub}} + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + rsaPub, err := NewPublicKey(&rsaKey.PublicKey) + if err != nil { + t.Fatal(err) + } + certRSA := &Certificate{Key: rsaPub} + + // SK raw keys: return a clone with the flag set, original untouched. + gotEC := skKeyWithoutUP(skEC) + if gotEC == skEC { + t.Error("skECDSAPublicKey: expected a clone, got the original pointer") + } + if !gotEC.(*skECDSAPublicKey).noTouchRequired { + t.Error("skECDSAPublicKey clone: noTouchRequired not set") + } + if skEC.noTouchRequired { + t.Error("skECDSAPublicKey: original mutated") + } + + gotED := skKeyWithoutUP(skED) + if gotED == skED { + t.Error("skEd25519PublicKey: expected a clone, got the original pointer") + } + if !gotED.(*skEd25519PublicKey).noTouchRequired { + t.Error("skEd25519PublicKey clone: noTouchRequired not set") + } + if skED.noTouchRequired { + t.Error("skEd25519PublicKey: original mutated") + } + + // Certificate wrapping SK: return a clone of the cert with a cloned + // SK key inside. Neither the original cert nor the original inner + // key must be mutated. + originalInnerEC := certEC.Key + gotCertEC := skKeyWithoutUP(certEC) + if gotCertEC == certEC { + t.Error("*Certificate(SK ecdsa): expected a clone, got the original pointer") + } + if got := gotCertEC.(*Certificate).Key.(*skECDSAPublicKey); !got.noTouchRequired { + t.Error("*Certificate(SK ecdsa): inner clone missing noTouchRequired") + } + if certEC.Key != originalInnerEC { + t.Error("*Certificate(SK ecdsa): original cert's Key pointer mutated") + } + if originalInnerEC.(*skECDSAPublicKey).noTouchRequired { + t.Error("*Certificate(SK ecdsa): original inner key mutated") + } + + gotCertED := skKeyWithoutUP(certED) + if gotCertED == certED { + t.Error("*Certificate(SK ed25519): expected a clone, got the original pointer") + } + if got := gotCertED.(*Certificate).Key.(*skEd25519PublicKey); !got.noTouchRequired { + t.Error("*Certificate(SK ed25519): inner clone missing noTouchRequired") + } + + // Non-SK key inside a cert: return original unchanged (nothing to clone). + if got := skKeyWithoutUP(certRSA); got != certRSA { + t.Error("*Certificate(RSA): expected the original pointer back") + } + + // Plain non-SK key: return original unchanged. + if got := skKeyWithoutUP(rsaPub); got != rsaPub { + t.Error("rsaPublicKey: expected the original pointer back") + } + + // Pathological: *Certificate whose Key is itself a *Certificate. + // The SSH cert format forbids this and parseCert rejects it, but + // a Go caller can still construct such a value. skKeyWithoutUP + // must not recurse into it (or panic); it returns the input + // unchanged. This also defends against a hypothetical cycle built + // from hand-constructed Certificate pointers. + nestedCert := &Certificate{Key: &Certificate{Key: skEC}} + if got := skKeyWithoutUP(nestedCert); got != nestedCert { + t.Error("*Certificate wrapping *Certificate: expected the original pointer back") + } + selfCycle := &Certificate{} + selfCycle.Key = selfCycle + if got := skKeyWithoutUP(selfCycle); got != selfCycle { + t.Error("self-referential *Certificate: expected the original pointer back") + } +} + +func TestNoTouchAllowed(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + sk := &skECDSAPublicKey{PublicKey: ecKey.PublicKey} + certNoExt := &Certificate{Key: sk} + certOptOut := &Certificate{ + Key: sk, + Permissions: Permissions{Extensions: map[string]string{"no-touch-required": ""}}, + } + // Non-empty value: OpenSSH writes "" but any value must be accepted + // because the check is presence-only. + certOptOutNonEmpty := &Certificate{ + Key: sk, + Permissions: Permissions{Extensions: map[string]string{"no-touch-required": "yes"}}, + } + // no-touch-required belongs in Extensions, never CriticalOptions. + // Putting it in CriticalOptions must NOT be treated as opt-out. + certCritOnly := &Certificate{ + Key: sk, + Permissions: Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}}, + } + + permsEmpty := &Permissions{} + permsOptOut := &Permissions{Extensions: map[string]string{"no-touch-required": ""}} + permsCritOnly := &Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}} + + cases := []struct { + name string + pub PublicKey + perms *Permissions + want bool + }{ + {"nil perms, no cert", sk, nil, false}, + {"empty perms, no cert", sk, permsEmpty, false}, + {"perms opt-out, raw key", sk, permsOptOut, true}, + {"nil perms, cert opt-out", certOptOut, nil, true}, + {"nil perms, cert opt-out non-empty value", certOptOutNonEmpty, nil, true}, + {"nil perms, cert no ext", certNoExt, nil, false}, + {"perms opt-out, cert no ext", certNoExt, permsOptOut, true}, + {"empty perms, cert opt-out", certOptOut, permsEmpty, true}, + // Negative controls: CriticalOptions must not waive UP. + {"critical-options only, raw key", sk, permsCritOnly, false}, + {"critical-options only, cert", certCritOnly, nil, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := noTouchAllowed(tc.pub, tc.perms); got != tc.want { + t.Errorf("noTouchAllowed = %v, want %v", got, tc.want) + } + }) + } +} + +func TestSKUserPresenceEd25519(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + h := skTestHarness{ + format: "sk-ssh-ed25519@openssh.com", + application: "ssh:", + data: []byte("test data"), + signBlob: func(blob []byte) []byte { + return ed25519.Sign(priv, blob) + }, + } + + pk := &skEd25519PublicKey{ + application: h.application, + PublicKey: pub, + } + + if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil { + t.Errorf("Verify failed with UP=1: %v", err) + } + sigNoUP := h.sign(t, 0) + if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) { + t.Errorf("expected errSKMissingUserPresence, got: %v", err) + } + pk.noTouchRequired = true + if err := pk.Verify(h.data, sigNoUP); err != nil { + t.Errorf("Verify with noTouchRequired failed: %v", err) + } +} + func TestNewSignerWithAlgos(t *testing.T) { algorithSigner, ok := testSigners["rsa"].(AlgorithmSigner) if !ok { diff --git a/ssh/server.go b/ssh/server.go index 7f7ecf9..dd1c327 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -34,15 +34,20 @@ type Permissions struct { // or not supported. CriticalOptions map[string]string - // Extensions are extra functionality that the server may - // offer on authenticated connections. Lack of support for an - // extension does not preclude authenticating a user. Common - // extensions are "permit-agent-forwarding", - // "permit-X11-forwarding". The Go SSH library currently does - // not act on any extension, and it is up to server - // implementations to honor them. Extensions can be used to - // pass data from the authentication callbacks to the server - // application layer. + // Extensions are extra functionality that the server may offer on + // authenticated connections. Lack of support for an extension does not + // preclude authenticating a user. Common extensions are + // "permit-agent-forwarding", "permit-X11-forwarding". In general the Go + // SSH library does not act on extensions and it is up to server + // implementations to honor them; extensions can also be used to pass data + // from the authentication callbacks to the server application layer. + // + // The one extension acted upon by this library is "no-touch-required", + // which applies only to security-key public keys + // (sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com). + // When present, it waives the default requirement that SK signatures + // assert user presence (i.e. a physical touch of the authenticator) + // during signature verification. Extensions map[string]string // ExtraData allows to store user defined data. @@ -84,6 +89,79 @@ type ServerPreAuthConn interface { SendAuthBanner(string) error } +// noTouchRequiredExtension is the extension name used by OpenSSH in +// authorized_keys options and certificate extensions to mark keys +// whose signatures do not need to assert user presence (touch). See +// ssh-keygen(1) and sshd(8). +const noTouchRequiredExtension = "no-touch-required" + +// noTouchAllowed reports whether the user presence requirement on +// SK signatures should be waived for this authentication attempt. The +// requirement is waived when the "no-touch-required" extension is +// present either in the Permissions returned by the auth callback +// (authorized_keys-level opt-out) or in the certificate's own +// Extensions (CA-level opt-out), matching OpenSSH behavior. OpenSSH +// reads the per-key opt-out only from cert Extensions and +// authorized_keys options (never from CriticalOptions); we follow the +// same rule. +func noTouchAllowed(pubKey PublicKey, perms *Permissions) bool { + if perms != nil { + if _, ok := perms.Extensions[noTouchRequiredExtension]; ok { + return true + } + } + if cert, ok := pubKey.(*Certificate); ok { + if _, ok := cert.Extensions[noTouchRequiredExtension]; ok { + return true + } + } + return false +} + +// skKeyWithoutUP returns a PublicKey equivalent to pubKey but whose +// Verify accepts SK signatures with the user-presence flag clear. If +// pubKey is not (and does not wrap) an SK key, pubKey is returned +// unchanged. The returned value never mutates pubKey: for SK keys a +// shallow copy is made so that the noTouchRequired flag is set only on +// the clone. +// +// The implementation is iterative rather than recursive. When pubKey +// is a *Certificate we unwrap exactly one level to look at the inner +// key. The SSH cert format forbids Certificate.Key from being another +// Certificate (parseCert rejects it), but nothing stops callers from +// constructing such a value directly in Go; a recursive descent could +// otherwise be driven to unbounded depth by a hand-crafted or cyclic +// Certificate. A malformed input of that shape simply returns +// unchanged here. +func skKeyWithoutUP(pubKey PublicKey) PublicKey { + cert, isCert := pubKey.(*Certificate) + target := pubKey + if isCert { + target = cert.Key + } + var cloned PublicKey + switch k := target.(type) { + case *skECDSAPublicKey: + c := *k + c.noTouchRequired = true + cloned = &c + case *skEd25519PublicKey: + c := *k + c.noTouchRequired = true + cloned = &c + default: + // Not an SK key (or a pathological *Certificate wrapping + // another *Certificate): pubKey is already usable for Verify. + return pubKey + } + if !isCert { + return cloned + } + c := *cert + c.Key = cloned + return &c +} + // ServerConfig holds server specific configuration data. type ServerConfig struct { // Config contains configuration shared between client and server. @@ -739,8 +817,15 @@ userAuthLoop: } signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData) - - if err := pubKey.Verify(signedData, sig); err != nil { + // pubKey is reused below for VerifiedPublicKeyCallback and + // must remain the key as presented by the client; derive a + // separate value for Verify that carries any applicable + // no-touch-required opt-out. + pubKeyForVerify := pubKey + if noTouchAllowed(pubKey, candidate.perms) { + pubKeyForVerify = skKeyWithoutUP(pubKey) + } + if err := pubKeyForVerify.Verify(signedData, sig); err != nil { return nil, err } diff --git a/ssh/server_test.go b/ssh/server_test.go index 9b514ad..2982ca3 100644 --- a/ssh/server_test.go +++ b/ssh/server_test.go @@ -6,9 +6,14 @@ package ssh import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" "errors" "fmt" "io" + "math/big" "net" "reflect" "strings" @@ -904,3 +909,126 @@ func (*markerConn) RemoteAddr() net.Addr { return nil } func (*markerConn) SetDeadline(t time.Time) error { return nil } func (*markerConn) SetReadDeadline(t time.Time) error { return nil } func (*markerConn) SetWriteDeadline(t time.Time) error { return nil } + +// skTestSigner is a Signer that produces SK-ECDSA signatures over +// user-auth data, simulating a FIDO/U2F authenticator. The flags +// byte (UP and other bits) is caller-controlled so tests can exercise +// the server's user-presence enforcement and its opt-out paths. This +// is test-only: the real Signer is hardware-backed. +type skTestSigner struct { + priv *ecdsa.PrivateKey + pub PublicKey + flags byte + application string +} + +func (s *skTestSigner) PublicKey() PublicKey { return s.pub } + +func (s *skTestSigner) Sign(r io.Reader, data []byte) (*Signature, error) { + h := sha256.New() + h.Write([]byte(s.application)) + appDigest := h.Sum(nil) + h.Reset() + h.Write(data) + dataDigest := h.Sum(nil) + var counter uint32 = 1 + blob := struct { + ApplicationDigest []byte `ssh:"rest"` + Flags byte + Counter uint32 + MessageDigest []byte `ssh:"rest"` + }{appDigest, s.flags, counter, dataDigest} + h.Reset() + h.Write(Marshal(blob)) + digest := h.Sum(nil) + x, y, err := ecdsa.Sign(r, s.priv, digest) + if err != nil { + return nil, err + } + return &Signature{ + Format: KeyAlgoSKECDSA256, + Blob: Marshal(struct{ R, S *big.Int }{x, y}), + Rest: Marshal(struct { + Flags byte + Counter uint32 + }{s.flags, counter}), + }, nil +} + +// TestServerAuthSKUserPresence drives the full userAuthLoop with an SK +// public-key client and verifies the server's wiring of the UP check +// and its two opt-out paths: the per-key Permissions.Extensions route +// and (via cert) the cert-level route. It also confirms that non-SK +// clients are unaffected by the new code path. +func TestServerAuthSKUserPresence(t *testing.T) { + userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + skPub := &skECDSAPublicKey{application: "ssh:", PublicKey: userKey.PublicKey} + + runAuth := func(t *testing.T, signer Signer, perms *Permissions) error { + t.Helper() + c1, c2, err := netPipe() + if err != nil { + t.Fatal(err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + PublicKeyCallback: func(ConnMetadata, PublicKey) (*Permissions, error) { + return perms, nil + }, + } + serverConf.AddHostKey(testSigners["ecdsa"]) + + serverErr := make(chan error, 1) + go func() { + _, _, _, err := NewServerConn(c1, serverConf) + serverErr <- err + }() + + clientConf := &ClientConfig{ + User: "user", + Auth: []AuthMethod{PublicKeys(signer)}, + HostKeyCallback: InsecureIgnoreHostKey(), + } + _, _, _, clientErr := NewClientConn(c2, "", clientConf) + <-serverErr + return clientErr + } + + optOut := &Permissions{Extensions: map[string]string{noTouchRequiredExtension: ""}} + + t.Run("UP=1, default perms accepts", func(t *testing.T) { + s := &skTestSigner{priv: userKey, pub: skPub, flags: flagUserPresence, application: "ssh:"} + if err := runAuth(t, s, nil); err != nil { + t.Errorf("expected auth to succeed: %v", err) + } + }) + t.Run("UP=0, default perms rejects", func(t *testing.T) { + s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"} + if err := runAuth(t, s, nil); err == nil { + t.Error("expected auth to fail with UP=0") + } + }) + t.Run("UP=0, perms opt-out accepts", func(t *testing.T) { + s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"} + if err := runAuth(t, s, optOut); err != nil { + t.Errorf("expected auth to succeed with opt-out: %v", err) + } + }) + t.Run("UP=0, perms CriticalOptions does NOT opt out", func(t *testing.T) { + s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"} + critOnly := &Permissions{CriticalOptions: map[string]string{noTouchRequiredExtension: ""}} + if err := runAuth(t, s, critOnly); err == nil { + t.Error("no-touch-required in CriticalOptions must not waive UP") + } + }) + t.Run("non-SK RSA signer unaffected", func(t *testing.T) { + if err := runAuth(t, testSigners["rsa"], nil); err != nil { + t.Errorf("plain RSA auth must still work: %v", err) + } + }) +}