Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f44d03d253 | ||
|
|
5ed494470b | ||
|
|
b07833c067 | ||
|
|
d701c51f7e | ||
|
|
999d053994 | ||
|
|
90f76b8ffe | ||
|
|
b53964a1ca | ||
|
|
626e40fc98 | ||
|
|
31914c699b | ||
|
|
f2135b814c | ||
|
|
ff03dafdb0 | ||
|
|
10b54ffa51 | ||
|
|
0f039f3a4a | ||
|
|
b27e1504e3 | ||
|
|
c1a3b97d70 | ||
|
|
b9229ecc70 | ||
|
|
e557b08ec2 | ||
|
|
4044611da8 | ||
|
|
e8ad5cb7aa | ||
|
|
fe1af3a418 | ||
|
|
243e02a382 | ||
|
|
cdce021fa6 | ||
|
|
d9474cc485 | ||
|
|
7626c50256 | ||
|
|
0471e7969e | ||
|
|
6435c37ab6 | ||
|
|
7d695da948 | ||
|
|
5b7f841599 | ||
|
|
0b316e7ee4 | ||
|
|
55aec0a86b | ||
|
|
5f2de1a9f1 | ||
|
|
45460e0797 | ||
|
|
d37c95e27d | ||
|
|
e2ffffe738 | ||
|
|
60e158ad30 | ||
|
|
1b77d23dc8 | ||
|
|
3872a2bc69 | ||
|
|
9f72eccdf6 | ||
|
|
8f405a4109 | ||
|
|
bb41b3dbe6 | ||
|
|
e04e721997 | ||
|
|
b315afd69a | ||
|
|
d4a85f4749 | ||
|
|
6f39c5226a | ||
|
|
4c4d20b72c | ||
|
|
e3e62d9601 | ||
|
|
5adb68b3e6 | ||
|
|
9beb694f97 | ||
|
|
e5306b2f3e | ||
|
|
d2fe592df1 | ||
|
|
d59570d1a3 | ||
|
|
4aab0d9d6a | ||
|
|
a1c0d99298 | ||
|
|
3c7c86938f | ||
|
|
533fb3f7e4 | ||
|
|
abbc44d451 | ||
|
|
e052873987 | ||
|
|
b61cf853a8 | ||
|
|
9c2cd33e8d | ||
|
|
890731877d | ||
|
|
ffd87b4878 | ||
|
|
4e7a7384ec | ||
|
|
b25012b37b | ||
|
|
6c195c8a97 | ||
|
|
f717e29698 | ||
|
|
e7c36ccb47 | ||
|
|
0fb843a472 | ||
|
|
e3d1254f1e | ||
|
|
a1ce0fee12 | ||
|
|
a749d17980 | ||
|
|
7ee59707ff | ||
|
|
44decbfe70 | ||
|
|
b8a14a8d65 | ||
|
|
9d9d507896 | ||
|
|
fd0b90d21f | ||
|
|
b9e53593a6 | ||
|
|
cc0e4fc1d4 | ||
|
|
a8e9237a21 | ||
|
|
03ca0dcccb | ||
|
|
8400f4a938 | ||
|
|
81c6cb34a8 | ||
|
|
982eaa62df | ||
|
|
159944f128 | ||
|
|
a408498e55 | ||
|
|
cab0f71854 | ||
|
|
2f26647a79 | ||
|
|
e08b06753d | ||
|
|
7d0074ccc6 | ||
|
|
506e022208 | ||
|
|
7dacc380ba | ||
|
|
19acf81bd7 | ||
|
|
3a1c6b4b61 | ||
|
|
f4602e4040 | ||
|
|
4e0068c009 | ||
|
|
e79546e28b | ||
|
|
f91f7a7c31 | ||
|
|
2df4153a03 | ||
|
|
bcf6a849ef | ||
|
|
b4f2b62076 | ||
|
|
79ec3a51fc | ||
|
|
122a78f140 | ||
|
|
c0531f9c34 | ||
|
|
0997000b45 | ||
|
|
017a1aaa2d | ||
|
|
cf29fa96f8 | ||
|
|
0b7aa0cfb0 | ||
|
|
1faea2975c |
+1
-1
@@ -690,7 +690,7 @@ func (c *Client) addNonce(h http.Header) {
|
||||
}
|
||||
|
||||
func (c *Client) fetchNonce(ctx context.Context, url string) (string, error) {
|
||||
r, err := http.NewRequest("HEAD", url, nil)
|
||||
r, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+1
-1
@@ -734,7 +734,7 @@ func TestTLSALPN01ChallengeCert(t *testing.T) {
|
||||
if !acmeExts[0].Critical {
|
||||
t.Errorf("acmeExt.Critical = %v; want true", acmeExts[0].Critical)
|
||||
}
|
||||
if bytes.Compare(acmeExts[0].Value, extValue) != 0 {
|
||||
if !bytes.Equal(acmeExts[0].Value, extValue) {
|
||||
t.Errorf("acmeExt.Value = %v; want %v", acmeExts[0].Value, extValue)
|
||||
}
|
||||
|
||||
|
||||
+27
-39
@@ -134,7 +134,8 @@ type Manager struct {
|
||||
// RenewBefore optionally specifies how early certificates should
|
||||
// be renewed before they expire.
|
||||
//
|
||||
// If zero, they're renewed 30 days before expiration.
|
||||
// If zero, they're renewed at the lesser of 30 days or
|
||||
// 1/3 of the certificate lifetime.
|
||||
RenewBefore time.Duration
|
||||
|
||||
// Client is used to perform low-level operations, such as account registration
|
||||
@@ -247,10 +248,6 @@ func (m *Manager) TLSConfig() *tls.Config {
|
||||
// If GetCertificate is used directly, instead of via Manager.TLSConfig, package users will
|
||||
// also have to add acme.ALPNProto to NextProtos for tls-alpn-01, or use HTTPHandler for http-01.
|
||||
func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if m.Prompt == nil {
|
||||
return nil, errors.New("acme/autocert: Manager.Prompt not set")
|
||||
}
|
||||
|
||||
name := hello.ServerName
|
||||
if name == "" {
|
||||
return nil, errors.New("acme/autocert: missing server name")
|
||||
@@ -464,7 +461,7 @@ func (m *Manager) cert(ctx context.Context, ck certKey) (*tls.Certificate, error
|
||||
leaf: cert.Leaf,
|
||||
}
|
||||
m.state[ck] = s
|
||||
m.startRenew(ck, s.key, s.leaf.NotAfter)
|
||||
m.startRenew(ck, s.key, s.leaf.NotBefore, s.leaf.NotAfter)
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
@@ -567,24 +564,21 @@ func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
|
||||
// If the domain is already being verified, it waits for the existing verification to complete.
|
||||
// Either way, createCert blocks for the duration of the whole process.
|
||||
func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate, error) {
|
||||
// TODO: maybe rewrite this whole piece using sync.Once
|
||||
state, err := m.certState(ck)
|
||||
state, owner, err := m.certState(ck)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// state may exist if another goroutine is already working on it
|
||||
// in which case just wait for it to finish
|
||||
if !state.locked {
|
||||
// If another goroutine is already working on this state, wait for it
|
||||
// to finish by taking the read lock
|
||||
if !owner {
|
||||
state.RLock()
|
||||
defer state.RUnlock()
|
||||
return state.tlscert()
|
||||
}
|
||||
|
||||
// We are the first; state is locked.
|
||||
// Unblock the readers when domain ownership is verified
|
||||
// and we got the cert or the process failed.
|
||||
// We are the first to work on this certKey, so state is write-locked.
|
||||
// Unblock the readers when our work is complete.
|
||||
defer state.Unlock()
|
||||
state.locked = false
|
||||
|
||||
der, leaf, err := m.authorizedCert(ctx, state.key, ck)
|
||||
if err != nil {
|
||||
@@ -610,14 +604,19 @@ func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate,
|
||||
}
|
||||
state.cert = der
|
||||
state.leaf = leaf
|
||||
m.startRenew(ck, state.key, state.leaf.NotAfter)
|
||||
m.startRenew(ck, state.key, state.leaf.NotBefore, state.leaf.NotAfter)
|
||||
return state.tlscert()
|
||||
}
|
||||
|
||||
// certState returns a new or existing certState.
|
||||
// If a new certState is returned, state.exist is false and the state is locked.
|
||||
// certState returns a new or existing certState along with a boolean
|
||||
// indicating whether the caller is the owner of the state.
|
||||
//
|
||||
// The owner of the state is responsible for performing the ACME work and
|
||||
// must unlock the state's write lock when done. Non-owner callers should
|
||||
// wait on the state's read lock for the owner to finish.
|
||||
//
|
||||
// The returned error is non-nil only in the case where a new state could not be created.
|
||||
func (m *Manager) certState(ck certKey) (*certState, error) {
|
||||
func (m *Manager) certState(ck certKey) (*certState, bool, error) {
|
||||
m.stateMu.Lock()
|
||||
defer m.stateMu.Unlock()
|
||||
if m.state == nil {
|
||||
@@ -625,7 +624,7 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
|
||||
}
|
||||
// existing state
|
||||
if state, ok := m.state[ck]; ok {
|
||||
return state, nil
|
||||
return state, false, nil
|
||||
}
|
||||
|
||||
// new locked state
|
||||
@@ -639,16 +638,13 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
|
||||
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
state := &certState{
|
||||
key: key,
|
||||
locked: true,
|
||||
}
|
||||
state := &certState{key: key}
|
||||
state.Lock() // will be unlocked by m.certState caller
|
||||
m.state[ck] = state
|
||||
return state, nil
|
||||
return state, true, nil
|
||||
}
|
||||
|
||||
// authorizedCert starts the domain ownership verification process and requests a new cert upon success.
|
||||
@@ -908,7 +904,7 @@ func httpTokenCacheKey(tokenPath string) string {
|
||||
//
|
||||
// The key argument is a certificate private key.
|
||||
// The exp argument is the cert expiration time (NotAfter).
|
||||
func (m *Manager) startRenew(ck certKey, key crypto.Signer, exp time.Time) {
|
||||
func (m *Manager) startRenew(ck certKey, key crypto.Signer, notBefore, notAfter time.Time) {
|
||||
m.renewalMu.Lock()
|
||||
defer m.renewalMu.Unlock()
|
||||
if m.renewal[ck] != nil {
|
||||
@@ -920,7 +916,7 @@ func (m *Manager) startRenew(ck certKey, key crypto.Signer, exp time.Time) {
|
||||
}
|
||||
dr := &domainRenewal{m: m, ck: ck, key: key}
|
||||
m.renewal[ck] = dr
|
||||
dr.start(exp)
|
||||
dr.start(notBefore, notAfter)
|
||||
}
|
||||
|
||||
// stopRenew stops all currently running cert renewal timers.
|
||||
@@ -1028,13 +1024,6 @@ func (m *Manager) hostPolicy() HostPolicy {
|
||||
return defaultHostPolicy
|
||||
}
|
||||
|
||||
func (m *Manager) renewBefore() time.Duration {
|
||||
if m.RenewBefore > renewJitter {
|
||||
return m.RenewBefore
|
||||
}
|
||||
return 720 * time.Hour // 30 days
|
||||
}
|
||||
|
||||
func (m *Manager) now() time.Time {
|
||||
if m.nowFunc != nil {
|
||||
return m.nowFunc()
|
||||
@@ -1045,10 +1034,9 @@ func (m *Manager) now() time.Time {
|
||||
// certState is ready when its mutex is unlocked for reading.
|
||||
type certState struct {
|
||||
sync.RWMutex
|
||||
locked bool // locked for read/write
|
||||
key crypto.Signer // private key for cert
|
||||
cert [][]byte // DER encoding
|
||||
leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
|
||||
key crypto.Signer // private key for cert
|
||||
cert [][]byte // DER encoding
|
||||
leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
|
||||
}
|
||||
|
||||
// tlscert creates a tls.Certificate from s.key and s.cert.
|
||||
|
||||
@@ -201,7 +201,7 @@ func TestGetCertificate(t *testing.T) {
|
||||
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
||||
man.Prompt = nil
|
||||
},
|
||||
expectError: "Manager.Prompt not set",
|
||||
expectError: "missing Manager.Prompt",
|
||||
},
|
||||
{
|
||||
name: "trailingDot",
|
||||
@@ -513,6 +513,33 @@ func TestGetCertificate_failedAttempt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCertificate_concurrent guards against a data race on certState
|
||||
// fields between the goroutine performing the ACME work for a domain and
|
||||
// the other goroutines waiting on the result. See golang/go#80119.
|
||||
func TestGetCertificate_concurrent(t *testing.T) {
|
||||
// An unreachable directory URL makes the owner goroutine fail
|
||||
// quickly inside createCert, so its writes overlap with the
|
||||
// non-owner goroutines reaching the same code path.
|
||||
m := &Manager{
|
||||
Prompt: AcceptTOS,
|
||||
Client: &acme.Client{DirectoryURL: "http://127.0.0.1:1/"},
|
||||
}
|
||||
defer m.stopRenew()
|
||||
|
||||
start := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 64; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
m.GetCertificate(clientHelloInfo(exampleDomain, algECDSA))
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestRevokeFailedAuthz(t *testing.T) {
|
||||
ca := acmetest.NewCAServer(t)
|
||||
// Make the authz unfulfillable on the client side, so it will be left
|
||||
|
||||
@@ -145,7 +145,7 @@ func (ca *CAServer) URL() string {
|
||||
return ca.url
|
||||
}
|
||||
|
||||
// Roots returns a pool cointaining the CA root.
|
||||
// Roots returns a pool containing the CA root.
|
||||
func (ca *CAServer) Roots() *x509.CertPool {
|
||||
if ca.url == "" {
|
||||
panic("Roots called before Start")
|
||||
@@ -239,7 +239,8 @@ type discovery struct {
|
||||
}
|
||||
|
||||
type discoveryMeta struct {
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
|
||||
Terms string `json:"termsOfService,omitempty"`
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
|
||||
}
|
||||
|
||||
type challenge struct {
|
||||
@@ -281,6 +282,7 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
|
||||
NewAccount: ca.serverURL("/new-account"),
|
||||
NewOrder: ca.serverURL("/new-order"),
|
||||
Meta: discoveryMeta{
|
||||
Terms: ca.serverURL("/terms"),
|
||||
ExternalAccountRequired: ca.eabRequired,
|
||||
},
|
||||
}
|
||||
|
||||
+19
-17
@@ -11,9 +11,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// renewJitter is the maximum deviation from Manager.RenewBefore.
|
||||
const renewJitter = time.Hour
|
||||
|
||||
// domainRenewal tracks the state used by the periodic timers
|
||||
// renewing a single domain's cert.
|
||||
type domainRenewal struct {
|
||||
@@ -30,13 +27,13 @@ type domainRenewal struct {
|
||||
// defined by the certificate expiration time exp.
|
||||
//
|
||||
// If the timer is already started, calling start is a noop.
|
||||
func (dr *domainRenewal) start(exp time.Time) {
|
||||
func (dr *domainRenewal) start(notBefore, notAfter time.Time) {
|
||||
dr.timerMu.Lock()
|
||||
defer dr.timerMu.Unlock()
|
||||
if dr.timer != nil {
|
||||
return
|
||||
}
|
||||
dr.timer = time.AfterFunc(dr.next(exp), dr.renew)
|
||||
dr.timer = time.AfterFunc(dr.next(notBefore, notAfter), dr.renew)
|
||||
}
|
||||
|
||||
// stop stops the cert renewal timer and waits for any in-flight calls to renew
|
||||
@@ -79,7 +76,7 @@ func (dr *domainRenewal) renew() {
|
||||
// TODO: rotate dr.key at some point?
|
||||
next, err := dr.do(ctx)
|
||||
if err != nil {
|
||||
next = renewJitter / 2
|
||||
next = time.Hour / 2
|
||||
next += time.Duration(pseudoRand.int63n(int64(next)))
|
||||
}
|
||||
testDidRenewLoop(next, err)
|
||||
@@ -107,8 +104,8 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
|
||||
// a race is likely unavoidable in a distributed environment
|
||||
// but we try nonetheless
|
||||
if tlscert, err := dr.m.cacheGet(ctx, dr.ck); err == nil {
|
||||
next := dr.next(tlscert.Leaf.NotAfter)
|
||||
if next > dr.m.renewBefore()+renewJitter {
|
||||
next := dr.next(tlscert.Leaf.NotBefore, tlscert.Leaf.NotAfter)
|
||||
if next > 0 {
|
||||
signer, ok := tlscert.PrivateKey.(crypto.Signer)
|
||||
if ok {
|
||||
state := &certState{
|
||||
@@ -139,18 +136,23 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
|
||||
return 0, err
|
||||
}
|
||||
dr.updateState(state)
|
||||
return dr.next(leaf.NotAfter), nil
|
||||
return dr.next(leaf.NotBefore, leaf.NotAfter), nil
|
||||
}
|
||||
|
||||
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
|
||||
d := expiry.Sub(dr.m.now()) - dr.m.renewBefore()
|
||||
// add a bit of randomness to renew deadline
|
||||
n := pseudoRand.int63n(int64(renewJitter))
|
||||
d -= time.Duration(n)
|
||||
if d < 0 {
|
||||
return 0
|
||||
// next returns the wait time before the next renewal should start.
|
||||
// If manager.RenewBefore is set, it uses that capped at 30 days,
|
||||
// otherwise it uses a default of 1/3 of the cert lifetime.
|
||||
// It builds in a jitter of 10% of the renew threshold, capped at 1 hour.
|
||||
func (dr *domainRenewal) next(notBefore, notAfter time.Time) time.Duration {
|
||||
threshold := min(notAfter.Sub(notBefore)/3, 30*24*time.Hour)
|
||||
if dr.m.RenewBefore > 0 {
|
||||
threshold = min(dr.m.RenewBefore, 30*24*time.Hour)
|
||||
}
|
||||
return d
|
||||
maxJitter := min(threshold/10, time.Hour)
|
||||
jitter := pseudoRand.int63n(int64(maxJitter))
|
||||
renewAt := notAfter.Add(-(threshold - time.Duration(jitter)))
|
||||
renewWait := renewAt.Sub(dr.m.now())
|
||||
return max(0, renewWait)
|
||||
}
|
||||
|
||||
var testDidRenewLoop = func(next time.Duration, err error) {}
|
||||
|
||||
@@ -17,27 +17,60 @@ import (
|
||||
|
||||
func TestRenewalNext(t *testing.T) {
|
||||
now := time.Now()
|
||||
man := &Manager{
|
||||
RenewBefore: 7 * 24 * time.Hour,
|
||||
nowFunc: func() time.Time { return now },
|
||||
}
|
||||
defer man.stopRenew()
|
||||
nowFn := func() time.Time { return now }
|
||||
tt := []struct {
|
||||
expiry time.Time
|
||||
min, max time.Duration
|
||||
name string
|
||||
renewBefore time.Duration // arg to Manager
|
||||
// leaf cert validity
|
||||
notBefore time.Time
|
||||
validFor time.Duration
|
||||
// wait time
|
||||
waitMin, waitMax time.Duration
|
||||
}{
|
||||
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - renewJitter, 83 * 24 * time.Hour},
|
||||
{now.Add(time.Hour), 0, 1},
|
||||
{now, 0, 1},
|
||||
{now.Add(-time.Hour), 0, 1},
|
||||
{"default renewal, 1h cert, valid",
|
||||
0, now, time.Hour, 40 * time.Minute, 50 * time.Minute},
|
||||
{"default renewal, 1h cert, should renew",
|
||||
0, now.Add(-50 * time.Minute), time.Hour, 0, 0},
|
||||
{"default renewal, 1h cert, expired",
|
||||
0, now.Add(-400 * 24 * time.Hour), time.Hour, 0, 0},
|
||||
{"default renewal, 6d cert, valid",
|
||||
0, now, 6 * 24 * time.Hour, 4 * 24 * time.Hour, (4*24 + 1) * time.Hour},
|
||||
{"default renewal, 6d cert, should renew",
|
||||
0, now.Add(-5 * 24 * time.Hour), 6 * 24 * time.Hour, 0, 0},
|
||||
{"default renewal, 6d cert, expired",
|
||||
0, now.Add(-400 * 24 * time.Hour), 6 * 24 * time.Hour, 0, 0},
|
||||
{"default renewal, 90d cert, valid",
|
||||
0, now, 90 * 24 * time.Hour, 60 * 24 * time.Hour, (60*24 + 1) * time.Hour},
|
||||
{"default renewal, 90d cert, should renew",
|
||||
0, now.Add(-70 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
|
||||
{"default renewal, 90d cert, expired",
|
||||
0, now.Add(-400 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
|
||||
{"default renewal, 398d cert, valid",
|
||||
0, now, 398 * 24 * time.Hour, (368 * 24) * time.Hour, (368*24 + 1) * time.Hour},
|
||||
{"default renewal, 398d cert, should renew",
|
||||
0, now.Add(-378 * 24 * time.Hour), 398 * 24 * time.Hour, 0, 0},
|
||||
{"default renewal, 398d cert, expired",
|
||||
0, now.Add(-400 * 24 * time.Hour), 398 * 24 * time.Hour, 0, 0},
|
||||
{"7d renewal, 90d cert, valid",
|
||||
7 * 24 * time.Hour, now, 90 * 24 * time.Hour, 83 * 24 * time.Hour, (83*24 + 1) * time.Hour},
|
||||
{"7d renewal, 90d cert, should not renew",
|
||||
7 * 24 * time.Hour, now.Add(-70 * 24 * time.Hour), 90 * 24 * time.Hour, 13 * 24 * time.Hour, (13*24 + 1) * time.Hour},
|
||||
{"7d renewal, 90d cert, should renew",
|
||||
7 * 24 * time.Hour, now.Add(-85 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
|
||||
{"7d renewal, 90d cert, expired",
|
||||
7 * 24 * time.Hour, now.Add(-400 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
|
||||
}
|
||||
|
||||
dr := &domainRenewal{m: man}
|
||||
for i, test := range tt {
|
||||
next := dr.next(test.expiry)
|
||||
if next < test.min || test.max < next {
|
||||
t.Errorf("%d: next = %v; want between %v and %v", i, next, test.min, test.max)
|
||||
}
|
||||
for _, test := range tt {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dr := &domainRenewal{m: &Manager{RenewBefore: test.renewBefore, nowFunc: nowFn}}
|
||||
defer dr.m.stopRenew()
|
||||
|
||||
next := dr.next(test.notBefore, test.notBefore.Add(test.validFor))
|
||||
if next < test.waitMin || next > test.waitMax {
|
||||
t.Errorf("expected wait time: %v <= %v <= %v", test.waitMin, next, test.waitMax)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +180,6 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) {
|
||||
|
||||
// set internal state to an almost expired cert
|
||||
oldCert := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldLeaf, err := validCert(exampleCertKey, oldCert.Certificate, oldCert.PrivateKey.(crypto.Signer), now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -239,7 +269,7 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) {
|
||||
}
|
||||
|
||||
// trigger renew
|
||||
man.startRenew(exampleCertKey, s.key, s.leaf.NotAfter)
|
||||
man.startRenew(exampleCertKey, s.key, s.leaf.NotBefore, s.leaf.NotAfter)
|
||||
<-renewed
|
||||
func() {
|
||||
man.renewalMu.Lock()
|
||||
|
||||
+2
-2
@@ -128,7 +128,7 @@ func wantStatus(codes ...int) resOkay {
|
||||
func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
|
||||
retry := c.retryTimer()
|
||||
for {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string,
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
+2
-2
@@ -383,7 +383,7 @@ func testIssuance(t *testing.T, env *environment, challSrv challengeServer) {
|
||||
order, err = client.WaitOrder(ctx, order.URI)
|
||||
if err != nil {
|
||||
var orderErr *acme.OrderError
|
||||
if errors.Is(err, orderErr) {
|
||||
if errors.As(err, &orderErr) {
|
||||
t.Fatalf("failed to wait for order %s: %s: %s", orderURL, err, orderErr.Problem)
|
||||
} else {
|
||||
t.Fatalf("failed to wait for order %s: %s", orderURL, err)
|
||||
@@ -757,7 +757,7 @@ func prepareBinaries(t *testing.T, pebbleDir string) string {
|
||||
|
||||
// We don't want to build in the module cache dir, which might not be
|
||||
// writable or to pollute the user's clone with binaries if pebbleLocalDir
|
||||
//is used.
|
||||
// is used.
|
||||
binDir := t.TempDir()
|
||||
|
||||
build := func(cmd string) {
|
||||
|
||||
+4
-1
@@ -53,6 +53,9 @@ func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tos
|
||||
Contact: acct.Contact,
|
||||
}
|
||||
if c.dir.Terms != "" {
|
||||
if prompt == nil {
|
||||
return nil, errors.New("acme: missing Manager.Prompt to accept server's terms of service")
|
||||
}
|
||||
req.TermsAgreed = prompt(c.dir.Terms)
|
||||
}
|
||||
|
||||
@@ -232,7 +235,7 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO
|
||||
return responseOrder(res)
|
||||
}
|
||||
|
||||
// GetOrder retrives an order identified by the given URL.
|
||||
// GetOrder retrieves an order identified by the given URL.
|
||||
// For orders created with AuthorizeOrder, the url value is Order.URI.
|
||||
//
|
||||
// If a caller needs to poll an order until its status is final,
|
||||
|
||||
+10
-5
@@ -114,14 +114,15 @@ type Error struct {
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
str := fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
|
||||
if len(e.Subproblems) > 0 {
|
||||
str += fmt.Sprintf("; subproblems:")
|
||||
sb.WriteString("; subproblems:")
|
||||
for _, sp := range e.Subproblems {
|
||||
str += fmt.Sprintf("\n\t%s", sp)
|
||||
fmt.Fprintf(&sb, "\n\t%s", sp)
|
||||
}
|
||||
}
|
||||
return str
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// AuthorizationError indicates that an authorization for an identifier
|
||||
@@ -167,7 +168,11 @@ type OrderError struct {
|
||||
}
|
||||
|
||||
func (oe *OrderError) Error() string {
|
||||
return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
|
||||
str := fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
|
||||
if oe.Problem != nil {
|
||||
str += fmt.Sprintf("; problem: %s", oe.Problem)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// RateLimit reports whether err represents a rate limit error and
|
||||
|
||||
@@ -217,3 +217,19 @@ func TestErrorStringerWithSubproblems(t *testing.T) {
|
||||
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderErrorProblem(t *testing.T) {
|
||||
oe := &OrderError{
|
||||
OrderURL: "url",
|
||||
Status: "invalid",
|
||||
Problem: &Error{
|
||||
StatusCode: 400,
|
||||
ProblemType: "type",
|
||||
Detail: "detail",
|
||||
},
|
||||
}
|
||||
want := "acme: order url status: invalid; problem: 400 type: detail"
|
||||
if got := oe.Error(); got != want {
|
||||
t.Errorf("oe.Error() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-17
@@ -6,7 +6,7 @@
|
||||
// Argon2 was selected as the winner of the Password Hashing Competition and can
|
||||
// be used to derive cryptographic keys from passwords.
|
||||
//
|
||||
// For a detailed specification of Argon2 see [1].
|
||||
// For a detailed specification of Argon2 see [argon2-specs.pdf].
|
||||
//
|
||||
// If you aren't sure which function you need, use Argon2id (IDKey) and
|
||||
// the parameter recommendations for your scenario.
|
||||
@@ -17,8 +17,8 @@
|
||||
// It uses data-independent memory access, which is preferred for password
|
||||
// hashing and password-based key derivation. Argon2i requires more passes over
|
||||
// memory than Argon2id to protect from trade-off attacks. The recommended
|
||||
// parameters (taken from [2]) for non-interactive operations are time=3 and to
|
||||
// use the maximum available memory.
|
||||
// parameters (taken from [RFC 9106 Section 7.3]) for non-interactive
|
||||
// operations are time=3 and to use the maximum available memory.
|
||||
//
|
||||
// # Argon2id
|
||||
//
|
||||
@@ -26,12 +26,15 @@
|
||||
// Argon2i and Argon2d. It uses data-independent memory access for the first
|
||||
// half of the first iteration over the memory and data-dependent memory access
|
||||
// for the rest. Argon2id is side-channel resistant and provides better brute-
|
||||
// force cost savings due to time-memory tradeoffs than Argon2i. The recommended
|
||||
// parameters for non-interactive operations (taken from [2]) are time=1 and to
|
||||
// use the maximum available memory.
|
||||
// force cost savings due to time-memory tradeoffs than Argon2i. [RFC 9106
|
||||
// Section 4] recommends time=1, memory=2*1024*1024 KiB (2 GiB), and threads=4
|
||||
// as the first recommended option. If much less memory is available, it
|
||||
// recommends time=3, memory=64*1024 KiB (64 MiB), and threads=4 as the second
|
||||
// recommended option.
|
||||
//
|
||||
// [1] https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
|
||||
// [2] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-9.3
|
||||
// [argon2-specs.pdf]: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
|
||||
// [RFC 9106 Section 4]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
|
||||
// [RFC 9106 Section 7.3]: https://www.rfc-editor.org/rfc/rfc9106.html#section-7.3
|
||||
package argon2
|
||||
|
||||
import (
|
||||
@@ -59,9 +62,9 @@ const (
|
||||
//
|
||||
// key := argon2.Key([]byte("some password"), salt, 3, 32*1024, 4, 32)
|
||||
//
|
||||
// The draft RFC recommends[2] time=3, and memory=32*1024 is a sensible number.
|
||||
// If using that amount of memory (32 MB) is not possible in some contexts then
|
||||
// the time parameter can be increased to compensate.
|
||||
// The example above uses time=3 and memory=32*1024. Argon2i generally
|
||||
// requires more passes over memory than Argon2id. If in doubt, prefer IDKey
|
||||
// and its Argon2id parameter recommendations.
|
||||
//
|
||||
// The time parameter specifies the number of passes over the memory and the
|
||||
// memory parameter specifies the size of the memory in KiB. For example
|
||||
@@ -81,18 +84,20 @@ func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint3
|
||||
// For example, you can get a derived key for e.g. AES-256 (which needs a
|
||||
// 32-byte key) by doing:
|
||||
//
|
||||
// key := argon2.IDKey([]byte("some password"), salt, 1, 64*1024, 4, 32)
|
||||
// key := argon2.IDKey([]byte("some password"), salt, 1, 2*1024*1024, 4, 32)
|
||||
//
|
||||
// The draft RFC recommends[2] time=1, and memory=64*1024 is a sensible number.
|
||||
// If using that amount of memory (64 MB) is not possible in some contexts then
|
||||
// the time parameter can be increased to compensate.
|
||||
// The example above uses the first [RFC 9106 Section 4] recommended option.
|
||||
// If much less memory is available, the second recommended option is time=3,
|
||||
// memory=64*1024 KiB (64 MiB), and threads=4.
|
||||
//
|
||||
// The time parameter specifies the number of passes over the memory and the
|
||||
// memory parameter specifies the size of the memory in KiB. For example
|
||||
// memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be
|
||||
// adjusted to the numbers of available CPUs. The cost parameters should be
|
||||
// memory=2*1024*1024 sets the memory cost to ~2 GiB. The number of threads can
|
||||
// be adjusted to the numbers of available CPUs. The cost parameters should be
|
||||
// increased as memory latency and CPU parallelism increases. Remember to get a
|
||||
// good random salt.
|
||||
//
|
||||
// [RFC 9106 Section 4]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
|
||||
func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
||||
return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var _ hash.XOF = (*xof)(nil)
|
||||
|
||||
func fromHex(s string) []byte {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ loop:
|
||||
MOVD $NUM_ROUNDS, R21
|
||||
VLD1 (R11), [V30.S4, V31.S4]
|
||||
|
||||
// load contants
|
||||
// load constants
|
||||
// VLD4R (R10), [V0.S4, V1.S4, V2.S4, V3.S4]
|
||||
WORD $0x4D60E940
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ type chacha20poly1305 struct {
|
||||
|
||||
// New returns a ChaCha20-Poly1305 AEAD that uses the given 256-bit key.
|
||||
func New(key []byte) (cipher.AEAD, error) {
|
||||
if fips140Enforced() {
|
||||
return nil, errors.New("chacha20poly1305: use of ChaCha20Poly1305 is not allowed in FIPS 140-only mode")
|
||||
}
|
||||
if len(key) != KeySize {
|
||||
return nil, errors.New("chacha20poly1305: bad key length")
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func chacha20Poly1305Open(dst []byte, key []uint32, src, ad []byte) bool
|
||||
func chacha20Poly1305Seal(dst []byte, key []uint32, src, ad []byte)
|
||||
|
||||
var (
|
||||
useAVX2 = cpu.X86.HasAVX2 && cpu.X86.HasBMI2
|
||||
useAVX2 = cpu.X86.HasSSSE3 && cpu.X86.HasAVX2 && cpu.X86.HasBMI2
|
||||
)
|
||||
|
||||
// setupState writes a ChaCha20 input matrix to state. See
|
||||
@@ -47,7 +47,7 @@ func setupState(state *[16]uint32, key *[32]byte, nonce []byte) {
|
||||
}
|
||||
|
||||
func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||
if !cpu.X86.HasSSSE3 {
|
||||
if !useAVX2 {
|
||||
return c.sealGeneric(dst, nonce, plaintext, additionalData)
|
||||
}
|
||||
|
||||
@@ -56,14 +56,17 @@ func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []
|
||||
|
||||
ret, out := sliceForAppend(dst, len(plaintext)+16)
|
||||
if alias.InexactOverlap(out, plaintext) {
|
||||
panic("chacha20poly1305: invalid buffer overlap")
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and input")
|
||||
}
|
||||
if alias.AnyOverlap(out, additionalData) {
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and additional data")
|
||||
}
|
||||
chacha20Poly1305Seal(out[:], state[:], plaintext, additionalData)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
if !cpu.X86.HasSSSE3 {
|
||||
if !useAVX2 {
|
||||
return c.openGeneric(dst, nonce, ciphertext, additionalData)
|
||||
}
|
||||
|
||||
@@ -73,7 +76,10 @@ func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) (
|
||||
ciphertext = ciphertext[:len(ciphertext)-16]
|
||||
ret, out := sliceForAppend(dst, len(ciphertext))
|
||||
if alias.InexactOverlap(out, ciphertext) {
|
||||
panic("chacha20poly1305: invalid buffer overlap")
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and input")
|
||||
}
|
||||
if alias.AnyOverlap(out, additionalData) {
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and additional data")
|
||||
}
|
||||
if !chacha20Poly1305Open(out, state[:], ciphertext, additionalData) {
|
||||
for i := range out {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,10 @@ func (c *chacha20poly1305) sealGeneric(dst, nonce, plaintext, additionalData []b
|
||||
ret, out := sliceForAppend(dst, len(plaintext)+poly1305.TagSize)
|
||||
ciphertext, tag := out[:len(plaintext)], out[len(plaintext):]
|
||||
if alias.InexactOverlap(out, plaintext) {
|
||||
panic("chacha20poly1305: invalid buffer overlap")
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and input")
|
||||
}
|
||||
if alias.AnyOverlap(out, additionalData) {
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and additional data")
|
||||
}
|
||||
|
||||
var polyKey [32]byte
|
||||
@@ -67,7 +70,10 @@ func (c *chacha20poly1305) openGeneric(dst, nonce, ciphertext, additionalData []
|
||||
|
||||
ret, out := sliceForAppend(dst, len(ciphertext))
|
||||
if alias.InexactOverlap(out, ciphertext) {
|
||||
panic("chacha20poly1305: invalid buffer overlap")
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and input")
|
||||
}
|
||||
if alias.AnyOverlap(out, additionalData) {
|
||||
panic("chacha20poly1305: invalid buffer overlap of output and additional data")
|
||||
}
|
||||
if !p.Verify(tag) {
|
||||
for i := range out {
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestRandom(t *testing.T) {
|
||||
t.Run("X", func(t *testing.T) { f(t, NonceSizeX) })
|
||||
}
|
||||
|
||||
func benchamarkChaCha20Poly1305Seal(b *testing.B, buf []byte, nonceSize int) {
|
||||
func benchmarkChaCha20Poly1305Seal(b *testing.B, buf []byte, nonceSize int) {
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(buf)))
|
||||
|
||||
@@ -176,7 +176,7 @@ func benchamarkChaCha20Poly1305Seal(b *testing.B, buf []byte, nonceSize int) {
|
||||
}
|
||||
}
|
||||
|
||||
func benchamarkChaCha20Poly1305Open(b *testing.B, buf []byte, nonceSize int) {
|
||||
func benchmarkChaCha20Poly1305Open(b *testing.B, buf []byte, nonceSize int) {
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(buf)))
|
||||
|
||||
@@ -204,17 +204,17 @@ func benchamarkChaCha20Poly1305Open(b *testing.B, buf []byte, nonceSize int) {
|
||||
func BenchmarkChacha20Poly1305(b *testing.B) {
|
||||
for _, length := range []int{64, 1350, 8 * 1024} {
|
||||
b.Run("Open-"+strconv.Itoa(length), func(b *testing.B) {
|
||||
benchamarkChaCha20Poly1305Open(b, make([]byte, length), NonceSize)
|
||||
benchmarkChaCha20Poly1305Open(b, make([]byte, length), NonceSize)
|
||||
})
|
||||
b.Run("Seal-"+strconv.Itoa(length), func(b *testing.B) {
|
||||
benchamarkChaCha20Poly1305Seal(b, make([]byte, length), NonceSize)
|
||||
benchmarkChaCha20Poly1305Seal(b, make([]byte, length), NonceSize)
|
||||
})
|
||||
|
||||
b.Run("Open-"+strconv.Itoa(length)+"-X", func(b *testing.B) {
|
||||
benchamarkChaCha20Poly1305Open(b, make([]byte, length), NonceSizeX)
|
||||
benchmarkChaCha20Poly1305Open(b, make([]byte, length), NonceSizeX)
|
||||
})
|
||||
b.Run("Seal-"+strconv.Itoa(length)+"-X", func(b *testing.B) {
|
||||
benchamarkChaCha20Poly1305Seal(b, make([]byte, length), NonceSizeX)
|
||||
benchmarkChaCha20Poly1305Seal(b, make([]byte, length), NonceSizeX)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.25
|
||||
//go:build !go1.26
|
||||
|
||||
package blake2b
|
||||
package chacha20poly1305
|
||||
|
||||
import "hash"
|
||||
|
||||
var _ hash.XOF = (*xof)(nil)
|
||||
func fips140Enforced() bool { return false }
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.26
|
||||
|
||||
package chacha20poly1305
|
||||
|
||||
import "crypto/fips140"
|
||||
|
||||
func fips140Enforced() bool { return fips140.Enforced() }
|
||||
@@ -22,6 +22,9 @@ type xchacha20poly1305 struct {
|
||||
// preferred when nonce uniqueness cannot be trivially ensured, or whenever
|
||||
// nonces are randomly generated.
|
||||
func NewX(key []byte) (cipher.AEAD, error) {
|
||||
if fips140Enforced() {
|
||||
return nil, errors.New("chacha20poly1305: use of ChaCha20Poly1305 is not allowed in FIPS 140-only mode")
|
||||
}
|
||||
if len(key) != KeySize {
|
||||
return nil, errors.New("chacha20poly1305: bad key length")
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module golang.org/x/crypto
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.45.0 // tagx:ignore
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/term v0.36.0
|
||||
golang.org/x/net v0.57.0 // tagx:ignore
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
)
|
||||
|
||||
require golang.org/x/text v0.30.0 // indirect
|
||||
require golang.org/x/text v0.41.0 // indirect
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
|
||||
+13
-8
@@ -11,6 +11,7 @@
|
||||
package hkdf
|
||||
|
||||
import (
|
||||
"crypto/hkdf"
|
||||
"crypto/hmac"
|
||||
"errors"
|
||||
"hash"
|
||||
@@ -24,15 +25,19 @@ import (
|
||||
// Expand invocations and different context values. Most common scenarios,
|
||||
// including the generation of multiple keys, should use New instead.
|
||||
func Extract(hash func() hash.Hash, secret, salt []byte) []byte {
|
||||
if salt == nil {
|
||||
salt = make([]byte, hash().Size())
|
||||
// Use the stdlib Extract, which disables FIPS 140 enforcement of the HMAC
|
||||
// key (which in HKDF is the salt). The only possible error is FIPS 140
|
||||
// enforcement of the hash, which had to panic under this API anyway. We
|
||||
// don't use the stdlib Expand, because it switched to returning a []byte
|
||||
// instead of an io.Reader, and Expand uses the HMAC key as a key.
|
||||
out, err := hkdf.Extract(hash, secret, salt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
extractor := hmac.New(hash, salt)
|
||||
extractor.Write(secret)
|
||||
return extractor.Sum(nil)
|
||||
return out
|
||||
}
|
||||
|
||||
type hkdf struct {
|
||||
type hkdfReader struct {
|
||||
expander hash.Hash
|
||||
size int
|
||||
|
||||
@@ -43,7 +48,7 @@ type hkdf struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (f *hkdf) Read(p []byte) (int, error) {
|
||||
func (f *hkdfReader) Read(p []byte) (int, error) {
|
||||
// Check whether enough data can be generated
|
||||
need := len(p)
|
||||
remains := len(f.buf) + int(255-f.counter+1)*f.size
|
||||
@@ -84,7 +89,7 @@ func (f *hkdf) Read(p []byte) (int, error) {
|
||||
// 3.3. Most common scenarios will want to use New instead.
|
||||
func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader {
|
||||
expander := hmac.New(hash, pseudorandomKey)
|
||||
return &hkdf{expander, expander.Size(), info, 1, nil, nil}
|
||||
return &hkdfReader{expander, expander.Size(), info, 1, nil, nil}
|
||||
}
|
||||
|
||||
// New returns a Reader, from which keys can be read, using the given hash,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (!amd64 && !loong64 && !ppc64le && !ppc64 && !s390x) || !gc || purego
|
||||
//go:build (!amd64 && !loong64 && !ppc64le && !ppc64 && !riscv64 && !s390x) || !gc || purego
|
||||
|
||||
package poly1305
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build gc && !purego && (amd64 || loong64 || ppc64 || ppc64le)
|
||||
//go:build gc && !purego && (amd64 || loong64 || ppc64 || ppc64le || riscv64)
|
||||
|
||||
package poly1305
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build gc && !purego
|
||||
|
||||
#define LOAD64U(base, offset, t0, t1, t2, t3, dst) \
|
||||
MOVBU (offset+0*1)(base), t0; \
|
||||
MOVBU (offset+1*1)(base), t1; \
|
||||
MOVBU (offset+2*1)(base), t2; \
|
||||
MOVBU (offset+3*1)(base), t3; \
|
||||
SLL $8, t1; \
|
||||
SLL $16, t2; \
|
||||
SLL $24, t3; \
|
||||
OR t1, t0; \
|
||||
OR t3, t2; \
|
||||
OR t2, t0, dst; \
|
||||
MOVBU (offset+4*1)(base), t0; \
|
||||
MOVBU (offset+5*1)(base), t1; \
|
||||
MOVBU (offset+6*1)(base), t2; \
|
||||
MOVBU (offset+7*1)(base), t3; \
|
||||
SLL $32, t0; \
|
||||
SLL $40, t1; \
|
||||
SLL $48, t2; \
|
||||
SLL $56, t3; \
|
||||
OR t1, t0; \
|
||||
OR t3, t2; \
|
||||
OR t2, t0; \
|
||||
OR t0, dst
|
||||
|
||||
// func update(state *macState, msg []byte)
|
||||
TEXT ·update(SB), $0-32
|
||||
MOV state+0(FP), X5
|
||||
MOV msg_base+8(FP), X6
|
||||
MOV msg_len+16(FP), X7
|
||||
|
||||
MOV $16, X8
|
||||
|
||||
AND $7, X6, X28
|
||||
|
||||
MOV (0*8)(X5), X9 // h0
|
||||
MOV (1*8)(X5), X10 // h1
|
||||
MOV (2*8)(X5), X11 // h2
|
||||
MOV (3*8)(X5), X12 // r0
|
||||
MOV (4*8)(X5), X13 // r1
|
||||
|
||||
BLT X7, X8, tail
|
||||
|
||||
loop:
|
||||
BEQZ X28, aligned_load
|
||||
|
||||
LOAD64U(X6, 0*8, X16, X18, X19, X20, X15) // msg[0:8]
|
||||
LOAD64U(X6, 1*8, X16, X18, X19, X20, X17) // msg[8:16]
|
||||
JMP block
|
||||
|
||||
aligned_load:
|
||||
MOV (0*8)(X6), X15 // msg[0:8]
|
||||
MOV (1*8)(X6), X17 // msg[8:16]
|
||||
|
||||
block:
|
||||
ADD X15, X9 // h0 (x1 + y1 = z1', if z1' < x1 then z1' overflow)
|
||||
SLTU X15, X9, X19 // h0.carry
|
||||
ADD X17, X10, X22
|
||||
SLTU X17, X22, X23
|
||||
ADD X22, X19, X10 // h1
|
||||
SLTU X22, X10, X19
|
||||
OR X23, X19 // h1.carry
|
||||
ADD $1, X19
|
||||
ADD X19, X11 // h2
|
||||
|
||||
ADD $16, X6 // msg = msg[16:]
|
||||
|
||||
multiply:
|
||||
MULHU X9, X12, X16 // h0r0.hi
|
||||
MUL X9, X12, X15 // h0r0.lo
|
||||
MULHU X10, X12, X17 // h1r0.hi
|
||||
MUL X10, X12, X14 // h1r0.lo
|
||||
ADD X14, X16
|
||||
SLTU X14, X16, X19
|
||||
ADD X19, X17
|
||||
MUL X11, X12, X20
|
||||
ADD X17, X20
|
||||
MULHU X9, X13, X17 // h0r1.hi
|
||||
MUL X9, X13, X14 // h0r1.lo
|
||||
ADD X14, X16
|
||||
SLTU X14, X16, X19
|
||||
ADD X19, X17
|
||||
MOV X17, X9
|
||||
MUL X11, X13, X21 // h2r1
|
||||
MULHU X10, X13, X17 // h1r1.hi
|
||||
MUL X10, X13, X14 // h1r1.lo
|
||||
ADD X14, X20
|
||||
ADD X17, X21, X22
|
||||
SLTU X14, X20, X19
|
||||
ADD X22, X19, X21
|
||||
ADD X9, X20
|
||||
SLTU X9, X20, X19
|
||||
ADD X19, X21
|
||||
AND $3, X20, X11
|
||||
AND $-4, X20, X18
|
||||
ADD X18, X15, X9
|
||||
ADD X21, X16, X22
|
||||
SLTU X18, X9, X19
|
||||
SLTU X21, X22, X23
|
||||
ADD X22, X19, X10
|
||||
SLTU X22, X10, X19
|
||||
OR X19, X23, X19
|
||||
ADD X19, X11
|
||||
SLL $62, X21, X22
|
||||
SRL $2, X20, X23
|
||||
SRL $2, X21, X21
|
||||
OR X22, X23, X20
|
||||
ADD X20, X9, X9
|
||||
ADD X21, X10, X22
|
||||
SLTU X20, X9, X19
|
||||
SLTU X21, X22, X23
|
||||
ADD X22, X19, X10
|
||||
SLTU X22, X10, X19
|
||||
OR X19, X23, X19
|
||||
ADD X19, X11, X11
|
||||
|
||||
SUB $16, X7, X7
|
||||
BGE X7, X8, loop
|
||||
|
||||
tail:
|
||||
BEQ X7, X0, done
|
||||
MOV $1, X15
|
||||
MOV $0, X16
|
||||
ADD X7, X6, X6
|
||||
|
||||
flush_buffer:
|
||||
MOVBU -1(X6), X20
|
||||
SRL $56, X15, X19
|
||||
SLL $8, X16, X23
|
||||
SLL $8, X15, X15
|
||||
OR X19, X23, X16
|
||||
XOR X20, X15
|
||||
SUB $1, X7, X7
|
||||
SUB $1, X6, X6
|
||||
BNE X7, X0, flush_buffer
|
||||
|
||||
ADD X15, X9
|
||||
SLTU X15, X9, X19
|
||||
ADD X16, X10, X22
|
||||
SLTU X16, X22, X23
|
||||
ADD X22, X19, X10
|
||||
SLTU X22, X10, X19
|
||||
OR X23, X19
|
||||
ADD X19, X11
|
||||
|
||||
MOV $16, X7
|
||||
JMP multiply
|
||||
|
||||
done:
|
||||
MOV X9, (0*8)(X5) // h0
|
||||
MOV X10, (1*8)(X5)
|
||||
MOV X11, (2*8)(X5)
|
||||
RET
|
||||
@@ -1,12 +0,0 @@
|
||||
This package runs a set of the Wycheproof tests provided by
|
||||
https://github.com/google/wycheproof.
|
||||
|
||||
The JSON test files live in
|
||||
https://github.com/google/wycheproof/tree/master/testvectors
|
||||
and are being fetched and cached at a pinned version every time
|
||||
these tests are run. To change the version of the wycheproof
|
||||
repository that is being used for testing, update wycheproofModVer.
|
||||
|
||||
The structs for these tests are generated from the
|
||||
schemas provided in https://github.com/google/wycheproof/tree/master/schemas
|
||||
using https://github.com/a-h/generate.
|
||||
@@ -1,176 +0,0 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
func TestAEAD(t *testing.T) {
|
||||
// AeadTestVector
|
||||
type AeadTestVector struct {
|
||||
|
||||
// additional authenticated data
|
||||
Aad string `json:"aad,omitempty"`
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// the ciphertext (without iv and tag)
|
||||
Ct string `json:"ct,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// the nonce
|
||||
Iv string `json:"iv,omitempty"`
|
||||
|
||||
// the key
|
||||
Key string `json:"key,omitempty"`
|
||||
|
||||
// the plaintext
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// the authentication tag
|
||||
Tag string `json:"tag,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// AeadTestGroup
|
||||
type AeadTestGroup struct {
|
||||
|
||||
// the IV size in bits
|
||||
IvSize int `json:"ivSize,omitempty"`
|
||||
|
||||
// the keySize in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
|
||||
// the expected size of the tag in bits
|
||||
TagSize int `json:"tagSize,omitempty"`
|
||||
Tests []*AeadTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*AeadTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
testSealOpen := func(t *testing.T, aead cipher.AEAD, tv *AeadTestVector, recoverBadNonce func()) {
|
||||
defer recoverBadNonce()
|
||||
|
||||
iv, tag, ct, msg, aad := decodeHex(tv.Iv), decodeHex(tv.Tag), decodeHex(tv.Ct), decodeHex(tv.Msg), decodeHex(tv.Aad)
|
||||
|
||||
genCT := aead.Seal(nil, iv, msg, aad)
|
||||
genMsg, err := aead.Open(nil, iv, genCT, aad)
|
||||
if err != nil {
|
||||
t.Errorf("failed to decrypt generated ciphertext: %s", err)
|
||||
}
|
||||
if !bytes.Equal(genMsg, msg) {
|
||||
t.Errorf("unexpected roundtripped plaintext: got %x, want %x", genMsg, msg)
|
||||
}
|
||||
|
||||
ctWithTag := append(ct, tag...)
|
||||
msg2, err := aead.Open(nil, iv, ctWithTag, aad)
|
||||
wantPass := shouldPass(tv.Result, tv.Flags, nil)
|
||||
if !wantPass && err == nil {
|
||||
t.Error("decryption succeeded when it should've failed")
|
||||
} else if wantPass {
|
||||
if err != nil {
|
||||
t.Fatalf("decryption failed: %s", err)
|
||||
}
|
||||
if !bytes.Equal(genCT, ctWithTag) {
|
||||
t.Errorf("generated ciphertext doesn't match expected: got %x, want %x", genCT, ctWithTag)
|
||||
}
|
||||
if !bytes.Equal(msg, msg2) {
|
||||
t.Errorf("decrypted ciphertext doesn't match expected: got %x, want %x", msg2, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vectors := map[string]func(*testing.T, []byte) cipher.AEAD{
|
||||
"aes_gcm_test.json": func(t *testing.T, key []byte) cipher.AEAD {
|
||||
aesCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to construct cipher: %s", err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(aesCipher)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to construct cipher: %s", err)
|
||||
}
|
||||
return aead
|
||||
},
|
||||
"chacha20_poly1305_test.json": func(t *testing.T, key []byte) cipher.AEAD {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to construct cipher: %s", err)
|
||||
}
|
||||
return aead
|
||||
},
|
||||
"xchacha20_poly1305_test.json": func(t *testing.T, key []byte) cipher.AEAD {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to construct cipher: %s", err)
|
||||
}
|
||||
return aead
|
||||
},
|
||||
}
|
||||
for file, cipherInit := range vectors {
|
||||
var root Root
|
||||
readTestVector(t, file, &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
for _, tv := range tg.Tests {
|
||||
testName := fmt.Sprintf("%s #%d", file, tv.TcId)
|
||||
if tv.Comment != "" {
|
||||
testName += fmt.Sprintf(" %s", tv.Comment)
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
aead := cipherInit(t, decodeHex(tv.Key))
|
||||
testSealOpen(t, aead, tv, func() {
|
||||
// A bad nonce causes a panic in AEAD.Seal and AEAD.Open,
|
||||
// so should be recovered. Fail the test if it broke for
|
||||
// some other reason.
|
||||
if r := recover(); r != nil {
|
||||
if tg.IvSize/8 == aead.NonceSize() {
|
||||
t.Error("unexpected panic")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAesCbc(t *testing.T) {
|
||||
// IndCpaTestVector
|
||||
type IndCpaTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// the raw ciphertext (without IV)
|
||||
Ct string `json:"ct,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// the initialization vector
|
||||
Iv string `json:"iv,omitempty"`
|
||||
|
||||
// the key
|
||||
Key string `json:"key,omitempty"`
|
||||
|
||||
// the plaintext
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// IndCpaTestGroup
|
||||
type IndCpaTestGroup struct {
|
||||
|
||||
// the IV size in bits
|
||||
IvSize int `json:"ivSize,omitempty"`
|
||||
|
||||
// the keySize in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
|
||||
// the expected size of the tag in bits
|
||||
TagSize int `json:"tagSize,omitempty"`
|
||||
Tests []*IndCpaTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*IndCpaTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
var root Root
|
||||
readTestVector(t, "aes_cbc_pkcs5_test.json", &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
tests:
|
||||
for _, tv := range tg.Tests {
|
||||
block, err := aes.NewCipher(decodeHex(tv.Key))
|
||||
if err != nil {
|
||||
t.Fatalf("#%d: %v", tv.TcId, err)
|
||||
}
|
||||
mode := cipher.NewCBCDecrypter(block, decodeHex(tv.Iv))
|
||||
ct := decodeHex(tv.Ct)
|
||||
if len(ct)%aes.BlockSize != 0 {
|
||||
panic(fmt.Sprintf("#%d: ciphertext is not a multiple of the block size", tv.TcId))
|
||||
}
|
||||
mode.CryptBlocks(ct, ct) // decrypt the block in place
|
||||
|
||||
// Skip the tests that are broken due to bad padding. Panic if there are any
|
||||
// tests left that are invalid for some other reason in the future, to
|
||||
// evaluate what to do with those tests.
|
||||
for _, flag := range tv.Flags {
|
||||
if flag == "BadPadding" {
|
||||
continue tests
|
||||
}
|
||||
}
|
||||
if !shouldPass(tv.Result, tv.Flags, nil) {
|
||||
panic(fmt.Sprintf("#%d: found an invalid test that is broken for some reason other than bad padding", tv.TcId))
|
||||
}
|
||||
|
||||
// Remove the PKCS#5 padding from the given ciphertext to validate it
|
||||
padding := ct[len(ct)-1]
|
||||
paddingNum := int(padding)
|
||||
for i := paddingNum; i > 0; i-- {
|
||||
if ct[len(ct)-i] != padding { // panic if the padding is unexpectedly bad
|
||||
panic(fmt.Sprintf("#%d: bad padding at index=%d of %v", tv.TcId, i, ct))
|
||||
}
|
||||
}
|
||||
ct = ct[:len(ct)-paddingNum]
|
||||
|
||||
if got, want := hex.EncodeToString(ct), tv.Msg; got != want {
|
||||
t.Errorf("#%d, type: %s, comment: %q, decoded ciphertext not equal: %s, want %s", tv.TcId, tv.Result, tv.Comment, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build boringcrypto
|
||||
|
||||
package wycheproof
|
||||
|
||||
const boringcryptoEnabled = true
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/dsa"
|
||||
"testing"
|
||||
|
||||
wdsa "golang.org/x/crypto/internal/wycheproof/internal/dsa"
|
||||
)
|
||||
|
||||
func TestDsa(t *testing.T) {
|
||||
// AsnSignatureTestVector
|
||||
type AsnSignatureTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// The message to sign
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// An ASN encoded signature for msg
|
||||
Sig string `json:"sig,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// DsaPublicKey
|
||||
type DsaPublicKey struct {
|
||||
|
||||
// the generator of the multiplicative subgroup
|
||||
G string `json:"g,omitempty"`
|
||||
|
||||
// the key size in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
|
||||
// the modulus p
|
||||
P string `json:"p,omitempty"`
|
||||
|
||||
// the order of the generator g
|
||||
Q string `json:"q,omitempty"`
|
||||
|
||||
// the key type
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// the public key value
|
||||
Y string `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
// DsaTestGroup
|
||||
type DsaTestGroup struct {
|
||||
|
||||
// unenocded DSA public key
|
||||
Key *DsaPublicKey `json:"key,omitempty"`
|
||||
|
||||
// DER encoded public key
|
||||
KeyDer string `json:"keyDer,omitempty"`
|
||||
|
||||
// Pem encoded public key
|
||||
KeyPem string `json:"keyPem,omitempty"`
|
||||
|
||||
// the hash function used for DSA
|
||||
Sha string `json:"sha,omitempty"`
|
||||
Tests []*AsnSignatureTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*DsaTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// An encoded ASN.1 integer missing a leading zero is invalid, but accepted by some implementations.
|
||||
"NoLeadingZero": false,
|
||||
}
|
||||
|
||||
var root Root
|
||||
readTestVector(t, "dsa_test.json", &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
pub := decodePublicKey(tg.KeyDer).(*dsa.PublicKey)
|
||||
h := parseHash(tg.Sha).New()
|
||||
for _, sig := range tg.Tests {
|
||||
h.Reset()
|
||||
h.Write(decodeHex(sig.Msg))
|
||||
hashed := h.Sum(nil)
|
||||
hashed = hashed[:pub.Q.BitLen()/8] // Truncate to the byte-length of the subgroup (Q)
|
||||
got := wdsa.VerifyASN1(pub, hashed, decodeHex(sig.Sig))
|
||||
if want := shouldPass(sig.Result, sig.Flags, flagsShouldPass); got != want {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t", sig.TcId, sig.Result, sig.Comment, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestECDHStdLib(t *testing.T) {
|
||||
type ECDHTestVector struct {
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
// the private key
|
||||
Private string `json:"private,omitempty"`
|
||||
// Encoded public key
|
||||
Public string `json:"public,omitempty"`
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
// The shared secret key
|
||||
Shared string `json:"shared,omitempty"`
|
||||
// Identifier of the test case
|
||||
TcID int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
type ECDHTestGroup struct {
|
||||
Curve string `json:"curve,omitempty"`
|
||||
Tests []*ECDHTestVector `json:"tests,omitempty"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
TestGroups []*ECDHTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// We don't support compressed points.
|
||||
"CompressedPoint": false,
|
||||
// We don't support decoding custom curves.
|
||||
"UnnamedCurve": false,
|
||||
// WrongOrder and UnusedParam are only found with UnnamedCurve.
|
||||
"WrongOrder": false,
|
||||
"UnusedParam": false,
|
||||
|
||||
// X25519 specific flags
|
||||
"Twist": true,
|
||||
"SmallPublicKey": false,
|
||||
"LowOrderPublic": false,
|
||||
"ZeroSharedSecret": false,
|
||||
"NonCanonicalPublic": true,
|
||||
}
|
||||
|
||||
// curveToCurve is a map of all elliptic curves supported
|
||||
// by crypto/elliptic, which can subsequently be parsed and tested.
|
||||
curveToCurve := map[string]ecdh.Curve{
|
||||
"secp256r1": ecdh.P256(),
|
||||
"secp384r1": ecdh.P384(),
|
||||
"secp521r1": ecdh.P521(),
|
||||
"curve25519": ecdh.X25519(),
|
||||
}
|
||||
|
||||
curveToKeySize := map[string]int{
|
||||
"secp256r1": 32,
|
||||
"secp384r1": 48,
|
||||
"secp521r1": 66,
|
||||
"curve25519": 32,
|
||||
}
|
||||
|
||||
for _, f := range []string{
|
||||
"ecdh_secp256r1_ecpoint_test.json",
|
||||
"ecdh_secp384r1_ecpoint_test.json",
|
||||
"ecdh_secp521r1_ecpoint_test.json",
|
||||
"x25519_test.json",
|
||||
} {
|
||||
var root Root
|
||||
readTestVector(t, f, &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
if _, ok := curveToCurve[tg.Curve]; !ok {
|
||||
continue
|
||||
}
|
||||
for _, tt := range tg.Tests {
|
||||
tg, tt := tg, tt
|
||||
t.Run(fmt.Sprintf("%s/%d", tg.Curve, tt.TcID), func(t *testing.T) {
|
||||
t.Logf("Type: %v", tt.Result)
|
||||
t.Logf("Flags: %q", tt.Flags)
|
||||
t.Log(tt.Comment)
|
||||
|
||||
shouldPass := shouldPass(tt.Result, tt.Flags, flagsShouldPass)
|
||||
|
||||
curve := curveToCurve[tg.Curve]
|
||||
p := decodeHex(tt.Public)
|
||||
pub, err := curve.NewPublicKey(p)
|
||||
if err != nil {
|
||||
if shouldPass {
|
||||
t.Errorf("NewPublicKey: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
privBytes := decodeHex(tt.Private)
|
||||
if len(privBytes) != curveToKeySize[tg.Curve] {
|
||||
t.Skipf("non-standard key size %d", len(privBytes))
|
||||
}
|
||||
|
||||
priv, err := curve.NewPrivateKey(privBytes)
|
||||
if err != nil {
|
||||
if shouldPass {
|
||||
t.Errorf("NewPrivateKey: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
shared := decodeHex(tt.Shared)
|
||||
x, err := priv.ECDH(pub)
|
||||
if err != nil {
|
||||
if tg.Curve == "curve25519" && !shouldPass {
|
||||
// ECDH is expected to only return an error when using X25519,
|
||||
// in all other cases an error is unexpected.
|
||||
return
|
||||
}
|
||||
t.Fatalf("ECDH: %v", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(shared, x) != shouldPass {
|
||||
if shouldPass {
|
||||
t.Errorf("ECDH = %x, want %x", shared, x)
|
||||
} else {
|
||||
t.Errorf("ECDH = %x, want anything else", shared)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
casn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
func TestECDH(t *testing.T) {
|
||||
type ECDHTestVector struct {
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
// the private key
|
||||
Private string `json:"private,omitempty"`
|
||||
// Encoded public key
|
||||
Public string `json:"public,omitempty"`
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
// The shared secret key
|
||||
Shared string `json:"shared,omitempty"`
|
||||
// Identifier of the test case
|
||||
TcID int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
type ECDHTestGroup struct {
|
||||
Curve string `json:"curve,omitempty"`
|
||||
Tests []*ECDHTestVector `json:"tests,omitempty"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
TestGroups []*ECDHTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// ParsePKIXPublicKey doesn't support compressed points, but we test
|
||||
// them against UnmarshalCompressed anyway.
|
||||
"CompressedPoint": true,
|
||||
// We don't support decoding custom curves.
|
||||
"UnnamedCurve": false,
|
||||
// WrongOrder and UnusedParam are only found with UnnamedCurve.
|
||||
"WrongOrder": false,
|
||||
"UnusedParam": false,
|
||||
}
|
||||
|
||||
// supportedCurves is a map of all elliptic curves supported
|
||||
// by crypto/elliptic, which can subsequently be parsed and tested.
|
||||
supportedCurves := map[string]bool{
|
||||
"secp224r1": true,
|
||||
"secp256r1": true,
|
||||
"secp384r1": true,
|
||||
"secp521r1": true,
|
||||
}
|
||||
|
||||
var root Root
|
||||
readTestVector(t, "ecdh_test.json", &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
if !supportedCurves[tg.Curve] {
|
||||
continue
|
||||
}
|
||||
for _, tt := range tg.Tests {
|
||||
tg, tt := tg, tt
|
||||
t.Run(fmt.Sprintf("%s/%d", tg.Curve, tt.TcID), func(t *testing.T) {
|
||||
t.Logf("Type: %v", tt.Result)
|
||||
t.Logf("Flags: %q", tt.Flags)
|
||||
t.Log(tt.Comment)
|
||||
|
||||
shouldPass := shouldPass(tt.Result, tt.Flags, flagsShouldPass)
|
||||
|
||||
p := decodeHex(tt.Public)
|
||||
pp, err := x509.ParsePKIXPublicKey(p)
|
||||
if err != nil {
|
||||
pp, err = decodeCompressedPKIX(p)
|
||||
}
|
||||
if err != nil {
|
||||
if shouldPass {
|
||||
t.Errorf("unexpected parsing error: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
pub := pp.(*ecdsa.PublicKey)
|
||||
|
||||
priv := decodeHex(tt.Private)
|
||||
shared := decodeHex(tt.Shared)
|
||||
|
||||
x, _ := pub.Curve.ScalarMult(pub.X, pub.Y, priv)
|
||||
xBytes := make([]byte, (pub.Curve.Params().BitSize+7)/8)
|
||||
got := bytes.Equal(shared, x.FillBytes(xBytes))
|
||||
|
||||
if want := shouldPass; got != want {
|
||||
t.Errorf("wanted success %v, got %v", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeCompressedPKIX(der []byte) (interface{}, error) {
|
||||
s := cryptobyte.String(der)
|
||||
var s1, s2 cryptobyte.String
|
||||
var algoOID, namedCurveOID asn1.ObjectIdentifier
|
||||
var pointDER []byte
|
||||
if !s.ReadASN1(&s1, casn1.SEQUENCE) || !s.Empty() ||
|
||||
!s1.ReadASN1(&s2, casn1.SEQUENCE) ||
|
||||
!s2.ReadASN1ObjectIdentifier(&algoOID) ||
|
||||
!s2.ReadASN1ObjectIdentifier(&namedCurveOID) || !s2.Empty() ||
|
||||
!s1.ReadASN1BitStringAsBytes(&pointDER) || !s1.Empty() {
|
||||
return nil, errors.New("failed to parse PKIX structure")
|
||||
}
|
||||
|
||||
if !algoOID.Equal(oidPublicKeyECDSA) {
|
||||
return nil, errors.New("wrong algorithm OID")
|
||||
}
|
||||
namedCurve := namedCurveFromOID(namedCurveOID)
|
||||
if namedCurve == nil {
|
||||
return nil, errors.New("unsupported elliptic curve")
|
||||
}
|
||||
x, y := elliptic.UnmarshalCompressed(namedCurve, pointDER)
|
||||
if x == nil {
|
||||
return nil, errors.New("failed to unmarshal elliptic curve point")
|
||||
}
|
||||
pub := &ecdsa.PublicKey{
|
||||
Curve: namedCurve,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
|
||||
oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33}
|
||||
oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}
|
||||
oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34}
|
||||
oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35}
|
||||
)
|
||||
|
||||
func namedCurveFromOID(oid asn1.ObjectIdentifier) elliptic.Curve {
|
||||
switch {
|
||||
case oid.Equal(oidNamedCurveP224):
|
||||
return elliptic.P224()
|
||||
case oid.Equal(oidNamedCurveP256):
|
||||
return elliptic.P256()
|
||||
case oid.Equal(oidNamedCurveP384):
|
||||
return elliptic.P384()
|
||||
case oid.Equal(oidNamedCurveP521):
|
||||
return elliptic.P521()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
func TestECDSA(t *testing.T) {
|
||||
type ASNSignatureTestVector struct {
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment"`
|
||||
// A list of flags
|
||||
Flags []string `json:"flags"`
|
||||
// The message to sign
|
||||
Msg string `json:"msg"`
|
||||
// Test result
|
||||
Result string `json:"result"`
|
||||
// An ASN.1 encoded signature for msg
|
||||
Sig string `json:"sig"`
|
||||
// Identifier of the test case
|
||||
TcID int `json:"tcId"`
|
||||
}
|
||||
|
||||
type ECPublicKey struct {
|
||||
// The EC group used by this public key
|
||||
Curve interface{} `json:"curve"`
|
||||
}
|
||||
|
||||
type ECDSATestGroup struct {
|
||||
// Unencoded EC public key
|
||||
Key *ECPublicKey `json:"key"`
|
||||
// DER encoded public key
|
||||
KeyDER string `json:"keyDer"`
|
||||
// the hash function used for ECDSA
|
||||
SHA string `json:"sha"`
|
||||
Tests []*ASNSignatureTestVector `json:"tests"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
TestGroups []*ECDSATestGroup `json:"testGroups"`
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// An encoded ASN.1 integer missing a leading zero is invalid, but
|
||||
// accepted by some implementations.
|
||||
"MissingZero": false,
|
||||
// A signature using a weaker hash than the EC params is not a security
|
||||
// risk, as long as the hash is secure.
|
||||
// https://www.imperialviolet.org/2014/05/25/strengthmatching.html
|
||||
"WeakHash": true,
|
||||
}
|
||||
|
||||
// supportedCurves is a map of all elliptic curves supported
|
||||
// by crypto/elliptic, which can subsequently be parsed and tested.
|
||||
supportedCurves := map[string]bool{
|
||||
"secp224r1": true,
|
||||
"secp256r1": true,
|
||||
"secp384r1": true,
|
||||
"secp521r1": true,
|
||||
}
|
||||
|
||||
var root Root
|
||||
readTestVector(t, "ecdsa_test.json", &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
curve := tg.Key.Curve.(string)
|
||||
if !supportedCurves[curve] {
|
||||
continue
|
||||
}
|
||||
pub := decodePublicKey(tg.KeyDER).(*ecdsa.PublicKey)
|
||||
h := parseHash(tg.SHA).New()
|
||||
for _, sig := range tg.Tests {
|
||||
h.Reset()
|
||||
h.Write(decodeHex(sig.Msg))
|
||||
hashed := h.Sum(nil)
|
||||
sigBytes := decodeHex(sig.Sig)
|
||||
got := ecdsa.VerifyASN1(pub, hashed, sigBytes)
|
||||
if want := shouldPass(sig.Result, sig.Flags, flagsShouldPass); got != want {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, VerifyASN1 wanted success: %t", sig.TcID, sig.Result, sig.Comment, want)
|
||||
}
|
||||
|
||||
var r, s big.Int
|
||||
var inner cryptobyte.String
|
||||
input := cryptobyte.String(sigBytes)
|
||||
if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
|
||||
!input.Empty() ||
|
||||
!inner.ReadASN1Integer(&r) ||
|
||||
!inner.ReadASN1Integer(&s) ||
|
||||
!inner.Empty() {
|
||||
continue
|
||||
}
|
||||
got = ecdsa.Verify(pub, hashed, &r, &s)
|
||||
if want := shouldPass(sig.Result, sig.Flags, flagsShouldPass); got != want {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, Verify wanted success: %t", sig.TcID, sig.Result, sig.Comment, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEddsa(t *testing.T) {
|
||||
// Jwk the private key in webcrypto format
|
||||
type Jwk struct {
|
||||
}
|
||||
|
||||
// Key unencoded key pair
|
||||
type Key struct {
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// SignatureTestVector
|
||||
type SignatureTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// The message to sign
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// A signature for msg
|
||||
Sig string `json:"sig,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// EddsaTestGroup
|
||||
type EddsaTestGroup struct {
|
||||
|
||||
// the private key in webcrypto format
|
||||
Jwk *Jwk `json:"jwk,omitempty"`
|
||||
|
||||
// unencoded key pair
|
||||
Key *Key `json:"key,omitempty"`
|
||||
|
||||
// Asn encoded public key
|
||||
KeyDer string `json:"keyDer,omitempty"`
|
||||
|
||||
// Pem encoded public key
|
||||
KeyPem string `json:"keyPem,omitempty"`
|
||||
Tests []*SignatureTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*EddsaTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
var root Root
|
||||
readTestVector(t, "eddsa_test.json", &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
pub := decodePublicKey(tg.KeyDer).(ed25519.PublicKey)
|
||||
for _, sig := range tg.Tests {
|
||||
got := ed25519.Verify(pub, decodeHex(sig.Msg), decodeHex(sig.Sig))
|
||||
if want := shouldPass(sig.Result, sig.Flags, nil); got != want {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t", sig.TcId, sig.Result, sig.Comment, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func TestHkdf(t *testing.T) {
|
||||
|
||||
// HkdfTestVector
|
||||
type HkdfTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// the key (input key material)
|
||||
Ikm string `json:"ikm,omitempty"`
|
||||
|
||||
// additional information used in the key derivation
|
||||
Info string `json:"info,omitempty"`
|
||||
|
||||
// the generated bytes (output key material)
|
||||
Okm string `json:"okm,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// the salt for the key derivation
|
||||
Salt string `json:"salt,omitempty"`
|
||||
|
||||
// the size of the output in bytes
|
||||
Size int `json:"size,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// HkdfTestGroup
|
||||
type HkdfTestGroup struct {
|
||||
|
||||
// the size of the ikm in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
Tests []*HkdfTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*HkdfTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
fileHashAlgorithms := map[string]string{
|
||||
"hkdf_sha1_test.json": "SHA-1",
|
||||
"hkdf_sha256_test.json": "SHA-256",
|
||||
"hkdf_sha384_test.json": "SHA-384",
|
||||
"hkdf_sha512_test.json": "SHA-512",
|
||||
}
|
||||
|
||||
for f := range fileHashAlgorithms {
|
||||
var root Root
|
||||
readTestVector(t, f, &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
for _, tv := range tg.Tests {
|
||||
h := parseHash(fileHashAlgorithms[f]).New
|
||||
hkdf := hkdf.New(h, decodeHex(tv.Ikm), decodeHex(tv.Salt), decodeHex(tv.Info))
|
||||
key := make([]byte, tv.Size)
|
||||
wantPass := shouldPass(tv.Result, tv.Flags, nil)
|
||||
_, err := io.ReadFull(hkdf, key)
|
||||
if (err == nil) != wantPass {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t, got: %v", tv.TcId, tv.Result, tv.Comment, wantPass, err)
|
||||
}
|
||||
if err != nil {
|
||||
continue // don't validate output text if reading failed
|
||||
}
|
||||
if got, want := key, decodeHex(tv.Okm); !bytes.Equal(got, want) {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, output bytes don't match", tv.TcId, tv.Result, tv.Comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHMAC(t *testing.T) {
|
||||
// MacTestVector
|
||||
type MacTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// the key
|
||||
Key string `json:"key,omitempty"`
|
||||
|
||||
// the plaintext
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// the authentication tag
|
||||
Tag string `json:"tag,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// MacTestGroup
|
||||
type MacTestGroup struct {
|
||||
|
||||
// the keySize in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
|
||||
// the expected size of the tag in bits
|
||||
TagSize int `json:"tagSize,omitempty"`
|
||||
Tests []*MacTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*MacTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
fileHashAlgs := map[string]string{
|
||||
"hmac_sha1_test.json": "SHA-1",
|
||||
"hmac_sha224_test.json": "SHA-224",
|
||||
"hmac_sha256_test.json": "SHA-256",
|
||||
"hmac_sha384_test.json": "SHA-384",
|
||||
"hmac_sha512_test.json": "SHA-512",
|
||||
}
|
||||
|
||||
for f := range fileHashAlgs {
|
||||
var root Root
|
||||
readTestVector(t, f, &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
h := parseHash(fileHashAlgs[f])
|
||||
// Skip test vectors where the tag length does not equal the
|
||||
// hash length, since crypto/hmac does not support generating
|
||||
// these truncated tags.
|
||||
if tg.TagSize/8 != h.Size() {
|
||||
continue
|
||||
}
|
||||
for _, tv := range tg.Tests {
|
||||
hm := hmac.New(h.New, decodeHex(tv.Key))
|
||||
hm.Write(decodeHex(tv.Msg))
|
||||
tag := hm.Sum(nil)
|
||||
got := hmac.Equal(decodeHex(tv.Tag), tag)
|
||||
if want := shouldPass(tv.Result, tv.Flags, nil); want != got {
|
||||
t.Errorf("%s, tcid: %d, type: %s, comment: %q, unexpected result", f, tv.TcId, tv.Result, tv.Comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package dsa provides an internal version of dsa.Verify
|
||||
// that is used for the Wycheproof tests.
|
||||
package dsa
|
||||
|
||||
import (
|
||||
"crypto/dsa"
|
||||
"math/big"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
// VerifyASN1 verifies the ASN1 encoded signature, sig, of hash using the
|
||||
// public key, pub. Its return value records whether the signature is valid.
|
||||
func VerifyASN1(pub *dsa.PublicKey, hash, sig []byte) bool {
|
||||
var (
|
||||
r, s = &big.Int{}, &big.Int{}
|
||||
inner cryptobyte.String
|
||||
)
|
||||
input := cryptobyte.String(sig)
|
||||
if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
|
||||
!input.Empty() ||
|
||||
!inner.ReadASN1Integer(r) ||
|
||||
!inner.ReadASN1Integer(s) ||
|
||||
!inner.Empty() {
|
||||
return false
|
||||
}
|
||||
return dsa.Verify(pub, hash, r, s)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !boringcrypto
|
||||
|
||||
package wycheproof
|
||||
|
||||
const boringcryptoEnabled = false
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRSAOAEPDecrypt(t *testing.T) {
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// RsaesOaepTestVector
|
||||
type RsaesOaepTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// An encryption of msg
|
||||
Ct string `json:"ct,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// The label used for the encryption
|
||||
Label string `json:"label,omitempty"`
|
||||
|
||||
// The encrypted message
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// RsaesOaepTestGroup
|
||||
type RsaesOaepTestGroup struct {
|
||||
|
||||
// The private exponent
|
||||
D string `json:"d,omitempty"`
|
||||
|
||||
// The public exponent
|
||||
E string `json:"e,omitempty"`
|
||||
|
||||
// the message generating function (e.g. MGF1)
|
||||
Mgf string `json:"mgf,omitempty"`
|
||||
|
||||
// The hash function used for the message generating function.
|
||||
MgfSha string `json:"mgfSha,omitempty"`
|
||||
|
||||
// The modulus of the key
|
||||
N string `json:"n,omitempty"`
|
||||
|
||||
// Pem encoded private key
|
||||
PrivateKeyPem string `json:"privateKeyPem,omitempty"`
|
||||
|
||||
// Pkcs 8 encoded private key.
|
||||
PrivateKeyPkcs8 string `json:"privateKeyPkcs8,omitempty"`
|
||||
|
||||
// The hash function for hashing the label.
|
||||
Sha string `json:"sha,omitempty"`
|
||||
Tests []*RsaesOaepTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*RsaesOaepTestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
// rsa.DecryptOAEP doesn't support using a different hash for the
|
||||
// MGF and the label, so skip all of the test vectors that use
|
||||
// these unbalanced constructions. rsa_oaep_misc_test.json contains
|
||||
// both balanced and unbalanced constructions so in that case
|
||||
// we just filter out any test groups where MgfSha != Sha
|
||||
files := []string{
|
||||
"rsa_oaep_2048_sha1_mgf1sha1_test.json",
|
||||
"rsa_oaep_2048_sha224_mgf1sha224_test.json",
|
||||
"rsa_oaep_2048_sha256_mgf1sha256_test.json",
|
||||
"rsa_oaep_2048_sha384_mgf1sha384_test.json",
|
||||
"rsa_oaep_2048_sha512_mgf1sha512_test.json",
|
||||
"rsa_oaep_3072_sha256_mgf1sha256_test.json",
|
||||
"rsa_oaep_3072_sha512_mgf1sha512_test.json",
|
||||
"rsa_oaep_4096_sha256_mgf1sha256_test.json",
|
||||
"rsa_oaep_4096_sha512_mgf1sha512_test.json",
|
||||
"rsa_oaep_misc_test.json",
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// rsa.DecryptOAEP happily supports small key sizes
|
||||
"SmallModulus": true,
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
var root Root
|
||||
readTestVector(t, f, &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
if tg.MgfSha != tg.Sha {
|
||||
continue
|
||||
}
|
||||
priv, err := x509.ParsePKCS8PrivateKey(decodeHex(tg.PrivateKeyPkcs8))
|
||||
if err != nil {
|
||||
t.Fatalf("%s failed to parse PKCS #8 private key: %s", f, err)
|
||||
}
|
||||
hash := parseHash(tg.Sha)
|
||||
for _, tv := range tg.Tests {
|
||||
t.Run(fmt.Sprintf("%s #%d", f, tv.TcId), func(t *testing.T) {
|
||||
wantPass := shouldPass(tv.Result, tv.Flags, flagsShouldPass)
|
||||
plaintext, err := rsa.DecryptOAEP(hash.New(), nil, priv.(*rsa.PrivateKey), decodeHex(tv.Ct), decodeHex(tv.Label))
|
||||
if wantPass {
|
||||
if err != nil {
|
||||
t.Fatalf("comment: %s, expected success: %s", tv.Comment, err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, decodeHex(tv.Msg)) {
|
||||
t.Errorf("comment: %s, unexpected plaintext: got %x, want %s", tv.Comment, plaintext, tv.Msg)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Errorf("comment: %s, expected failure", tv.Comment)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRsaPss(t *testing.T) {
|
||||
// KeyJwk Public key in JWK format
|
||||
type KeyJwk struct {
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// SignatureTestVector
|
||||
type SignatureTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// The message to sign
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// A signature for msg
|
||||
Sig string `json:"sig,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// RsassaPkcs1TestGroup
|
||||
type RsassaPkcs1TestGroup struct {
|
||||
|
||||
// The private exponent
|
||||
D string `json:"d,omitempty"`
|
||||
|
||||
// The public exponent
|
||||
E string `json:"e,omitempty"`
|
||||
|
||||
// ASN encoding of the sequence [n, e]
|
||||
KeyAsn string `json:"keyAsn,omitempty"`
|
||||
|
||||
// ASN encoding of the public key
|
||||
KeyDer string `json:"keyDer,omitempty"`
|
||||
|
||||
// Public key in JWK format
|
||||
KeyJwk *KeyJwk `json:"keyJwk,omitempty"`
|
||||
|
||||
// Pem encoded public key
|
||||
KeyPem string `json:"keyPem,omitempty"`
|
||||
|
||||
// the size of the modulus in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
|
||||
// The modulus of the key
|
||||
N string `json:"n,omitempty"`
|
||||
|
||||
// The salt length
|
||||
SLen int `json:"sLen,omitempty"`
|
||||
|
||||
// the hash function used for the message
|
||||
Sha string `json:"sha,omitempty"`
|
||||
Tests []*SignatureTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*RsassaPkcs1TestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// A signature using a weaker hash than the EC params is not a security risk, as long as the hash is secure.
|
||||
// https://www.imperialviolet.org/2014/05/25/strengthmatching.html
|
||||
"WeakHash": true,
|
||||
}
|
||||
|
||||
// filesOverrideToPassZeroSLen is a map of all test files
|
||||
// and which TcIds that should be overridden to pass if the
|
||||
// rsa.PSSOptions.SaltLength is zero.
|
||||
// These tests expect a failure with a PSSOptions.SaltLength: 0
|
||||
// and a signature that uses a different salt length. However,
|
||||
// a salt length of 0 is defined as rsa.PSSSaltLengthAuto which
|
||||
// works deterministically to auto-detect the length when
|
||||
// verifying, so these tests actually pass as they should.
|
||||
filesOverrideToPassZeroSLen := map[string][]int{
|
||||
"rsa_pss_2048_sha1_mgf1_20_test.json": []int{46, 47},
|
||||
"rsa_pss_2048_sha256_mgf1_0_test.json": []int{67, 68},
|
||||
"rsa_pss_2048_sha256_mgf1_32_test.json": []int{67, 68},
|
||||
"rsa_pss_3072_sha256_mgf1_32_test.json": []int{67, 68},
|
||||
"rsa_pss_4096_sha256_mgf1_32_test.json": []int{67, 68},
|
||||
"rsa_pss_4096_sha512_mgf1_32_test.json": []int{136, 137},
|
||||
// "rsa_pss_misc_test.json": nil, // TODO: This ones seems to be broken right now, but can enable later on.
|
||||
}
|
||||
|
||||
if !boringcryptoEnabled {
|
||||
// boringcrypto doesn't support the truncated SHA-512 hashes, so only
|
||||
// test them if boringcrypto isn't enabled.
|
||||
filesOverrideToPassZeroSLen["rsa_pss_2048_sha512_256_mgf1_28_test.json"] = []int{13, 14, 15}
|
||||
filesOverrideToPassZeroSLen["rsa_pss_2048_sha512_256_mgf1_32_test.json"] = []int{13, 14}
|
||||
}
|
||||
|
||||
for f := range filesOverrideToPassZeroSLen {
|
||||
var root Root
|
||||
readTestVector(t, f, &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
pub := decodePublicKey(tg.KeyDer).(*rsa.PublicKey)
|
||||
ch := parseHash(tg.Sha)
|
||||
h := ch.New()
|
||||
opts := &rsa.PSSOptions{
|
||||
Hash: ch,
|
||||
SaltLength: rsa.PSSSaltLengthAuto,
|
||||
}
|
||||
// Run all the tests twice: the first time with the salt length
|
||||
// as PSSSaltLengthAuto, and the second time with the salt length
|
||||
// explictily set to tg.SLen.
|
||||
for i := 0; i < 2; i++ {
|
||||
for _, sig := range tg.Tests {
|
||||
h.Reset()
|
||||
h.Write(decodeHex(sig.Msg))
|
||||
hashed := h.Sum(nil)
|
||||
err := rsa.VerifyPSS(pub, ch, hashed, decodeHex(sig.Sig), opts)
|
||||
want := shouldPass(sig.Result, sig.Flags, flagsShouldPass)
|
||||
if opts.SaltLength == 0 {
|
||||
for _, id := range filesOverrideToPassZeroSLen[f] {
|
||||
if sig.TcId == id {
|
||||
want = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (err == nil) != want {
|
||||
t.Errorf("file: %v, tcid: %d, type: %s, opts.SaltLength: %v, comment: %q, wanted success: %t", f, sig.TcId, sig.Result, opts.SaltLength, sig.Comment, want)
|
||||
}
|
||||
}
|
||||
// Update opts.SaltLength for the second run of the tests.
|
||||
opts.SaltLength = tg.SLen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRsa(t *testing.T) {
|
||||
// KeyJwk Public key in JWK format
|
||||
type KeyJwk struct {
|
||||
}
|
||||
|
||||
// Notes a description of the labels used in the test vectors
|
||||
type Notes struct {
|
||||
}
|
||||
|
||||
// SignatureTestVector
|
||||
type SignatureTestVector struct {
|
||||
|
||||
// A brief description of the test case
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// A list of flags
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
|
||||
// The message to sign
|
||||
Msg string `json:"msg,omitempty"`
|
||||
|
||||
// Test result
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// A signature for msg
|
||||
Sig string `json:"sig,omitempty"`
|
||||
|
||||
// Identifier of the test case
|
||||
TcId int `json:"tcId,omitempty"`
|
||||
}
|
||||
|
||||
// RsassaPkcs1TestGroup
|
||||
type RsassaPkcs1TestGroup struct {
|
||||
|
||||
// The private exponent
|
||||
D string `json:"d,omitempty"`
|
||||
|
||||
// The public exponent
|
||||
E string `json:"e,omitempty"`
|
||||
|
||||
// ASN encoding of the sequence [n, e]
|
||||
KeyAsn string `json:"keyAsn,omitempty"`
|
||||
|
||||
// ASN encoding of the public key
|
||||
KeyDer string `json:"keyDer,omitempty"`
|
||||
|
||||
// Public key in JWK format
|
||||
KeyJwk *KeyJwk `json:"keyJwk,omitempty"`
|
||||
|
||||
// Pem encoded public key
|
||||
KeyPem string `json:"keyPem,omitempty"`
|
||||
|
||||
// the size of the modulus in bits
|
||||
KeySize int `json:"keySize,omitempty"`
|
||||
|
||||
// The modulus of the key
|
||||
N string `json:"n,omitempty"`
|
||||
|
||||
// the hash function used for the message
|
||||
Sha string `json:"sha,omitempty"`
|
||||
Tests []*SignatureTestVector `json:"tests,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Root
|
||||
type Root struct {
|
||||
|
||||
// the primitive tested in the test file
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
|
||||
// the version of the test vectors.
|
||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
||||
|
||||
// additional documentation
|
||||
Header []string `json:"header,omitempty"`
|
||||
|
||||
// a description of the labels used in the test vectors
|
||||
Notes *Notes `json:"notes,omitempty"`
|
||||
|
||||
// the number of test vectors in this test
|
||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
||||
Schema interface{} `json:"schema,omitempty"`
|
||||
TestGroups []*RsassaPkcs1TestGroup `json:"testGroups,omitempty"`
|
||||
}
|
||||
|
||||
flagsShouldPass := map[string]bool{
|
||||
// Omitting the parameter field in an ASN encoded integer is a legacy behavior.
|
||||
"MissingNull": false,
|
||||
// Keys with a modulus less than 2048 bits are supported by crypto/rsa.
|
||||
"SmallModulus": true,
|
||||
// Small public keys are supported by crypto/rsa.
|
||||
"SmallPublicKey": true,
|
||||
}
|
||||
|
||||
var root Root
|
||||
readTestVector(t, "rsa_signature_test.json", &root)
|
||||
for _, tg := range root.TestGroups {
|
||||
pub := decodePublicKey(tg.KeyDer).(*rsa.PublicKey)
|
||||
ch := parseHash(tg.Sha)
|
||||
h := ch.New()
|
||||
for _, sig := range tg.Tests {
|
||||
h.Reset()
|
||||
h.Write(decodeHex(sig.Msg))
|
||||
hashed := h.Sum(nil)
|
||||
err := rsa.VerifyPKCS1v15(pub, ch, hashed, decodeHex(sig.Sig))
|
||||
want := shouldPass(sig.Result, sig.Flags, flagsShouldPass)
|
||||
if (err == nil) != want {
|
||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t", sig.TcId, sig.Result, sig.Comment, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package wycheproof runs a set of the Wycheproof tests
|
||||
// provided by https://github.com/google/wycheproof.
|
||||
package wycheproof
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "crypto/sha1"
|
||||
_ "crypto/sha256"
|
||||
_ "crypto/sha512"
|
||||
)
|
||||
|
||||
const wycheproofModVer = "v0.0.0-20191219022705-2196000605e4"
|
||||
|
||||
var wycheproofTestVectorsDir string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
if flag.Lookup("test.short").Value.(flag.Getter).Get().(bool) {
|
||||
log.Println("skipping test that downloads testdata via 'go mod download' in short mode")
|
||||
os.Exit(0)
|
||||
}
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
log.Printf("skipping test because 'go' command is unavailable: %v", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
if os.Getenv("GO_BUILDER_FLAKY_NET") != "" {
|
||||
log.Printf("skipping test because GO_BUILDER_FLAKY_NET is set")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Download the JSON test files from github.com/google/wycheproof
|
||||
// using `go mod download -json` so the cached source of the testdata
|
||||
// can be used in the following tests.
|
||||
path := "github.com/google/wycheproof@" + wycheproofModVer
|
||||
cmd := exec.Command("go", "mod", "download", "-json", path)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to run `go mod download -json %s`, output: %s", path, output)
|
||||
}
|
||||
var dm struct {
|
||||
Dir string // absolute path to cached source root directory
|
||||
}
|
||||
if err := json.Unmarshal(output, &dm); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Now that the module has been downloaded, use the absolute path of the
|
||||
// cached source as the root directory for all tests going forward.
|
||||
wycheproofTestVectorsDir = filepath.Join(dm.Dir, "testvectors")
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func readTestVector(t *testing.T, f string, dest interface{}) {
|
||||
b, err := os.ReadFile(filepath.Join(wycheproofTestVectorsDir, f))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read json file: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(b, &dest); err != nil {
|
||||
t.Fatalf("failed to unmarshal json file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeHex(s string) []byte {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func decodePublicKey(der string) interface{} {
|
||||
d := decodeHex(der)
|
||||
pub, err := x509.ParsePKIXPublicKey(d)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to parse DER encoded public key: %v", err))
|
||||
}
|
||||
return pub
|
||||
}
|
||||
|
||||
func parseHash(h string) crypto.Hash {
|
||||
switch h {
|
||||
case "SHA-1":
|
||||
return crypto.SHA1
|
||||
case "SHA-256":
|
||||
return crypto.SHA256
|
||||
case "SHA-224":
|
||||
return crypto.SHA224
|
||||
case "SHA-384":
|
||||
return crypto.SHA384
|
||||
case "SHA-512":
|
||||
return crypto.SHA512
|
||||
case "SHA-512/224":
|
||||
return crypto.SHA512_224
|
||||
case "SHA-512/256":
|
||||
return crypto.SHA512_256
|
||||
default:
|
||||
panic(fmt.Sprintf("could not identify SHA hash algorithm: %q", h))
|
||||
}
|
||||
}
|
||||
|
||||
// shouldPass returns whether or not the test should pass.
|
||||
// flagsShouldPass is a map associated with whether or not
|
||||
// a flag for an "acceptable" result should pass.
|
||||
// Every possible flag value that's associated with an
|
||||
// "acceptable" result should be explicitly specified,
|
||||
// otherwise the test will panic.
|
||||
func shouldPass(result string, flags []string, flagsShouldPass map[string]bool) bool {
|
||||
switch result {
|
||||
case "valid":
|
||||
return true
|
||||
case "invalid":
|
||||
return false
|
||||
case "acceptable":
|
||||
for _, flag := range flags {
|
||||
pass, ok := flagsShouldPass[flag]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unspecified flag: %q", flag))
|
||||
}
|
||||
if !pass {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true // There are no flags, or all are meant to pass.
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected result: %v", result))
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func TestSealOpenAnonymous(t *testing.T) {
|
||||
t.Fatal("expected out to be unchanged")
|
||||
}
|
||||
if !bytes.HasPrefix(box, orig) {
|
||||
t.Fatal("expected out to be coppied to returned slice")
|
||||
t.Fatal("expected out to be copied to returned slice")
|
||||
}
|
||||
_, ok = OpenAnonymous(nil, box[len(out):], publicKey, privateKey)
|
||||
if !ok {
|
||||
|
||||
@@ -69,6 +69,6 @@ func TestGenerateSignOpen(t *testing.T) {
|
||||
}
|
||||
|
||||
if !bytes.Equal(message, testMessage) {
|
||||
t.Fatalf("verified message does not match signed messge, got\n%x\n, expected\n%x", message, testMessage)
|
||||
t.Fatalf("verified message does not match signed message, got\n%x\n, expected\n%x", message, testMessage)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -85,7 +85,8 @@ type certID struct {
|
||||
|
||||
// https://tools.ietf.org/html/rfc2560#section-4.1.1
|
||||
type ocspRequest struct {
|
||||
TBSRequest tbsRequest
|
||||
TBSRequest tbsRequest
|
||||
OptionalSignature asn1.RawValue `asn1:"explicit,tag:0,optional"`
|
||||
}
|
||||
|
||||
type tbsRequest struct {
|
||||
@@ -321,10 +322,10 @@ type Request struct {
|
||||
func (req *Request) Marshal() ([]byte, error) {
|
||||
hashAlg := getOIDFromHashAlgorithm(req.HashAlgorithm)
|
||||
if hashAlg == nil {
|
||||
return nil, errors.New("Unknown hash algorithm")
|
||||
return nil, errors.New("unknown hash algorithm")
|
||||
}
|
||||
return asn1.Marshal(ocspRequest{
|
||||
tbsRequest{
|
||||
TBSRequest: tbsRequest{
|
||||
Version: 0,
|
||||
RequestList: []request{
|
||||
{
|
||||
@@ -418,8 +419,10 @@ func (p ParseError) Error() string {
|
||||
}
|
||||
|
||||
// ParseRequest parses an OCSP request in DER form. It only supports
|
||||
// requests for a single certificate. Signed requests are not supported.
|
||||
// If a request includes a signature, it will result in a ParseError.
|
||||
// requests for a single certificate identifier. If a request includes
|
||||
// multiple certificate identifiers, only the first will be included in
|
||||
// the parsed Request. Signed requests are not supported. If a request
|
||||
// includes a signature, it will result in a ParseError.
|
||||
func ParseRequest(bytes []byte) (*Request, error) {
|
||||
var req ocspRequest
|
||||
rest, err := asn1.Unmarshal(bytes, &req)
|
||||
@@ -430,6 +433,10 @@ func ParseRequest(bytes []byte) (*Request, error) {
|
||||
return nil, ParseError("trailing data in OCSP request")
|
||||
}
|
||||
|
||||
if len(req.OptionalSignature.FullBytes) > 0 {
|
||||
return nil, ParseError("signed OCSP requests are not supported")
|
||||
}
|
||||
|
||||
if len(req.TBSRequest.RequestList) == 0 {
|
||||
return nil, ParseError("OCSP request contains no request body")
|
||||
}
|
||||
|
||||
+127
-1
@@ -15,10 +15,15 @@ import (
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
func TestOCSPDecode(t *testing.T) {
|
||||
@@ -190,7 +195,7 @@ func TestOCSPRequest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.Compare(expectedBytes, marshaledRequest) != 0 {
|
||||
if !bytes.Equal(expectedBytes, marshaledRequest) {
|
||||
t.Errorf(
|
||||
"Marshaled request doesn't match expected: wanted %x, got %x",
|
||||
expectedBytes,
|
||||
@@ -199,6 +204,127 @@ func TestOCSPRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCSPRequestErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "signed request",
|
||||
input: testOCSPRequest(1, true),
|
||||
wantErr: "signed",
|
||||
},
|
||||
{
|
||||
name: "trailing data",
|
||||
input: append(testOCSPRequest(1, false), 0x00),
|
||||
wantErr: "trailing",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := ParseRequest(tc.input)
|
||||
if err == nil {
|
||||
t.Fatalf(
|
||||
"ParseRequest succeeded, want ParseError containing %q",
|
||||
tc.wantErr,
|
||||
)
|
||||
}
|
||||
var parseError ParseError
|
||||
if !errors.As(err, &parseError) {
|
||||
t.Fatalf("ParseRequest returned %T, want ParseError", err)
|
||||
}
|
||||
if !strings.Contains(parseError.Error(), tc.wantErr) {
|
||||
t.Fatalf(
|
||||
"ParseRequest error = %q, want it to contain %q",
|
||||
parseError.Error(), tc.wantErr,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCSPMultipleCertRequest(t *testing.T) {
|
||||
req, err := ParseRequest(testOCSPRequest(2, false))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseRequest errored for multiple request input: %q", err)
|
||||
}
|
||||
|
||||
// testOCSPRequest uses the request cert counter as the issuerNameHash,
|
||||
// issuerKeyHash and serial. We expect the first request cert to be used
|
||||
// (counter=1), not the second (counter=2).
|
||||
wantHash := []byte{0x01}
|
||||
if actual := req.IssuerNameHash; !bytes.Equal(actual, wantHash) {
|
||||
t.Errorf(
|
||||
"ParseRequest.IssuerNameHash returned = %q, wanted %q",
|
||||
actual, wantHash,
|
||||
)
|
||||
}
|
||||
if actual := req.IssuerKeyHash; !bytes.Equal(actual, wantHash) {
|
||||
t.Errorf("ParseRequest.IssuerKeyHash returned = %q, wanted %q",
|
||||
actual, wantHash)
|
||||
}
|
||||
wantSerial := big.NewInt(0x01)
|
||||
if actual := req.SerialNumber; actual.Cmp(wantSerial) != 0 {
|
||||
t.Errorf("ParseRequest.SerialNumber returned = %q, wanted %q",
|
||||
actual, wantHash)
|
||||
}
|
||||
}
|
||||
|
||||
// testOCSPRequest constructs an unsupported OCSP request in DER form for testing.
|
||||
//
|
||||
// We build these by hand using cryptobyte to specifically target shapes that are
|
||||
// not supported by ParseRequest, and can't be created with CreateRequest.
|
||||
//
|
||||
// The resulting requestList will contain numCerts RequestList entries. If
|
||||
// includeSignature is true, the request will carry a signature field (with an
|
||||
// arbitrary signatureAlgorithm and signature).
|
||||
//
|
||||
// See RFC 6960, §4.1.1.
|
||||
func testOCSPRequest(numCerts int, includeSignature bool) []byte {
|
||||
sha1AlgID, err := asn1.Marshal(pkix.AlgorithmIdentifier{
|
||||
Algorithm: asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26},
|
||||
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var b cryptobyte.Builder
|
||||
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // OCSPRequest
|
||||
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // tbsRequest
|
||||
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // requestList
|
||||
for i := 0; i < numCerts; i++ {
|
||||
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // Request
|
||||
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // CertID
|
||||
b.AddBytes(sha1AlgID) // hashAlgorithm
|
||||
b.AddASN1OctetString([]byte{byte(i + 1)}) // issuerNameHash
|
||||
b.AddASN1OctetString([]byte{byte(i + 1)}) // issuerKeyHash
|
||||
b.AddASN1Int64(int64(i + 1)) // serialNumber
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
if includeSignature {
|
||||
// optionalSignature [0] EXPLICIT Signature
|
||||
b.AddASN1(cbasn1.Tag(0).ContextSpecific().Constructed(), func(b *cryptobyte.Builder) {
|
||||
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // Signature
|
||||
b.AddBytes(sha1AlgID) // signatureAlgorithm
|
||||
b.AddASN1BitString([]byte{0x09, 0x0A, 0x0B, 0x0C}) // signature
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
der, err := b.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return der
|
||||
}
|
||||
|
||||
func TestOCSPResponse(t *testing.T) {
|
||||
leafCert, _ := hex.DecodeString(leafCertHex)
|
||||
leaf, err := x509.ParseCertificate(leafCert)
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
// Package armor implements OpenPGP ASCII Armor, see RFC 4880. OpenPGP Armor is
|
||||
// very similar to PEM except that it has an additional CRC checksum.
|
||||
//
|
||||
// Deprecated: this package is unmaintained except for security fixes. New
|
||||
// applications should consider a more focused, modern alternative to OpenPGP
|
||||
// for their specific task. If you are required to interoperate with OpenPGP
|
||||
// systems and need a maintained package, consider a community fork.
|
||||
// See https://golang.org/issue/44226.
|
||||
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||
// issues. It is not maintained, and should not be used. New applications should
|
||||
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||
package armor
|
||||
|
||||
import (
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
// Clearsigned messages are cryptographically signed, but the contents of the
|
||||
// message are kept in plaintext so that it can be read without special tools.
|
||||
//
|
||||
// Deprecated: this package is unmaintained except for security fixes. New
|
||||
// applications should consider a more focused, modern alternative to OpenPGP
|
||||
// for their specific task. If you are required to interoperate with OpenPGP
|
||||
// systems and need a maintained package, consider a community fork.
|
||||
// See https://golang.org/issue/44226.
|
||||
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||
// issues. It is not maintained, and should not be used. New applications should
|
||||
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||
package clearsign
|
||||
|
||||
import (
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
// Package errors contains common error types for the OpenPGP packages.
|
||||
//
|
||||
// Deprecated: this package is unmaintained except for security fixes. New
|
||||
// applications should consider a more focused, modern alternative to OpenPGP
|
||||
// for their specific task. If you are required to interoperate with OpenPGP
|
||||
// systems and need a maintained package, consider a community fork.
|
||||
// See https://golang.org/issue/44226.
|
||||
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||
// issues. It is not maintained, and should not be used. New applications should
|
||||
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||
package errors
|
||||
|
||||
import (
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestOpaqueParseReason(t *testing.T) {
|
||||
const expectedBad = 3
|
||||
// Test post-conditions, make sure we actually parsed packets as expected.
|
||||
if badPackets != expectedBad {
|
||||
t.Errorf("unexpected # unparseable packets: %d (want %d)", badPackets, expectedBad)
|
||||
t.Errorf("unexpected # unparsable packets: %d (want %d)", badPackets, expectedBad)
|
||||
}
|
||||
if uid == nil {
|
||||
t.Errorf("failed to find expected UID in unsupported keyring")
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
// Package packet implements parsing and serialization of OpenPGP packets, as
|
||||
// specified in RFC 4880.
|
||||
//
|
||||
// Deprecated: this package is unmaintained except for security fixes. New
|
||||
// applications should consider a more focused, modern alternative to OpenPGP
|
||||
// for their specific task. If you are required to interoperate with OpenPGP
|
||||
// systems and need a maintained package, consider a community fork.
|
||||
// See https://golang.org/issue/44226.
|
||||
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||
// issues. It is not maintained, and should not be used. New applications should
|
||||
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||
package packet
|
||||
|
||||
import (
|
||||
|
||||
+6
-5
@@ -4,11 +4,12 @@
|
||||
|
||||
// Package openpgp implements high level operations on OpenPGP messages.
|
||||
//
|
||||
// Deprecated: this package is unmaintained except for security fixes. New
|
||||
// applications should consider a more focused, modern alternative to OpenPGP
|
||||
// for their specific task. If you are required to interoperate with OpenPGP
|
||||
// systems and need a maintained package, consider a community fork.
|
||||
// See https://golang.org/issue/44226.
|
||||
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||
// issues. It is not maintained, and should not be used. New applications should
|
||||
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||
package openpgp
|
||||
|
||||
import (
|
||||
|
||||
+7
-6
@@ -5,11 +5,12 @@
|
||||
// Package s2k implements the various OpenPGP string-to-key transforms as
|
||||
// specified in RFC 4800 section 3.7.1.
|
||||
//
|
||||
// Deprecated: this package is unmaintained except for security fixes. New
|
||||
// applications should consider a more focused, modern alternative to OpenPGP
|
||||
// for their specific task. If you are required to interoperate with OpenPGP
|
||||
// systems and need a maintained package, consider a community fork.
|
||||
// See https://golang.org/issue/44226.
|
||||
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||
// issues. It is not maintained, and should not be used. New applications should
|
||||
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||
package s2k
|
||||
|
||||
import (
|
||||
@@ -53,7 +54,7 @@ func (c *Config) hash() crypto.Hash {
|
||||
|
||||
func (c *Config) encodedCount() uint8 {
|
||||
if c == nil || c.S2KCount == 0 {
|
||||
return 96 // The common case. Correspoding to 65536
|
||||
return 96 // The common case. Corresponding to 65536
|
||||
}
|
||||
|
||||
i := c.S2KCount
|
||||
|
||||
+3
-3
@@ -75,8 +75,8 @@ const libOTRPrivateKey = `(privkeys
|
||||
(account
|
||||
(name "foo@example.com")
|
||||
(protocol prpl-jabber)
|
||||
(private-key
|
||||
(dsa
|
||||
(private-key
|
||||
(dsa
|
||||
(p #00FC07ABCF0DC916AFF6E9AE47BEF60C7AB9B4D6B2469E436630E36F8A489BE812486A09F30B71224508654940A835301ACC525A4FF133FC152CC53DCC59D65C30A54F1993FE13FE63E5823D4C746DB21B90F9B9C00B49EC7404AB1D929BA7FBA12F2E45C6E0A651689750E8528AB8C031D3561FECEE72EBB4A090D450A9B7A857#)
|
||||
(q #00997BD266EF7B1F60A5C23F3A741F2AEFD07A2081#)
|
||||
(g #535E360E8A95EBA46A4F7DE50AD6E9B2A6DB785A66B64EB9F20338D2A3E8FB0E94725848F1AA6CC567CB83A1CC517EC806F2E92EAE71457E80B2210A189B91250779434B41FC8A8873F6DB94BEA7D177F5D59E7E114EE10A49CFD9CEF88AE43387023B672927BA74B04EB6BBB5E57597766A2F9CE3857D7ACE3E1E3BC1FC6F26#)
|
||||
@@ -402,7 +402,7 @@ func TestAgainstLibOTR(t *testing.T) {
|
||||
// This test requires otr.c.test to be built as /tmp/a.out.
|
||||
// If enabled, this tests runs forever performing OTR handshakes in a
|
||||
// loop.
|
||||
return
|
||||
t.Skip("This test requires otr.c.test to be built as /tmp/a.out")
|
||||
|
||||
alicePrivateKey, _ := hex.DecodeString(alicePrivateKeyHex)
|
||||
var alice Conversation
|
||||
|
||||
+13
-60
@@ -2,24 +2,17 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC
|
||||
2898 / PKCS #5 v2.0.
|
||||
|
||||
A key derivation function is useful when encrypting data based on a password
|
||||
or any other not-fully-random data. It uses a pseudorandom function to derive
|
||||
a secure encryption key based on the password.
|
||||
|
||||
While v2.0 of the standard defines only one pseudorandom function to use,
|
||||
HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved
|
||||
Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To
|
||||
choose, you can pass the `New` functions from the different SHA packages to
|
||||
pbkdf2.Key.
|
||||
*/
|
||||
// Package pbkdf2 implements the key derivation function PBKDF2 as defined in
|
||||
// RFC 8018 (PKCS #5 v2.1).
|
||||
//
|
||||
// This package is a wrapper for the PBKDF2 implementation in the
|
||||
// [crypto/pbkdf2] package. It is [frozen] and is not accepting new features.
|
||||
//
|
||||
// [frozen]: https://go.dev/wiki/Frozen
|
||||
package pbkdf2
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/pbkdf2"
|
||||
"hash"
|
||||
)
|
||||
|
||||
@@ -27,51 +20,11 @@ import (
|
||||
// []byte of length keylen that can be used as cryptographic key. The key is
|
||||
// derived based on the method described as PBKDF2 with the HMAC variant using
|
||||
// the supplied hash function.
|
||||
//
|
||||
// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you
|
||||
// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by
|
||||
// doing:
|
||||
//
|
||||
// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
|
||||
//
|
||||
// Remember to get a good random salt. At least 8 bytes is recommended by the
|
||||
// RFC.
|
||||
//
|
||||
// Using a higher iteration count will increase the cost of an exhaustive
|
||||
// search but will also make derivation proportionally slower.
|
||||
func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
|
||||
prf := hmac.New(h, password)
|
||||
hashLen := prf.Size()
|
||||
numBlocks := (keyLen + hashLen - 1) / hashLen
|
||||
|
||||
var buf [4]byte
|
||||
dk := make([]byte, 0, numBlocks*hashLen)
|
||||
U := make([]byte, hashLen)
|
||||
for block := 1; block <= numBlocks; block++ {
|
||||
// N.B.: || means concatenation, ^ means XOR
|
||||
// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
|
||||
// U_1 = PRF(password, salt || uint(i))
|
||||
prf.Reset()
|
||||
prf.Write(salt)
|
||||
buf[0] = byte(block >> 24)
|
||||
buf[1] = byte(block >> 16)
|
||||
buf[2] = byte(block >> 8)
|
||||
buf[3] = byte(block)
|
||||
prf.Write(buf[:4])
|
||||
dk = prf.Sum(dk)
|
||||
T := dk[len(dk)-hashLen:]
|
||||
copy(U, T)
|
||||
|
||||
// U_n = PRF(password, U_(n-1))
|
||||
for n := 2; n <= iter; n++ {
|
||||
prf.Reset()
|
||||
prf.Write(U)
|
||||
U = U[:0]
|
||||
U = prf.Sum(U)
|
||||
for x := range U {
|
||||
T[x] ^= U[x]
|
||||
}
|
||||
}
|
||||
out, err := pbkdf2.Key(h, string(password), salt, iter, keyLen)
|
||||
if err != nil {
|
||||
// FIPS 140 enforcement, or an invalid key length.
|
||||
panic(err)
|
||||
}
|
||||
return dk[:keyLen]
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ func pbDecrypterFor(algorithm pkix.AlgorithmIdentifier, password []byte) (cipher
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if params.Iterations < 0 || params.Iterations > maxIterations {
|
||||
return nil, 0, NotImplementedError("iteration count is invalid or too high")
|
||||
}
|
||||
|
||||
key := cipherType.deriveKey(params.Salt, password, params.Iterations)
|
||||
iv := cipherType.deriveIV(params.Salt, password, params.Iterations)
|
||||
|
||||
|
||||
@@ -13,6 +13,46 @@ import (
|
||||
|
||||
var sha1WithTripleDES = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 1, 3})
|
||||
|
||||
func TestPbDecrypterForIterationLimit(t *testing.T) {
|
||||
pass, _ := bmpString("Sesame open")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
iterations int
|
||||
wantErr bool
|
||||
}{
|
||||
{"at limit", maxIterations, false},
|
||||
{"over limit", maxIterations + 1, true},
|
||||
{"negative", -1, true},
|
||||
{"max int", 1<<31 - 1, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
params, _ := asn1.Marshal(pbeParams{
|
||||
Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
Iterations: tt.iterations,
|
||||
})
|
||||
alg := pkix.AlgorithmIdentifier{
|
||||
Algorithm: sha1WithTripleDES,
|
||||
Parameters: asn1.RawValue{
|
||||
FullBytes: params,
|
||||
},
|
||||
}
|
||||
_, _, err := pbDecrypterFor(alg, pass)
|
||||
if tt.wantErr {
|
||||
if _, ok := err.(NotImplementedError); !ok {
|
||||
t.Errorf("iterations=%d: got %v, want NotImplementedError", tt.iterations, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("iterations=%d: unexpected error: %v", tt.iterations, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPbDecrypterFor(t *testing.T) {
|
||||
params, _ := asn1.Marshal(pbeParams{
|
||||
Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
|
||||
@@ -27,11 +27,19 @@ var (
|
||||
oidSHA1 = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26})
|
||||
)
|
||||
|
||||
// maxIterations is a safety limit to prevent CPU exhaustion from
|
||||
// crafted PKCS#12 files with unreasonable iteration counts.
|
||||
const maxIterations = 1 << 20 // ~1 million
|
||||
|
||||
func verifyMac(macData *macData, message, password []byte) error {
|
||||
if !macData.Mac.Algorithm.Algorithm.Equal(oidSHA1) {
|
||||
return NotImplementedError("unknown digest algorithm: " + macData.Mac.Algorithm.Algorithm.String())
|
||||
}
|
||||
|
||||
if macData.Iterations < 0 || macData.Iterations > maxIterations {
|
||||
return NotImplementedError("iteration count is invalid or too high")
|
||||
}
|
||||
|
||||
key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password, macData.Iterations, 3, 20)
|
||||
|
||||
mac := hmac.New(sha1.New, key)
|
||||
|
||||
@@ -5,10 +5,52 @@
|
||||
package pkcs12
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVerifyMacIterationLimit(t *testing.T) {
|
||||
password, _ := bmpString("Sesame open")
|
||||
message := []byte{11, 12, 13, 14, 15}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
iterations int
|
||||
wantErr bool
|
||||
}{
|
||||
{"at limit", maxIterations, false},
|
||||
{"over limit", maxIterations + 1, true},
|
||||
{"negative", -1, true},
|
||||
{"max int", 1<<31 - 1, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
td := macData{
|
||||
Mac: digestInfo{
|
||||
Algorithm: pkix.AlgorithmIdentifier{
|
||||
Algorithm: oidSHA1,
|
||||
},
|
||||
Digest: nil, // will fail MAC check, but iteration check comes first
|
||||
},
|
||||
MacSalt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
Iterations: tt.iterations,
|
||||
}
|
||||
err := verifyMac(&td, message, password)
|
||||
if tt.wantErr {
|
||||
if _, ok := err.(NotImplementedError); !ok {
|
||||
t.Errorf("iterations=%d: got %v, want NotImplementedError", tt.iterations, err)
|
||||
}
|
||||
} else {
|
||||
if _, ok := err.(NotImplementedError); ok {
|
||||
t.Errorf("iterations=%d: got unexpected NotImplementedError", tt.iterations)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMac(t *testing.T) {
|
||||
td := macData{
|
||||
Mac: digestInfo{
|
||||
|
||||
@@ -196,6 +196,9 @@ func Key(password, salt []byte, N, r, p, keyLen int) ([]byte, error) {
|
||||
if N <= 1 || N&(N-1) != 0 {
|
||||
return nil, errors.New("scrypt: N must be > 1 and a power of 2")
|
||||
}
|
||||
if r <= 0 || p <= 0 {
|
||||
return nil, errors.New("scrypt: parameters must be > 0")
|
||||
}
|
||||
if uint64(r)*uint64(p) >= 1<<30 || r > maxInt/128/p || r > maxInt/256 || N > maxInt/128/r {
|
||||
return nil, errors.New("scrypt: parameters are too large")
|
||||
}
|
||||
|
||||
@@ -133,6 +133,10 @@ var bad = []testVector{
|
||||
{"p", "s", 1, 1, 1, nil}, // N == 1
|
||||
{"p", "s", 7, 8, 1, nil}, // N is not power of 2
|
||||
{"p", "s", 16, maxInt / 2, maxInt / 2, nil}, // p * r too large
|
||||
{"p", "s", 2, 0, 1, nil}, // r too small
|
||||
{"p", "s", 2, 1, 0, nil}, // p too small
|
||||
{"p", "s", 2, -1, 1, nil}, // r is negative
|
||||
{"p", "s", 2, 1, -1, nil}, // p is negative
|
||||
}
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
module sha3/_asm
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/mmcloughlin/avo v0.6.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
|
||||
github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
@@ -1,438 +0,0 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This code was translated into a form compatible with 6a from the public
|
||||
// domain sources at https://github.com/gvanas/KeccakCodePackage
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
. "github.com/mmcloughlin/avo/build"
|
||||
. "github.com/mmcloughlin/avo/operand"
|
||||
. "github.com/mmcloughlin/avo/reg"
|
||||
_ "golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
//go:generate go run . -out ../keccakf_amd64.s -pkg sha3
|
||||
|
||||
// Round Constants for use in the ι step.
|
||||
var RoundConstants = [24]uint64{
|
||||
0x0000000000000001,
|
||||
0x0000000000008082,
|
||||
0x800000000000808A,
|
||||
0x8000000080008000,
|
||||
0x000000000000808B,
|
||||
0x0000000080000001,
|
||||
0x8000000080008081,
|
||||
0x8000000000008009,
|
||||
0x000000000000008A,
|
||||
0x0000000000000088,
|
||||
0x0000000080008009,
|
||||
0x000000008000000A,
|
||||
0x000000008000808B,
|
||||
0x800000000000008B,
|
||||
0x8000000000008089,
|
||||
0x8000000000008003,
|
||||
0x8000000000008002,
|
||||
0x8000000000000080,
|
||||
0x000000000000800A,
|
||||
0x800000008000000A,
|
||||
0x8000000080008081,
|
||||
0x8000000000008080,
|
||||
0x0000000080000001,
|
||||
0x8000000080008008,
|
||||
}
|
||||
|
||||
var (
|
||||
// Temporary registers
|
||||
rT1 GPPhysical = RAX
|
||||
|
||||
// Round vars
|
||||
rpState = Mem{Base: RDI}
|
||||
rpStack = Mem{Base: RSP}
|
||||
|
||||
rDa = RBX
|
||||
rDe = RCX
|
||||
rDi = RDX
|
||||
rDo = R8
|
||||
rDu = R9
|
||||
|
||||
rBa = R10
|
||||
rBe = R11
|
||||
rBi = R12
|
||||
rBo = R13
|
||||
rBu = R14
|
||||
|
||||
rCa = RSI
|
||||
rCe = RBP
|
||||
rCi = rBi
|
||||
rCo = rBo
|
||||
rCu = R15
|
||||
)
|
||||
|
||||
const (
|
||||
_ba = iota * 8
|
||||
_be
|
||||
_bi
|
||||
_bo
|
||||
_bu
|
||||
_ga
|
||||
_ge
|
||||
_gi
|
||||
_go
|
||||
_gu
|
||||
_ka
|
||||
_ke
|
||||
_ki
|
||||
_ko
|
||||
_ku
|
||||
_ma
|
||||
_me
|
||||
_mi
|
||||
_mo
|
||||
_mu
|
||||
_sa
|
||||
_se
|
||||
_si
|
||||
_so
|
||||
_su
|
||||
)
|
||||
|
||||
func main() {
|
||||
Package("golang.org/x/crypto/sha3")
|
||||
ConstraintExpr("amd64,!purego,gc")
|
||||
keccakF1600()
|
||||
Generate()
|
||||
}
|
||||
|
||||
func MOVQ_RBI_RCE() { MOVQ(rBi, rCe) }
|
||||
func XORQ_RT1_RCA() { XORQ(rT1, rCa) }
|
||||
func XORQ_RT1_RCE() { XORQ(rT1, rCe) }
|
||||
func XORQ_RBA_RCU() { XORQ(rBa, rCu) }
|
||||
func XORQ_RBE_RCU() { XORQ(rBe, rCu) }
|
||||
func XORQ_RDU_RCU() { XORQ(rDu, rCu) }
|
||||
func XORQ_RDA_RCA() { XORQ(rDa, rCa) }
|
||||
func XORQ_RDE_RCE() { XORQ(rDe, rCe) }
|
||||
|
||||
type ArgMacro func()
|
||||
|
||||
func mKeccakRound(
|
||||
iState, oState Mem,
|
||||
rc U64,
|
||||
B_RBI_RCE, G_RT1_RCA, G_RT1_RCE, G_RBA_RCU,
|
||||
K_RT1_RCA, K_RT1_RCE, K_RBA_RCU, M_RT1_RCA,
|
||||
M_RT1_RCE, M_RBE_RCU, S_RDU_RCU, S_RDA_RCA,
|
||||
S_RDE_RCE ArgMacro,
|
||||
) {
|
||||
Comment("Prepare round")
|
||||
MOVQ(rCe, rDa)
|
||||
ROLQ(Imm(1), rDa)
|
||||
|
||||
MOVQ(iState.Offset(_bi), rCi)
|
||||
XORQ(iState.Offset(_gi), rDi)
|
||||
XORQ(rCu, rDa)
|
||||
XORQ(iState.Offset(_ki), rCi)
|
||||
XORQ(iState.Offset(_mi), rDi)
|
||||
XORQ(rDi, rCi)
|
||||
|
||||
MOVQ(rCi, rDe)
|
||||
ROLQ(Imm(1), rDe)
|
||||
|
||||
MOVQ(iState.Offset(_bo), rCo)
|
||||
XORQ(iState.Offset(_go), rDo)
|
||||
XORQ(rCa, rDe)
|
||||
XORQ(iState.Offset(_ko), rCo)
|
||||
XORQ(iState.Offset(_mo), rDo)
|
||||
XORQ(rDo, rCo)
|
||||
|
||||
MOVQ(rCo, rDi)
|
||||
ROLQ(Imm(1), rDi)
|
||||
|
||||
MOVQ(rCu, rDo)
|
||||
XORQ(rCe, rDi)
|
||||
ROLQ(Imm(1), rDo)
|
||||
|
||||
MOVQ(rCa, rDu)
|
||||
XORQ(rCi, rDo)
|
||||
ROLQ(Imm(1), rDu)
|
||||
|
||||
Comment("Result b")
|
||||
MOVQ(iState.Offset(_ba), rBa)
|
||||
MOVQ(iState.Offset(_ge), rBe)
|
||||
XORQ(rCo, rDu)
|
||||
MOVQ(iState.Offset(_ki), rBi)
|
||||
MOVQ(iState.Offset(_mo), rBo)
|
||||
MOVQ(iState.Offset(_su), rBu)
|
||||
XORQ(rDe, rBe)
|
||||
ROLQ(Imm(44), rBe)
|
||||
XORQ(rDi, rBi)
|
||||
XORQ(rDa, rBa)
|
||||
ROLQ(Imm(43), rBi)
|
||||
|
||||
MOVQ(rBe, rCa)
|
||||
MOVQ(rc, rT1)
|
||||
ORQ(rBi, rCa)
|
||||
XORQ(rBa, rT1)
|
||||
XORQ(rT1, rCa)
|
||||
MOVQ(rCa, oState.Offset(_ba))
|
||||
|
||||
XORQ(rDu, rBu)
|
||||
ROLQ(Imm(14), rBu)
|
||||
MOVQ(rBa, rCu)
|
||||
ANDQ(rBe, rCu)
|
||||
XORQ(rBu, rCu)
|
||||
MOVQ(rCu, oState.Offset(_bu))
|
||||
|
||||
XORQ(rDo, rBo)
|
||||
ROLQ(Imm(21), rBo)
|
||||
MOVQ(rBo, rT1)
|
||||
ANDQ(rBu, rT1)
|
||||
XORQ(rBi, rT1)
|
||||
MOVQ(rT1, oState.Offset(_bi))
|
||||
|
||||
NOTQ(rBi)
|
||||
ORQ(rBa, rBu)
|
||||
ORQ(rBo, rBi)
|
||||
XORQ(rBo, rBu)
|
||||
XORQ(rBe, rBi)
|
||||
MOVQ(rBu, oState.Offset(_bo))
|
||||
MOVQ(rBi, oState.Offset(_be))
|
||||
B_RBI_RCE()
|
||||
|
||||
Comment("Result g")
|
||||
MOVQ(iState.Offset(_gu), rBe)
|
||||
XORQ(rDu, rBe)
|
||||
MOVQ(iState.Offset(_ka), rBi)
|
||||
ROLQ(Imm(20), rBe)
|
||||
XORQ(rDa, rBi)
|
||||
ROLQ(Imm(3), rBi)
|
||||
MOVQ(iState.Offset(_bo), rBa)
|
||||
MOVQ(rBe, rT1)
|
||||
ORQ(rBi, rT1)
|
||||
XORQ(rDo, rBa)
|
||||
MOVQ(iState.Offset(_me), rBo)
|
||||
MOVQ(iState.Offset(_si), rBu)
|
||||
ROLQ(Imm(28), rBa)
|
||||
XORQ(rBa, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ga))
|
||||
G_RT1_RCA()
|
||||
|
||||
XORQ(rDe, rBo)
|
||||
ROLQ(Imm(45), rBo)
|
||||
MOVQ(rBi, rT1)
|
||||
ANDQ(rBo, rT1)
|
||||
XORQ(rBe, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ge))
|
||||
G_RT1_RCE()
|
||||
|
||||
XORQ(rDi, rBu)
|
||||
ROLQ(Imm(61), rBu)
|
||||
MOVQ(rBu, rT1)
|
||||
ORQ(rBa, rT1)
|
||||
XORQ(rBo, rT1)
|
||||
MOVQ(rT1, oState.Offset(_go))
|
||||
|
||||
ANDQ(rBe, rBa)
|
||||
XORQ(rBu, rBa)
|
||||
MOVQ(rBa, oState.Offset(_gu))
|
||||
NOTQ(rBu)
|
||||
G_RBA_RCU()
|
||||
|
||||
ORQ(rBu, rBo)
|
||||
XORQ(rBi, rBo)
|
||||
MOVQ(rBo, oState.Offset(_gi))
|
||||
|
||||
Comment("Result k")
|
||||
MOVQ(iState.Offset(_be), rBa)
|
||||
MOVQ(iState.Offset(_gi), rBe)
|
||||
MOVQ(iState.Offset(_ko), rBi)
|
||||
MOVQ(iState.Offset(_mu), rBo)
|
||||
MOVQ(iState.Offset(_sa), rBu)
|
||||
XORQ(rDi, rBe)
|
||||
ROLQ(Imm(6), rBe)
|
||||
XORQ(rDo, rBi)
|
||||
ROLQ(Imm(25), rBi)
|
||||
MOVQ(rBe, rT1)
|
||||
ORQ(rBi, rT1)
|
||||
XORQ(rDe, rBa)
|
||||
ROLQ(Imm(1), rBa)
|
||||
XORQ(rBa, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ka))
|
||||
K_RT1_RCA()
|
||||
|
||||
XORQ(rDu, rBo)
|
||||
ROLQ(Imm(8), rBo)
|
||||
MOVQ(rBi, rT1)
|
||||
ANDQ(rBo, rT1)
|
||||
XORQ(rBe, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ke))
|
||||
K_RT1_RCE()
|
||||
|
||||
XORQ(rDa, rBu)
|
||||
ROLQ(Imm(18), rBu)
|
||||
NOTQ(rBo)
|
||||
MOVQ(rBo, rT1)
|
||||
ANDQ(rBu, rT1)
|
||||
XORQ(rBi, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ki))
|
||||
|
||||
MOVQ(rBu, rT1)
|
||||
ORQ(rBa, rT1)
|
||||
XORQ(rBo, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ko))
|
||||
|
||||
ANDQ(rBe, rBa)
|
||||
XORQ(rBu, rBa)
|
||||
MOVQ(rBa, oState.Offset(_ku))
|
||||
K_RBA_RCU()
|
||||
|
||||
Comment("Result m")
|
||||
MOVQ(iState.Offset(_ga), rBe)
|
||||
XORQ(rDa, rBe)
|
||||
MOVQ(iState.Offset(_ke), rBi)
|
||||
ROLQ(Imm(36), rBe)
|
||||
XORQ(rDe, rBi)
|
||||
MOVQ(iState.Offset(_bu), rBa)
|
||||
ROLQ(Imm(10), rBi)
|
||||
MOVQ(rBe, rT1)
|
||||
MOVQ(iState.Offset(_mi), rBo)
|
||||
ANDQ(rBi, rT1)
|
||||
XORQ(rDu, rBa)
|
||||
MOVQ(iState.Offset(_so), rBu)
|
||||
ROLQ(Imm(27), rBa)
|
||||
XORQ(rBa, rT1)
|
||||
MOVQ(rT1, oState.Offset(_ma))
|
||||
M_RT1_RCA()
|
||||
|
||||
XORQ(rDi, rBo)
|
||||
ROLQ(Imm(15), rBo)
|
||||
MOVQ(rBi, rT1)
|
||||
ORQ(rBo, rT1)
|
||||
XORQ(rBe, rT1)
|
||||
MOVQ(rT1, oState.Offset(_me))
|
||||
M_RT1_RCE()
|
||||
|
||||
XORQ(rDo, rBu)
|
||||
ROLQ(Imm(56), rBu)
|
||||
NOTQ(rBo)
|
||||
MOVQ(rBo, rT1)
|
||||
ORQ(rBu, rT1)
|
||||
XORQ(rBi, rT1)
|
||||
MOVQ(rT1, oState.Offset(_mi))
|
||||
|
||||
ORQ(rBa, rBe)
|
||||
XORQ(rBu, rBe)
|
||||
MOVQ(rBe, oState.Offset(_mu))
|
||||
|
||||
ANDQ(rBa, rBu)
|
||||
XORQ(rBo, rBu)
|
||||
MOVQ(rBu, oState.Offset(_mo))
|
||||
M_RBE_RCU()
|
||||
|
||||
Comment("Result s")
|
||||
MOVQ(iState.Offset(_bi), rBa)
|
||||
MOVQ(iState.Offset(_go), rBe)
|
||||
MOVQ(iState.Offset(_ku), rBi)
|
||||
XORQ(rDi, rBa)
|
||||
MOVQ(iState.Offset(_ma), rBo)
|
||||
ROLQ(Imm(62), rBa)
|
||||
XORQ(rDo, rBe)
|
||||
MOVQ(iState.Offset(_se), rBu)
|
||||
ROLQ(Imm(55), rBe)
|
||||
|
||||
XORQ(rDu, rBi)
|
||||
MOVQ(rBa, rDu)
|
||||
XORQ(rDe, rBu)
|
||||
ROLQ(Imm(2), rBu)
|
||||
ANDQ(rBe, rDu)
|
||||
XORQ(rBu, rDu)
|
||||
MOVQ(rDu, oState.Offset(_su))
|
||||
|
||||
ROLQ(Imm(39), rBi)
|
||||
S_RDU_RCU()
|
||||
NOTQ(rBe)
|
||||
XORQ(rDa, rBo)
|
||||
MOVQ(rBe, rDa)
|
||||
ANDQ(rBi, rDa)
|
||||
XORQ(rBa, rDa)
|
||||
MOVQ(rDa, oState.Offset(_sa))
|
||||
S_RDA_RCA()
|
||||
|
||||
ROLQ(Imm(41), rBo)
|
||||
MOVQ(rBi, rDe)
|
||||
ORQ(rBo, rDe)
|
||||
XORQ(rBe, rDe)
|
||||
MOVQ(rDe, oState.Offset(_se))
|
||||
S_RDE_RCE()
|
||||
|
||||
MOVQ(rBo, rDi)
|
||||
MOVQ(rBu, rDo)
|
||||
ANDQ(rBu, rDi)
|
||||
ORQ(rBa, rDo)
|
||||
XORQ(rBi, rDi)
|
||||
XORQ(rBo, rDo)
|
||||
MOVQ(rDi, oState.Offset(_si))
|
||||
MOVQ(rDo, oState.Offset(_so))
|
||||
}
|
||||
|
||||
// keccakF1600 applies the Keccak permutation to a 1600b-wide
|
||||
// state represented as a slice of 25 uint64s.
|
||||
func keccakF1600() {
|
||||
Implement("keccakF1600")
|
||||
AllocLocal(200)
|
||||
|
||||
Load(Param("a"), rpState.Base)
|
||||
|
||||
Comment("Convert the user state into an internal state")
|
||||
NOTQ(rpState.Offset(_be))
|
||||
NOTQ(rpState.Offset(_bi))
|
||||
NOTQ(rpState.Offset(_go))
|
||||
NOTQ(rpState.Offset(_ki))
|
||||
NOTQ(rpState.Offset(_mi))
|
||||
NOTQ(rpState.Offset(_sa))
|
||||
|
||||
Comment("Execute the KeccakF permutation")
|
||||
MOVQ(rpState.Offset(_ba), rCa)
|
||||
MOVQ(rpState.Offset(_be), rCe)
|
||||
MOVQ(rpState.Offset(_bu), rCu)
|
||||
|
||||
XORQ(rpState.Offset(_ga), rCa)
|
||||
XORQ(rpState.Offset(_ge), rCe)
|
||||
XORQ(rpState.Offset(_gu), rCu)
|
||||
|
||||
XORQ(rpState.Offset(_ka), rCa)
|
||||
XORQ(rpState.Offset(_ke), rCe)
|
||||
XORQ(rpState.Offset(_ku), rCu)
|
||||
|
||||
XORQ(rpState.Offset(_ma), rCa)
|
||||
XORQ(rpState.Offset(_me), rCe)
|
||||
XORQ(rpState.Offset(_mu), rCu)
|
||||
|
||||
XORQ(rpState.Offset(_sa), rCa)
|
||||
XORQ(rpState.Offset(_se), rCe)
|
||||
MOVQ(rpState.Offset(_si), rDi)
|
||||
MOVQ(rpState.Offset(_so), rDo)
|
||||
XORQ(rpState.Offset(_su), rCu)
|
||||
|
||||
for i, rc := range RoundConstants[:len(RoundConstants)-1] {
|
||||
var iState, oState Mem
|
||||
if i%2 == 0 {
|
||||
iState, oState = rpState, rpStack
|
||||
} else {
|
||||
iState, oState = rpStack, rpState
|
||||
}
|
||||
mKeccakRound(iState, oState, U64(rc), MOVQ_RBI_RCE, XORQ_RT1_RCA, XORQ_RT1_RCE, XORQ_RBA_RCU, XORQ_RT1_RCA, XORQ_RT1_RCE, XORQ_RBA_RCU, XORQ_RT1_RCA, XORQ_RT1_RCE, XORQ_RBE_RCU, XORQ_RDU_RCU, XORQ_RDA_RCA, XORQ_RDE_RCE)
|
||||
}
|
||||
mKeccakRound(rpStack, rpState, U64(RoundConstants[len(RoundConstants)-1]), NOP, NOP, NOP, NOP, NOP, NOP, NOP, NOP, NOP, NOP, NOP, NOP, NOP)
|
||||
|
||||
Comment("Revert the internal state to the user state")
|
||||
NOTQ(rpState.Offset(_be))
|
||||
NOTQ(rpState.Offset(_bi))
|
||||
NOTQ(rpState.Offset(_go))
|
||||
NOTQ(rpState.Offset(_ki))
|
||||
NOTQ(rpState.Offset(_mi))
|
||||
NOTQ(rpState.Offset(_sa))
|
||||
|
||||
RET()
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !noopt
|
||||
|
||||
package sha3_test
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
var sink byte
|
||||
|
||||
func TestAllocations(t *testing.T) {
|
||||
want := 0.0
|
||||
|
||||
if runtime.GOARCH == "s390x" {
|
||||
// On s390x the returned hash.Hash is conditional so it escapes.
|
||||
want = 3.0
|
||||
}
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
if allocs := testing.AllocsPerRun(10, func() {
|
||||
h := sha3.New256()
|
||||
b := []byte("ABC")
|
||||
h.Write(b)
|
||||
out := make([]byte, 0, 32)
|
||||
out = h.Sum(out)
|
||||
sink ^= out[0]
|
||||
}); allocs > want {
|
||||
t.Errorf("expected zero allocations, got %0.1f", allocs)
|
||||
}
|
||||
})
|
||||
t.Run("NewShake", func(t *testing.T) {
|
||||
if allocs := testing.AllocsPerRun(10, func() {
|
||||
h := sha3.NewShake128()
|
||||
b := []byte("ABC")
|
||||
h.Write(b)
|
||||
out := make([]byte, 0, 32)
|
||||
out = h.Sum(out)
|
||||
sink ^= out[0]
|
||||
h.Read(out)
|
||||
sink ^= out[0]
|
||||
}); allocs > want {
|
||||
t.Errorf("expected zero allocations, got %0.1f", allocs)
|
||||
}
|
||||
})
|
||||
t.Run("Sum", func(t *testing.T) {
|
||||
if allocs := testing.AllocsPerRun(10, func() {
|
||||
b := []byte("ABC")
|
||||
out := sha3.Sum256(b)
|
||||
sink ^= out[0]
|
||||
}); allocs > want {
|
||||
t.Errorf("expected zero allocations, got %0.1f", allocs)
|
||||
}
|
||||
})
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package sha3 implements the SHA-3 fixed-output-length hash functions and
|
||||
// the SHAKE variable-output-length hash functions defined by FIPS-202.
|
||||
//
|
||||
// All types in this package also implement [encoding.BinaryMarshaler],
|
||||
// [encoding.BinaryAppender] and [encoding.BinaryUnmarshaler] to marshal and
|
||||
// unmarshal the internal state of the hash.
|
||||
//
|
||||
// Both types of hash function use the "sponge" construction and the Keccak
|
||||
// permutation. For a detailed specification see http://keccak.noekeon.org/
|
||||
//
|
||||
// # Guidance
|
||||
//
|
||||
// If you aren't sure what function you need, use SHAKE256 with at least 64
|
||||
// bytes of output. The SHAKE instances are faster than the SHA3 instances;
|
||||
// the latter have to allocate memory to conform to the hash.Hash interface.
|
||||
//
|
||||
// If you need a secret-key MAC (message authentication code), prepend the
|
||||
// secret key to the input, hash with SHAKE256 and read at least 32 bytes of
|
||||
// output.
|
||||
//
|
||||
// # Security strengths
|
||||
//
|
||||
// The SHA3-x (x equals 224, 256, 384, or 512) functions have a security
|
||||
// strength against preimage attacks of x bits. Since they only produce "x"
|
||||
// bits of output, their collision-resistance is only "x/2" bits.
|
||||
//
|
||||
// The SHAKE-256 and -128 functions have a generic security strength of 256 and
|
||||
// 128 bits against all attacks, provided that at least 2x bits of their output
|
||||
// is used. Requesting more than 64 or 32 bytes of output, respectively, does
|
||||
// not increase the collision-resistance of the SHAKE functions.
|
||||
//
|
||||
// # The sponge construction
|
||||
//
|
||||
// A sponge builds a pseudo-random function from a public pseudo-random
|
||||
// permutation, by applying the permutation to a state of "rate + capacity"
|
||||
// bytes, but hiding "capacity" of the bytes.
|
||||
//
|
||||
// A sponge starts out with a zero state. To hash an input using a sponge, up
|
||||
// to "rate" bytes of the input are XORed into the sponge's state. The sponge
|
||||
// is then "full" and the permutation is applied to "empty" it. This process is
|
||||
// repeated until all the input has been "absorbed". The input is then padded.
|
||||
// The digest is "squeezed" from the sponge in the same way, except that output
|
||||
// is copied out instead of input being XORed in.
|
||||
//
|
||||
// A sponge is parameterized by its generic security strength, which is equal
|
||||
// to half its capacity; capacity + rate is equal to the permutation's width.
|
||||
// Since the KeccakF-1600 permutation is 1600 bits (200 bytes) wide, this means
|
||||
// that the security strength of a sponge instance is equal to (1600 - bitrate) / 2.
|
||||
//
|
||||
// # Recommendations
|
||||
//
|
||||
// The SHAKE functions are recommended for most new uses. They can produce
|
||||
// output of arbitrary length. SHAKE256, with an output length of at least
|
||||
// 64 bytes, provides 256-bit security against all attacks. The Keccak team
|
||||
// recommends it for most applications upgrading from SHA2-512. (NIST chose a
|
||||
// much stronger, but much slower, sponge instance for SHA3-512.)
|
||||
//
|
||||
// The SHA-3 functions are "drop-in" replacements for the SHA-2 functions.
|
||||
// They produce output of the same length, with the same security strengths
|
||||
// against all attacks. This means, in particular, that SHA3-256 only has
|
||||
// 128-bit collision resistance, because its output length is 32 bytes.
|
||||
package sha3
|
||||
+50
-83
@@ -2,127 +2,94 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package sha3 implements the SHA-3 hash algorithms and the SHAKE extendable
|
||||
// output functions defined in FIPS 202.
|
||||
//
|
||||
// Most of this package is a wrapper around the crypto/sha3 package in the
|
||||
// standard library. The only exception is the legacy Keccak hash functions.
|
||||
package sha3
|
||||
|
||||
// This file provides functions for creating instances of the SHA-3
|
||||
// and SHAKE hash functions, as well as utility functions for hashing
|
||||
// bytes.
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/sha3"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// New224 creates a new SHA3-224 hash.
|
||||
// Its generic security strength is 224 bits against preimage attacks,
|
||||
// and 112 bits against collision attacks.
|
||||
//
|
||||
// It is a wrapper for the [sha3.New224] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func New224() hash.Hash {
|
||||
return new224()
|
||||
return sha3.New224()
|
||||
}
|
||||
|
||||
// New256 creates a new SHA3-256 hash.
|
||||
// Its generic security strength is 256 bits against preimage attacks,
|
||||
// and 128 bits against collision attacks.
|
||||
//
|
||||
// It is a wrapper for the [sha3.New256] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func New256() hash.Hash {
|
||||
return new256()
|
||||
return sha3.New256()
|
||||
}
|
||||
|
||||
// New384 creates a new SHA3-384 hash.
|
||||
// Its generic security strength is 384 bits against preimage attacks,
|
||||
// and 192 bits against collision attacks.
|
||||
//
|
||||
// It is a wrapper for the [sha3.New384] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func New384() hash.Hash {
|
||||
return new384()
|
||||
return sha3.New384()
|
||||
}
|
||||
|
||||
// New512 creates a new SHA3-512 hash.
|
||||
// Its generic security strength is 512 bits against preimage attacks,
|
||||
// and 256 bits against collision attacks.
|
||||
//
|
||||
// It is a wrapper for the [sha3.New512] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func New512() hash.Hash {
|
||||
return new512()
|
||||
}
|
||||
|
||||
func init() {
|
||||
crypto.RegisterHash(crypto.SHA3_224, New224)
|
||||
crypto.RegisterHash(crypto.SHA3_256, New256)
|
||||
crypto.RegisterHash(crypto.SHA3_384, New384)
|
||||
crypto.RegisterHash(crypto.SHA3_512, New512)
|
||||
}
|
||||
|
||||
const (
|
||||
dsbyteSHA3 = 0b00000110
|
||||
dsbyteKeccak = 0b00000001
|
||||
dsbyteShake = 0b00011111
|
||||
dsbyteCShake = 0b00000100
|
||||
|
||||
// rateK[c] is the rate in bytes for Keccak[c] where c is the capacity in
|
||||
// bits. Given the sponge size is 1600 bits, the rate is 1600 - c bits.
|
||||
rateK256 = (1600 - 256) / 8
|
||||
rateK448 = (1600 - 448) / 8
|
||||
rateK512 = (1600 - 512) / 8
|
||||
rateK768 = (1600 - 768) / 8
|
||||
rateK1024 = (1600 - 1024) / 8
|
||||
)
|
||||
|
||||
func new224Generic() *state {
|
||||
return &state{rate: rateK448, outputLen: 28, dsbyte: dsbyteSHA3}
|
||||
}
|
||||
|
||||
func new256Generic() *state {
|
||||
return &state{rate: rateK512, outputLen: 32, dsbyte: dsbyteSHA3}
|
||||
}
|
||||
|
||||
func new384Generic() *state {
|
||||
return &state{rate: rateK768, outputLen: 48, dsbyte: dsbyteSHA3}
|
||||
}
|
||||
|
||||
func new512Generic() *state {
|
||||
return &state{rate: rateK1024, outputLen: 64, dsbyte: dsbyteSHA3}
|
||||
}
|
||||
|
||||
// NewLegacyKeccak256 creates a new Keccak-256 hash.
|
||||
//
|
||||
// Only use this function if you require compatibility with an existing cryptosystem
|
||||
// that uses non-standard padding. All other users should use New256 instead.
|
||||
func NewLegacyKeccak256() hash.Hash {
|
||||
return &state{rate: rateK512, outputLen: 32, dsbyte: dsbyteKeccak}
|
||||
}
|
||||
|
||||
// NewLegacyKeccak512 creates a new Keccak-512 hash.
|
||||
//
|
||||
// Only use this function if you require compatibility with an existing cryptosystem
|
||||
// that uses non-standard padding. All other users should use New512 instead.
|
||||
func NewLegacyKeccak512() hash.Hash {
|
||||
return &state{rate: rateK1024, outputLen: 64, dsbyte: dsbyteKeccak}
|
||||
return sha3.New512()
|
||||
}
|
||||
|
||||
// Sum224 returns the SHA3-224 digest of the data.
|
||||
func Sum224(data []byte) (digest [28]byte) {
|
||||
h := New224()
|
||||
h.Write(data)
|
||||
h.Sum(digest[:0])
|
||||
return
|
||||
//
|
||||
// It is a wrapper for the [sha3.Sum224] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func Sum224(data []byte) [28]byte {
|
||||
return sha3.Sum224(data)
|
||||
}
|
||||
|
||||
// Sum256 returns the SHA3-256 digest of the data.
|
||||
func Sum256(data []byte) (digest [32]byte) {
|
||||
h := New256()
|
||||
h.Write(data)
|
||||
h.Sum(digest[:0])
|
||||
return
|
||||
//
|
||||
// It is a wrapper for the [sha3.Sum256] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func Sum256(data []byte) [32]byte {
|
||||
return sha3.Sum256(data)
|
||||
}
|
||||
|
||||
// Sum384 returns the SHA3-384 digest of the data.
|
||||
func Sum384(data []byte) (digest [48]byte) {
|
||||
h := New384()
|
||||
h.Write(data)
|
||||
h.Sum(digest[:0])
|
||||
return
|
||||
//
|
||||
// It is a wrapper for the [sha3.Sum384] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func Sum384(data []byte) [48]byte {
|
||||
return sha3.Sum384(data)
|
||||
}
|
||||
|
||||
// Sum512 returns the SHA3-512 digest of the data.
|
||||
func Sum512(data []byte) (digest [64]byte) {
|
||||
h := New512()
|
||||
h.Write(data)
|
||||
h.Sum(digest[:0])
|
||||
return
|
||||
//
|
||||
// It is a wrapper for the [sha3.Sum512] function in the standard library.
|
||||
//
|
||||
//go:fix inline
|
||||
func Sum512(data []byte) [64]byte {
|
||||
return sha3.Sum512(data)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !gc || purego || !s390x
|
||||
|
||||
package sha3
|
||||
|
||||
func new224() *state {
|
||||
return new224Generic()
|
||||
}
|
||||
|
||||
func new256() *state {
|
||||
return new256Generic()
|
||||
}
|
||||
|
||||
func new384() *state {
|
||||
return new384Generic()
|
||||
}
|
||||
|
||||
func new512() *state {
|
||||
return new512Generic()
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build amd64 && !purego && gc
|
||||
|
||||
package sha3
|
||||
|
||||
// This function is implemented in keccakf_amd64.s.
|
||||
|
||||
//go:noescape
|
||||
|
||||
func keccakF1600(a *[25]uint64)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,15 +4,46 @@
|
||||
|
||||
package sha3
|
||||
|
||||
// This implementation is only used for NewLegacyKeccak256 and
|
||||
// NewLegacyKeccak512, which are not implemented by crypto/sha3.
|
||||
// All other functions in this package are wrappers around crypto/sha3.
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
)
|
||||
|
||||
const (
|
||||
dsbyteKeccak = 0b00000001
|
||||
|
||||
// rateK[c] is the rate in bytes for Keccak[c] where c is the capacity in
|
||||
// bits. Given the sponge size is 1600 bits, the rate is 1600 - c bits.
|
||||
rateK256 = (1600 - 256) / 8
|
||||
rateK512 = (1600 - 512) / 8
|
||||
rateK1024 = (1600 - 1024) / 8
|
||||
)
|
||||
|
||||
// NewLegacyKeccak256 creates a new Keccak-256 hash.
|
||||
//
|
||||
// Only use this function if you require compatibility with an existing cryptosystem
|
||||
// that uses non-standard padding. All other users should use New256 instead.
|
||||
func NewLegacyKeccak256() hash.Hash {
|
||||
return &state{rate: rateK512, outputLen: 32, dsbyte: dsbyteKeccak}
|
||||
}
|
||||
|
||||
// NewLegacyKeccak512 creates a new Keccak-512 hash.
|
||||
//
|
||||
// Only use this function if you require compatibility with an existing cryptosystem
|
||||
// that uses non-standard padding. All other users should use New512 instead.
|
||||
func NewLegacyKeccak512() hash.Hash {
|
||||
return &state{rate: rateK1024, outputLen: 64, dsbyte: dsbyteKeccak}
|
||||
}
|
||||
|
||||
// spongeDirection indicates the direction bytes are flowing through the sponge.
|
||||
type spongeDirection int
|
||||
|
||||
@@ -173,12 +204,9 @@ func (d *state) Sum(in []byte) []byte {
|
||||
}
|
||||
|
||||
const (
|
||||
magicSHA3 = "sha\x08"
|
||||
magicShake = "sha\x09"
|
||||
magicCShake = "sha\x0a"
|
||||
magicKeccak = "sha\x0b"
|
||||
// magic || rate || main state || n || sponge direction
|
||||
marshaledSize = len(magicSHA3) + 1 + 200 + 1 + 1
|
||||
marshaledSize = len(magicKeccak) + 1 + 200 + 1 + 1
|
||||
)
|
||||
|
||||
func (d *state) MarshalBinary() ([]byte, error) {
|
||||
@@ -187,12 +215,6 @@ func (d *state) MarshalBinary() ([]byte, error) {
|
||||
|
||||
func (d *state) AppendBinary(b []byte) ([]byte, error) {
|
||||
switch d.dsbyte {
|
||||
case dsbyteSHA3:
|
||||
b = append(b, magicSHA3...)
|
||||
case dsbyteShake:
|
||||
b = append(b, magicShake...)
|
||||
case dsbyteCShake:
|
||||
b = append(b, magicCShake...)
|
||||
case dsbyteKeccak:
|
||||
b = append(b, magicKeccak...)
|
||||
default:
|
||||
@@ -210,12 +232,9 @@ func (d *state) UnmarshalBinary(b []byte) error {
|
||||
return errors.New("sha3: invalid hash state")
|
||||
}
|
||||
|
||||
magic := string(b[:len(magicSHA3)])
|
||||
b = b[len(magicSHA3):]
|
||||
magic := string(b[:len(magicKeccak)])
|
||||
b = b[len(magicKeccak):]
|
||||
switch {
|
||||
case magic == magicSHA3 && d.dsbyte == dsbyteSHA3:
|
||||
case magic == magicShake && d.dsbyte == dsbyteShake:
|
||||
case magic == magicCShake && d.dsbyte == dsbyteCShake:
|
||||
case magic == magicKeccak && d.dsbyte == dsbyteKeccak:
|
||||
default:
|
||||
return errors.New("sha3: invalid hash state identifier")
|
||||
@@ -2,10 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !amd64 || purego || !gc
|
||||
|
||||
package sha3
|
||||
|
||||
// This implementation is only used for NewLegacyKeccak256 and
|
||||
// NewLegacyKeccak512, which are not implemented by crypto/sha3.
|
||||
// All other functions in this package are wrappers around crypto/sha3.
|
||||
|
||||
import "math/bits"
|
||||
|
||||
// rc stores the round constants for use in the ι step.
|
||||
@@ -1,303 +0,0 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build gc && !purego
|
||||
|
||||
package sha3
|
||||
|
||||
// This file contains code for using the 'compute intermediate
|
||||
// message digest' (KIMD) and 'compute last message digest' (KLMD)
|
||||
// instructions to compute SHA-3 and SHAKE hashes on IBM Z.
|
||||
|
||||
import (
|
||||
"hash"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
)
|
||||
|
||||
// codes represent 7-bit KIMD/KLMD function codes as defined in
|
||||
// the Principles of Operation.
|
||||
type code uint64
|
||||
|
||||
const (
|
||||
// function codes for KIMD/KLMD
|
||||
sha3_224 code = 32
|
||||
sha3_256 = 33
|
||||
sha3_384 = 34
|
||||
sha3_512 = 35
|
||||
shake_128 = 36
|
||||
shake_256 = 37
|
||||
nopad = 0x100
|
||||
)
|
||||
|
||||
// kimd is a wrapper for the 'compute intermediate message digest' instruction.
|
||||
// src must be a multiple of the rate for the given function code.
|
||||
//
|
||||
//go:noescape
|
||||
func kimd(function code, chain *[200]byte, src []byte)
|
||||
|
||||
// klmd is a wrapper for the 'compute last message digest' instruction.
|
||||
// src padding is handled by the instruction.
|
||||
//
|
||||
//go:noescape
|
||||
func klmd(function code, chain *[200]byte, dst, src []byte)
|
||||
|
||||
type asmState struct {
|
||||
a [200]byte // 1600 bit state
|
||||
buf []byte // care must be taken to ensure cap(buf) is a multiple of rate
|
||||
rate int // equivalent to block size
|
||||
storage [3072]byte // underlying storage for buf
|
||||
outputLen int // output length for full security
|
||||
function code // KIMD/KLMD function code
|
||||
state spongeDirection // whether the sponge is absorbing or squeezing
|
||||
}
|
||||
|
||||
func newAsmState(function code) *asmState {
|
||||
var s asmState
|
||||
s.function = function
|
||||
switch function {
|
||||
case sha3_224:
|
||||
s.rate = 144
|
||||
s.outputLen = 28
|
||||
case sha3_256:
|
||||
s.rate = 136
|
||||
s.outputLen = 32
|
||||
case sha3_384:
|
||||
s.rate = 104
|
||||
s.outputLen = 48
|
||||
case sha3_512:
|
||||
s.rate = 72
|
||||
s.outputLen = 64
|
||||
case shake_128:
|
||||
s.rate = 168
|
||||
s.outputLen = 32
|
||||
case shake_256:
|
||||
s.rate = 136
|
||||
s.outputLen = 64
|
||||
default:
|
||||
panic("sha3: unrecognized function code")
|
||||
}
|
||||
|
||||
// limit s.buf size to a multiple of s.rate
|
||||
s.resetBuf()
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *asmState) clone() *asmState {
|
||||
c := *s
|
||||
c.buf = c.storage[:len(s.buf):cap(s.buf)]
|
||||
return &c
|
||||
}
|
||||
|
||||
// copyIntoBuf copies b into buf. It will panic if there is not enough space to
|
||||
// store all of b.
|
||||
func (s *asmState) copyIntoBuf(b []byte) {
|
||||
bufLen := len(s.buf)
|
||||
s.buf = s.buf[:len(s.buf)+len(b)]
|
||||
copy(s.buf[bufLen:], b)
|
||||
}
|
||||
|
||||
// resetBuf points buf at storage, sets the length to 0 and sets cap to be a
|
||||
// multiple of the rate.
|
||||
func (s *asmState) resetBuf() {
|
||||
max := (cap(s.storage) / s.rate) * s.rate
|
||||
s.buf = s.storage[:0:max]
|
||||
}
|
||||
|
||||
// Write (via the embedded io.Writer interface) adds more data to the running hash.
|
||||
// It never returns an error.
|
||||
func (s *asmState) Write(b []byte) (int, error) {
|
||||
if s.state != spongeAbsorbing {
|
||||
panic("sha3: Write after Read")
|
||||
}
|
||||
length := len(b)
|
||||
for len(b) > 0 {
|
||||
if len(s.buf) == 0 && len(b) >= cap(s.buf) {
|
||||
// Hash the data directly and push any remaining bytes
|
||||
// into the buffer.
|
||||
remainder := len(b) % s.rate
|
||||
kimd(s.function, &s.a, b[:len(b)-remainder])
|
||||
if remainder != 0 {
|
||||
s.copyIntoBuf(b[len(b)-remainder:])
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
if len(s.buf) == cap(s.buf) {
|
||||
// flush the buffer
|
||||
kimd(s.function, &s.a, s.buf)
|
||||
s.buf = s.buf[:0]
|
||||
}
|
||||
|
||||
// copy as much as we can into the buffer
|
||||
n := len(b)
|
||||
if len(b) > cap(s.buf)-len(s.buf) {
|
||||
n = cap(s.buf) - len(s.buf)
|
||||
}
|
||||
s.copyIntoBuf(b[:n])
|
||||
b = b[n:]
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// Read squeezes an arbitrary number of bytes from the sponge.
|
||||
func (s *asmState) Read(out []byte) (n int, err error) {
|
||||
// The 'compute last message digest' instruction only stores the digest
|
||||
// at the first operand (dst) for SHAKE functions.
|
||||
if s.function != shake_128 && s.function != shake_256 {
|
||||
panic("sha3: can only call Read for SHAKE functions")
|
||||
}
|
||||
|
||||
n = len(out)
|
||||
|
||||
// need to pad if we were absorbing
|
||||
if s.state == spongeAbsorbing {
|
||||
s.state = spongeSqueezing
|
||||
|
||||
// write hash directly into out if possible
|
||||
if len(out)%s.rate == 0 {
|
||||
klmd(s.function, &s.a, out, s.buf) // len(out) may be 0
|
||||
s.buf = s.buf[:0]
|
||||
return
|
||||
}
|
||||
|
||||
// write hash into buffer
|
||||
max := cap(s.buf)
|
||||
if max > len(out) {
|
||||
max = (len(out)/s.rate)*s.rate + s.rate
|
||||
}
|
||||
klmd(s.function, &s.a, s.buf[:max], s.buf)
|
||||
s.buf = s.buf[:max]
|
||||
}
|
||||
|
||||
for len(out) > 0 {
|
||||
// flush the buffer
|
||||
if len(s.buf) != 0 {
|
||||
c := copy(out, s.buf)
|
||||
out = out[c:]
|
||||
s.buf = s.buf[c:]
|
||||
continue
|
||||
}
|
||||
|
||||
// write hash directly into out if possible
|
||||
if len(out)%s.rate == 0 {
|
||||
klmd(s.function|nopad, &s.a, out, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// write hash into buffer
|
||||
s.resetBuf()
|
||||
if cap(s.buf) > len(out) {
|
||||
s.buf = s.buf[:(len(out)/s.rate)*s.rate+s.rate]
|
||||
}
|
||||
klmd(s.function|nopad, &s.a, s.buf, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sum appends the current hash to b and returns the resulting slice.
|
||||
// It does not change the underlying hash state.
|
||||
func (s *asmState) Sum(b []byte) []byte {
|
||||
if s.state != spongeAbsorbing {
|
||||
panic("sha3: Sum after Read")
|
||||
}
|
||||
|
||||
// Copy the state to preserve the original.
|
||||
a := s.a
|
||||
|
||||
// Hash the buffer. Note that we don't clear it because we
|
||||
// aren't updating the state.
|
||||
switch s.function {
|
||||
case sha3_224, sha3_256, sha3_384, sha3_512:
|
||||
klmd(s.function, &a, nil, s.buf)
|
||||
return append(b, a[:s.outputLen]...)
|
||||
case shake_128, shake_256:
|
||||
d := make([]byte, s.outputLen, 64)
|
||||
klmd(s.function, &a, d, s.buf)
|
||||
return append(b, d[:s.outputLen]...)
|
||||
default:
|
||||
panic("sha3: unknown function")
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the Hash to its initial state.
|
||||
func (s *asmState) Reset() {
|
||||
for i := range s.a {
|
||||
s.a[i] = 0
|
||||
}
|
||||
s.resetBuf()
|
||||
s.state = spongeAbsorbing
|
||||
}
|
||||
|
||||
// Size returns the number of bytes Sum will return.
|
||||
func (s *asmState) Size() int {
|
||||
return s.outputLen
|
||||
}
|
||||
|
||||
// BlockSize returns the hash's underlying block size.
|
||||
// The Write method must be able to accept any amount
|
||||
// of data, but it may operate more efficiently if all writes
|
||||
// are a multiple of the block size.
|
||||
func (s *asmState) BlockSize() int {
|
||||
return s.rate
|
||||
}
|
||||
|
||||
// Clone returns a copy of the ShakeHash in its current state.
|
||||
func (s *asmState) Clone() ShakeHash {
|
||||
return s.clone()
|
||||
}
|
||||
|
||||
// new224 returns an assembly implementation of SHA3-224 if available,
|
||||
// otherwise it returns a generic implementation.
|
||||
func new224() hash.Hash {
|
||||
if cpu.S390X.HasSHA3 {
|
||||
return newAsmState(sha3_224)
|
||||
}
|
||||
return new224Generic()
|
||||
}
|
||||
|
||||
// new256 returns an assembly implementation of SHA3-256 if available,
|
||||
// otherwise it returns a generic implementation.
|
||||
func new256() hash.Hash {
|
||||
if cpu.S390X.HasSHA3 {
|
||||
return newAsmState(sha3_256)
|
||||
}
|
||||
return new256Generic()
|
||||
}
|
||||
|
||||
// new384 returns an assembly implementation of SHA3-384 if available,
|
||||
// otherwise it returns a generic implementation.
|
||||
func new384() hash.Hash {
|
||||
if cpu.S390X.HasSHA3 {
|
||||
return newAsmState(sha3_384)
|
||||
}
|
||||
return new384Generic()
|
||||
}
|
||||
|
||||
// new512 returns an assembly implementation of SHA3-512 if available,
|
||||
// otherwise it returns a generic implementation.
|
||||
func new512() hash.Hash {
|
||||
if cpu.S390X.HasSHA3 {
|
||||
return newAsmState(sha3_512)
|
||||
}
|
||||
return new512Generic()
|
||||
}
|
||||
|
||||
// newShake128 returns an assembly implementation of SHAKE-128 if available,
|
||||
// otherwise it returns a generic implementation.
|
||||
func newShake128() ShakeHash {
|
||||
if cpu.S390X.HasSHA3 {
|
||||
return newAsmState(shake_128)
|
||||
}
|
||||
return newShake128Generic()
|
||||
}
|
||||
|
||||
// newShake256 returns an assembly implementation of SHAKE-256 if available,
|
||||
// otherwise it returns a generic implementation.
|
||||
func newShake256() ShakeHash {
|
||||
if cpu.S390X.HasSHA3 {
|
||||
return newAsmState(shake_256)
|
||||
}
|
||||
return newShake256Generic()
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build gc && !purego
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func kimd(function code, chain *[200]byte, src []byte)
|
||||
TEXT ·kimd(SB), NOFRAME|NOSPLIT, $0-40
|
||||
MOVD function+0(FP), R0
|
||||
MOVD chain+8(FP), R1
|
||||
LMG src+16(FP), R2, R3 // R2=base, R3=len
|
||||
|
||||
continue:
|
||||
WORD $0xB93E0002 // KIMD --, R2
|
||||
BVS continue // continue if interrupted
|
||||
MOVD $0, R0 // reset R0 for pre-go1.8 compilers
|
||||
RET
|
||||
|
||||
// func klmd(function code, chain *[200]byte, dst, src []byte)
|
||||
TEXT ·klmd(SB), NOFRAME|NOSPLIT, $0-64
|
||||
// TODO: SHAKE support
|
||||
MOVD function+0(FP), R0
|
||||
MOVD chain+8(FP), R1
|
||||
LMG dst+16(FP), R2, R3 // R2=base, R3=len
|
||||
LMG src+40(FP), R4, R5 // R4=base, R5=len
|
||||
|
||||
continue:
|
||||
WORD $0xB93F0024 // KLMD R2, R4
|
||||
BVS continue // continue if interrupted
|
||||
MOVD $0, R0 // reset R0 for pre-go1.8 compilers
|
||||
RET
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math/rand"
|
||||
@@ -523,124 +522,3 @@ func testMarshalUnmarshal(t *testing.T, h hash.Hash) {
|
||||
t.Errorf("got %x, want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPermutationFunction measures the speed of the permutation function
|
||||
// with no input data.
|
||||
func BenchmarkPermutationFunction(b *testing.B) {
|
||||
b.SetBytes(int64(200))
|
||||
var lanes [25]uint64
|
||||
for i := 0; i < b.N; i++ {
|
||||
keccakF1600(&lanes)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkHash tests the speed to hash num buffers of buflen each.
|
||||
func benchmarkHash(b *testing.B, h hash.Hash, size, num int) {
|
||||
b.StopTimer()
|
||||
h.Reset()
|
||||
data := sequentialBytes(size)
|
||||
b.SetBytes(int64(size * num))
|
||||
b.StartTimer()
|
||||
|
||||
var state []byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < num; j++ {
|
||||
h.Write(data)
|
||||
}
|
||||
state = h.Sum(state[:0])
|
||||
}
|
||||
b.StopTimer()
|
||||
h.Reset()
|
||||
}
|
||||
|
||||
// benchmarkShake is specialized to the Shake instances, which don't
|
||||
// require a copy on reading output.
|
||||
func benchmarkShake(b *testing.B, h ShakeHash, size, num int) {
|
||||
b.StopTimer()
|
||||
h.Reset()
|
||||
data := sequentialBytes(size)
|
||||
d := make([]byte, 32)
|
||||
|
||||
b.SetBytes(int64(size * num))
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
h.Reset()
|
||||
for j := 0; j < num; j++ {
|
||||
h.Write(data)
|
||||
}
|
||||
h.Read(d)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSha3_512_MTU(b *testing.B) { benchmarkHash(b, New512(), 1350, 1) }
|
||||
func BenchmarkSha3_384_MTU(b *testing.B) { benchmarkHash(b, New384(), 1350, 1) }
|
||||
func BenchmarkSha3_256_MTU(b *testing.B) { benchmarkHash(b, New256(), 1350, 1) }
|
||||
func BenchmarkSha3_224_MTU(b *testing.B) { benchmarkHash(b, New224(), 1350, 1) }
|
||||
|
||||
func BenchmarkShake128_MTU(b *testing.B) { benchmarkShake(b, NewShake128(), 1350, 1) }
|
||||
func BenchmarkShake256_MTU(b *testing.B) { benchmarkShake(b, NewShake256(), 1350, 1) }
|
||||
func BenchmarkShake256_16x(b *testing.B) { benchmarkShake(b, NewShake256(), 16, 1024) }
|
||||
func BenchmarkShake256_1MiB(b *testing.B) { benchmarkShake(b, NewShake256(), 1024, 1024) }
|
||||
|
||||
func BenchmarkSha3_512_1MiB(b *testing.B) { benchmarkHash(b, New512(), 1024, 1024) }
|
||||
|
||||
func Example_sum() {
|
||||
buf := []byte("some data to hash")
|
||||
// A hash needs to be 64 bytes long to have 256-bit collision resistance.
|
||||
h := make([]byte, 64)
|
||||
// Compute a 64-byte hash of buf and put it in h.
|
||||
ShakeSum256(h, buf)
|
||||
fmt.Printf("%x\n", h)
|
||||
// Output: 0f65fe41fc353e52c55667bb9e2b27bfcc8476f2c413e9437d272ee3194a4e3146d05ec04a25d16b8f577c19b82d16b1424c3e022e783d2b4da98de3658d363d
|
||||
}
|
||||
|
||||
func Example_mac() {
|
||||
k := []byte("this is a secret key; you should generate a strong random key that's at least 32 bytes long")
|
||||
buf := []byte("and this is some data to authenticate")
|
||||
// A MAC with 32 bytes of output has 256-bit security strength -- if you use at least a 32-byte-long key.
|
||||
h := make([]byte, 32)
|
||||
d := NewShake256()
|
||||
// Write the key into the hash.
|
||||
d.Write(k)
|
||||
// Now write the data.
|
||||
d.Write(buf)
|
||||
// Read 32 bytes of output from the hash into h.
|
||||
d.Read(h)
|
||||
fmt.Printf("%x\n", h)
|
||||
// Output: 78de2974bd2711d5549ffd32b753ef0f5fa80a0db2556db60f0987eb8a9218ff
|
||||
}
|
||||
|
||||
func ExampleNewCShake256() {
|
||||
out := make([]byte, 32)
|
||||
msg := []byte("The quick brown fox jumps over the lazy dog")
|
||||
|
||||
// Example 1: Simple cshake
|
||||
c1 := NewCShake256([]byte("NAME"), []byte("Partition1"))
|
||||
c1.Write(msg)
|
||||
c1.Read(out)
|
||||
fmt.Println(hex.EncodeToString(out))
|
||||
|
||||
// Example 2: Different customization string produces different digest
|
||||
c1 = NewCShake256([]byte("NAME"), []byte("Partition2"))
|
||||
c1.Write(msg)
|
||||
c1.Read(out)
|
||||
fmt.Println(hex.EncodeToString(out))
|
||||
|
||||
// Example 3: Longer output length produces longer digest
|
||||
out = make([]byte, 64)
|
||||
c1 = NewCShake256([]byte("NAME"), []byte("Partition1"))
|
||||
c1.Write(msg)
|
||||
c1.Read(out)
|
||||
fmt.Println(hex.EncodeToString(out))
|
||||
|
||||
// Example 4: Next read produces different result
|
||||
c1.Read(out)
|
||||
fmt.Println(hex.EncodeToString(out))
|
||||
|
||||
// Output:
|
||||
//a90a4c6ca9af2156eba43dc8398279e6b60dcd56fb21837afe6c308fd4ceb05b
|
||||
//a8db03e71f3e4da5c4eee9d28333cdd355f51cef3c567e59be5beb4ecdbb28f0
|
||||
//a90a4c6ca9af2156eba43dc8398279e6b60dcd56fb21837afe6c308fd4ceb05b9dd98c6ee866ca7dc5a39d53e960f400bcd5a19c8a2d6ec6459f63696543a0d8
|
||||
//85e73a72228d08b46515553ca3a29d47df3047e5d84b12d6c2c63e579f4fd1105716b7838e92e981863907f434bfd4443c9e56ea09da998d2f9b47db71988109
|
||||
}
|
||||
|
||||
+49
-123
@@ -4,24 +4,10 @@
|
||||
|
||||
package sha3
|
||||
|
||||
// This file defines the ShakeHash interface, and provides
|
||||
// functions for creating SHAKE and cSHAKE instances, as well as utility
|
||||
// functions for hashing bytes to arbitrary-length output.
|
||||
//
|
||||
//
|
||||
// SHAKE implementation is based on FIPS PUB 202 [1]
|
||||
// cSHAKE implementations is based on NIST SP 800-185 [2]
|
||||
//
|
||||
// [1] https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf
|
||||
// [2] https://doi.org/10.6028/NIST.SP.800-185
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"crypto/sha3"
|
||||
"hash"
|
||||
"io"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
// ShakeHash defines the interface to hash functions that support
|
||||
@@ -32,7 +18,7 @@ type ShakeHash interface {
|
||||
hash.Hash
|
||||
|
||||
// Read reads more output from the hash; reading affects the hash's
|
||||
// state. (ShakeHash.Read is thus very different from Hash.Sum)
|
||||
// state. (ShakeHash.Read is thus very different from Hash.Sum.)
|
||||
// It never returns an error, but subsequent calls to Write or Sum
|
||||
// will panic.
|
||||
io.Reader
|
||||
@@ -41,115 +27,18 @@ type ShakeHash interface {
|
||||
Clone() ShakeHash
|
||||
}
|
||||
|
||||
// cSHAKE specific context
|
||||
type cshakeState struct {
|
||||
*state // SHA-3 state context and Read/Write operations
|
||||
|
||||
// initBlock is the cSHAKE specific initialization set of bytes. It is initialized
|
||||
// by newCShake function and stores concatenation of N followed by S, encoded
|
||||
// by the method specified in 3.3 of [1].
|
||||
// It is stored here in order for Reset() to be able to put context into
|
||||
// initial state.
|
||||
initBlock []byte
|
||||
}
|
||||
|
||||
func bytepad(data []byte, rate int) []byte {
|
||||
out := make([]byte, 0, 9+len(data)+rate-1)
|
||||
out = append(out, leftEncode(uint64(rate))...)
|
||||
out = append(out, data...)
|
||||
if padlen := rate - len(out)%rate; padlen < rate {
|
||||
out = append(out, make([]byte, padlen)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func leftEncode(x uint64) []byte {
|
||||
// Let n be the smallest positive integer for which 2^(8n) > x.
|
||||
n := (bits.Len64(x) + 7) / 8
|
||||
if n == 0 {
|
||||
n = 1
|
||||
}
|
||||
// Return n || x with n as a byte and x an n bytes in big-endian order.
|
||||
b := make([]byte, 9)
|
||||
binary.BigEndian.PutUint64(b[1:], x)
|
||||
b = b[9-n-1:]
|
||||
b[0] = byte(n)
|
||||
return b
|
||||
}
|
||||
|
||||
func newCShake(N, S []byte, rate, outputLen int, dsbyte byte) ShakeHash {
|
||||
c := cshakeState{state: &state{rate: rate, outputLen: outputLen, dsbyte: dsbyte}}
|
||||
c.initBlock = make([]byte, 0, 9+len(N)+9+len(S)) // leftEncode returns max 9 bytes
|
||||
c.initBlock = append(c.initBlock, leftEncode(uint64(len(N))*8)...)
|
||||
c.initBlock = append(c.initBlock, N...)
|
||||
c.initBlock = append(c.initBlock, leftEncode(uint64(len(S))*8)...)
|
||||
c.initBlock = append(c.initBlock, S...)
|
||||
c.Write(bytepad(c.initBlock, c.rate))
|
||||
return &c
|
||||
}
|
||||
|
||||
// Reset resets the hash to initial state.
|
||||
func (c *cshakeState) Reset() {
|
||||
c.state.Reset()
|
||||
c.Write(bytepad(c.initBlock, c.rate))
|
||||
}
|
||||
|
||||
// Clone returns copy of a cSHAKE context within its current state.
|
||||
func (c *cshakeState) Clone() ShakeHash {
|
||||
b := make([]byte, len(c.initBlock))
|
||||
copy(b, c.initBlock)
|
||||
return &cshakeState{state: c.clone(), initBlock: b}
|
||||
}
|
||||
|
||||
// Clone returns copy of SHAKE context within its current state.
|
||||
func (c *state) Clone() ShakeHash {
|
||||
return c.clone()
|
||||
}
|
||||
|
||||
func (c *cshakeState) MarshalBinary() ([]byte, error) {
|
||||
return c.AppendBinary(make([]byte, 0, marshaledSize+len(c.initBlock)))
|
||||
}
|
||||
|
||||
func (c *cshakeState) AppendBinary(b []byte) ([]byte, error) {
|
||||
b, err := c.state.AppendBinary(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b = append(b, c.initBlock...)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *cshakeState) UnmarshalBinary(b []byte) error {
|
||||
if len(b) <= marshaledSize {
|
||||
return errors.New("sha3: invalid hash state")
|
||||
}
|
||||
if err := c.state.UnmarshalBinary(b[:marshaledSize]); err != nil {
|
||||
return err
|
||||
}
|
||||
c.initBlock = bytes.Clone(b[marshaledSize:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewShake128 creates a new SHAKE128 variable-output-length ShakeHash.
|
||||
// Its generic security strength is 128 bits against all attacks if at
|
||||
// least 32 bytes of its output are used.
|
||||
func NewShake128() ShakeHash {
|
||||
return newShake128()
|
||||
return &shakeWrapper{sha3.NewSHAKE128(), 32, false, sha3.NewSHAKE128}
|
||||
}
|
||||
|
||||
// NewShake256 creates a new SHAKE256 variable-output-length ShakeHash.
|
||||
// Its generic security strength is 256 bits against all attacks if
|
||||
// at least 64 bytes of its output are used.
|
||||
func NewShake256() ShakeHash {
|
||||
return newShake256()
|
||||
}
|
||||
|
||||
func newShake128Generic() *state {
|
||||
return &state{rate: rateK256, outputLen: 32, dsbyte: dsbyteShake}
|
||||
}
|
||||
|
||||
func newShake256Generic() *state {
|
||||
return &state{rate: rateK512, outputLen: 64, dsbyte: dsbyteShake}
|
||||
return &shakeWrapper{sha3.NewSHAKE256(), 64, false, sha3.NewSHAKE256}
|
||||
}
|
||||
|
||||
// NewCShake128 creates a new instance of cSHAKE128 variable-output-length ShakeHash,
|
||||
@@ -159,10 +48,9 @@ func newShake256Generic() *state {
|
||||
// computations on same input with different S yield unrelated outputs.
|
||||
// When N and S are both empty, this is equivalent to NewShake128.
|
||||
func NewCShake128(N, S []byte) ShakeHash {
|
||||
if len(N) == 0 && len(S) == 0 {
|
||||
return NewShake128()
|
||||
}
|
||||
return newCShake(N, S, rateK256, 32, dsbyteCShake)
|
||||
return &shakeWrapper{sha3.NewCSHAKE128(N, S), 32, false, func() *sha3.SHAKE {
|
||||
return sha3.NewCSHAKE128(N, S)
|
||||
}}
|
||||
}
|
||||
|
||||
// NewCShake256 creates a new instance of cSHAKE256 variable-output-length ShakeHash,
|
||||
@@ -172,10 +60,9 @@ func NewCShake128(N, S []byte) ShakeHash {
|
||||
// computations on same input with different S yield unrelated outputs.
|
||||
// When N and S are both empty, this is equivalent to NewShake256.
|
||||
func NewCShake256(N, S []byte) ShakeHash {
|
||||
if len(N) == 0 && len(S) == 0 {
|
||||
return NewShake256()
|
||||
}
|
||||
return newCShake(N, S, rateK512, 64, dsbyteCShake)
|
||||
return &shakeWrapper{sha3.NewCSHAKE256(N, S), 64, false, func() *sha3.SHAKE {
|
||||
return sha3.NewCSHAKE256(N, S)
|
||||
}}
|
||||
}
|
||||
|
||||
// ShakeSum128 writes an arbitrary-length digest of data into hash.
|
||||
@@ -191,3 +78,42 @@ func ShakeSum256(hash, data []byte) {
|
||||
h.Write(data)
|
||||
h.Read(hash)
|
||||
}
|
||||
|
||||
// shakeWrapper adds the Size, Sum, and Clone methods to a sha3.SHAKE
|
||||
// to implement the ShakeHash interface.
|
||||
type shakeWrapper struct {
|
||||
*sha3.SHAKE
|
||||
outputLen int
|
||||
squeezing bool
|
||||
newSHAKE func() *sha3.SHAKE
|
||||
}
|
||||
|
||||
func (w *shakeWrapper) Read(p []byte) (n int, err error) {
|
||||
w.squeezing = true
|
||||
return w.SHAKE.Read(p)
|
||||
}
|
||||
|
||||
func (w *shakeWrapper) Clone() ShakeHash {
|
||||
s := w.newSHAKE()
|
||||
b, err := w.MarshalBinary()
|
||||
if err != nil {
|
||||
panic(err) // unreachable
|
||||
}
|
||||
if err := s.UnmarshalBinary(b); err != nil {
|
||||
panic(err) // unreachable
|
||||
}
|
||||
return &shakeWrapper{s, w.outputLen, w.squeezing, w.newSHAKE}
|
||||
}
|
||||
|
||||
func (w *shakeWrapper) Size() int { return w.outputLen }
|
||||
|
||||
func (w *shakeWrapper) Sum(b []byte) []byte {
|
||||
if w.squeezing {
|
||||
panic("sha3: Sum after Read")
|
||||
}
|
||||
out := make([]byte, w.outputLen)
|
||||
// Clone the state so that we don't affect future Write calls.
|
||||
s := w.Clone()
|
||||
s.Read(out)
|
||||
return append(b, out...)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !gc || purego || !s390x
|
||||
|
||||
package sha3
|
||||
|
||||
func newShake128() *state {
|
||||
return newShake128Generic()
|
||||
}
|
||||
|
||||
func newShake256() *state {
|
||||
return newShake256Generic()
|
||||
}
|
||||
+233
-3
@@ -26,6 +26,7 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -307,17 +308,50 @@ func parseKey(in []byte) (out *Key, rest []byte, err error) {
|
||||
}, record.Rest, nil
|
||||
}
|
||||
|
||||
// pipelineMaxInFlight is the maximum number of outstanding requests the
|
||||
// client will pipeline to the agent before applying backpressure.
|
||||
const pipelineMaxInFlight = 32
|
||||
|
||||
// client is a client for an ssh-agent process.
|
||||
//
|
||||
// Exactly one of pipeline / (mu, conn) is set, chosen by NewClient
|
||||
// based on whether the underlying transport implements io.Closer.
|
||||
type client struct {
|
||||
// conn is typically a *net.UnixConn
|
||||
// pipeline, if non-nil, dispatches requests over a pipelined
|
||||
// connection: requests are written as soon as the wire is
|
||||
// available and responses are routed back to per-call reply
|
||||
// channels in FIFO order by a background reader goroutine.
|
||||
pipeline *pipeline
|
||||
|
||||
// mu and conn are used in fully-serialized mode, when the
|
||||
// transport does not implement io.Closer. Each call takes mu,
|
||||
// writes its request, reads the matching response, and releases
|
||||
// mu before returning. There is no background goroutine.
|
||||
mu sync.Mutex
|
||||
conn io.ReadWriter
|
||||
// mu is used to prevent concurrent access to the agent
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewClient returns an Agent that talks to an ssh-agent process over
|
||||
// the given connection.
|
||||
//
|
||||
// If rw also implements io.Closer (like *net.UnixConn and ssh.Channel
|
||||
// do), the returned client pipelines concurrent requests over the
|
||||
// connection: callers can issue Sign and other operations from
|
||||
// multiple goroutines and they will be written to the agent as soon
|
||||
// as the wire is available, rather than waiting for the previous
|
||||
// responses. The ssh-agent protocol still requires responses to be
|
||||
// returned in request order, so a slow request delays subsequent
|
||||
// responses on the same connection (head-of-line blocking).
|
||||
//
|
||||
// Pipelining requires io.Closer because, on a Write error, the
|
||||
// background reader goroutine must be unblocked by closing the
|
||||
// underlying connection. When rw does not implement io.Closer
|
||||
// this is not possible, so NewClient falls back to fully
|
||||
// serializing each request: a single in-flight call at a time.
|
||||
func NewClient(rw io.ReadWriter) ExtendedAgent {
|
||||
if rwc, ok := rw.(io.ReadWriteCloser); ok {
|
||||
return &client{pipeline: newPipeline(rwc)}
|
||||
}
|
||||
return &client{conn: rw}
|
||||
}
|
||||
|
||||
@@ -340,6 +374,16 @@ func (c *client) call(req []byte) (reply interface{}, err error) {
|
||||
// bytes of the response are returned; no unmarshalling is
|
||||
// performed on the response.
|
||||
func (c *client) callRaw(req []byte) (reply []byte, err error) {
|
||||
if c.pipeline != nil {
|
||||
return c.pipeline.call(req)
|
||||
}
|
||||
return c.serialCall(req)
|
||||
}
|
||||
|
||||
// serialCall implements the fully-serialized request/response path
|
||||
// used when the transport is not an io.Closer. It writes req under mu
|
||||
// and reads the matching response before returning.
|
||||
func (c *client) serialCall(req []byte) (reply []byte, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -577,6 +621,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
|
||||
Constraints: constraints,
|
||||
})
|
||||
case ed25519.PrivateKey:
|
||||
if len(k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519KeyMsg{
|
||||
Type: ssh.KeyAlgoED25519,
|
||||
Pub: []byte(k)[32:],
|
||||
@@ -588,6 +635,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
|
||||
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
||||
// We still support the pointer variant for backwards compatibility.
|
||||
case *ed25519.PrivateKey:
|
||||
if len(*k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519KeyMsg{
|
||||
Type: ssh.KeyAlgoED25519,
|
||||
Pub: []byte(*k)[32:],
|
||||
@@ -663,6 +713,13 @@ func (c *client) Add(key AddedKey) error {
|
||||
constraints = append(constraints, agentConstrainConfirm)
|
||||
}
|
||||
|
||||
for _, ext := range key.ConstraintExtensions {
|
||||
constraints = append(constraints, ssh.Marshal(constrainExtensionAgentMsg{
|
||||
ExtensionName: ext.ExtensionName,
|
||||
ExtensionDetails: ext.ExtensionDetails,
|
||||
})...)
|
||||
}
|
||||
|
||||
cert := key.Certificate
|
||||
if cert == nil {
|
||||
return c.insertKey(key.PrivateKey, key.Comment, constraints)
|
||||
@@ -705,6 +762,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
|
||||
Constraints: constraints,
|
||||
})
|
||||
case ed25519.PrivateKey:
|
||||
if len(k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519CertMsg{
|
||||
Type: cert.Type(),
|
||||
CertBytes: cert.Marshal(),
|
||||
@@ -717,6 +777,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
|
||||
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
||||
// We still support the pointer variant for backwards compatibility.
|
||||
case *ed25519.PrivateKey:
|
||||
if len(*k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519CertMsg{
|
||||
Type: cert.Type(),
|
||||
CertBytes: cert.Marshal(),
|
||||
@@ -854,3 +917,170 @@ func (c *client) Extension(extensionType string, contents []byte) ([]byte, error
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// pipelineResult carries either a raw agent reply or an error back to a
|
||||
// caller waiting on the response channel.
|
||||
type pipelineResult struct {
|
||||
reply []byte
|
||||
err error
|
||||
}
|
||||
|
||||
// pipeline implements request pipelining over a single agent connection.
|
||||
//
|
||||
// Writers serialize on writeMu to both register a reply channel in the
|
||||
// pending FIFO queue and write the request bytes on the wire; the two
|
||||
// must be atomic so the queue order matches the wire order. A single
|
||||
// reader goroutine decodes responses from the connection and dispatches
|
||||
// each one to the channel at the head of the queue.
|
||||
//
|
||||
// pending is a chan-of-chan acting as a FIFO queue with a fixed
|
||||
// capacity of pipelineMaxInFlight. The outer channel provides ordering
|
||||
// (reads happen in send order) and natural backpressure (a full queue
|
||||
// blocks new writers). Each inner channel is buffered with capacity
|
||||
// one and is sent to exactly once: either by the reader goroutine
|
||||
// with the agent reply, or by shutdown with the terminal error during
|
||||
// drain. The cap-one buffer makes the producer's send non-blocking,
|
||||
// so the reader and shutdown never have to wait for the caller to be
|
||||
// scheduled on the receive.
|
||||
//
|
||||
// When the reader goroutine exits (on read error or protocol
|
||||
// violation), it closes exitCh to wake any writer blocked on the
|
||||
// pending queue, then serializes with any in-flight writer to close
|
||||
// the pending channel, and finally drains the remaining entries
|
||||
// delivering the terminal error to each waiting caller. The
|
||||
// pipeline relies on conn implementing io.Closer so a writer that
|
||||
// hits a Write error can close the connection to unblock the reader
|
||||
// goroutine; NewClient is responsible for only constructing a
|
||||
// pipeline when this guarantee holds.
|
||||
type pipeline struct {
|
||||
conn io.ReadWriteCloser
|
||||
|
||||
writeMu sync.Mutex
|
||||
// pending is the FIFO queue of reply channels with capacity
|
||||
// pipelineMaxInFlight. See type-level documentation.
|
||||
pending chan chan pipelineResult
|
||||
exitCh chan struct{}
|
||||
|
||||
// err carries the terminal error to callers blocked on a closed
|
||||
// pipeline. It is stored exactly once by the reader goroutine
|
||||
// before exitCh is closed; every read happens after observing
|
||||
// exitCh closed, so the load synchronises through the close and
|
||||
// is guaranteed to return the stored value (never nil).
|
||||
err atomic.Pointer[error]
|
||||
}
|
||||
|
||||
func newPipeline(conn io.ReadWriteCloser) *pipeline {
|
||||
p := &pipeline{
|
||||
conn: conn,
|
||||
pending: make(chan chan pipelineResult, pipelineMaxInFlight),
|
||||
exitCh: make(chan struct{}),
|
||||
}
|
||||
go p.readLoop()
|
||||
return p
|
||||
}
|
||||
|
||||
// readLoop decodes responses from conn and dispatches them in FIFO order
|
||||
// to reply channels in pending. On any failure it invokes shutdown.
|
||||
func (p *pipeline) readLoop() {
|
||||
var finalErr error
|
||||
for {
|
||||
var sizeBuf [4]byte
|
||||
if _, err := io.ReadFull(p.conn, sizeBuf[:]); err != nil {
|
||||
finalErr = err
|
||||
break
|
||||
}
|
||||
respSize := binary.BigEndian.Uint32(sizeBuf[:])
|
||||
if respSize > maxAgentResponseBytes {
|
||||
finalErr = errors.New("response too large")
|
||||
break
|
||||
}
|
||||
buf := make([]byte, respSize)
|
||||
if _, err := io.ReadFull(p.conn, buf); err != nil {
|
||||
finalErr = err
|
||||
break
|
||||
}
|
||||
// Successful writes always enqueue before sending bytes, so
|
||||
// pending has a waiting channel for this response.
|
||||
ch := <-p.pending
|
||||
// The reply channel is buffered with capacity 1 and is only
|
||||
// ever written to once, so this send cannot block.
|
||||
ch <- pipelineResult{reply: buf}
|
||||
}
|
||||
p.shutdown(clientErr(finalErr))
|
||||
}
|
||||
|
||||
// shutdown is called exactly once, from readLoop, when the reader is
|
||||
// terminating. It unblocks pending writers and fails all in-flight
|
||||
// requests with finalErr.
|
||||
func (p *pipeline) shutdown(finalErr error) {
|
||||
// Publish the terminal error before closing exitCh so any
|
||||
// writer that subsequently observes exitCh closed sees err.
|
||||
p.err.Store(&finalErr)
|
||||
|
||||
// Wake any writer blocked waiting for a slot in the pending queue.
|
||||
close(p.exitCh)
|
||||
|
||||
// Wait for any writer currently inside its critical section to
|
||||
// complete. After this lock, no new writer can reach the send on
|
||||
// pending: they will observe exitCh closed in the select and bail
|
||||
// out before attempting the send.
|
||||
p.writeMu.Lock()
|
||||
close(p.pending)
|
||||
p.writeMu.Unlock()
|
||||
|
||||
// Drain entries that were enqueued but never answered, delivering
|
||||
// the terminal error to their waiting callers. The reply channels
|
||||
// are buffered (cap 1) and written to exactly once, so these sends
|
||||
// cannot block.
|
||||
for ch := range p.pending {
|
||||
ch <- pipelineResult{err: finalErr}
|
||||
}
|
||||
}
|
||||
|
||||
// call sends req to the agent and returns the matching raw response.
|
||||
func (p *pipeline) call(req []byte) ([]byte, error) {
|
||||
replyCh := make(chan pipelineResult, 1)
|
||||
|
||||
p.writeMu.Lock()
|
||||
|
||||
// Priority check: if the reader has already finished shutdown,
|
||||
// pending is closed and sending to it would panic. Bail out now.
|
||||
// Once we pass this check while holding writeMu, shutdown cannot
|
||||
// complete close(pending) until we release writeMu, so the send
|
||||
// below is safe against concurrent closure.
|
||||
select {
|
||||
case <-p.exitCh:
|
||||
p.writeMu.Unlock()
|
||||
return nil, *p.err.Load()
|
||||
default:
|
||||
}
|
||||
|
||||
// Enqueue the reply channel before writing the request, so FIFO
|
||||
// order on the wire matches FIFO order in the pending queue. The
|
||||
// exitCh arm handles the case where the reader errors while we
|
||||
// block on a full queue.
|
||||
select {
|
||||
case p.pending <- replyCh:
|
||||
case <-p.exitCh:
|
||||
p.writeMu.Unlock()
|
||||
return nil, *p.err.Load()
|
||||
}
|
||||
|
||||
msg := make([]byte, 4+len(req))
|
||||
binary.BigEndian.PutUint32(msg, uint32(len(req)))
|
||||
copy(msg[4:], req)
|
||||
_, werr := p.conn.Write(msg)
|
||||
p.writeMu.Unlock()
|
||||
|
||||
if werr != nil {
|
||||
// The connection is in an undefined state. Close it so the
|
||||
// reader unblocks promptly and triggers shutdown for every
|
||||
// other in-flight caller. NewClient guarantees conn is a
|
||||
// real io.Closer when the pipeline is in use.
|
||||
p.conn.Close()
|
||||
return nil, clientErr(werr)
|
||||
}
|
||||
|
||||
res := <-replyCh
|
||||
return res.reply, res.err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPipelineParallelSigns(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
keyring := NewKeyring()
|
||||
if err := keyring.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"]}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go ServeAgent(keyring, c2)
|
||||
|
||||
client := NewClient(c1)
|
||||
pubKey := testPublicKeys["rsa"]
|
||||
|
||||
const N = 64
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, N)
|
||||
for i := range N {
|
||||
wg.Go(func() {
|
||||
data := []byte{byte(i), byte(i >> 8)}
|
||||
sig, err := client.Sign(pubKey, data)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if err := pubKey.Verify(data, sig); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineBackpressure(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
|
||||
const total = pipelineMaxInFlight + 8
|
||||
|
||||
// Fake agent: the reader drains requests from the wire independently
|
||||
// of response production, so the write path is never self-blocked.
|
||||
// The responder only writes a canned success reply once unblocked
|
||||
// via gate.
|
||||
var reqsRead int64
|
||||
gate := make(chan struct{}, total)
|
||||
pending := make(chan struct{}, total+pipelineMaxInFlight)
|
||||
readerDone := make(chan struct{})
|
||||
responderDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readerDone)
|
||||
defer close(pending)
|
||||
for {
|
||||
var sizeBuf [4]byte
|
||||
if _, err := io.ReadFull(c2, sizeBuf[:]); err != nil {
|
||||
return
|
||||
}
|
||||
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||
req := make([]byte, size)
|
||||
if _, err := io.ReadFull(c2, req); err != nil {
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&reqsRead, 1)
|
||||
pending <- struct{}{}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(responderDone)
|
||||
for range pending {
|
||||
<-gate
|
||||
var out [5]byte
|
||||
binary.BigEndian.PutUint32(out[:4], 1)
|
||||
out[4] = agentSuccess
|
||||
if _, err := c2.Write(out[:]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
<-readerDone
|
||||
<-responderDone
|
||||
}()
|
||||
|
||||
client := NewClient(c1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, total)
|
||||
for range total {
|
||||
wg.Go(func() {
|
||||
// RemoveAll maps to a simpleCall expecting agentSuccess.
|
||||
if err := client.RemoveAll(); err != nil {
|
||||
errs <- err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for the client to fill the pipeline.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if atomic.LoadInt64(&reqsRead) >= pipelineMaxInFlight {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if got := atomic.LoadInt64(&reqsRead); got != pipelineMaxInFlight {
|
||||
t.Errorf("requests in flight before any response: got %d, want %d", got, pipelineMaxInFlight)
|
||||
}
|
||||
|
||||
// Release all responses; every caller should eventually complete.
|
||||
for range total {
|
||||
gate <- struct{}{}
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt64(&reqsRead); got != total {
|
||||
t.Errorf("total requests received: got %d, want %d", got, total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineConnCloseFailsInFlight(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
|
||||
// Server that reads requests but never replies, so all calls are
|
||||
// parked waiting for responses.
|
||||
serverExit := make(chan struct{})
|
||||
go func() {
|
||||
defer close(serverExit)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
if _, err := c2.Read(buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewClient(c1)
|
||||
|
||||
const N = 8
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, N)
|
||||
for range N {
|
||||
wg.Go(func() {
|
||||
errs <- client.RemoveAll()
|
||||
})
|
||||
}
|
||||
|
||||
// Give callers time to enter the pipeline, then tear down the conn.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
|
||||
// All callers must return promptly with an error.
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(doneCh)
|
||||
}()
|
||||
select {
|
||||
case <-doneCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("pipelined callers did not complete after conn close")
|
||||
}
|
||||
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
if err == nil {
|
||||
t.Error("expected error after conn close, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Subsequent calls also fail fast rather than hanging.
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- client.RemoveAll() }()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err == nil {
|
||||
t.Error("expected error on post-close call, got nil")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("post-close call hung")
|
||||
}
|
||||
|
||||
<-serverExit
|
||||
}
|
||||
|
||||
// readWriter exposes only io.Reader and io.Writer, hiding any Close
|
||||
// method on the wrapped value so NewClient cannot take the pipelined
|
||||
// path.
|
||||
type readWriter struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func TestNewClientSerialFallback(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
keyring := NewKeyring()
|
||||
if err := keyring.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"]}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go ServeAgent(keyring, c2)
|
||||
|
||||
rw := readWriter{Reader: c1, Writer: c1}
|
||||
ag := NewClient(rw)
|
||||
|
||||
ci, ok := ag.(*client)
|
||||
if !ok {
|
||||
t.Fatalf("NewClient: got %T, want *client", ag)
|
||||
}
|
||||
if ci.pipeline != nil {
|
||||
t.Error("NewClient with non-Closer rw: pipeline must be nil")
|
||||
}
|
||||
if ci.conn == nil {
|
||||
t.Error("NewClient with non-Closer rw: conn must be set")
|
||||
}
|
||||
|
||||
// The serialized path correctly handles a request/response cycle.
|
||||
keys, err := ag.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Errorf("List: got %d keys, want 1", len(keys))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerialFallbackParallelSigns(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
keyring := NewKeyring()
|
||||
if err := keyring.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"]}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go ServeAgent(keyring, c2)
|
||||
|
||||
rw := readWriter{Reader: c1, Writer: c1}
|
||||
ag := NewClient(rw)
|
||||
|
||||
if ci, ok := ag.(*client); !ok {
|
||||
t.Fatalf("NewClient: got %T, want *client", ag)
|
||||
} else if ci.pipeline != nil {
|
||||
t.Fatal("pipeline must be nil for non-Closer rw")
|
||||
}
|
||||
|
||||
pubKey := testPublicKeys["rsa"]
|
||||
|
||||
const N = 64
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, N)
|
||||
for i := range N {
|
||||
wg.Go(func() {
|
||||
data := []byte{byte(i), byte(i >> 8)}
|
||||
sig, err := ag.Sign(pubKey, data)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if err := pubKey.Verify(data, sig); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// writeFailConn wraps a net.Conn and forces Write to return an error when
|
||||
// the fail flag is set. Close delegates to the underlying conn so the test
|
||||
// can observe whether the pipeline closed it.
|
||||
type writeFailConn struct {
|
||||
net.Conn
|
||||
fail atomic.Bool
|
||||
}
|
||||
|
||||
func (c *writeFailConn) Write(p []byte) (int, error) {
|
||||
if c.fail.Load() {
|
||||
return 0, errors.New("synthetic write failure")
|
||||
}
|
||||
return c.Conn.Write(p)
|
||||
}
|
||||
|
||||
func TestPipelineWriteErrorClosesConn(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c2.Close()
|
||||
|
||||
// Peer that reads and discards, exiting when the conn closes.
|
||||
serverExit := make(chan struct{})
|
||||
go func() {
|
||||
defer close(serverExit)
|
||||
io.Copy(io.Discard, c2)
|
||||
}()
|
||||
|
||||
wrapper := &writeFailConn{Conn: c1}
|
||||
wrapper.fail.Store(true)
|
||||
|
||||
client := NewClient(wrapper)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- client.RemoveAll() }()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err == nil {
|
||||
t.Fatal("expected error on forced write failure, got nil")
|
||||
}
|
||||
// The caller whose Write failed must receive the actual
|
||||
// write error, not the reader's "closed connection" error
|
||||
// from the post-shutdown drain.
|
||||
if !strings.Contains(err.Error(), "synthetic write failure") {
|
||||
t.Errorf("got %q, want error containing %q", err, "synthetic write failure")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("RemoveAll hung after write failure")
|
||||
}
|
||||
|
||||
// The pipeline must have closed the underlying conn, so the peer
|
||||
// goroutine returns from its Read.
|
||||
select {
|
||||
case <-serverExit:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("connection was not closed after write failure")
|
||||
}
|
||||
|
||||
// After shutdown, subsequent calls must also fail promptly.
|
||||
errCh2 := make(chan error, 1)
|
||||
go func() { errCh2 <- client.RemoveAll() }()
|
||||
select {
|
||||
case err := <-errCh2:
|
||||
if err == nil {
|
||||
t.Error("expected error on post-shutdown call, got nil")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("post-shutdown call hung")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineResponseTooLarge(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
// Server: read the request, then reply with a size header that
|
||||
// exceeds maxAgentResponseBytes. The size check fires before the
|
||||
// body is read, so no body needs to follow.
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
var sizeBuf [4]byte
|
||||
if _, err := io.ReadFull(c2, sizeBuf[:]); err != nil {
|
||||
return
|
||||
}
|
||||
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||
if _, err := io.CopyN(io.Discard, c2, int64(size)); err != nil {
|
||||
return
|
||||
}
|
||||
var out [4]byte
|
||||
binary.BigEndian.PutUint32(out[:], maxAgentResponseBytes+1)
|
||||
c2.Write(out[:])
|
||||
}()
|
||||
|
||||
client := NewClient(c1)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- client.RemoveAll() }()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "response too large") {
|
||||
t.Errorf("got %q, want error containing %q", err, "response too large")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("call hung")
|
||||
}
|
||||
|
||||
// Subsequent calls must also fail: the pipeline is shut down.
|
||||
errCh2 := make(chan error, 1)
|
||||
go func() { errCh2 <- client.RemoveAll() }()
|
||||
select {
|
||||
case err := <-errCh2:
|
||||
if err == nil {
|
||||
t.Error("expected error on post-shutdown call, got nil")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("post-shutdown call hung")
|
||||
}
|
||||
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestPipelineShutdownWithBlockedWriter(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
|
||||
// Server drains requests off the wire so writes never block on
|
||||
// backpressure from the transport, but never replies. The reader
|
||||
// goroutine therefore stays parked on Read, never dequeues from
|
||||
// pending, and the queue fills up.
|
||||
var reqsRead int64
|
||||
serverExit := make(chan struct{})
|
||||
go func() {
|
||||
defer close(serverExit)
|
||||
for {
|
||||
var sizeBuf [4]byte
|
||||
if _, err := io.ReadFull(c2, sizeBuf[:]); err != nil {
|
||||
return
|
||||
}
|
||||
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||
if _, err := io.CopyN(io.Discard, c2, int64(size)); err != nil {
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&reqsRead, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewClient(c1)
|
||||
|
||||
const total = pipelineMaxInFlight + 4
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, total)
|
||||
for range total {
|
||||
wg.Go(func() {
|
||||
errs <- client.RemoveAll()
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for the queue to fill: once pipelineMaxInFlight requests
|
||||
// are on the wire, pending is at capacity and the +1 writer is
|
||||
// blocked in the second-select arm; the rest are queued behind
|
||||
// writeMu.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if atomic.LoadInt64(&reqsRead) >= pipelineMaxInFlight {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if got := atomic.LoadInt64(&reqsRead); got < pipelineMaxInFlight {
|
||||
t.Fatalf("requests on wire: got %d, want >= %d", got, pipelineMaxInFlight)
|
||||
}
|
||||
// Give the +1 writer a moment to actually park in the second
|
||||
// select before we trigger shutdown.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Trigger shutdown: closing the server side makes the client's
|
||||
// reader hit EOF, which calls shutdown(...).
|
||||
c2.Close()
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(doneCh)
|
||||
}()
|
||||
select {
|
||||
case <-doneCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("blocked callers did not complete after shutdown")
|
||||
}
|
||||
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
if err == nil {
|
||||
t.Error("expected error after shutdown, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
<-serverExit
|
||||
}
|
||||
|
||||
// multiDeviceAgent simulates an ssh-agent backed by numDevices signing
|
||||
// devices, each of which takes latency to process a request. Incoming
|
||||
// requests are round-robin dispatched to devices, and responses are
|
||||
// serialized back to the wire in request order as the protocol requires.
|
||||
//
|
||||
// The simulation does not emit a semantically meaningful reply; it just
|
||||
// writes a 1-byte SSH_AGENT_SUCCESS after the simulated latency has
|
||||
// elapsed. This is enough to exercise callRaw/simpleCall (RemoveAll).
|
||||
func multiDeviceAgent(t testing.TB, conn io.ReadWriteCloser, numDevices int, latency time.Duration) (done <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
type job struct {
|
||||
ready chan struct{}
|
||||
}
|
||||
|
||||
devices := make([]chan job, numDevices)
|
||||
var devicesWG sync.WaitGroup
|
||||
for i := range numDevices {
|
||||
devices[i] = make(chan job, 1024)
|
||||
ch := devices[i]
|
||||
devicesWG.Go(func() {
|
||||
for j := range ch {
|
||||
time.Sleep(latency)
|
||||
close(j.ready)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Order-preserving responder: responses are written in the same
|
||||
// order the requests were read, regardless of which device finished
|
||||
// first.
|
||||
orderCh := make(chan chan struct{}, 1024)
|
||||
responderDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(responderDone)
|
||||
for ready := range orderCh {
|
||||
<-ready
|
||||
var out [5]byte
|
||||
binary.BigEndian.PutUint32(out[:4], 1)
|
||||
out[4] = agentSuccess
|
||||
if _, err := conn.Write(out[:]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
defer close(finished)
|
||||
defer func() {
|
||||
close(orderCh)
|
||||
<-responderDone
|
||||
for _, d := range devices {
|
||||
close(d)
|
||||
}
|
||||
devicesWG.Wait()
|
||||
}()
|
||||
next := 0
|
||||
for {
|
||||
var sizeBuf [4]byte
|
||||
if _, err := io.ReadFull(conn, sizeBuf[:]); err != nil {
|
||||
return
|
||||
}
|
||||
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||
if _, err := io.CopyN(io.Discard, conn, int64(size)); err != nil {
|
||||
return
|
||||
}
|
||||
ready := make(chan struct{})
|
||||
devices[next%numDevices] <- job{ready: ready}
|
||||
next++
|
||||
orderCh <- ready
|
||||
}
|
||||
}()
|
||||
|
||||
return finished
|
||||
}
|
||||
|
||||
// BenchmarkPipelineMultiDevice measures throughput of the pipelined
|
||||
// agent client against a simulated multi-device agent, varying the
|
||||
// number of backend devices.
|
||||
//
|
||||
// Setup: each simulated signing device takes 2ms per request. The
|
||||
// protocol serializes responses in request order, but pipelining lets
|
||||
// the client keep multiple devices busy concurrently; throughput
|
||||
// should scale roughly linearly with numDevices up to the pipeline
|
||||
// depth (pipelineMaxInFlight).
|
||||
//
|
||||
// Run with: go test -benchmem -bench BenchmarkPipelineMultiDevice -benchtime=2s ./ssh/agent/
|
||||
func BenchmarkPipelineMultiDevice(b *testing.B) {
|
||||
const (
|
||||
deviceLatency = 2 * time.Millisecond
|
||||
parallelism = 32
|
||||
)
|
||||
|
||||
for _, numDevices := range []int{1, 2, 4, 8, 16} {
|
||||
b.Run(fmt.Sprintf("Devices=%d", numDevices), func(b *testing.B) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
b.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
serverDone := multiDeviceAgent(b, c2, numDevices, deviceLatency)
|
||||
|
||||
client := NewClient(c1)
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetParallelism(parallelism)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if err := client.RemoveAll(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
<-serverDone
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
@@ -40,7 +41,11 @@ func startOpenSSHAgent(t *testing.T) (client ExtendedAgent, socket string, clean
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin, "-s")
|
||||
cmd.Env = []string{} // Do not let the user's environment influence ssh-agent behavior.
|
||||
cmd.Env = []string{
|
||||
// ssh-agent creates ~/.ssh and ~/.ssh/agent;
|
||||
// ensure a writeable home directory.
|
||||
"HOME=" + t.TempDir(),
|
||||
} // Do not let the user's environment influence ssh-agent behavior.
|
||||
cmd.Stderr = new(bytes.Buffer)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -590,3 +595,96 @@ func TestAgentExtensions(t *testing.T) {
|
||||
t.Fatal("should have gotten agent extension failure")
|
||||
}
|
||||
}
|
||||
|
||||
// capturingAgent records the AddedKey observed on the server side of the
|
||||
// agent protocol. It forwards to a keyring with constraint fields stripped,
|
||||
// so the keyring's own rejection of unsupported constraints does not
|
||||
// interfere with what this test is measuring: the client-side wire
|
||||
// serialization of ConstraintExtensions.
|
||||
type capturingAgent struct {
|
||||
Agent
|
||||
lastAdd AddedKey
|
||||
}
|
||||
|
||||
func newCapturingAgent() *capturingAgent {
|
||||
return &capturingAgent{Agent: NewKeyring()}
|
||||
}
|
||||
|
||||
func (a *capturingAgent) Add(key AddedKey) error {
|
||||
a.lastAdd = key
|
||||
stripped := key
|
||||
stripped.ConstraintExtensions = nil
|
||||
stripped.ConfirmBeforeUse = false
|
||||
return a.Agent.Add(stripped)
|
||||
}
|
||||
|
||||
// TestAddConstraintExtensionsWireFormat verifies that client.Add serializes
|
||||
// ConstraintExtensions into the SSH_AGENTC_ADD_IDENTITY payload and the
|
||||
// server deserializes them back into the AddedKey delivered to the backend.
|
||||
// Regressions in the client marshal loop (missing, swapped fields, wrong
|
||||
// framing) would be invisible to a keyring-based rejection test, which
|
||||
// signals only "extensions were present", not "the right ones arrived".
|
||||
func TestAddConstraintExtensionsWireFormat(t *testing.T) {
|
||||
capturing := newCapturingAgent()
|
||||
client, cleanup := startAgent(t, capturing)
|
||||
defer cleanup()
|
||||
|
||||
constraints := []ConstraintExtension{
|
||||
{ExtensionName: "ext-one@example.com", ExtensionDetails: []byte("details-one")},
|
||||
{ExtensionName: "ext-two@example.com", ExtensionDetails: []byte("\x00\x01\x02\xff")},
|
||||
}
|
||||
|
||||
if err := client.Add(AddedKey{
|
||||
PrivateKey: testPrivateKeys["rsa"],
|
||||
Comment: "wire-format-test",
|
||||
ConstraintExtensions: constraints,
|
||||
}); err != nil {
|
||||
t.Fatalf("client.Add: %v", err)
|
||||
}
|
||||
|
||||
got := capturing.lastAdd.ConstraintExtensions
|
||||
if len(got) != len(constraints) {
|
||||
t.Fatalf("server received %d extensions, want %d", len(got), len(constraints))
|
||||
}
|
||||
for i, want := range constraints {
|
||||
if got[i].ExtensionName != want.ExtensionName {
|
||||
t.Errorf("extension[%d] name: got %q, want %q", i, got[i].ExtensionName, want.ExtensionName)
|
||||
}
|
||||
if !bytes.Equal(got[i].ExtensionDetails, want.ExtensionDetails) {
|
||||
t.Errorf("extension[%d] details: got %x, want %x", i, got[i].ExtensionDetails, want.ExtensionDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRejectsShortEd25519Key(t *testing.T) {
|
||||
short := ed25519.PrivateKey(make([]byte, 16))
|
||||
// insertCert reaches its ed25519 branch only when AddedKey.Certificate
|
||||
// is non-nil. The length check returns before cert.Type/Marshal run, so
|
||||
// an empty certificate is sufficient to drive the path.
|
||||
emptyCert := &ssh.Certificate{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
add AddedKey
|
||||
}{
|
||||
{"insertKey value", AddedKey{PrivateKey: short}},
|
||||
{"insertKey pointer", AddedKey{PrivateKey: &short}},
|
||||
{"insertCert value", AddedKey{PrivateKey: short, Certificate: emptyCert}},
|
||||
{"insertCert pointer", AddedKey{PrivateKey: &short, Certificate: emptyCert}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, cleanup := startKeyringAgent(t)
|
||||
defer cleanup()
|
||||
|
||||
err := client.Add(tc.add)
|
||||
if err == nil {
|
||||
t.Fatal("Add accepted ed25519 key shorter than 64 bytes")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "bad ED25519 key size") {
|
||||
t.Errorf("got error %q, want substring %q", err.Error(), "bad ED25519 key size")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ func ForwardToAgent(client *ssh.Client, keyring Agent) error {
|
||||
continue
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
go io.Copy(io.Discard, channel.Stderr())
|
||||
go func() {
|
||||
ServeAgent(keyring, channel)
|
||||
channel.Close()
|
||||
@@ -72,6 +73,7 @@ func ForwardToRemote(client *ssh.Client, addr string) error {
|
||||
continue
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
go io.Copy(io.Discard, channel.Stderr())
|
||||
go forwardUnixSocket(channel, addr)
|
||||
}
|
||||
}()
|
||||
|
||||
+20
-7
@@ -32,8 +32,10 @@ type keyring struct {
|
||||
|
||||
var errLocked = errors.New("agent: locked")
|
||||
|
||||
// NewKeyring returns an Agent that holds keys in memory. It is safe
|
||||
// for concurrent use by multiple goroutines.
|
||||
// NewKeyring returns an Agent that holds keys in memory. It is safe for
|
||||
// concurrent use by multiple goroutines.
|
||||
//
|
||||
// The returned Agent only supports the "lifetime" constraint.
|
||||
func NewKeyring() Agent {
|
||||
return &keyring{}
|
||||
}
|
||||
@@ -102,7 +104,7 @@ func (r *keyring) Unlock(passphrase []byte) error {
|
||||
if !r.locked {
|
||||
return errors.New("agent: not locked")
|
||||
}
|
||||
if 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
|
||||
if subtle.ConstantTimeCompare(passphrase, r.passphrase) != 1 {
|
||||
return fmt.Errorf("agent: incorrect passphrase")
|
||||
}
|
||||
|
||||
@@ -112,7 +114,7 @@ func (r *keyring) Unlock(passphrase []byte) error {
|
||||
}
|
||||
|
||||
// expireKeysLocked removes expired keys from the keyring. If a key was added
|
||||
// with a lifetimesecs contraint and seconds >= lifetimesecs seconds have
|
||||
// with a lifetimesecs constraint and seconds >= lifetimesecs seconds have
|
||||
// elapsed, it is removed. The caller *must* be holding the keyring mutex.
|
||||
func (r *keyring) expireKeysLocked() {
|
||||
for _, k := range r.keys {
|
||||
@@ -143,15 +145,26 @@ func (r *keyring) List() ([]*Key, error) {
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// Insert adds a private key to the keyring. If a certificate
|
||||
// is given, that certificate is added as public key. Note that
|
||||
// any constraints given are ignored.
|
||||
// Add adds a private key to the keyring. If a certificate is given, that
|
||||
// certificate is added as public key.
|
||||
//
|
||||
// Add returns an error if key contains ConstraintExtensions or
|
||||
// ConfirmBeforeUse.
|
||||
func (r *keyring) Add(key AddedKey) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.locked {
|
||||
return errLocked
|
||||
}
|
||||
|
||||
if key.ConfirmBeforeUse {
|
||||
return errors.New("agent: confirm before use constraint is not supported")
|
||||
}
|
||||
|
||||
if len(key.ConstraintExtensions) > 0 {
|
||||
return errors.New("agent: constraint extensions are present but not supported")
|
||||
}
|
||||
|
||||
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ func validateListedKeys(t *testing.T, a Agent, expectedKeys []string) {
|
||||
return
|
||||
}
|
||||
if len(listedKeys) != len(expectedKeys) {
|
||||
t.Fatalf("expeted %d key, got %d", len(expectedKeys), len(listedKeys))
|
||||
t.Fatalf("expected %d key, got %d", len(expectedKeys), len(listedKeys))
|
||||
return
|
||||
}
|
||||
actualKeys := make(map[string]bool)
|
||||
@@ -120,3 +120,42 @@ func TestAddDuplicateKey(t *testing.T) {
|
||||
t.Fatal("key with the updated comment not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddKeyWithConstraints(t *testing.T) {
|
||||
// Verifies the keyring refuses keys carrying constraint extensions it
|
||||
// cannot enforce.
|
||||
agent, cleanup := startKeyringAgent(t)
|
||||
defer cleanup()
|
||||
|
||||
constraints := []ConstraintExtension{
|
||||
{
|
||||
ExtensionName: "extension1",
|
||||
ExtensionDetails: []byte("details1"),
|
||||
},
|
||||
}
|
||||
|
||||
key := testPrivateKeys["rsa"]
|
||||
|
||||
err := agent.Add(AddedKey{
|
||||
PrivateKey: key,
|
||||
ConstraintExtensions: constraints,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("adding a key with unsupported constraints succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddKeyWithConfirmBeforeUse(t *testing.T) {
|
||||
agent, cleanup := startKeyringAgent(t)
|
||||
defer cleanup()
|
||||
|
||||
key := testPrivateKeys["rsa"]
|
||||
|
||||
err := agent.Add(AddedKey{
|
||||
PrivateKey: key,
|
||||
ConfirmBeforeUse: true,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("adding a key with confirm before use constraint succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
+83
-11
@@ -36,7 +36,7 @@ func (s *server) processRequestBytes(reqData []byte) []byte {
|
||||
return []byte{agentFailure}
|
||||
}
|
||||
|
||||
if err == nil && rep == nil {
|
||||
if rep == nil {
|
||||
return []byte{agentSuccess}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,9 @@ func parseConstraints(constraints []byte) (lifetimeSecs uint32, confirmBeforeUse
|
||||
for len(constraints) != 0 {
|
||||
switch constraints[0] {
|
||||
case agentConstrainLifetime:
|
||||
if len(constraints) < 5 {
|
||||
return 0, false, nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
lifetimeSecs = binary.BigEndian.Uint32(constraints[1:5])
|
||||
constraints = constraints[5:]
|
||||
case agentConstrainConfirm:
|
||||
@@ -237,13 +240,35 @@ func setConstraints(key *AddedKey, constraintBytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRSAKeyParams enforces the same bounds as parseRSA in the ssh
|
||||
// package, and additionally caps the prime factors. Without this,
|
||||
// the rsa.PrivateKey built from an Add request would call Precompute()
|
||||
// on arbitrary inputs; the CRT coefficient recomputation is cubic in
|
||||
// |p| and can consume excessive CPU on oversized keys.
|
||||
func checkRSAKeyParams(N, E, P, Q *big.Int) error {
|
||||
if N.BitLen() > 16384 {
|
||||
return errors.New("agent: RSA modulus too large")
|
||||
}
|
||||
if P.BitLen() > 8192 || Q.BitLen() > 8192 {
|
||||
return errors.New("agent: RSA prime too large")
|
||||
}
|
||||
if E.BitLen() > 24 {
|
||||
return errors.New("agent: RSA public exponent too large")
|
||||
}
|
||||
e := E.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
return errors.New("agent: incorrect RSA public exponent")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRSAKey(req []byte) (*AddedKey, error) {
|
||||
var k rsaKeyMsg
|
||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if k.E.BitLen() > 30 {
|
||||
return nil, errors.New("agent: RSA public exponent too large")
|
||||
if err := checkRSAKeyParams(k.N, k.E, k.P, k.Q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
@@ -267,6 +292,9 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
|
||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(k.Priv) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
|
||||
}
|
||||
priv := ed25519.PrivateKey(k.Priv)
|
||||
|
||||
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
|
||||
@@ -276,19 +304,60 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
|
||||
return addedKey, nil
|
||||
}
|
||||
|
||||
func checkDSAParams(param *dsa.Parameters) error {
|
||||
// SSH specifies FIPS 186-2, which only provided a single size
|
||||
// (1024 bits) DSA key. FIPS 186-3 allows for larger key
|
||||
// sizes, which would confuse SSH.
|
||||
if l := param.P.BitLen(); l != 1024 {
|
||||
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
|
||||
}
|
||||
|
||||
// FIPS 186-2 specifies that Q must be exactly 160 bits. We must enforce
|
||||
// this to prevent DoS attacks where an attacker sends a huge Q which makes
|
||||
// verification slow.
|
||||
if l := param.Q.BitLen(); l != 160 {
|
||||
return fmt.Errorf("ssh: unsupported DSA sub-prime size %d", l)
|
||||
}
|
||||
|
||||
// The generator G is an element of the group, so it must be strictly less
|
||||
// than the modulus P.
|
||||
if param.G.Cmp(param.P) >= 0 {
|
||||
return errors.New("ssh: DSA generator larger than modulus")
|
||||
}
|
||||
|
||||
// G must be positive.
|
||||
if param.G.Sign() <= 0 {
|
||||
return errors.New("ssh: DSA generator must be positive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDSAKey(req []byte) (*AddedKey, error) {
|
||||
var k dsaKeyMsg
|
||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params := dsa.Parameters{
|
||||
P: k.P,
|
||||
Q: k.Q,
|
||||
G: k.G,
|
||||
}
|
||||
if err := checkDSAParams(¶ms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The public value Y must be a non-zero element of the group, i.e. strictly
|
||||
// between 0 and P, to prevent a maliciously oversized Y from slowing
|
||||
// signature operations.
|
||||
if k.Y.Sign() <= 0 || k.Y.Cmp(k.P) >= 0 {
|
||||
return nil, errors.New("agent: DSA public value Y out of range")
|
||||
}
|
||||
|
||||
priv := &dsa.PrivateKey{
|
||||
PublicKey: dsa.PublicKey{
|
||||
Parameters: dsa.Parameters{
|
||||
P: k.P,
|
||||
Q: k.Q,
|
||||
G: k.G,
|
||||
},
|
||||
Y: k.Y,
|
||||
Parameters: params,
|
||||
Y: k.Y,
|
||||
},
|
||||
X: k.X,
|
||||
}
|
||||
@@ -333,6 +402,9 @@ func parseEd25519Cert(req []byte) (*AddedKey, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(k.Priv) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
|
||||
}
|
||||
priv := ed25519.PrivateKey(k.Priv)
|
||||
cert, ok := pubKey.(*ssh.Certificate)
|
||||
if !ok {
|
||||
@@ -390,8 +462,8 @@ func parseRSACert(req []byte) (*AddedKey, error) {
|
||||
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
||||
}
|
||||
|
||||
if rsaPub.E.BitLen() > 30 {
|
||||
return nil, errors.New("agent: RSA public exponent too large")
|
||||
if err := checkRSAKeyParams(rsaPub.N, rsaPub.E, k.P, k.Q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priv := rsa.PrivateKey{
|
||||
|
||||
@@ -8,10 +8,13 @@ import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
pseudorand "math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -87,6 +90,96 @@ func TestSetupForwardAgent(t *testing.T) {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestForwardAgentDiscardsStderr(t *testing.T) {
|
||||
a, b, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
serverConf := ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
incoming := make(chan *ssh.ServerConn, 1)
|
||||
go func() {
|
||||
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
|
||||
incoming <- conn
|
||||
if err != nil {
|
||||
t.Errorf("NewServerConn error: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
conf := ssh.ClientConfig{
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClientConn: %v", err)
|
||||
}
|
||||
client := ssh.NewClient(conn, chans, reqs)
|
||||
defer client.Close()
|
||||
|
||||
if err := ForwardToAgent(client, NewKeyring()); err != nil {
|
||||
t.Fatalf("ForwardToAgent: %v", err)
|
||||
}
|
||||
|
||||
server := <-incoming
|
||||
if server == nil {
|
||||
t.Fatal("Unable to get server")
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
ch, chanReqs, err := server.OpenChannel(channelType, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenChannel(%q): %v", channelType, err)
|
||||
}
|
||||
go ssh.DiscardRequests(chanReqs)
|
||||
defer ch.Close()
|
||||
|
||||
// Write more than the default channel receive window (2 MiB) on the
|
||||
// stderr stream. If the forwarder did not drain it the remote window
|
||||
// would be exhausted and this write would block forever, because no
|
||||
// window-adjust messages are emitted for an extended stream that is
|
||||
// never read.
|
||||
const payload = 4 * 1024 * 1024
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
buf := make([]byte, 32*1024)
|
||||
remaining := payload
|
||||
for remaining > 0 {
|
||||
n := len(buf)
|
||||
if remaining < n {
|
||||
n = remaining
|
||||
}
|
||||
w, err := ch.Stderr().Write(buf[:n])
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
remaining -= w
|
||||
}
|
||||
done <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("write to stderr: %v", err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("write to stderr blocked: ForwardToAgent is not draining channel.Stderr()")
|
||||
}
|
||||
|
||||
// The agent must still be reachable on the main stream after the
|
||||
// stderr traffic.
|
||||
if _, err := NewClient(ch).List(); err != nil {
|
||||
t.Fatalf("List after stderr flood: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1ProtocolMessages(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
@@ -153,6 +246,75 @@ func addKeyToAgent(key crypto.PrivateKey) error {
|
||||
return verifyKey(sshAgent)
|
||||
}
|
||||
|
||||
func TestParseDSAKeyHugeQ(t *testing.T) {
|
||||
P := new(big.Int).Lsh(big.NewInt(1), 1023)
|
||||
Q := new(big.Int).Lsh(big.NewInt(1), 20000) // very large
|
||||
// G and Y: dummy values, just needs to be < P to pass that specific check.
|
||||
G := big.NewInt(2)
|
||||
Y := big.NewInt(5)
|
||||
|
||||
req := ssh.Marshal(dsaKeyMsg{
|
||||
Type: ssh.InsecureKeyAlgoDSA,
|
||||
P: P,
|
||||
Q: Q,
|
||||
G: G,
|
||||
Y: Y,
|
||||
X: big.NewInt(1),
|
||||
})
|
||||
|
||||
_, err := parseDSAKey(req)
|
||||
if err == nil {
|
||||
t.Fatal("parseDSAKey accepted a DSA key with large Q")
|
||||
}
|
||||
|
||||
expectedError := "unsupported DSA sub-prime size"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDSAKeyYOutOfRange(t *testing.T) {
|
||||
// Valid 1024/160 parameters (values don't need to be a real DSA group,
|
||||
// they only need to pass the checkDSAParams bit-length checks and the
|
||||
// G < P / G > 0 checks).
|
||||
P := new(big.Int).Lsh(big.NewInt(1), 1023)
|
||||
P.SetBit(P, 0, 1) // make P odd so it can pass as a prime candidate shape
|
||||
Q := new(big.Int).Lsh(big.NewInt(1), 159)
|
||||
Q.SetBit(Q, 0, 1)
|
||||
G := big.NewInt(2)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
Y *big.Int
|
||||
}{
|
||||
{"Y_zero", big.NewInt(0)},
|
||||
{"Y_negative", big.NewInt(-1)},
|
||||
{"Y_equals_P", new(big.Int).Set(P)},
|
||||
{"Y_greater_than_P", new(big.Int).Add(P, big.NewInt(1))},
|
||||
{"Y_much_greater_than_P", new(big.Int).Lsh(big.NewInt(1), 20000)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := ssh.Marshal(dsaKeyMsg{
|
||||
Type: ssh.InsecureKeyAlgoDSA,
|
||||
P: P,
|
||||
Q: Q,
|
||||
G: G,
|
||||
Y: tc.Y,
|
||||
X: big.NewInt(1),
|
||||
})
|
||||
|
||||
_, err := parseDSAKey(req)
|
||||
if err == nil {
|
||||
t.Fatalf("parseDSAKey accepted a DSA key with Y=%s (P=%s)", tc.Y, P)
|
||||
}
|
||||
expectedError := "DSA public value Y out of range"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyTypes(t *testing.T) {
|
||||
for k, v := range testPrivateKeys {
|
||||
if err := addKeyToAgent(v); err != nil {
|
||||
@@ -258,9 +420,47 @@ func TestParseConstraints(t *testing.T) {
|
||||
t.Errorf("got extension %v, want %v", extensions, expect)
|
||||
}
|
||||
|
||||
// Test Malformed Constraint
|
||||
_, _, _, err = parseConstraints([]byte{1})
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
t.Errorf("got %v, want %v", err, io.ErrUnexpectedEOF)
|
||||
}
|
||||
|
||||
// Test Unknown Constraint
|
||||
_, _, _, err = parseConstraints([]byte{128})
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown constraint") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEd25519KeyShortPanic(t *testing.T) {
|
||||
msg := ssh.Marshal(ed25519KeyMsg{
|
||||
Type: ssh.KeyAlgoED25519,
|
||||
Pub: []byte{1, 2, 3},
|
||||
Priv: []byte{1, 2, 3, 4, 5},
|
||||
})
|
||||
|
||||
a, b, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- ServeAgent(NewKeyring(), a) }()
|
||||
|
||||
c := NewClient(b)
|
||||
_, err = c.(*client).call(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
keys, err := c.List()
|
||||
if err != nil {
|
||||
t.Fatalf("agent died: %v", err)
|
||||
}
|
||||
if len(keys) != 0 {
|
||||
t.Error("short ed25519 key was accepted into keyring")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ func TestBufferReadwrite(t *testing.T) {
|
||||
b.write(alphabet[5:15])
|
||||
r, _ = b.Read(make([]byte, 10))
|
||||
r2, _ := b.Read(make([]byte, 10))
|
||||
if r != 10 || r2 != 5 || 15 != r+r2 {
|
||||
if r != 10 || r2 != 5 || r+r2 != 15 {
|
||||
t.Fatal("Expected written == read == 15")
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -229,15 +229,20 @@ func parseCert(in []byte, privAlgo string) (*Certificate, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.Reserved = g.Reserved
|
||||
// Reject a certificate whose signature key is itself a certificate before
|
||||
// parsing it. Certificates signed by certificates are not supported (see
|
||||
// PROTOCOL.certkeys), and rejecting after ParsePublicKey returns would allow
|
||||
// a chain of nested certificates to recurse once per level, exhausting the
|
||||
// goroutine stack.
|
||||
if sigAlgo, _, ok := parseString(g.SignatureKey); !ok {
|
||||
return nil, errShortRead
|
||||
} else if _, ok := certKeyAlgoNames[string(sigAlgo)]; ok {
|
||||
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", sigAlgo)
|
||||
}
|
||||
k, err := ParsePublicKey(g.SignatureKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The Type() function is intended to return only certificate key types, but
|
||||
// we use certKeyAlgoNames anyway for safety, to match [Certificate.Type].
|
||||
if _, ok := certKeyAlgoNames[k.Type()]; ok {
|
||||
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", k.Type())
|
||||
}
|
||||
c.SignatureKey = k
|
||||
c.Signature, rest, ok = parseSignatureBody(g.Signature)
|
||||
if !ok || len(rest) > 0 {
|
||||
@@ -348,6 +353,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
|
||||
if cert.CertType != HostCert {
|
||||
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
|
||||
}
|
||||
if c.IsHostAuthority == nil {
|
||||
return errors.New("ssh: cannot verify certificate, IsHostAuthority not set")
|
||||
}
|
||||
if !c.IsHostAuthority(cert.SignatureKey, addr) {
|
||||
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
|
||||
}
|
||||
@@ -375,6 +383,9 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
|
||||
if cert.CertType != UserCert {
|
||||
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
||||
}
|
||||
if c.IsUserAuthority == nil {
|
||||
return nil, errors.New("ssh: cannot verify certificate, IsUserAuthority not set")
|
||||
}
|
||||
if !c.IsUserAuthority(cert.SignatureKey) {
|
||||
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
|
||||
}
|
||||
@@ -438,7 +449,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")
|
||||
}
|
||||
|
||||
|
||||
+128
-4
@@ -9,10 +9,13 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -43,6 +46,34 @@ func TestParseCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCertNestedSignatureKey(t *testing.T) {
|
||||
signer, err := NewSignerFromKey(testPrivateKeys["ed25519"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cert := &Certificate{
|
||||
Key: signer.PublicKey(),
|
||||
CertType: UserCert,
|
||||
ValidBefore: CertTimeInfinity,
|
||||
}
|
||||
if err := cert.SignCert(rand.Reader, signer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inner := *cert
|
||||
cert.SignatureKey = &inner
|
||||
blob := cert.Marshal()
|
||||
|
||||
_, err = ParsePublicKey(blob)
|
||||
if err == nil {
|
||||
t.Fatal("ParsePublicKey: expected error for certificate signed by a certificate, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid for certificates") {
|
||||
t.Errorf("ParsePublicKey: got error %q, want it to mention the signature key is invalid for certificates", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Cert generated by ssh-keygen OpenSSH_6.8p1 OS X 10.10.3
|
||||
// % ssh-keygen -s ca -I testcert -O source-address=192.168.1.0/24 -O force-command=/bin/sleep user.pub
|
||||
// user.pub key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMN
|
||||
@@ -128,6 +159,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"},
|
||||
@@ -330,10 +458,6 @@ func TestCertTypes(t *testing.T) {
|
||||
go NewServerConn(c1, conf)
|
||||
|
||||
priv := m.signer
|
||||
if err != nil {
|
||||
t.Fatalf("error generating ssh pubkey: %v", err)
|
||||
}
|
||||
|
||||
cert := &Certificate{
|
||||
CertType: UserCert,
|
||||
Key: priv.PublicKey(),
|
||||
|
||||
+104
-31
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -131,11 +132,17 @@ func (r RejectionReason) String() string {
|
||||
return fmt.Sprintf("unknown reason %d", int(r))
|
||||
}
|
||||
|
||||
func min(a uint32, b int) uint32 {
|
||||
if a < uint32(b) {
|
||||
return a
|
||||
// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
|
||||
// to compute the size of the next channel data packet from the remaining
|
||||
// payload. The comparison is done in int64 because length is an int — on
|
||||
// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
|
||||
// cast would silently truncate to 0 at every multiple of 2^32, causing
|
||||
// WriteExtended's loop to spin without making progress.
|
||||
func minPayloadSize(limit uint32, length int) uint32 {
|
||||
if int64(length) > int64(limit) {
|
||||
return limit
|
||||
}
|
||||
return uint32(b)
|
||||
return uint32(length)
|
||||
}
|
||||
|
||||
type channelDirection uint8
|
||||
@@ -177,6 +184,12 @@ type channel struct {
|
||||
// with WantReply=true outstanding. This lock is held by a
|
||||
// goroutine that has such an outgoing request pending.
|
||||
sentRequestMu sync.Mutex
|
||||
// sentRequestPending is set to true while a SendRequest call with
|
||||
// WantReply=true is in flight. handlePacket uses it as a gate: responses
|
||||
// arriving while no request is pending are dropped to prevent a
|
||||
// misbehaving peer from stalling the mux read loop by filling ch.msg
|
||||
// with unsolicited channelRequestSuccess/Failure messages.
|
||||
sentRequestPending atomic.Bool
|
||||
|
||||
incomingRequests chan *Request
|
||||
|
||||
@@ -203,6 +216,10 @@ type channel struct {
|
||||
// packetPool has a buffer for each extended channel ID to
|
||||
// save allocations during writes.
|
||||
packetPool map[uint32][]byte
|
||||
|
||||
// closeOnce guards close so it is idempotent: closing the internal Go
|
||||
// channels (msg, incomingRequests) more than once would panic.
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// writePacket sends a packet. If the packet is a channel close, it updates
|
||||
@@ -251,7 +268,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
|
||||
ch.writeMu.Unlock()
|
||||
|
||||
for len(data) > 0 {
|
||||
space := min(ch.maxRemotePayload, len(data))
|
||||
space := minPayloadSize(ch.maxRemotePayload, len(data))
|
||||
if space, err = ch.remoteWin.reserve(space); err != nil {
|
||||
return n, err
|
||||
}
|
||||
@@ -327,7 +344,18 @@ func (ch *channel) handleData(packet []byte) error {
|
||||
if extended == 1 {
|
||||
ch.extPending.write(data)
|
||||
} else if extended > 0 {
|
||||
// discard other extended data.
|
||||
// RFC 4254, Section 5.2 defines no extended data types other
|
||||
// than stderr (type 1, handled above) and this package provides
|
||||
// no API to read them, so the data is discarded. Credit its
|
||||
// window back immediately: it can never be read, so the
|
||||
// deduction above would otherwise shrink the window permanently.
|
||||
// adjustWindow returns io.EOF if the local side has already
|
||||
// sent a channel close; ignore it like ReadExtended does, since
|
||||
// an error returned here would terminate the mux read loop and
|
||||
// tear down the whole connection.
|
||||
if err := ch.adjustWindow(length); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ch.pending.write(data)
|
||||
}
|
||||
@@ -380,17 +408,19 @@ func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error)
|
||||
}
|
||||
|
||||
func (c *channel) close() {
|
||||
c.pending.eof()
|
||||
c.extPending.eof()
|
||||
close(c.msg)
|
||||
close(c.incomingRequests)
|
||||
c.writeMu.Lock()
|
||||
// This is not necessary for a normal channel teardown, but if
|
||||
// there was another error, it is.
|
||||
c.sentClose = true
|
||||
c.writeMu.Unlock()
|
||||
// Unblock writers.
|
||||
c.remoteWin.close()
|
||||
c.closeOnce.Do(func() {
|
||||
c.pending.eof()
|
||||
c.extPending.eof()
|
||||
close(c.msg)
|
||||
close(c.incomingRequests)
|
||||
c.writeMu.Lock()
|
||||
// This is not necessary for a normal channel teardown, but if
|
||||
// there was another error, it is.
|
||||
c.sentClose = true
|
||||
c.writeMu.Unlock()
|
||||
// Unblock writers.
|
||||
c.remoteWin.close()
|
||||
})
|
||||
}
|
||||
|
||||
// responseMessageReceived is called when a success or failure message is
|
||||
@@ -460,6 +490,18 @@ func (ch *channel) handlePacket(packet []byte) error {
|
||||
}
|
||||
|
||||
ch.incomingRequests <- &req
|
||||
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
|
||||
// Drop responses that arrive when no SendRequest is waiting, to
|
||||
// prevent a malicious peer from filling ch.msg and stalling the
|
||||
// mux read loop. The non-blocking send additionally protects the
|
||||
// loop if a well-behaved caller is slow to read.
|
||||
if !ch.sentRequestPending.Load() {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case ch.msg <- msg:
|
||||
default:
|
||||
}
|
||||
default:
|
||||
ch.msg <- msg
|
||||
}
|
||||
@@ -468,19 +510,20 @@ func (ch *channel) handlePacket(packet []byte) error {
|
||||
|
||||
func (m *mux) newChannel(chanType string, direction channelDirection, extraData []byte) *channel {
|
||||
ch := &channel{
|
||||
remoteWin: window{Cond: newCond()},
|
||||
myWindow: channelWindowSize,
|
||||
pending: newBuffer(),
|
||||
extPending: newBuffer(),
|
||||
direction: direction,
|
||||
incomingRequests: make(chan *Request, chanSize),
|
||||
msg: make(chan interface{}, chanSize),
|
||||
chanType: chanType,
|
||||
extraData: extraData,
|
||||
mux: m,
|
||||
packetPool: make(map[uint32][]byte),
|
||||
remoteWin: window{Cond: newCond()},
|
||||
myWindow: channelWindowSize,
|
||||
maxIncomingPayload: channelMaxPacket,
|
||||
pending: newBuffer(),
|
||||
extPending: newBuffer(),
|
||||
direction: direction,
|
||||
incomingRequests: make(chan *Request, chanSize),
|
||||
msg: make(chan interface{}, chanSize),
|
||||
chanType: chanType,
|
||||
extraData: extraData,
|
||||
mux: m,
|
||||
packetPool: make(map[uint32][]byte),
|
||||
}
|
||||
ch.localId = m.chanList.add(ch)
|
||||
m.chanList.add(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
@@ -504,7 +547,6 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
|
||||
if ch.decided {
|
||||
return nil, nil, errDecidedAlready
|
||||
}
|
||||
ch.maxIncomingPayload = channelMaxPacket
|
||||
confirm := channelOpenConfirmMsg{
|
||||
PeersID: ch.remoteId,
|
||||
MyID: ch.localId,
|
||||
@@ -530,7 +572,17 @@ func (ch *channel) Reject(reason RejectionReason, message string) error {
|
||||
Language: "en",
|
||||
}
|
||||
ch.decided = true
|
||||
return ch.sendMessage(reject)
|
||||
err := ch.sendMessage(reject)
|
||||
|
||||
// Remove the channel from the mux to prevent memory leaks.
|
||||
// Do not call ch.close() here: no goroutine holds a reference to a
|
||||
// rejected channel's internal channels (msg, incomingRequests), so
|
||||
// removing it from chanList is sufficient for GC. Calling close()
|
||||
// would race with the mux loop goroutine (handlePacket or dropAll),
|
||||
// causing a panic from closing an already-closed channel.
|
||||
ch.mux.chanList.remove(ch.localId)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ch *channel) Read(data []byte) (int, error) {
|
||||
@@ -586,6 +638,27 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
|
||||
if wantReply {
|
||||
ch.sentRequestMu.Lock()
|
||||
defer ch.sentRequestMu.Unlock()
|
||||
|
||||
// Open the gate so that responses arriving while this request is in
|
||||
// flight are allowed to reach ch.msg. Responses arriving while no
|
||||
// request is pending are dropped by handlePacket.
|
||||
ch.sentRequestPending.Store(true)
|
||||
defer ch.sentRequestPending.Store(false)
|
||||
|
||||
// Drain any spurious responses that may have been buffered. This
|
||||
// prevents a previously buffered unexpected response from being
|
||||
// consumed instead of the actual response for this request.
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-ch.msg:
|
||||
if !ok {
|
||||
break drain
|
||||
}
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg := channelRequestMsg{
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math/bits"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestMinPayloadSize(t *testing.T) {
|
||||
// 4 GiB (2^32). Declared as a var (not a const) so that int(bigPayload)
|
||||
// is a runtime conversion: a constant conversion would fail to compile
|
||||
// on 32-bit platforms with "constant 4294967296 overflows int". On
|
||||
// 32-bit the value truncates to 0 at runtime, but the is64Bit cases
|
||||
// that reference it are skipped by the runtime check below.
|
||||
var bigPayload int64 = 1 << 32
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
maxPayload uint32
|
||||
dataLen int
|
||||
want uint32
|
||||
is64Bit bool // Flag to run only on 64-bit architectures
|
||||
}{
|
||||
{
|
||||
name: "Normal Case - Data fits in payload",
|
||||
maxPayload: 32768,
|
||||
dataLen: 1000,
|
||||
want: 1000,
|
||||
},
|
||||
{
|
||||
name: "Normal Case - Data larger than payload",
|
||||
maxPayload: 32768,
|
||||
dataLen: 50000,
|
||||
want: 32768,
|
||||
},
|
||||
{
|
||||
name: "Boundary Case - Data zero",
|
||||
maxPayload: 32768,
|
||||
dataLen: 0,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Overflow Case - Data is exactly 4GB (1<<32)",
|
||||
maxPayload: 32768,
|
||||
dataLen: int(bigPayload),
|
||||
want: 32768,
|
||||
is64Bit: true,
|
||||
},
|
||||
{
|
||||
name: "Overflow Case - Data is 4GB + small amount",
|
||||
maxPayload: 32768,
|
||||
dataLen: int(bigPayload + 100),
|
||||
want: 32768,
|
||||
is64Bit: true,
|
||||
},
|
||||
}
|
||||
|
||||
is64Bit := bits.UintSize == 64
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.is64Bit && !is64Bit {
|
||||
t.Skip("Skipping test requiring 64-bit int")
|
||||
}
|
||||
got := minPayloadSize(tt.maxPayload, tt.dataLen)
|
||||
if got != tt.want {
|
||||
t.Errorf("minPayloadSize(%d, %d) = %d; want %d", tt.maxPayload, tt.dataLen, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteExtendedNoInfiniteLoopOnLargeWrite is an end-to-end regression
|
||||
// test for the integer-overflow bug in WriteExtended. Before the fix, a
|
||||
// write whose len(data) was a multiple of 2^32 caused minPayloadSize to
|
||||
// return 0; WriteExtended then spun forever, reserving 0 bytes per
|
||||
// iteration and never advancing the data slice.
|
||||
//
|
||||
// We exercise the real WriteExtended path with a slice whose declared
|
||||
// length is exactly 2^32. Allocating 4 GiB is unnecessary: each iteration
|
||||
// only reads up to maxRemotePayload bytes from the head of the slice, and
|
||||
// the loop blocks in remoteWin.reserve() once the channel window is
|
||||
// exhausted — before the slice base advances past the underlying buffer.
|
||||
//
|
||||
// With the fix, the loop blocks in reserve(); we detect that via
|
||||
// waitWriterBlocked(), then close the window to let WriteExtended return.
|
||||
// With the bug, the loop never blocks and the test times out.
|
||||
//
|
||||
//go:nocheckptr
|
||||
func TestWriteExtendedNoInfiniteLoopOnLargeWrite(t *testing.T) {
|
||||
if bits.UintSize < 64 {
|
||||
t.Skip("test requires 64-bit int to construct a slice with len >= 2^32")
|
||||
}
|
||||
|
||||
reader, writer, mux := channelPair(t)
|
||||
defer reader.Close()
|
||||
defer writer.Close()
|
||||
defer mux.Close()
|
||||
|
||||
// Sized to hold the full pre-update remote window so that no iteration
|
||||
// reads past the backing buffer before reserve() blocks.
|
||||
backing := make([]byte, channelWindowSize)
|
||||
var bigLen int64 = 1 << 32
|
||||
bigSlice := unsafe.Slice(&backing[0], int(bigLen))
|
||||
|
||||
done := make(chan int, 1)
|
||||
go func() {
|
||||
n, _ := writer.Write(bigSlice)
|
||||
done <- n
|
||||
}()
|
||||
|
||||
blocked := make(chan struct{})
|
||||
go func() {
|
||||
writer.remoteWin.waitWriterBlocked()
|
||||
close(blocked)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-blocked:
|
||||
// Good — the loop made progress and is now blocked in reserve().
|
||||
// Close the window to let WriteExtended return.
|
||||
writer.remoteWin.close()
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("WriteExtended did not block in reserve within 2s — minPayloadSize likely returned 0 (integer overflow regression)")
|
||||
}
|
||||
|
||||
select {
|
||||
case n := <-done:
|
||||
if n == 0 {
|
||||
t.Fatalf("WriteExtended returned n=0; expected progress")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("WriteExtended did not return after closing the window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscardedExtendedDataReturnsWindowCredit(t *testing.T) {
|
||||
client, _, serverChans := forwardingPair(t)
|
||||
|
||||
clientCh, reqs, err := client.OpenChannel("test", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenChannel: %v", err)
|
||||
}
|
||||
go DiscardRequests(reqs)
|
||||
defer clientCh.Close()
|
||||
|
||||
serverCh := (<-serverChans).(*channel)
|
||||
defer serverCh.Close()
|
||||
|
||||
// Write more than the default channel receive window (2 MiB) of
|
||||
// extended data with a type code other than stderr. The peer
|
||||
// discards such data: if it did not return the window credit the
|
||||
// write would block forever once the window is exhausted.
|
||||
const payload = 4 * 1024 * 1024
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
buf := make([]byte, 32*1024)
|
||||
remaining := payload
|
||||
for remaining > 0 {
|
||||
n := min(len(buf), remaining)
|
||||
nw, err := serverCh.WriteExtended(buf[:n], 2)
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
remaining -= nw
|
||||
}
|
||||
done <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("write of discarded extended data: %v", err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("write of discarded extended data blocked: window credit is not returned")
|
||||
}
|
||||
|
||||
// The main stream must still be usable.
|
||||
want := []byte("main stream after extended data")
|
||||
if _, err := serverCh.Write(want); err != nil {
|
||||
t.Fatalf("write to main stream: %v", err)
|
||||
}
|
||||
got := make([]byte, len(want))
|
||||
if _, err := io.ReadFull(clientCh, got); err != nil {
|
||||
t.Fatalf("read from main stream: %v", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("read %q from main stream, want %q", got, want)
|
||||
}
|
||||
|
||||
// Reading the main stream payload above orders the test after all
|
||||
// preceding packets: everything received has now been either read or
|
||||
// discarded, so the full window credit must have been accounted.
|
||||
// Anything less indicates a partial-credit leak that the liveness
|
||||
// check above would not detect.
|
||||
cc := clientCh.(*channel)
|
||||
cc.windowMu.Lock()
|
||||
window := cc.myWindow + cc.myConsumed
|
||||
cc.windowMu.Unlock()
|
||||
if window != channelWindowSize {
|
||||
t.Errorf("myWindow+myConsumed = %d, want %d", window, channelWindowSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscardedExtendedDataAfterClose(t *testing.T) {
|
||||
client, _, serverChans := forwardingPair(t)
|
||||
|
||||
clientCh, reqs, err := client.OpenChannel("test", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenChannel: %v", err)
|
||||
}
|
||||
go DiscardRequests(reqs)
|
||||
serverCh := <-serverChans
|
||||
defer serverCh.Close()
|
||||
|
||||
ch := clientCh.(*channel)
|
||||
// Close the channel locally: sentClose is now set, but the channel
|
||||
// stays in the mux chanList until the peer's close arrives, so
|
||||
// in-flight data packets are still dispatched to handleData.
|
||||
if err := ch.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
|
||||
// Deliver extended data packets with an unknown type code, enough to
|
||||
// cross the adjustWindow send threshold of 3*maxIncomingPayload.
|
||||
// The window adjust message cannot be sent after the close and
|
||||
// adjustWindow fails with io.EOF: handleData must swallow that
|
||||
// error, since any error it returns terminates the mux read loop
|
||||
// and tears down every channel on the connection.
|
||||
const packetLen = 32 * 1024
|
||||
packet := make([]byte, 13+packetLen)
|
||||
packet[0] = msgChannelExtendedData
|
||||
binary.BigEndian.PutUint32(packet[1:], ch.localId)
|
||||
binary.BigEndian.PutUint32(packet[5:], 2)
|
||||
binary.BigEndian.PutUint32(packet[9:], packetLen)
|
||||
for range 5 {
|
||||
if err := ch.handleData(packet); err != nil {
|
||||
t.Fatalf("handleData after local close: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -407,7 +407,7 @@ func (c *gcmCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
|
||||
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
|
||||
}
|
||||
|
||||
if int(padding+1) >= len(plain) {
|
||||
if int(padding)+1 >= len(plain) {
|
||||
return nil, fmt.Errorf("ssh: padding %d too large", padding)
|
||||
}
|
||||
plain = plain[1 : length-uint32(padding)]
|
||||
@@ -586,7 +586,7 @@ func (c *cbcCipher) writeCipherPacket(seqNum uint32, w io.Writer, rand io.Reader
|
||||
|
||||
// Length of encrypted portion of the packet (header, payload, padding).
|
||||
// Enforce minimum padding and packet size.
|
||||
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
|
||||
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPacketSize)
|
||||
// Enforce block size.
|
||||
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
|
||||
|
||||
|
||||
@@ -88,6 +88,32 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan
|
||||
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
||||
}
|
||||
|
||||
// NewControlClientConn establishes an SSH connection over an OpenSSH
|
||||
// ControlMaster socket c in proxy mode.
|
||||
//
|
||||
// Note that this package only implements the client side of the multiplexing
|
||||
// protocol. The provided net.Conn must be a local, secure connection (such as a
|
||||
// Unix domain socket) connected to an already-running OpenSSH process acting as
|
||||
// the ControlMaster.
|
||||
//
|
||||
// WARNING: Because proxy mode bypasses the standard cryptographic handshake
|
||||
// passing a standard network connection (e.g., TCP) will result in plaintext
|
||||
// data leakage.
|
||||
//
|
||||
// The Request and NewChannel channels must be serviced or the connection
|
||||
// will hang.
|
||||
func NewControlClientConn(c net.Conn) (Conn, <-chan NewChannel, <-chan *Request, error) {
|
||||
conn := &connection{
|
||||
sshConn: sshConn{conn: c},
|
||||
}
|
||||
var err error
|
||||
if conn.transport, err = handshakeControlProxy(c); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("ssh: control proxy handshake failed: %w", err)
|
||||
}
|
||||
conn.mux = newMux(conn.transport)
|
||||
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
||||
}
|
||||
|
||||
// clientHandshake performs the client side key exchange. See RFC 4253 Section
|
||||
// 7.
|
||||
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
|
||||
@@ -197,6 +223,59 @@ type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
||||
// the server. A BannerCallback receives the message sent by the remote server.
|
||||
type BannerCallback func(message string) error
|
||||
|
||||
// ClientAuthContext contains information about the current state of the
|
||||
// authentication process, passed to [ClientAuthCallback].
|
||||
type ClientAuthContext struct {
|
||||
// Metadata contains the connection metadata.
|
||||
Metadata ConnMetadata
|
||||
|
||||
// Algorithms contains the negotiated algorithms.
|
||||
Algorithms NegotiatedAlgorithms
|
||||
|
||||
// AllowedMethods lists the authentication methods currently accepted
|
||||
// by the server. These are the protocol-level names defined in RFC 4252
|
||||
// such as "publickey", "password".
|
||||
AllowedMethods []string
|
||||
|
||||
// PartialSuccessMethods lists the authentication methods that have already
|
||||
// succeeded, indicating a multi-step authentication flow. This list
|
||||
// represents the exact sequence of partial successes and may contain
|
||||
// duplicates if the same method succeeded multiple times.
|
||||
PartialSuccessMethods []string
|
||||
|
||||
// TriedMethods lists the methods that have already been attempted and
|
||||
// failed during this session. This list represents the exact sequence of
|
||||
// failures and may contain duplicates. This allows the callback to also
|
||||
// track the number of failed attempts for a specific method.
|
||||
TriedMethods []string
|
||||
}
|
||||
|
||||
// ClientAuthCallback is a hook invoked before each authentication attempt. It
|
||||
// allows the client to dynamically select an authentication method based on the
|
||||
// current context, server capabilities, or previous failures.
|
||||
//
|
||||
// The callback is invoked after the initial "none" authentication method, once
|
||||
// the server's supported authentication methods are known.
|
||||
//
|
||||
// Return values:
|
||||
// - (AuthMethod, nil): The client will attempt this specific method next.
|
||||
// The returned method does NOT need to be present in [ClientConfig.Auth].
|
||||
// This allows for dynamic authentication strategies (e.g., prompting
|
||||
// for a password only if public key auth fails). Callers should inspect
|
||||
// [ClientAuthContext.TriedMethods] to avoid repeatedly returning the
|
||||
// same failing method.
|
||||
// - (nil, nil): The client selects from [ClientConfig.Auth] the first
|
||||
// instance of a method that has not been tried yet, or aborts if none
|
||||
// are left. If authentication is not successful, the callback is invoked
|
||||
// again before the following attempt.
|
||||
// - (nil, error): The authentication process is aborted immediately,
|
||||
// causing the ongoing SSH handshake to fail with the provided error.
|
||||
//
|
||||
// To bound resource use, the client caps the total number of authentication
|
||||
// attempts (failures and partial successes combined) at 64. If the cap is
|
||||
// exceeded the handshake aborts with an error.
|
||||
type ClientAuthCallback func(ctx *ClientAuthContext) (AuthMethod, error)
|
||||
|
||||
// A ClientConfig structure is used to configure a Client. It must not be
|
||||
// modified after having been passed to an SSH function.
|
||||
type ClientConfig struct {
|
||||
@@ -210,6 +289,9 @@ type ClientConfig struct {
|
||||
// Auth contains possible authentication methods to use with the
|
||||
// server. Only the first instance of a particular RFC 4252 method will
|
||||
// be used during authentication.
|
||||
//
|
||||
// If AuthCallback is set, these AuthMethod are only used if the
|
||||
// callback returns nil.
|
||||
Auth []AuthMethod
|
||||
|
||||
// HostKeyCallback is called during the cryptographic
|
||||
@@ -240,6 +322,9 @@ type ClientConfig struct {
|
||||
//
|
||||
// A Timeout of zero means no timeout.
|
||||
Timeout time.Duration
|
||||
|
||||
// AuthCallback, if non-nil, is invoked before each authentication attempt.
|
||||
AuthCallback ClientAuthCallback
|
||||
}
|
||||
|
||||
// InsecureIgnoreHostKey returns a function that can be used for
|
||||
|
||||
+58
-18
@@ -21,6 +21,12 @@ const (
|
||||
authSuccess
|
||||
)
|
||||
|
||||
// maxAuthClientTried bounds the total number of authentication attempts
|
||||
// (failures and partial successes combined) the client makes before
|
||||
// aborting the loop, to prevent unbounded growth when an AuthCallback
|
||||
// keeps supplying methods.
|
||||
const maxAuthClientTried = 64
|
||||
|
||||
// clientAuthenticate authenticates with the remote server. See RFC 4252.
|
||||
func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
||||
// initiate user auth session
|
||||
@@ -67,32 +73,62 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
||||
// then any untried methods suggested by the server.
|
||||
var tried []string
|
||||
var lastMethods []string
|
||||
var partialSuccess []string
|
||||
|
||||
sessionID := c.transport.getSessionID()
|
||||
for auth := AuthMethod(new(noneAuth)); auth != nil; {
|
||||
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
|
||||
if err != nil {
|
||||
// On disconnect, return error immediately
|
||||
if _, ok := err.(*disconnectMsg); ok {
|
||||
if _, isDisconnect := err.(*disconnectMsg); isDisconnect {
|
||||
return err
|
||||
}
|
||||
// We return the error later if there is no other method left to
|
||||
// try.
|
||||
// We return the error later if there is no other method
|
||||
// left to try.
|
||||
ok = authFailure
|
||||
}
|
||||
if ok == authSuccess {
|
||||
// success
|
||||
|
||||
switch ok {
|
||||
case authSuccess:
|
||||
return nil
|
||||
} else if ok == authFailure {
|
||||
if m := auth.method(); !slices.Contains(tried, m) {
|
||||
tried = append(tried, m)
|
||||
}
|
||||
case authPartialSuccess:
|
||||
partialSuccess = append(partialSuccess, auth.method())
|
||||
case authFailure:
|
||||
tried = append(tried, auth.method())
|
||||
}
|
||||
if len(partialSuccess)+len(tried) > maxAuthClientTried {
|
||||
return fmt.Errorf("ssh: too many authentication attempts (%d), aborting",
|
||||
len(partialSuccess)+len(tried))
|
||||
}
|
||||
|
||||
if methods == nil {
|
||||
methods = lastMethods
|
||||
}
|
||||
lastMethods = methods
|
||||
|
||||
// If AuthCallback is set it takes precedence: it picks the next
|
||||
// AuthMethod dynamically. The returned method need not be in
|
||||
// config.Auth. If the callback returns (nil, nil) we fall back to
|
||||
// selecting the next untried method from config.Auth below; on
|
||||
// (nil, error) the handshake aborts.
|
||||
if config.AuthCallback != nil {
|
||||
ctx := &ClientAuthContext{
|
||||
Metadata: c,
|
||||
Algorithms: c.Algorithms(),
|
||||
AllowedMethods: slices.Clone(methods),
|
||||
PartialSuccessMethods: slices.Clone(partialSuccess),
|
||||
TriedMethods: slices.Clone(tried),
|
||||
}
|
||||
altAuth, cbErr := config.AuthCallback(ctx)
|
||||
if cbErr != nil {
|
||||
return cbErr
|
||||
}
|
||||
if altAuth != nil {
|
||||
auth = altAuth
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
auth = nil
|
||||
|
||||
findNext:
|
||||
@@ -274,10 +310,14 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
|
||||
}
|
||||
|
||||
// Filter algorithms based on those supported by MultiAlgorithmSigner.
|
||||
// Iterate over the signer's algorithms first to preserve its preference order.
|
||||
supportedKeyAlgos := algorithmsForKeyFormat(keyFormat)
|
||||
var keyAlgos []string
|
||||
for _, algo := range algorithmsForKeyFormat(keyFormat) {
|
||||
if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) {
|
||||
keyAlgos = append(keyAlgos, algo)
|
||||
for _, signerAlgo := range as.Algorithms() {
|
||||
if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool {
|
||||
return underlyingAlgo(algo) == signerAlgo
|
||||
}); idx >= 0 {
|
||||
keyAlgos = append(keyAlgos, supportedKeyAlgos[idx])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
|
||||
return authFailure, nil, err
|
||||
}
|
||||
|
||||
// If authentication succeeds or the list of available methods does not
|
||||
// contain the "publickey" method, do not attempt to authenticate with any
|
||||
// other keys. According to RFC 4252 Section 7, the latter can occur when
|
||||
// additional authentication methods are required.
|
||||
if success == authSuccess || !slices.Contains(methods, cb.method()) {
|
||||
// If authentication succeeds or partially succeeds, return immediately
|
||||
// so the caller can select the next auth method. According to RFC 4252
|
||||
// Section 7, if the server no longer lists "publickey" among its
|
||||
// allowed methods, do not attempt to authenticate with any other keys.
|
||||
if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) {
|
||||
return success, methods, err
|
||||
}
|
||||
}
|
||||
@@ -758,7 +798,7 @@ func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn,
|
||||
return authFailure, nil, fmt.Errorf("GSS-API Error:\n"+
|
||||
"Major Status: %d\n"+
|
||||
"Minor Status: %d\n"+
|
||||
"Error Message: %s\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
|
||||
"Error Message: %q\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
|
||||
userAuthGSSAPIErrorResp.Message)
|
||||
case msgUserAuthGSSAPIToken:
|
||||
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
|
||||
|
||||
+834
-1
@@ -7,10 +7,12 @@ package ssh
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -169,6 +171,723 @@ func TestClientAuthThirdKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// invalidRSASigner offers an RSA public key the server cannot parse: its public
|
||||
// exponent is even, which parseRSA rejects regardless of any key size limit.
|
||||
// The key is intentionally invalid for a structural reason rather than for its
|
||||
// size, so the test stays meaningful even if the accepted modulus size changes.
|
||||
// Its Sign method is never reached, because the key is rejected during the
|
||||
// initial public key query.
|
||||
type invalidRSASigner struct{}
|
||||
|
||||
func (invalidRSASigner) PublicKey() PublicKey {
|
||||
n := new(big.Int).Lsh(big.NewInt(1), 2048)
|
||||
pub, err := NewPublicKey(&rsa.PublicKey{N: n, E: 2}) // incorrect exponent.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pub
|
||||
}
|
||||
|
||||
func (invalidRSASigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
|
||||
return nil, errors.New("ssh: invalid test key must not be used to sign")
|
||||
}
|
||||
|
||||
func TestClientAuthInvalidPublicKey(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(invalidRSASigner{}, testSigners["rsa"]),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
}
|
||||
if err := tryAuth(t, config); err != nil {
|
||||
t.Fatalf("client auth failed but should have fallen back to a valid key: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// partialSuccessPublicKeyAndKbdInteractiveServer returns a server config
|
||||
// where the rsa key gets partial success and the server offers both publickey
|
||||
// and keyboard-interactive as next methods. The VerifiedPublicKeyCallback
|
||||
// tracks state so that the same key only triggers partial success once,
|
||||
// avoiding an impossible auth loop.
|
||||
func partialSuccessPublicKeyAndKbdInteractiveServer() *ServerConfig {
|
||||
rsaDone := false
|
||||
serverConf := &ServerConfig{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||
if !rsaDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
rsaDone = true
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid token")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
return serverConf
|
||||
}
|
||||
|
||||
// partialSuccessMultiKeyServer returns a server config that requires two
|
||||
// public keys in sequence: the first key (rsa) triggers partial success
|
||||
// requiring a second key (ecdsa) to complete authentication.
|
||||
func partialSuccessMultiKeyServer() *ServerConfig {
|
||||
firstKeyDone := false
|
||||
serverConf := &ServerConfig{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||
if !firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
firstKeyDone = true
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
return serverConf
|
||||
}
|
||||
|
||||
func TestStaticMultiKeyPartialSuccess(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessMultiKeyServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticPartialSuccess(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(testSigners["rsa"]),
|
||||
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// partialSuccessKeyAThenKeyBServer returns a server config where keyA (rsa)
|
||||
// gets partial success and the server offers BOTH publickey and
|
||||
// keyboard-interactive. keyB (ecdsa) then completes auth successfully.
|
||||
func partialSuccessKeyAThenKeyBServer() *ServerConfig {
|
||||
keyADone := false
|
||||
serverConf := &ServerConfig{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||
if !keyADone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
keyADone = true
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid token")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if keyADone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
return serverConf
|
||||
}
|
||||
|
||||
func TestStaticPartialSuccessKeyAThenKeyB(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// partialSuccessMultiKeyThenKbdInteractiveServer returns a server config
|
||||
// that strictly requires keyA (rsa) AND keyB (ecdsa) as public keys before
|
||||
// allowing keyboard-interactive. After keyA, ONLY publickey is offered
|
||||
// (for keyB). After keyB, ONLY keyboard-interactive is offered.
|
||||
func partialSuccessMultiKeyThenKbdInteractiveServer() *ServerConfig {
|
||||
serverConf := &ServerConfig{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid token")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
return serverConf
|
||||
}
|
||||
|
||||
func TestStaticMultiKeyPartialThenKbdInteractive(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
||||
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMethodDynamic(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: nil,
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if len(ctx.Metadata.SessionID()) == 0 {
|
||||
t.Error("empty sessionID")
|
||||
}
|
||||
if ctx.Algorithms.HostKey == "" {
|
||||
t.Error("empty Host Key")
|
||||
}
|
||||
if slices.Contains(ctx.AllowedMethods, "password") {
|
||||
return Password(clientPassword), nil
|
||||
}
|
||||
return nil, errors.New("auth callback error")
|
||||
},
|
||||
}
|
||||
|
||||
if err := tryAuth(t, config); err != nil {
|
||||
t.Fatalf("unable to dial remote side: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackError(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []AuthMethod{
|
||||
Password(clientPassword),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
return nil, errors.New("auth callback error")
|
||||
},
|
||||
}
|
||||
|
||||
if err := tryAuth(t, config); err == nil {
|
||||
t.Fatal("authentication unexpectedly succeeded after AuthCallback returned an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackPartialSuccess(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
||||
return PublicKeys(testSigners["rsa"]), nil
|
||||
}
|
||||
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackMultiKeyPartialSuccess(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
||||
return PublicKeys(testSigners["rsa"]), nil
|
||||
}
|
||||
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||
return PublicKeys(testSigners["ecdsa"]), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessMultiKeyServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackMultiKeyPartialThenKbdInteractive(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
partialCount := len(ctx.PartialSuccessMethods)
|
||||
if partialCount == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||
return PublicKeys(testSigners["rsa"]), nil
|
||||
}
|
||||
if partialCount == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||
return PublicKeys(testSigners["ecdsa"]), nil
|
||||
}
|
||||
if partialCount >= 2 && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
|
||||
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackPartialSuccessKeyAThenKeyB(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if len(ctx.PartialSuccessMethods) == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||
return PublicKeys(testSigners["rsa"]), nil
|
||||
}
|
||||
if len(ctx.PartialSuccessMethods) == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||
return PublicKeys(testSigners["ecdsa"]), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v",
|
||||
ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackPartialSuccessWithStaticAuth(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(testSigners["rsa"]),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}), nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func partialSuccessPubKeyForbiddenAfterPartialServer(maxTries int) *ServerConfig {
|
||||
serverConf := &ServerConfig{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
return nil, errors.New("legacy public key authentication is forbidden")
|
||||
},
|
||||
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("invalid token")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
},
|
||||
MaxAuthTries: maxTries,
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
return serverConf
|
||||
}
|
||||
|
||||
func TestAuthCallbackPartialSuccessWithStaticMultiKey(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
Auth: []AuthMethod{
|
||||
PublicKeys(
|
||||
testSigners["rsa"],
|
||||
testSigners["ecdsa"],
|
||||
testSigners["ed25519"],
|
||||
),
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}), nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, partialSuccessPubKeyForbiddenAfterPartialServer(2))
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackPartialSuccessShortCircuit(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
serverConf := partialSuccessPubKeyForbiddenAfterPartialServer(2)
|
||||
|
||||
pubkeyAttempts := 0
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
||||
pubkeyAttempts++
|
||||
return PublicKeys(testSigners["rsa"], testSigners["ecdsa"], testSigners["ed25519"]), nil
|
||||
}
|
||||
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"correct-token"}, nil
|
||||
}), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
|
||||
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, serverConf)
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("client login error: %s", err)
|
||||
}
|
||||
if pubkeyAttempts != 1 {
|
||||
t.Errorf("expected publickey to be attempted once before short-circuit, got %d", pubkeyAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCallbackTriedLoopBound(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
serverConf := &ServerConfig{
|
||||
MaxAuthTries: -1,
|
||||
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
|
||||
return nil, errors.New("password rejected")
|
||||
},
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
|
||||
invocations := 0
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
invocations++
|
||||
return Password("wrong"), nil
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, serverConf)
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err == nil {
|
||||
t.Fatal("expected the client to abort after exceeding the attempts cap")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too many authentication attempts") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if invocations != maxAuthClientTried {
|
||||
t.Errorf("AuthCallback invoked %d times; expected %d",
|
||||
invocations, maxAuthClientTried)
|
||||
}
|
||||
}
|
||||
|
||||
func alwaysPartialPubKeyServer() *ServerConfig {
|
||||
var alwaysPartial func(ConnMetadata, PublicKey) (*Permissions, error)
|
||||
alwaysPartial = func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||
if !bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
return nil, &PartialSuccessError{
|
||||
Next: ServerAuthCallbacks{
|
||||
PublicKeyCallback: alwaysPartial,
|
||||
},
|
||||
}
|
||||
}
|
||||
serverConf := &ServerConfig{
|
||||
MaxAuthTries: -1,
|
||||
PublicKeyCallback: alwaysPartial,
|
||||
}
|
||||
serverConf.AddHostKey(testSigners["rsa"])
|
||||
return serverConf
|
||||
}
|
||||
|
||||
func TestAuthCallbackPartialSuccessLoopBound(t *testing.T) {
|
||||
c1, c2, err := netPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("netPipe: %v", err)
|
||||
}
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
invocations := 0
|
||||
clientConf := &ClientConfig{
|
||||
User: "user",
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||
invocations++
|
||||
return PublicKeys(testSigners["rsa"]), nil
|
||||
},
|
||||
}
|
||||
|
||||
go NewServerConn(c1, alwaysPartialPubKeyServer())
|
||||
|
||||
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||
if err == nil {
|
||||
t.Fatal("expected the client to abort after exceeding the attempts cap")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too many authentication attempts") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if invocations != maxAuthClientTried {
|
||||
t.Errorf("AuthCallback invoked %d times; expected %d",
|
||||
invocations, maxAuthClientTried)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMethodPassword(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
@@ -971,6 +1690,74 @@ func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type maliciousGSSAPIWithMICAuthMethod struct{}
|
||||
|
||||
func (maliciousGSSAPIWithMICAuthMethod) method() string {
|
||||
// Reuse an advertised auth method name so the client attempts this
|
||||
// AuthMethod even when gssapi-with-mic is not advertised.
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (maliciousGSSAPIWithMICAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
|
||||
req := &userAuthRequestMsg{
|
||||
User: user,
|
||||
Service: serviceSSH,
|
||||
Method: "gssapi-with-mic",
|
||||
}
|
||||
req.Payload = appendU32(req.Payload, 1)
|
||||
req.Payload = appendString(req.Payload, string(krb5OID))
|
||||
if err := c.writePacket(Marshal(req)); err != nil {
|
||||
return authFailure, nil, err
|
||||
}
|
||||
|
||||
packet, err := c.readPacket()
|
||||
if err != nil {
|
||||
return authFailure, nil, err
|
||||
}
|
||||
switch packet[0] {
|
||||
case msgUserAuthFailure:
|
||||
var msg userAuthFailureMsg
|
||||
if err := Unmarshal(packet, &msg); err != nil {
|
||||
return authFailure, nil, err
|
||||
}
|
||||
if msg.PartialSuccess {
|
||||
return authPartialSuccess, msg.Methods, nil
|
||||
}
|
||||
return authFailure, msg.Methods, nil
|
||||
default:
|
||||
return authFailure, nil, unexpectedMessageError(msgUserAuthFailure, packet[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGSSAPIWithMICPartialConfigRejectedAtRuntime(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []AuthMethod{
|
||||
maliciousGSSAPIWithMICAuthMethod{},
|
||||
},
|
||||
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
clientErr, serverErrs := tryAuthBothSides(t, config, &GSSAPIWithMICConfig{
|
||||
Server: &FakeServer{},
|
||||
})
|
||||
|
||||
if clientErr == nil || !strings.Contains(clientErr.Error(), "ssh: unable to authenticate") {
|
||||
t.Fatalf("client got %v, want authentication failure", clientErr)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, err := range serverErrs {
|
||||
if err != nil && strings.Contains(err.Error(), "ssh: gssapi-with-mic auth not configured") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("server auth errors = %v, want gssapi-with-mic auth not configured", serverErrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompatibleAlgoAndSignatures(t *testing.T) {
|
||||
type testcase struct {
|
||||
algo string
|
||||
@@ -1136,7 +1923,7 @@ func TestPickSignatureAlgorithm(t *testing.T) {
|
||||
t.Fatalf("error generating cert signer: %v", err)
|
||||
}
|
||||
// The signer supports the public key algorithm and the
|
||||
// public key format is a certificate type so the cerificate
|
||||
// public key format is a certificate type so the certificate
|
||||
// algorithm matching the key format must be returned
|
||||
_, algo, err = pickSignatureAlgorithm(certSigner, c.extensions)
|
||||
if err != nil {
|
||||
@@ -1159,6 +1946,52 @@ func TestPickSignatureAlgorithm(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickSignatureAlgorithmRespectsSignerPreference(t *testing.T) {
|
||||
algoSigner, ok := testSigners["rsa"].(AlgorithmSigner)
|
||||
if !ok {
|
||||
t.Fatalf("rsa test signer does not implement the AlgorithmSigner interface")
|
||||
}
|
||||
|
||||
serverExtensions := map[string][]byte{
|
||||
"server-sig-algs": []byte(KeyAlgoRSASHA256 + "," + KeyAlgoRSASHA512),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
signerPrefs []string
|
||||
expectedAlgo string
|
||||
}{
|
||||
{
|
||||
name: "Signer prefers SHA512 then SHA256",
|
||||
signerPrefs: []string{KeyAlgoRSASHA512, KeyAlgoRSASHA256},
|
||||
expectedAlgo: KeyAlgoRSASHA512,
|
||||
},
|
||||
{
|
||||
name: "Signer prefers SHA256 then SHA512",
|
||||
signerPrefs: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512},
|
||||
expectedAlgo: KeyAlgoRSASHA256,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
orderedSigner, err := NewSignerWithAlgorithms(algoSigner, tc.signerPrefs)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create ordered signer: %v", err)
|
||||
}
|
||||
|
||||
_, selectedAlgo, err := pickSignatureAlgorithm(orderedSigner, serverExtensions)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if selectedAlgo != tc.expectedAlgo {
|
||||
t.Errorf("Algorithm mismatch; got %q want %q", selectedAlgo, tc.expectedAlgo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// configurablePublicKeyCallback is a public key callback that allows to
|
||||
// configure the signature algorithm and format. This way we can emulate the
|
||||
// behavior of buggy clients.
|
||||
|
||||
+4
-4
@@ -419,7 +419,7 @@ type AlgorithmNegotiationError struct {
|
||||
}
|
||||
|
||||
func (a *AlgorithmNegotiationError) Error() string {
|
||||
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %v, peer offered: %v",
|
||||
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %q, peer offered: %q",
|
||||
a.What, a.SupportedAlgorithms, a.RequestedAlgorithms)
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@ func (c *Config) SetDefaults() {
|
||||
if c.Rand == nil {
|
||||
c.Rand = rand.Reader
|
||||
}
|
||||
if c.Ciphers == nil {
|
||||
if len(c.Ciphers) == 0 {
|
||||
c.Ciphers = defaultCiphers
|
||||
}
|
||||
var ciphers []string
|
||||
@@ -556,7 +556,7 @@ func (c *Config) SetDefaults() {
|
||||
}
|
||||
c.Ciphers = ciphers
|
||||
|
||||
if c.KeyExchanges == nil {
|
||||
if len(c.KeyExchanges) == 0 {
|
||||
c.KeyExchanges = defaultKexAlgos
|
||||
}
|
||||
var kexs []string
|
||||
@@ -571,7 +571,7 @@ func (c *Config) SetDefaults() {
|
||||
}
|
||||
c.KeyExchanges = kexs
|
||||
|
||||
if c.MACs == nil {
|
||||
if len(c.MACs) == 0 {
|
||||
c.MACs = defaultMACs
|
||||
}
|
||||
var macs []string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user