]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] lib/fips140: re-seal v1.0.0
authorFilippo Valsorda <filippo@golang.org>
Sun, 7 Sep 2025 14:52:08 +0000 (16:52 +0200)
committerJunyang Shao <shaojunyang@google.com>
Fri, 26 Sep 2025 17:44:50 +0000 (10:44 -0700)
Exceptionally, we decided to make a compliance-related change following
CMVP's updated Implementation Guidance on September 2nd.

The Security Policy will be updated to reflect the new zip hash.

mkzip.go has been modified to accept versions of the form vX.Y.Z-hash,
where the -hash suffix is ignored for fips140.Version() but used to
name the zip file and the unpacked cache directory.

The new zip is generated with

go run ../../src/cmd/go/internal/fips140/mkzip.go -b c2097c7c v1.0.0-c2097c7c

from c2097c7c which is the current release-branch.go1.24 head.

The full diff between the zip file contents is included below.

Fixes #75524
For #74947
Updates #69536

$ diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c

diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/cast.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/cast.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/cast.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/cast.go 1980-01-10 00:00:00.000000000 +0100
@@ -56,9 +56,10 @@
 }

 // PCT runs the named Pairwise Consistency Test (if operated in FIPS mode) and
-// returns any errors. If an error is returned, the key must not be used.
+// aborts the program (stopping the module input/output and entering the "error
+// state") if the test fails.
 //
-// PCTs are mandatory for every key pair that is generated/imported, including
+// PCTs are mandatory for every generated (but not imported) key pair, including
 // ephemeral keys (which effectively doubles the cost of key establishment). See
 // Implementation Guidance 10.3.A Additional Comment 1.
 //
@@ -66,17 +67,23 @@
 //
 // If a package p calls PCT during key generation, an invocation of that
 // function should be added to fipstest.TestConditionals.
-func PCT(name string, f func() error) error {
+func PCT(name string, f func() error) {
  if strings.ContainsAny(name, ",#=:") {
  panic("fips: invalid self-test name: " + name)
  }
  if !Enabled {
- return nil
+ return
  }

  err := f()
  if name == failfipscast {
  err = errors.New("simulated PCT failure")
  }
- return err
+ if err != nil {
+ fatal("FIPS 140-3 self-test failed: " + name + ": " + err.Error())
+ panic("unreachable")
+ }
+ if debug {
+ println("FIPS 140-3 PCT passed:", name)
+ }
 }
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdh/ecdh.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdh/ecdh.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdh/ecdh.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdh/ecdh.go 1980-01-10 00:00:00.000000000 +0100
@@ -161,6 +161,27 @@
  if err != nil {
  continue
  }
+
+ // A "Pairwise Consistency Test" makes no sense if we just generated the
+ // public key from an ephemeral private key. Moreover, there is no way to
+ // check it aside from redoing the exact same computation again. SP 800-56A
+ // Rev. 3, Section 5.6.2.1.4 acknowledges that, and doesn't require it.
+ // However, ISO 19790:2012, Section 7.10.3.3 has a blanket requirement for a
+ // PCT for all generated keys (AS10.35) and FIPS 140-3 IG 10.3.A, Additional
+ // Comment 1 goes out of its way to say that "the PCT shall be performed
+ // consistent [...], even if the underlying standard does not require a
+ // PCT". So we do it. And make ECDH nearly 50% slower (only) in FIPS mode.
+ fips140.PCT("ECDH PCT", func() error {
+ p1, err := c.newPoint().ScalarBaseMult(privateKey.d)
+ if err != nil {
+ return err
+ }
+ if !bytes.Equal(p1.Bytes(), privateKey.pub.q) {
+ return errors.New("crypto/ecdh: public key does not match private key")
+ }
+ return nil
+ })
+
  return privateKey, nil
  }
 }
@@ -188,28 +209,6 @@
  panic("crypto/ecdh: internal error: public key is the identity element")
  }

- // A "Pairwise Consistency Test" makes no sense if we just generated the
- // public key from an ephemeral private key. Moreover, there is no way to
- // check it aside from redoing the exact same computation again. SP 800-56A
- // Rev. 3, Section 5.6.2.1.4 acknowledges that, and doesn't require it.
- // However, ISO 19790:2012, Section 7.10.3.3 has a blanket requirement for a
- // PCT for all generated keys (AS10.35) and FIPS 140-3 IG 10.3.A, Additional
- // Comment 1 goes out of its way to say that "the PCT shall be performed
- // consistent [...], even if the underlying standard does not require a
- // PCT". So we do it. And make ECDH nearly 50% slower (only) in FIPS mode.
- if err := fips140.PCT("ECDH PCT", func() error {
- p1, err := c.newPoint().ScalarBaseMult(key)
- if err != nil {
- return err
- }
- if !bytes.Equal(p1.Bytes(), publicKey) {
- return errors.New("crypto/ecdh: public key does not match private key")
- }
- return nil
- }); err != nil {
- panic(err)
- }
-
  k := &PrivateKey{d: bytes.Clone(key), pub: PublicKey{curve: c.curve, q: publicKey}}
  return k, nil
 }
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdsa/cast.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdsa/cast.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdsa/cast.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdsa/cast.go 1980-01-10 00:00:00.000000000 +0100
@@ -51,8 +51,8 @@
  }
 }

-func fipsPCT[P Point[P]](c *Curve[P], k *PrivateKey) error {
- return fips140.PCT("ECDSA PCT", func() error {
+func fipsPCT[P Point[P]](c *Curve[P], k *PrivateKey) {
+ fips140.PCT("ECDSA PCT", func() error {
  hash := testHash()
  drbg := newDRBG(sha512.New, k.d, bits2octets(P256(), hash), nil)
  sig, err := sign(c, k, drbg, hash)
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdsa/ecdsa.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdsa/ecdsa.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdsa/ecdsa.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdsa/ecdsa.go 1980-01-10 00:00:00.000000000 +0100
@@ -166,11 +166,6 @@
  return nil, err
  }
  priv := &PrivateKey{pub: *pub, d: d.Bytes(c.N)}
- if err := fipsPCT(c, priv); err != nil {
- // This can happen if the application went out of its way to make an
- // ecdsa.PrivateKey with a mismatching PublicKey.
- return nil, err
- }
  return priv, nil
 }

@@ -203,10 +198,7 @@
  },
  d: k.Bytes(c.N),
  }
- if err := fipsPCT(c, priv); err != nil {
- // This clearly can't happen, but FIPS 140-3 mandates that we check it.
- panic(err)
- }
+ fipsPCT(c, priv)
  return priv, nil
 }

diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdsa/hmacdrbg.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdsa/hmacdrbg.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/ecdsa/hmacdrbg.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ecdsa/hmacdrbg.go 1980-01-10 00:00:00.000000000 +0100
@@ -121,7 +121,7 @@
 //
 // This should only be used for ACVP testing. hmacDRBG is not intended to be
 // used directly.
-func TestingOnlyNewDRBG(hash func() fips140.Hash, entropy, nonce []byte, s []byte) *hmacDRBG {
+func TestingOnlyNewDRBG[H fips140.Hash](hash func() H, entropy, nonce []byte, s []byte) *hmacDRBG {
  return newDRBG(hash, entropy, nonce, plainPersonalizationString(s))
 }

diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ed25519/cast.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ed25519/cast.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/ed25519/cast.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ed25519/cast.go 1980-01-10 00:00:00.000000000 +0100
@@ -12,8 +12,8 @@
  "sync"
 )

-func fipsPCT(k *PrivateKey) error {
- return fips140.PCT("Ed25519 sign and verify PCT", func() error {
+func fipsPCT(k *PrivateKey) {
+ fips140.PCT("Ed25519 sign and verify PCT", func() error {
  return pairwiseTest(k)
  })
 }
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/ed25519/ed25519.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ed25519/ed25519.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/ed25519/ed25519.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/ed25519/ed25519.go 1980-01-10 00:00:00.000000000 +0100
@@ -69,10 +69,7 @@
  fips140.RecordApproved()
  drbg.Read(priv.seed[:])
  precomputePrivateKey(priv)
- if err := fipsPCT(priv); err != nil {
- // This clearly can't happen, but FIPS 140-3 requires that we check.
- panic(err)
- }
+ fipsPCT(priv)
  return priv, nil
 }

@@ -88,10 +85,6 @@
  }
  copy(priv.seed[:], seed)
  precomputePrivateKey(priv)
- if err := fipsPCT(priv); err != nil {
- // This clearly can't happen, but FIPS 140-3 requires that we check.
- panic(err)
- }
  return priv, nil
 }

@@ -137,12 +130,6 @@

  copy(priv.prefix[:], h[32:])

- if err := fipsPCT(priv); err != nil {
- // This can happen if the application messed with the private key
- // encoding, and the public key doesn't match the seed anymore.
- return nil, err
- }
-
  return priv, nil
 }

diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/fips140.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/fips140.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/fips140.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/fips140.go 1980-01-10 00:00:00.000000000 +0100
@@ -62,6 +62,10 @@
  return "Go Cryptographic Module"
 }

+// Version returns the formal version (such as "v1.0.0") if building against a
+// frozen module with GOFIPS140. Otherwise, it returns "latest".
 func Version() string {
- return "v1.0"
+ // This return value is replaced by mkzip.go, it must not be changed or
+ // moved to a different file.
+ return "v1.0.0"
 }
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/mlkem/mlkem1024.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/mlkem/mlkem1024.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/mlkem/mlkem1024.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/mlkem/mlkem1024.go 1980-01-10 00:00:00.000000000 +0100
@@ -118,10 +118,7 @@
  var z [32]byte
  drbg.Read(z[:])
  kemKeyGen1024(dk, &d, &z)
- if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT1024(dk) }); err != nil {
- // This clearly can't happen, but FIPS 140-3 requires us to check.
- panic(err)
- }
+ fips140.PCT("ML-KEM PCT", func() error { return kemPCT1024(dk) })
  fips140.RecordApproved()
  return dk, nil
 }
@@ -149,10 +146,6 @@
  d := (*[32]byte)(seed[:32])
  z := (*[32]byte)(seed[32:])
  kemKeyGen1024(dk, d, z)
- if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT1024(dk) }); err != nil {
- // This clearly can't happen, but FIPS 140-3 requires us to check.
- panic(err)
- }
  fips140.RecordApproved()
  return dk, nil
 }
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/mlkem/mlkem768.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/mlkem/mlkem768.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/mlkem/mlkem768.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/mlkem/mlkem768.go 1980-01-10 00:00:00.000000000 +0100
@@ -177,10 +177,7 @@
  var z [32]byte
  drbg.Read(z[:])
  kemKeyGen(dk, &d, &z)
- if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT(dk) }); err != nil {
- // This clearly can't happen, but FIPS 140-3 requires us to check.
- panic(err)
- }
+ fips140.PCT("ML-KEM PCT", func() error { return kemPCT(dk) })
  fips140.RecordApproved()
  return dk, nil
 }
@@ -208,10 +205,6 @@
  d := (*[32]byte)(seed[:32])
  z := (*[32]byte)(seed[32:])
  kemKeyGen(dk, d, z)
- if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT(dk) }); err != nil {
- // This clearly can't happen, but FIPS 140-3 requires us to check.
- panic(err)
- }
  fips140.RecordApproved()
  return dk, nil
 }
diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/rsa/keygen.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/rsa/keygen.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/rsa/keygen.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/rsa/keygen.go 1980-01-10 00:00:00.000000000 +0100
@@ -105,7 +105,28 @@
  // negligible chance of failure we can defer the check to the end of key
  // generation and return an error if it fails. See [checkPrivateKey].

- return newPrivateKey(N, 65537, d, P, Q)
+ k, err := newPrivateKey(N, 65537, d, P, Q)
+ if err != nil {
+ return nil, err
+ }
+
+ if k.fipsApproved {
+ fips140.PCT("RSA sign and verify PCT", func() error {
+ hash := []byte{
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+ 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
+ }
+ sig, err := signPKCS1v15(k, "SHA-256", hash)
+ if err != nil {
+ return err
+ }
+ return verifyPKCS1v15(k.PublicKey(), "SHA-256", hash, sig)
+ })
+ }
+
+ return k, nil
  }
 }

diff -ru golang.org/fips140@v1.0.0/fips140/v1.0.0/rsa/rsa.go golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/rsa/rsa.go
--- golang.org/fips140@v1.0.0/fips140/v1.0.0/rsa/rsa.go 1980-01-10 00:00:00.000000000 +0100
+++ golang.org/fips140@v1.0.0-c2097c7c/fips140/v1.0.0-c2097c7c/rsa/rsa.go 1980-01-10 00:00:00.000000000 +0100
@@ -310,26 +310,6 @@
  return errors.New("crypto/rsa: d too small")
  }

- // If the key is still in scope for FIPS mode, perform a Pairwise
- // Consistency Test.
- if priv.fipsApproved {
- if err := fips140.PCT("RSA sign and verify PCT", func() error {
- hash := []byte{
- 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
- 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
- 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
- 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
- }
- sig, err := signPKCS1v15(priv, "SHA-256", hash)
- if err != nil {
- return err
- }
- return verifyPKCS1v15(priv.PublicKey(), "SHA-256", hash, sig)
- }); err != nil {
- return err
- }
- }
-
  return nil
 }

Change-Id: I6a6a6964b1780f19ec2b5202052de58b47d9342c
Reviewed-on: https://go-review.googlesource.com/c/go/+/701520
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Commit-Queue: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/706719

lib/fips140/fips140.sum
lib/fips140/inprocess.txt
lib/fips140/v1.0.0-c2097c7c.zip [moved from lib/fips140/v1.0.0.zip with 78% similarity]
lib/fips140/v1.0.0.txt [new file with mode: 0644]
src/cmd/go/internal/fips140/mkzip.go
src/cmd/go/testdata/script/fipssnap.txt
src/crypto/internal/fips140test/fips_test.go
src/internal/buildcfg/cfg.go

index 66b1e23dfe619d1c1fcc866e3ec17e7cdf8f148e..703d1dc60e58ab7c960c15001d22d1247d6a727c 100644 (file)
@@ -9,4 +9,4 @@
 #
 #      go test cmd/go/internal/fips140 -update
 #
-v1.0.0.zip b50508feaeff05d22516b21e1fd210bbf5d6a1e422eaf2cfa23fe379342713b8
+v1.0.0-c2097c7c.zip daf3614e0406f67ae6323c902db3f953a1effb199142362a039e7526dfb9368b
index 0ec25f7505c14bad77f34453b2730417ab431293..efd3caba85ba621b519d17ce34d92f35291b51b1 100644 (file)
@@ -1 +1 @@
-v1.0.0
+v1.0.0-c2097c7c
similarity index 78%
rename from lib/fips140/v1.0.0.zip
rename to lib/fips140/v1.0.0-c2097c7c.zip
index bd9d3c19d05b9075a2587806d00224c3f8859ba1..aabf762d0fecb29fcf3e42fbf8acefc994cf8358 100644 (file)
Binary files a/lib/fips140/v1.0.0.zip and b/lib/fips140/v1.0.0-c2097c7c.zip differ
diff --git a/lib/fips140/v1.0.0.txt b/lib/fips140/v1.0.0.txt
new file mode 100644 (file)
index 0000000..efd3cab
--- /dev/null
@@ -0,0 +1 @@
+v1.0.0-c2097c7c
index 7a6ba803248e37d763d48742c8d45dd0fbfcd4ca..a139a0f2e256f7b710c217171dc46ec617d0babd 100644 (file)
@@ -27,10 +27,10 @@ import (
        "log"
        "os"
        "path/filepath"
-       "regexp"
        "strings"
 
        "golang.org/x/mod/module"
+       "golang.org/x/mod/semver"
        modzip "golang.org/x/mod/zip"
 )
 
@@ -61,7 +61,7 @@ func main() {
 
        // Must have valid version, and must not overwrite existing file.
        version := flag.Arg(0)
-       if !regexp.MustCompile(`^v\d+\.\d+\.\d+$`).MatchString(version) {
+       if semver.Canonical(version) != version {
                log.Fatalf("invalid version %q; must be vX.Y.Z", version)
        }
        if _, err := os.Stat(version + ".zip"); err == nil {
@@ -117,7 +117,9 @@ func main() {
                        if !bytes.Contains(contents, []byte(returnLine)) {
                                log.Fatalf("did not find %q in fips140.go", returnLine)
                        }
-                       newLine := `return "` + version + `"`
+                       // Use only the vX.Y.Z part of a possible vX.Y.Z-hash version.
+                       v, _, _ := strings.Cut(version, "-")
+                       newLine := `return "` + v + `"`
                        contents = bytes.ReplaceAll(contents, []byte(returnLine), []byte(newLine))
                        wf, err := zw.Create(f.Name)
                        if err != nil {
index 9888bc82f11d81ed3506d46c4eb7ff9023ad165a..4d96aedf2a472d8798a6af2324877817c4b5b9fc 100644 (file)
@@ -1,4 +1,4 @@
-env snap=v1.0.0
+env snap=v1.0.0-c2097c7c
 env alias=inprocess
 
 env GOFIPS140=$snap
@@ -23,8 +23,7 @@ stdout crypto/internal/fips140/$snap/sha256
 ! stdout crypto/internal/fips140/check
 
 # again with GOFIPS140=$alias
-# TODO: enable when we add inprocess.txt
-# env GOFIPS140=$alias
+env GOFIPS140=$alias
 
 # default GODEBUG includes fips140=on
 go list -f '{{.DefaultGODEBUG}}'
index 6220e536f4aa6eb6c327ecbd7d7b83a62de87674..52fc9d3488600373936239221735569bd0532d2a 100644 (file)
@@ -74,6 +74,9 @@ func TestVersion(t *testing.T) {
                        continue
                }
                exp := setting.Value
+               // Remove the -hash suffix, if any.
+               // The version from fips140.Version omits it.
+               exp, _, _ = strings.Cut(exp, "-")
                if v := fips140.Version(); v != exp {
                        t.Errorf("Version is %q, expected %q", v, exp)
                }
index 5ae4c0c7adc8fec633692cf20dd3254297fb55b4..50b122fdb8524ec94d01f711f47dcfb115b7478a 100644 (file)
@@ -85,7 +85,7 @@ func gofips140() string {
 }
 
 // isFIPSVersion reports whether v is a valid FIPS version,
-// of the form vX.Y.Z.
+// of the form vX.Y.Z or vX.Y.Z-hash.
 func isFIPSVersion(v string) bool {
        if !strings.HasPrefix(v, "v") {
                return false
@@ -99,7 +99,8 @@ func isFIPSVersion(v string) bool {
                return false
        }
        v, ok = skipNum(v[len("."):])
-       return ok && v == ""
+       hasHash := strings.HasPrefix(v, "-") && len(v) == len("-")+8
+       return ok && (v == "" || hasHash)
 }
 
 // skipNum skips the leading text matching [0-9]+